From 4ef35bf238fc6f7217e4b6de4aee37192ec503ec Mon Sep 17 00:00:00 2001
From: LAPTOP-SNT8I5JK\Boounion <Chenluhua@qq.com>
Date: 星期三, 17 九月 2025 13:36:27 +0800
Subject: [PATCH] 1.自绘CListCtrl用于ProcessJob的Carrier选择

---
 SourceCode/Bond/Servo/CCjPage2.cpp              |   54 +++
 SourceCode/Bond/Servo/Servo.vcxproj             |    2 
 SourceCode/Bond/Servo/Servo.vcxproj.filters     |    2 
 SourceCode/Bond/Servo/resource.h                |    0 
 SourceCode/Bond/Servo/CCarrierSlotSelector.cpp  |  673 ++++++++++++++++++++++++++++++++++++++++++++
 SourceCode/Bond/Servo/CCarrierSlotSelector.h    |  122 ++++++++
 SourceCode/Bond/Servo/CCjPage2.h                |    4 
 SourceCode/Bond/Servo/Servo.rc                  |    0 
 SourceCode/Bond/Servo/CControlJobManagerDlg.cpp |   19 +
 9 files changed, 876 insertions(+), 0 deletions(-)

diff --git a/SourceCode/Bond/Servo/CCarrierSlotSelector.cpp b/SourceCode/Bond/Servo/CCarrierSlotSelector.cpp
new file mode 100644
index 0000000..20be4d4
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotSelector.cpp
@@ -0,0 +1,673 @@
+#include "stdafx.h"
+#include "CCarrierSlotSelector.h"
+
+#define SAFE_PORT(p)    ((p) >= 0 && (p) < (int)m_ports.size())
+#define SAFE_SLOT(s)    ((s) >= 0 && (s) < m_nSlots)
+
+#ifndef LVS_EX_DOUBLEBUFFER
+#define LVS_EX_DOUBLEBUFFER 0x00010000
+#endif
+
+BEGIN_MESSAGE_MAP(CCarrierSlotSelector, CListCtrl)
+    ON_WM_SHOWWINDOW()
+    ON_WM_WINDOWPOSCHANGED()
+    ON_WM_SIZE()
+    ON_WM_ERASEBKGND()
+    ON_WM_PAINT()                           // ★ 新增
+    ON_WM_LBUTTONDOWN()
+    ON_WM_MOUSEMOVE()
+    ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &CCarrierSlotSelector::OnCustomDraw)
+END_MESSAGE_MAP()
+
+CCarrierSlotSelector::CCarrierSlotSelector() {}
+CCarrierSlotSelector::~CCarrierSlotSelector()
+{
+    if ((HFONT)m_fntText)  m_fntText.DeleteObject();
+    if ((HFONT)m_fntBold)  m_fntBold.DeleteObject();
+    if ((HFONT)m_fntSmall) m_fntSmall.DeleteObject();
+    if ((HIMAGELIST)m_ilRowHeight) m_ilRowHeight.DeleteImageList();
+}
+
+void CCarrierSlotSelector::PreSubclassWindow()
+{
+    CListCtrl::PreSubclassWindow();
+
+    ModifyStyle(LVS_TYPEMASK, LVS_REPORT | LVS_SHOWSELALWAYS);
+    ModifyStyle(LVS_OWNERDRAWFIXED | LVS_OWNERDATA, 0);
+
+    DWORD ex = GetExtendedStyle();
+    ex |= LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER;
+    ex &= ~LVS_EX_GRIDLINES; // 关闭系统网格,改自绘
+    SetExtendedStyle(ex);
+
+    // 让默认绘制用我们的底色,进一步降低白底机会(即使哪处走了默认路径)
+    ListView_SetBkColor(m_hWnd, m_colBgNorm);
+    ListView_SetTextBkColor(m_hWnd, m_colBgNorm);
+
+    EnsureFonts();
+    SetRowHeight(m_rowHeight);
+}
+
+void CCarrierSlotSelector::OnShowWindow(BOOL bShow, UINT nStatus)
+{
+    CListCtrl::OnShowWindow(bShow, nStatus);
+    if (bShow && !m_bFirstShown)
+    {
+        m_bFirstShown = TRUE;
+        RedrawWindow(nullptr, nullptr,
+            RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW);
+    }
+}
+
+void CCarrierSlotSelector::OnWindowPosChanged(WINDOWPOS* wp)
+{
+    CListCtrl::OnWindowPosChanged(wp);
+    if (wp && (wp->flags & SWP_SHOWWINDOW))
+    {
+        RedrawWindow(nullptr, nullptr,
+            RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW);
+    }
+}
+
+void CCarrierSlotSelector::EnsureFonts()
+{
+    if (!(HFONT)m_fntText)
+    {
+        LOGFONT lf = { 0 };
+        CFont* pSys = GetFont();
+        if (pSys) pSys->GetLogFont(&lf);
+        else { lf.lfHeight = -14; lstrcpy(lf.lfFaceName, _T("Segoe UI")); }
+        m_fntText.CreateFontIndirect(&lf);
+
+        lf.lfWeight = FW_SEMIBOLD;
+        m_fntBold.CreateFontIndirect(&lf);
+
+        lf.lfWeight = FW_NORMAL; lf.lfHeight = -12;
+        m_fntSmall.CreateFontIndirect(&lf);
+    }
+}
+
+void CCarrierSlotSelector::InitGrid(int nPorts, int nSlots)
+{
+    ASSERT(nPorts >= 1 && nSlots >= 1);
+    m_ports.clear();
+    m_ports.resize(nPorts);
+    m_nSlots = nSlots;
+    for (auto& pc : m_ports) pc.slots.resize(m_nSlots);
+
+    SetRedraw(FALSE);
+    DeleteAllItems();
+    while (GetHeaderCtrl() && GetHeaderCtrl()->GetItemCount() > 0)
+        DeleteColumn(0);
+
+    InsertColumn(0, _T("Slot"), LVCFMT_LEFT, m_slotColWidth);
+    for (int c = 0; c < nPorts; ++c)
+    {
+        CString col; col.Format(_T("Port %d"), c + 1);
+        InsertColumn(c + 1, col, LVCFMT_LEFT, m_portColWidth);
+        m_ports[c].portName = col;
+        m_ports[c].carrierName.Empty();
+    }
+
+    UpdateRowCount();
+    RebuildTexts();
+    SetRedraw(TRUE);
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetColumnWidths(int slotColWidth, int portColWidth)
+{
+    if (slotColWidth > 0) m_slotColWidth = slotColWidth;
+    if (portColWidth > 0) m_portColWidth = portColWidth;
+    SetColumnWidth(0, m_slotColWidth);
+    for (int c = 0; c < (int)m_ports.size(); ++c) SetColumnWidth(c + 1, m_portColWidth);
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetRowHeight(int cy)
+{
+    cy = max(18, min(cy, 64));
+    m_rowHeight = cy;
+
+    if ((HIMAGELIST)m_ilRowHeight) m_ilRowHeight.DeleteImageList();
+    m_ilRowHeight.Create(1, m_rowHeight, ILC_COLOR32, 1, 1);
+
+    // 1×cy 透明位图
+    CBitmap bmp; bmp.CreateBitmap(1, m_rowHeight, 1, 32, nullptr);
+    m_ilRowHeight.Add(&bmp, RGB(0, 0, 0));
+    SetImageList(&m_ilRowHeight, LVSIL_SMALL);
+
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::UpdateRowCount()
+{
+    int cur = GetItemCount();
+    if (cur < m_nSlots)
+    {
+        for (int i = cur; i < m_nSlots; ++i)
+        {
+            CString s; s.Format(_T("Slot %d"), i + 1);
+            InsertItem(i, s, 0); // 设 iImage=0,行高来自 small image list
+        }
+    }
+    else if (cur > m_nSlots)
+    {
+        for (int i = cur - 1; i >= m_nSlots; --i) DeleteItem(i);
+    }
+
+    // 已有行也统一设 iImage=0
+    LVITEM lvi = { 0 };
+    lvi.mask = LVIF_IMAGE; lvi.iImage = 0;
+    for (int i = 0; i < m_nSlots; ++i) { lvi.iItem = i; SetItem(&lvi); }
+}
+
+void CCarrierSlotSelector::RebuildTexts()
+{
+    // 列头:显示 “已选数/总槽数 + [x]/[ ]”
+    for (int c = 0; c < (int)m_ports.size(); ++c)
+    {
+        int selected = 0;
+        int total = m_nSlots;
+
+        bool anyCheckable = false;
+        bool allChecked = true;
+
+        for (int r = 0; r < m_nSlots; ++r)
+        {
+            const auto& cell = m_ports[c].slots[r];
+            if (cell.hasGlass)
+            {
+                anyCheckable = true;
+                if (cell.checked) ++selected; else allChecked = false;
+            }
+        }
+        if (!anyCheckable) allChecked = false;
+
+        CString head;
+        CString chk = allChecked ? _T("[x]") : _T("[ ]");
+
+        if (!m_ports[c].carrierName.IsEmpty())
+            head.Format(_T("%s (%s)   %d/%d  %s%s"),
+                m_ports[c].portName.GetString(), m_ports[c].carrierName.GetString(),
+                selected, total,
+                m_ports[c].allocated ? _T("[LOCK]  ") : _T(""),
+                chk.GetString());
+        else
+            head.Format(_T("%s   %d/%d  %s%s"),
+                m_ports[c].portName.GetString(), selected, total,
+                m_ports[c].allocated ? _T("[LOCK]  ") : _T(""),
+                chk.GetString());
+
+        LVCOLUMN lvc = { 0 }; lvc.mask = LVCF_TEXT; lvc.pszText = head.GetBuffer();
+        SetColumn(c + 1, &lvc);
+        head.ReleaseBuffer();
+    }
+
+    // 行头文本
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        CString s; s.Format(_T("Slot %d"), r + 1);
+        SetItemText(r, 0, s);
+    }
+
+    // 单元格临时文本
+    for (int r = 0; r < m_nSlots; ++r)
+        for (int c = 0; c < (int)m_ports.size(); ++c)
+            SetItemText(r, c + 1, GetDisplayId(c, r));
+}
+
+void CCarrierSlotSelector::SetShowMaterialToggle(BOOL bShow)
+{
+    m_bShowMatToggle = bShow;
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    if (portName)    m_ports[portIndex].portName = portName;
+    if (carrierName) m_ports[portIndex].carrierName = carrierName;
+    RebuildTexts();
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    m_ports[portIndex].allocated = !!allocated;
+    if (byName) m_ports[portIndex].allocatedBy = byName; else m_ports[portIndex].allocatedBy.Empty();
+
+    if (allocated)
+        for (int r = 0; r < m_nSlots; ++r) m_ports[portIndex].slots[r].checked = false;
+
+    // 刷新整列
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        CRect rc; GetSubItemRect(r, portIndex + 1, LVIR_BOUNDS, rc);
+        InvalidateRect(&rc, FALSE);
+    }
+    RebuildTexts();
+}
+
+BOOL CCarrierSlotSelector::IsPortAllocated(int portIndex) const
+{
+    if (!SAFE_PORT(portIndex)) return FALSE;
+    return m_ports[portIndex].allocated;
+}
+
+void CCarrierSlotSelector::SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    cell.hasGlass = !!hasGlass;
+    cell.coreId = coreId ? coreId : _T("");
+    cell.material = (material == MAT_G2) ? MAT_G2 : MAT_G1;
+    if (!cell.hasGlass) cell.checked = false;
+
+    CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
+    InvalidateRect(&rc, FALSE);
+    RebuildTexts();
+}
+
+void CCarrierSlotSelector::SetSlotChecked(int portIndex, int slotIndex, BOOL checked)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    if (m_ports[portIndex].allocated) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return;
+
+    cell.checked = !!checked;
+    NotifySelectionChanged(portIndex, slotIndex, cell.checked);
+
+    CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
+    InvalidateRect(&rc, FALSE);
+    RebuildTexts();
+}
+
+BOOL CCarrierSlotSelector::GetSlotChecked(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return FALSE;
+    return m_ports[portIndex].slots[slotIndex].checked ? TRUE : FALSE;
+}
+
+int CCarrierSlotSelector::GetSlotMaterialType(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return MAT_G1;
+    return m_ports[portIndex].slots[slotIndex].material;
+}
+
+void CCarrierSlotSelector::SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    if (m_ports[portIndex].allocated) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return;
+
+    int mt = (material == MAT_G2) ? MAT_G2 : MAT_G1;
+    if (cell.material != mt)
+    {
+        cell.material = mt;
+        if (bNotify) NotifyMaterialChanged(portIndex, slotIndex, cell.material);
+        CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
+        InvalidateRect(&rc, FALSE);
+    }
+}
+
+CString CCarrierSlotSelector::GetDisplayId(int portIndex, int slotIndex) const
+{
+    CString s(_T("—"));
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return s;
+    const auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return s;
+    s.Format(_T("G%d-%s"), (cell.material == MAT_G2) ? 2 : 1, cell.coreId.GetString());
+    return s;
+}
+
+void CCarrierSlotSelector::CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    if (m_ports[portIndex].allocated) return;
+
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        auto& cell = m_ports[portIndex].slots[r];
+        if (!cell.hasGlass) continue;
+        if (cell.checked != !!checked)
+        {
+            cell.checked = !!checked;
+            if (bNotify) NotifySelectionChanged(portIndex, r, cell.checked);
+        }
+        CRect rc; GetSubItemRect(r, portIndex + 1, LVIR_BOUNDS, rc);
+        InvalidateRect(&rc, FALSE);
+    }
+    RebuildTexts();
+}
+
+BOOL CCarrierSlotSelector::OnEraseBkgnd(CDC* /*pDC*/)
+{
+    // 关键:不让系统擦白背景;用自绘在 PREPAINT 阶段统一铺底
+    return TRUE;
+}
+
+void CCarrierSlotSelector::OnPaint()
+{
+    CPaintDC dc(this);
+    CRect rc; GetClientRect(&rc);
+
+    CDC memDC;
+    memDC.CreateCompatibleDC(&dc);
+    CBitmap bmp;
+    bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
+    HGDIOBJ hOldBmp = memDC.SelectObject(bmp);
+
+    // 统一底色(避免任何擦白)
+    memDC.FillSolidRect(rc, m_colBgNorm);
+
+    // 让控件把客户区内容“画到”内存DC(会触发 NM_CUSTOMDRAW,走你现有自绘逻辑)
+    // PRF_ERASEBKGND 让内部如果想擦背景,也在内存里擦,不会闪屏
+    SendMessage(WM_PRINTCLIENT,
+        reinterpret_cast<WPARAM>(memDC.m_hDC),
+        PRF_CLIENT | PRF_ERASEBKGND | PRF_CHILDREN | PRF_OWNED);
+
+    // 回显到屏幕
+    dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY);
+
+    memDC.SelectObject(hOldBmp);
+    // bmp, memDC 析构自动释放
+}
+
+void CCarrierSlotSelector::OnSize(UINT nType, int cx, int cy)
+{
+    CListCtrl::OnSize(nType, cx, cy);
+}
+
+CRect CCarrierSlotSelector::GetCheckboxRect(const CRect& cell) const
+{
+    int sz = max(14, min(int(m_rowHeight * 0.70), 20));
+    int leftPad = 8;
+    int top = cell.top + (m_rowHeight - sz) / 2;
+    return CRect(cell.left + leftPad, top, cell.left + leftPad + sz, top + sz);
+}
+
+CRect CCarrierSlotSelector::GetMaterialTagRect(const CRect& cell) const
+{
+    int tagH = max(14, min(int(m_rowHeight * 0.65), m_rowHeight - 8));
+    int tagW = 32;
+    int rightPadForDot = 16;
+    int gap = 6;
+    int top = cell.top + (m_rowHeight - tagH) / 2;
+    int right = cell.right - rightPadForDot - gap;
+    return CRect(right - tagW, top, right, top + tagH);
+}
+
+CRect CCarrierSlotSelector::GetStatusDotRect(const CRect& cell) const
+{
+    int d = max(8, min(int(m_rowHeight * 0.42), 12));
+    int rightPad = 6;
+    int top = cell.top + (m_rowHeight - d) / 2;
+    return CRect(cell.right - rightPad - d, top, cell.right - rightPad, top + d);
+}
+
+void CCarrierSlotSelector::DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled)
+{
+    CBrush br(disabled ? RGB(245, 245, 245) : RGB(255, 255, 255));
+    CPen   pen(PS_SOLID, 1, disabled ? RGB(200, 200, 200) : RGB(130, 130, 135));
+    CBrush* pOldB = pDC->SelectObject(&br);
+    CPen* pOldP = pDC->SelectObject(&pen);
+    pDC->RoundRect(r, CPoint(3, 3));
+    pDC->SelectObject(pOldB);
+    pDC->SelectObject(pOldP);
+    br.DeleteObject(); pen.DeleteObject();
+
+    if (!checked) return;
+
+    COLORREF c = disabled ? RGB(160, 160, 160) : RGB(40, 150, 90);
+    CPen penTick(PS_SOLID, max(2, r.Height() / 8), c);
+    CPen* pOld = pDC->SelectObject(&penTick);
+    POINT p1 = { r.left + r.Width() * 2 / 9, r.top + r.Height() * 5 / 9 };
+    POINT p2 = { r.left + r.Width() * 4 / 9, r.top + r.Height() * 7 / 9 };
+    POINT p3 = { r.left + r.Width() * 7 / 9, r.top + r.Height() * 3 / 9 };
+    pDC->MoveTo(p1); pDC->LineTo(p2); pDC->LineTo(p3);
+    pDC->SelectObject(pOld);
+    penTick.DeleteObject();
+}
+
+void CCarrierSlotSelector::OnLButtonDown(UINT nFlags, CPoint point)
+{
+    LVHITTESTINFO ht = { 0 }; ht.pt = point;
+    int row = SubItemHitTest(&ht);
+    int sub = (row >= 0) ? ht.iSubItem : -1;
+
+    if (row >= 0 && sub >= 1)
+    {
+        int port = sub - 1;
+        if (SAFE_PORT(port) && SAFE_SLOT(row))
+        {
+            auto& pc = m_ports[port];
+            auto& cell = pc.slots[row];
+            CRect rcCell; GetSubItemRect(row, sub, LVIR_BOUNDS, rcCell);
+
+            if (pc.allocated || !cell.hasGlass)
+            {
+                CListCtrl::OnLButtonDown(nFlags, point);
+                return;
+            }
+
+            if (GetCheckboxRect(rcCell).PtInRect(point))
+            {
+                cell.checked = !cell.checked;
+                NotifySelectionChanged(port, row, cell.checked);
+                InvalidateRect(&rcCell, FALSE);
+                RebuildTexts();
+                return;
+            }
+
+            if (m_bShowMatToggle && GetMaterialTagRect(rcCell).PtInRect(point))
+            {
+                cell.material = (cell.material == MAT_G1) ? MAT_G2 : MAT_G1;
+                NotifyMaterialChanged(port, row, cell.material);
+                InvalidateRect(&rcCell, FALSE);
+                return;
+            }
+        }
+    }
+
+    CListCtrl::OnLButtonDown(nFlags, point);
+}
+
+void CCarrierSlotSelector::OnMouseMove(UINT nFlags, CPoint point)
+{
+    CListCtrl::OnMouseMove(nFlags, point);
+}
+
+void CCarrierSlotSelector::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
+{
+    NMLVCUSTOMDRAW* pCD = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR);
+
+    switch (pCD->nmcd.dwDrawStage)
+    {
+    case CDDS_PREPAINT:
+    {
+        // 关键:整控件预先铺底,避免初次 hover/显示时“刷白”
+        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
+        CRect rcClient; GetClientRect(&rcClient);
+        pDC->FillSolidRect(rcClient, m_colBgNorm);
+        *pResult = CDRF_NOTIFYITEMDRAW;
+        return;
+    }
+
+    case CDDS_ITEMPREPAINT:
+        *pResult = CDRF_NOTIFYSUBITEMDRAW;
+        return;
+
+    case (CDDS_SUBITEM | CDDS_ITEMPREPAINT):
+    {
+        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
+        int row = (int)pCD->nmcd.dwItemSpec;
+        int sub = pCD->iSubItem;
+
+        CRect rc; GetSubItemRect(row, sub, LVIR_BOUNDS, rc);
+
+        // 背景
+        COLORREF bk = m_colBgNorm;
+        if (sub >= 1)
+        {
+            int port = sub - 1;
+            if (port % 2 == 0) bk = m_colBgAlt;             // 交替列
+            if (SAFE_PORT(port) && m_ports[port].allocated) // 锁定列
+                bk = m_colLockBg;
+        }
+        pDC->FillSolidRect(rc, bk);
+
+        // 网格(上/左加粗;右/下细线)
+        CPen penTopLeft(PS_SOLID, (sub >= 1 && ((sub - 1) % 2 == 0)) ? 2 : 1, RGB(210, 214, 220));
+        CPen* pOldPen = pDC->SelectObject(&penTopLeft);
+        pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.right, rc.top);
+        pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.left, rc.bottom);
+        pDC->SelectObject(pOldPen); penTopLeft.DeleteObject();
+
+        CPen penThin(PS_SOLID, 1, RGB(220, 224, 230));
+        pOldPen = pDC->SelectObject(&penThin);
+        if (sub == GetHeaderCtrl()->GetItemCount() - 1) { pDC->MoveTo(rc.right - 1, rc.top); pDC->LineTo(rc.right - 1, rc.bottom); }
+        if (row == m_nSlots - 1) { pDC->MoveTo(rc.left, rc.bottom - 1); pDC->LineTo(rc.right, rc.bottom - 1); }
+        pDC->SelectObject(pOldPen); penThin.DeleteObject();
+
+        if (sub == 0)
+        {
+            CString s; s.Format(_T("Slot %d"), row + 1);
+            CFont* pOld = pDC->SelectObject(&m_fntBold);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SetTextColor(RGB(60, 60, 64));
+            CRect rText = rc; rText.DeflateRect(8, 0, 8, 0);
+            pDC->DrawText(s, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
+            pDC->SelectObject(pOld);
+        }
+        else if (SAFE_PORT(sub - 1) && SAFE_SLOT(row))
+        {
+            int port = sub - 1;
+            const auto& pc = m_ports[port];
+            const auto& cell = pc.slots[row];
+
+            // 扁平复选框
+            CRect rChk = GetCheckboxRect(rc);
+            DrawFlatCheckbox(pDC, rChk, cell.checked, pc.allocated || !cell.hasGlass);
+
+            // 文本
+            CString s = GetDisplayId(port, row);
+            CRect rText = rc;
+            int leftPad = rChk.right + 6;
+            CRect rDot = GetStatusDotRect(rc);
+            CRect rTag = GetMaterialTagRect(rc);
+            int rightPad = rc.right - min(rTag.left - 6, rDot.left - 6);
+            rText.DeflateRect(leftPad - rc.left, 0, rightPad, 0);
+
+            CFont* pOld = pDC->SelectObject(&m_fntText);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SetTextColor(cell.hasGlass ? RGB(40, 40, 40) : RGB(150, 150, 150));
+            pDC->DrawText(s, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+            pDC->SelectObject(pOld);
+
+            // 物料标签(可隐藏)
+            if (m_bShowMatToggle)
+            {
+                CRect rT = rTag;
+                COLORREF crBorder = (cell.material == MAT_G2) ? RGB(180, 150, 220) : RGB(120, 160, 220);
+                COLORREF crFill = (cell.material == MAT_G2) ? RGB(243, 235, 250) : RGB(233, 240, 252);
+                COLORREF crText = (cell.material == MAT_G2) ? RGB(90, 60, 150) : RGB(50, 90, 160);
+                if (pc.allocated || !cell.hasGlass) { crBorder = RGB(210, 210, 210); crFill = RGB(245, 245, 245); crText = RGB(160, 160, 160); }
+                CBrush br(crFill); CPen tagPen(PS_SOLID, 1, crBorder);
+                CPen* pOldP = pDC->SelectObject(&tagPen);
+                CBrush* pOldB = pDC->SelectObject(&br);
+                pDC->RoundRect(rT, CPoint(6, 6));
+                pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
+                br.DeleteObject(); tagPen.DeleteObject();
+
+                CString t; t.Format(_T("G%d"), (cell.material == MAT_G2) ? 2 : 1);
+                CFont* pOldS = pDC->SelectObject(&m_fntSmall);
+                pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(crText);
+                pDC->DrawText(t, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
+                pDC->SelectObject(pOldS);
+            }
+
+            // 状态点
+            CRect rD = GetStatusDotRect(rc);
+            COLORREF dot = cell.hasGlass ? (pc.allocated ? RGB(215, 160, 60) : RGB(60, 170, 80)) : RGB(160, 160, 160);
+            CBrush bDot(dot); CBrush* pOldB = pDC->SelectObject(&bDot);
+            pDC->Ellipse(rD);
+            pDC->SelectObject(pOldB); bDot.DeleteObject();
+        }
+
+        *pResult = CDRF_SKIPDEFAULT; // 子项完全自绘
+        return;
+    }
+
+    default:
+        break;
+    }
+
+    *pResult = 0;
+}
+
+BOOL CCarrierSlotSelector::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
+{
+    NMHDR* pNM = reinterpret_cast<NMHDR*>(lParam);
+    if (pNM && pNM->hwndFrom == GetHeaderCtrl()->GetSafeHwnd())
+    {
+        switch (pNM->code)
+        {
+        case HDN_ITEMCLICKA:
+        case HDN_ITEMCLICKW:
+        {
+            HD_NOTIFY* phdn = reinterpret_cast<HD_NOTIFY*>(lParam);
+            OnHeaderClick(phdn->iItem);
+            if (pResult) *pResult = 0;
+            return TRUE; // 我们已处理
+        }
+        default: break;
+        }
+    }
+    return CListCtrl::OnNotify(wParam, lParam, pResult);
+}
+
+void CCarrierSlotSelector::OnHeaderClick(int iItem)
+{
+    // iItem: 0=Slot 列,>=1 为 Port 列
+    if (iItem <= 0) return;
+    int port = iItem - 1;
+    if (!SAFE_PORT(port) || m_ports[port].allocated) return;
+
+    // 计算是否“全选”
+    bool anyCheckable = false;
+    bool allChecked = true;
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        const auto& cell = m_ports[port].slots[r];
+        if (!cell.hasGlass) continue;
+        anyCheckable = true;
+        if (!cell.checked) { allChecked = false; break; }
+    }
+    if (!anyCheckable) return;
+
+    // 切换:若已全选 -> 取消全选;否则 -> 全选
+    CheckAllInPort(port, allChecked ? FALSE : TRUE, TRUE);
+}
+
+// ==== 通知 ====
+void CCarrierSlotSelector::NotifySelectionChanged(int /*port*/, int /*slot*/, BOOL /*checked*/)
+{
+    if (GetParent())
+    {
+        const int code = 0x2001; // CSSN_SELECTION_CHANGED
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+}
+
+void CCarrierSlotSelector::NotifyMaterialChanged(int /*port*/, int /*slot*/, int /*material*/)
+{
+    if (GetParent())
+    {
+        const int code = 0x2002; // CSSN_MATERIAL_CHANGED
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+}
diff --git a/SourceCode/Bond/Servo/CCarrierSlotSelector.h b/SourceCode/Bond/Servo/CCarrierSlotSelector.h
new file mode 100644
index 0000000..9ecbf97
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotSelector.h
@@ -0,0 +1,122 @@
+#include "stdafx.h"
+#pragma once
+
+#include <vector>
+#include <string>
+
+#ifndef _AFX
+#include <afxwin.h>
+#include <afxcmn.h>
+#endif
+
+class CCarrierSlotSelector : public CListCtrl
+{
+public:
+    enum MaterialType { MAT_G1 = 1, MAT_G2 = 2 };
+
+    struct SlotCell
+    {
+        bool    hasGlass = false;   // 是否有片
+        CString coreId;             // 固定核心ID(不含前缀)
+        int     material = MAT_G1;  // 仅影响展示(G1/G2)
+        bool    checked = false;   // 是否勾选加工
+    };
+
+    struct PortColumn
+    {
+        CString               portName;     // "Port 1" ...
+        CString               carrierName;  // "Carrier A" ...
+        bool                  allocated = false; // 整列锁定
+        CString               allocatedBy;       // 占用者
+        std::vector<SlotCell> slots;             // size = m_nSlots
+    };
+
+public:
+    CCarrierSlotSelector();
+    virtual ~CCarrierSlotSelector();
+
+    // 初始化 / 尺寸
+    void    InitGrid(int nPorts, int nSlots);
+    void    SetColumnWidths(int slotColWidth, int portColWidth);
+    void    SetRowHeight(int cy); // small image list 控制行高
+
+    // 外观
+    void    SetShowMaterialToggle(BOOL bShow);
+    BOOL    GetShowMaterialToggle() const { return m_bShowMatToggle; }
+
+    // Port 接口
+    int     GetPortCount() const { return (int)m_ports.size(); }
+    int     GetSlotCount() const { return m_nSlots; }
+    void    SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName);
+    void    SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName = nullptr);
+    BOOL    IsPortAllocated(int portIndex) const;
+
+    // Slot 接口
+    void    SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId /*可空*/, int material /*1=G1,2=G2*/);
+    void    SetSlotChecked(int portIndex, int slotIndex, BOOL checked);
+    BOOL    GetSlotChecked(int portIndex, int slotIndex) const;
+
+    int     GetSlotMaterialType(int portIndex, int slotIndex) const; // 1/2
+    void    SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify = TRUE);
+
+    CString GetDisplayId(int portIndex, int slotIndex) const; // "Gx-core" 或 "—"
+    void    RebuildTexts(); // 列头计数、单元格临时文本
+
+    // 工具:整列全选/全不选(只影响 hasGlass==true 的格子)
+    void    CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify = TRUE);
+
+protected:
+    // 内部数据
+    BOOL                    m_bFirstShown = FALSE; // 子页面首次显示后强制一次全量重绘
+    int                     m_nSlots = 0;
+    std::vector<PortColumn> m_ports;
+    BOOL                    m_bShowMatToggle = TRUE;
+
+    // UI metrics
+    int     m_rowHeight = 24;
+    int     m_slotColWidth = 100;
+    int     m_portColWidth = 180;
+
+    // 行高图像列表
+    CImageList m_ilRowHeight;
+
+    // 颜色
+    COLORREF m_colBgAlt = RGB(240, 242, 245); // 交替列浅灰
+    COLORREF m_colBgNorm = RGB(255, 255, 255);
+    COLORREF m_colLockBg = RGB(255, 244, 214); // 锁定列淡黄
+
+    // 字体
+    CFont   m_fntText;
+    CFont   m_fntBold;
+    CFont   m_fntSmall;
+
+    // 区域计算
+    CRect   GetCheckboxRect(const CRect& cell) const;
+    CRect   GetMaterialTagRect(const CRect& cell) const;
+    CRect   GetStatusDotRect(const CRect& cell) const;
+
+    // 工具
+    void    EnsureFonts();
+    void    UpdateRowCount();
+    void    DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled); // 扁平复选框
+
+    // 通知父窗口(WM_COMMAND 风格)
+    void    NotifySelectionChanged(int port, int slot, BOOL checked);
+    void    NotifyMaterialChanged(int port, int slot, int material);
+
+protected:
+    // MFC
+    virtual void PreSubclassWindow() override;
+    afx_msg void OnShowWindow(BOOL bShow, UINT nStatus);
+    afx_msg void OnWindowPosChanged(WINDOWPOS* lpwndpos);
+    afx_msg void OnSize(UINT nType, int cx, int cy);
+    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
+    afx_msg void OnPaint();                // ★ 新增:自带双缓冲绘制
+    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
+    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
+    afx_msg void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult);
+    afx_msg BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult); // 捕获 Header 通知
+    void    OnHeaderClick(int iItem);
+
+    DECLARE_MESSAGE_MAP()
+};
diff --git a/SourceCode/Bond/Servo/CCjPage2.cpp b/SourceCode/Bond/Servo/CCjPage2.cpp
index 0f4aac9..b736ba4 100644
--- a/SourceCode/Bond/Servo/CCjPage2.cpp
+++ b/SourceCode/Bond/Servo/CCjPage2.cpp
@@ -48,6 +48,41 @@
     CCjPageBase::OnInitDialog();
 
 
+    // 鑻ヤ綘鐨勮祫婧愰噷鏈変竴涓崰浣嶇殑 ListCtrl锛堟瘮濡� IDC_LIST1锛夛紝杩欓噷鍋氬瓙绫诲寲锛�
+    m_selector.SubclassDlgItem(IDC_LIST_SELECTOR, this);
+
+    // 鍒濆鍖栵細4 鍒� 脳 8 琛�
+    m_selector.InitGrid(4, 8);
+    m_selector.SetColumnWidths(100, 180);
+    m_selector.SetRowHeight(36);
+
+    // 璁剧疆鍒椾俊鎭�
+    m_selector.SetPortInfo(0, _T("Port 1"), _T("Carrier A"));
+    m_selector.SetPortInfo(1, _T("Port 2"), _T("Carrier B"));
+    m_selector.SetPortInfo(2, _T("Port 3"), _T("Carrier C"));
+    m_selector.SetPortInfo(3, _T("Port 4"), _T("Carrier D"));
+
+    // 璁剧疆閮ㄥ垎 Glass锛堟牳蹇僆D鍥哄畾锛�
+    m_selector.SetSlotGlass(0, 0, TRUE, _T("001"), CCarrierSlotSelector::MAT_G1);
+    m_selector.SetSlotGlass(0, 1, TRUE, _T("002"), CCarrierSlotSelector::MAT_G1);
+    m_selector.SetSlotGlass(0, 3, TRUE, _T("004"), CCarrierSlotSelector::MAT_G1);
+
+    m_selector.SetSlotGlass(1, 0, TRUE, _T("101"), CCarrierSlotSelector::MAT_G2);
+    m_selector.SetSlotGlass(1, 1, TRUE, _T("102"), CCarrierSlotSelector::MAT_G2);
+    m_selector.SetSlotGlass(1, 2, TRUE, _T("103"), CCarrierSlotSelector::MAT_G2);
+    m_selector.SetSlotGlass(1, 5, TRUE, _T("106"), CCarrierSlotSelector::MAT_G2);
+    m_selector.SetSlotGlass(1, 7, TRUE, _T("108"), CCarrierSlotSelector::MAT_G2);
+
+    m_selector.SetSlotGlass(2, 5, TRUE, _T("206"), CCarrierSlotSelector::MAT_G1);
+    m_selector.SetSlotGlass(2, 6, TRUE, _T("207"), CCarrierSlotSelector::MAT_G1);
+    m_selector.SetSlotGlass(2, 7, TRUE, _T("208"), CCarrierSlotSelector::MAT_G1);
+
+    m_selector.SetSlotGlass(3, 0, TRUE, _T("301"), CCarrierSlotSelector::MAT_G1);
+
+    // 閿佸畾 Port 1锛堢ず渚嬶級
+    m_selector.SetPortAllocated(0, TRUE, _T("ProcessJob 1"));
+
+
     UpdatePjData();
  
         
@@ -87,6 +122,25 @@
     GetDlgItemText(IDC_EDIT_PJ_ID, szBuffer, 256);
     pProcessJob->setId(std::string(szBuffer));
 
+    // 鏇存柊閰嶆柟
+    CString strRecipe;
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_RECIPE);
+    int idx = pComboBox->GetCurSel();
+    if (idx >= 0) {
+        pComboBox->GetLBText(idx, strRecipe);
+#ifdef UNICODE
+        CT2A utf8Str(strRecipe, CP_UTF8);
+        std::string recipe(utf8Str);
+#else
+        std::string recipe(strRecipe.GetString());
+#endif
+
+        pProcessJob->setRecipe(SERVO::RecipeMethod::NoTuning, recipe);
+    }
+
+
+
+
     ContentChanged(1);
 }
 
diff --git a/SourceCode/Bond/Servo/CCjPage2.h b/SourceCode/Bond/Servo/CCjPage2.h
index e277ac4..5a7338b 100644
--- a/SourceCode/Bond/Servo/CCjPage2.h
+++ b/SourceCode/Bond/Servo/CCjPage2.h
@@ -1,6 +1,7 @@
 锘�#pragma once
 #include "CCjPageBase.h"
 #include "ProcessJob.h"
+#include "CCarrierSlotSelector.h"
 
 
 // CPjPage1 瀵硅瘽妗�
@@ -21,6 +22,9 @@
 private:
 	void UpdatePjData();
 
+private:
+	CCarrierSlotSelector m_selector;
+
 // 瀵硅瘽妗嗘暟鎹�
 #ifdef AFX_DESIGN_TIME
 	enum { IDD = IDD_CJ_PAGE1 };
diff --git a/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp b/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
index e125c68..6783528 100644
--- a/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
+++ b/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
@@ -357,6 +357,7 @@
 
 void CControlJobManagerDlg::UpProcessJobId(SERVO::CProcessJob* pJob)
 {
+	// 鏇存柊listbox
 	int count = m_listBox.GetCount();
 	for (int idx = 0; idx < count; idx++) {
 		if ((m_listBox.GetItemDataPtr(idx) == (void*)pJob)) {
@@ -371,4 +372,22 @@
 			}
 		}
 	}
+
+	// 鏇存柊鏍戞帶浠�
+	// 閬嶅巻鏍硅妭鐐�
+	HTREEITEM hRoot = m_tree.GetRootItem();
+	while (hRoot) {
+		// 閬嶅巻绗簩灞傚瓙鑺傜偣
+		HTREEITEM hChild = m_tree.GetChildItem(hRoot);
+		while (hChild) {
+			DWORD_PTR data = m_tree.GetItemData(hChild);
+			if ((void*)data == pJob) {
+				m_tree.SetItemText(hChild, pJob->id().c_str());
+				return; // 鎵惧埌灏辫繑鍥�
+			}
+			hChild = m_tree.GetNextSiblingItem(hChild);
+		}
+
+		hRoot = m_tree.GetNextSiblingItem(hRoot);
+	}
 }
diff --git a/SourceCode/Bond/Servo/Servo.rc b/SourceCode/Bond/Servo/Servo.rc
index fe77aef..4bacf31 100644
--- a/SourceCode/Bond/Servo/Servo.rc
+++ b/SourceCode/Bond/Servo/Servo.rc
Binary files differ
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj b/SourceCode/Bond/Servo/Servo.vcxproj
index 1b2bcc9..a80c7ab 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj
+++ b/SourceCode/Bond/Servo/Servo.vcxproj
@@ -210,6 +210,7 @@
     <ClInclude Include="..\jsoncpp\include\json\writer.h" />
     <ClInclude Include="..\jsoncpp\lib_json\json_batchallocator.h" />
     <ClInclude Include="CBaseDlg.h" />
+    <ClInclude Include="CCarrierSlotSelector.h" />
     <ClInclude Include="CCjPage2.h" />
     <ClInclude Include="CCjPage3.h" />
     <ClInclude Include="CCjPageBase.h" />
@@ -392,6 +393,7 @@
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
     </ClCompile>
     <ClCompile Include="CBaseDlg.cpp" />
+    <ClCompile Include="CCarrierSlotSelector.cpp" />
     <ClCompile Include="CCjPage2.cpp" />
     <ClCompile Include="CCjPage3.cpp" />
     <ClCompile Include="CCjPageBase.cpp" />
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj.filters b/SourceCode/Bond/Servo/Servo.vcxproj.filters
index 9202388..fb2a15e 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj.filters
+++ b/SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -204,6 +204,7 @@
     <ClCompile Include="CCjPage2.cpp" />
     <ClCompile Include="CCjPage3.cpp" />
     <ClCompile Include="CCjPageBase.cpp" />
+    <ClCompile Include="CCarrierSlotSelector.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="AlarmManager.h" />
@@ -433,6 +434,7 @@
     <ClInclude Include="CCjPage2.h" />
     <ClInclude Include="CCjPage3.h" />
     <ClInclude Include="CCjPageBase.h" />
+    <ClInclude Include="CCarrierSlotSelector.h" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="Servo.rc" />
diff --git a/SourceCode/Bond/Servo/resource.h b/SourceCode/Bond/Servo/resource.h
index 0da8d69..5a9214e 100644
--- a/SourceCode/Bond/Servo/resource.h
+++ b/SourceCode/Bond/Servo/resource.h
Binary files differ

--
Gitblit v1.9.3