From e8a27bb203fe2aff70390a5eca002d7438da9b0f Mon Sep 17 00:00:00 2001
From: mrDarker <mr.darker@163.com>
Date: 星期三, 22 十月 2025 14:24:34 +0800
Subject: [PATCH] Merge branch 'clh' into liuyang

---
 SourceCode/Bond/Servo/CCarrierSlotGrid.cpp | 1083 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 1,083 insertions(+), 0 deletions(-)

diff --git a/SourceCode/Bond/Servo/CCarrierSlotGrid.cpp b/SourceCode/Bond/Servo/CCarrierSlotGrid.cpp
new file mode 100644
index 0000000..c44a1b5
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotGrid.cpp
@@ -0,0 +1,1083 @@
+锘�#include "stdafx.h"
+#include "CCarrierSlotGrid.h"
+#include <gdiplus.h>
+#pragma comment(lib, "gdiplus.lib")
+
+using namespace Gdiplus;
+
+#define SAFE_PORT(p) ((p) >= 0 && (p) < (int)m_ports.size())
+#define SAFE_SLOT(s) ((s) >= 0 && (s) < m_nSlots)
+
+static bool       s_gdiplusInited = false;
+static ULONG_PTR  s_gdiplusToken = 0;
+static void EnsureGdiplus()
+{
+    if (!s_gdiplusInited)
+    {
+        GdiplusStartupInput in;
+        if (GdiplusStartup(&s_gdiplusToken, &in, nullptr) == Ok)
+            s_gdiplusInited = true;
+    }
+}
+
+BEGIN_MESSAGE_MAP(CCarrierSlotGrid, CWnd)
+    ON_WM_ERASEBKGND()
+    ON_WM_PAINT()
+    ON_WM_CREATE()
+    ON_WM_SIZE()
+    ON_WM_HSCROLL()
+    ON_WM_VSCROLL()          // 鈽� 鏂板
+    ON_WM_LBUTTONDOWN()
+    ON_WM_LBUTTONUP()
+    ON_WM_MOUSEWHEEL()
+    ON_WM_MOUSEMOVE()
+    ON_WM_SETCURSOR()
+    ON_WM_SHOWWINDOW()
+    ON_WM_WINDOWPOSCHANGED()
+END_MESSAGE_MAP()
+
+CCarrierSlotGrid::CCarrierSlotGrid() {}
+CCarrierSlotGrid::~CCarrierSlotGrid()
+{
+    if ((HFONT)m_fntText)  m_fntText.DeleteObject();
+    if ((HFONT)m_fntBold)  m_fntBold.DeleteObject();
+    if ((HFONT)m_fntSmall) m_fntSmall.DeleteObject();
+}
+
+void CCarrierSlotGrid::PreSubclassWindow()
+{
+    CWnd::PreSubclassWindow();
+    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
+    EnsureFonts();
+    EnsureGdiplus();
+
+    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+}
+
+int CCarrierSlotGrid::OnCreate(LPCREATESTRUCT cs)
+{
+    if (CWnd::OnCreate(cs) == -1) return -1;
+    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
+    EnsureFonts();
+    EnsureGdiplus();
+
+    return 0;
+}
+
+void CCarrierSlotGrid::EnsureFonts()
+{
+    if ((HFONT)m_fntText) return;
+
+    LOGFONT lf{}; bool ok = false;
+    if (GetParent() && GetParent()->GetFont()) { GetParent()->GetFont()->GetLogFont(&lf); ok = true; }
+    else if (GetFont()) { GetFont()->GetLogFont(&lf); ok = true; }
+    else {
+        NONCLIENTMETRICS ncm = { 0 }; ncm.cbSize = sizeof(NONCLIENTMETRICS);
+        if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncm, 0)) { lf = ncm.lfMessageFont; ok = true; }
+    }
+    if (!ok) { CFont* pDef = CFont::FromHandle((HFONT)GetStockObject(DEFAULT_GUI_FONT)); pDef->GetLogFont(&lf); }
+
+    m_fntText.CreateFontIndirect(&lf);
+    LOGFONT lfb = lf; lfb.lfWeight = FW_SEMIBOLD;  m_fntBold.CreateFontIndirect(&lfb);
+    LOGFONT lfs = lf; lfs.lfHeight = (LONG)(lf.lfHeight * 0.9); if (lfs.lfHeight == 0) lfs.lfHeight = lf.lfHeight - 1; m_fntSmall.CreateFontIndirect(&lfs);
+}
+
+void CCarrierSlotGrid::InitGrid(int nPorts, int nSlots)
+{
+    ASSERT(nPorts >= 1 && nSlots >= 1);
+    m_ports.assign(nPorts, PortColumn{});
+    m_portColCXs.assign(nPorts, 180);
+    m_nSlots = nSlots;
+    for (auto& pc : m_ports) pc.slots.assign(m_nSlots, SlotCell{});
+    UpdateScrollRange();
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetColumnWidths(int slotColWidth, int portColWidth)
+{
+    if (slotColWidth > 0) m_slotColCX = slotColWidth;
+    if (portColWidth > 0)
+        for (size_t i = 0; i < m_portColCXs.size(); ++i) m_portColCXs[i] = portColWidth;
+    UpdateScrollRange();
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetRowHeight(int cy) { m_rowHeight = max(18, min(cy, 64)); UpdateScrollRange(); Invalidate(FALSE); }
+void CCarrierSlotGrid::SetHeaderHeight(int cy) { m_headerCY = max(20, min(cy, 48)); UpdateScrollRange(); Invalidate(FALSE); }
+void CCarrierSlotGrid::SetShowMaterialToggle(BOOL bShow) { m_bShowMatToggle = bShow; Invalidate(FALSE); }
+
+void CCarrierSlotGrid::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;
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    auto& pc = m_ports[portIndex];
+    pc.allocated = !!allocated;
+    pc.allocatedBy = byName ? byName : _T("");
+    if (pc.allocated)
+        for (auto& cell : pc.slots) cell.checked = false;
+    Invalidate(FALSE);
+}
+
+BOOL CCarrierSlotGrid::IsPortAllocated(int portIndex) const
+{
+    if (!SAFE_PORT(portIndex)) return FALSE; return m_ports[portIndex].allocated;
+}
+
+void CCarrierSlotGrid::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;
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetSlotChecked(int portIndex, int slotIndex, BOOL checked, BOOL bNotify/* = FALSE*/)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& pc = m_ports[portIndex];
+    if (pc.allocated) return;
+    auto& cell = pc.slots[slotIndex];
+    if (!cell.hasGlass) return;
+    cell.checked = !!checked;
+    if(bNotify) NotifySelectionChanged(portIndex, slotIndex, cell.checked);
+    Invalidate(FALSE);
+}
+
+BOOL CCarrierSlotGrid::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 CCarrierSlotGrid::GetSlotMaterialType(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return MAT_G1;
+    return m_ports[portIndex].slots[slotIndex].material;
+}
+
+void CCarrierSlotGrid::SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& pc = m_ports[portIndex];
+    if (pc.allocated) return;
+    auto& cell = pc.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);
+        Invalidate(FALSE);
+    }
+}
+
+CString CCarrierSlotGrid::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 CCarrierSlotGrid::CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    auto& pc = m_ports[portIndex];
+    if (pc.allocated) return;
+    for (int r = 0; r < m_nSlots; ++r) {
+        auto& cell = pc.slots[r];
+        if (!cell.hasGlass) continue;
+        if (cell.checked != !!checked) {
+            cell.checked = !!checked;
+            if (bNotify) NotifySelectionChanged(portIndex, r, cell.checked);
+        }
+    }
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::RebuildTexts() { Invalidate(FALSE); }
+
+CSize CCarrierSlotGrid::CalcBestClientSize(int nSlotsOverride) const
+{
+    const int slots = (nSlotsOverride > 0) ? nSlotsOverride : m_nSlots;
+    int w = m_slotColCX;
+    for (int cx : m_portColCXs) w += cx;
+    int h = m_headerCY + slots * m_rowHeight;
+    return CSize(w, h);
+}
+
+CSize CCarrierSlotGrid::CalcBestWindowSize(BOOL includeNonClient,
+    int nSlotsOverride,
+    int extraPadX,
+    int extraPadY) const
+{
+    // 1) 鍩虹瀹㈡埛鍖哄昂瀵革紙鍚垜浠湪瀹㈡埛鍖虹敾鐨� 1px 杈规锛氬乏鍙�+2/涓婁笅+2锛�
+    const CSize content = CalcBestClientSize(nSlotsOverride);
+
+    // 2) 鍙� DPI銆佹粴鍔ㄦ潯灏哄锛堝敖閲忕敤 ForDpi锛屽洖閫�鍒版櫘閫氾級
+    UINT dpi = 96;
+#if (_WIN32_WINNT >= 0x0603)
+    if (m_hWnd) {
+        HMODULE hUser32 = ::GetModuleHandleW(L"user32.dll");
+        if (hUser32) {
+            typedef UINT(WINAPI* PFN_GETDPIFORWINDOW)(HWND);
+            auto pGetDpiForWindow = (PFN_GETDPIFORWINDOW)::GetProcAddress(hUser32, "GetDpiForWindow");
+            if (pGetDpiForWindow) dpi = pGetDpiForWindow(m_hWnd);
+        }
+    }
+#endif
+    int cxVScroll = ::GetSystemMetrics(SM_CXVSCROLL);
+    int cyHScroll = ::GetSystemMetrics(SM_CYHSCROLL);
+#if (_WIN32_WINNT >= 0x0A00) // Win10: 鍙敤 GetSystemMetricsForDpi
+    HMODULE hUser32_2 = ::GetModuleHandleW(L"user32.dll");
+    if (hUser32_2) {
+        typedef int (WINAPI* PFN_GSMFD)(int, UINT);
+        auto pGsmForDpi = (PFN_GSMFD)::GetProcAddress(hUser32_2, "GetSystemMetricsForDpi");
+        if (pGsmForDpi) {
+            cxVScroll = pGsmForDpi(SM_CXVSCROLL, dpi);
+            cyHScroll = pGsmForDpi(SM_CYHSCROLL, dpi);
+        }
+    }
+#endif
+
+    // 3) DPI 鑷�傚簲瀹夊叏浣欓噺锛堥伩鍏嶅彇鏁磋宸�/涓婚宸紓锛�
+    const int autoPad = max(1, MulDiv(2, (int)dpi, 96)); // 绾︾瓑浜� 2px@96DPI
+    const int padX = (extraPadX >= 0) ? extraPadX : autoPad;
+    const int padY = (extraPadY >= 0) ? extraPadY : autoPad;
+
+    // 4) 杩唬锛氳�冭檻婊氬姩鏉$浉浜掑奖鍝嶏紝鐩村埌绋冲畾涓嶉渶瑕佹粴鍔ㄦ潯
+    int needCx = content.cx + padX;
+    int needCy = content.cy + padY;
+
+    while (true) {
+        bool needV = (GetTotalContentWidth() > needCx);                      // 瀹戒笉澶熲啋浼氬嚭鐜版í鍚戞粴鍔ㄦ潯锛燂紙娉ㄦ剰锛氭í鏉″崰楂樺害锛�
+        bool needH = (m_headerCY + (nSlotsOverride > 0 ? nSlotsOverride : m_nSlots) * m_rowHeight + 2 /*瀹㈡埛鍖鸿竟妗�*/ > needCy); // 楂樹笉澶熲啋浼氬嚭鐜扮旱鏉★紵锛堢旱鏉″崰瀹藉害锛�
+
+        // 娉ㄦ剰锛氬嚭鐜扳�滅旱鍚戞潯鈥濅細鍑忓皯鍙敤瀹藉害锛涘嚭鐜扳�滄í鍚戞潯鈥濅細鍑忓皯鍙敤楂樺害
+        // 鎴戜滑鐩爣鏄鈥滃嵆浣挎墸鎺夎繖浜涘崰浣嶁�濆悗涔熶粛鐒� >= 鍐呭灏哄
+        int adjCx = content.cx + padX + (needH ? cxVScroll : 0);
+        int adjCy = content.cy + padY + (needV ? cyHScroll : 0);
+
+        if (adjCx <= needCx && adjCy <= needCy) break; // 绋冲畾锛氬綋鍓� needCx/needCy 瓒冲
+        needCx = max(needCx, adjCx);
+        needCy = max(needCy, adjCy);
+    }
+
+    if (!includeNonClient) return CSize(needCx, needCy);
+
+    // 5) 鎶娾�滅悊鎯冲鎴峰尯灏哄鈥濇崲绠楁垚绐楀彛澶栨灏哄锛堝幓鎺� WS_H/VSCROLL 鍋氭崲绠楋級
+    RECT rc = { 0, 0, needCx, needCy };
+    DWORD style = GetStyle();          // 鉁� 鐢ㄧ湡瀹炴牱寮忥紝璁╃郴缁熸妸婊氬姩鏉¢潪瀹㈡埛鍖轰竴骞剁畻杩涘幓
+    DWORD exStyle = GetExStyle();
+    ::AdjustWindowRectEx(&rc, style, FALSE, exStyle);
+    return CSize(rc.right - rc.left, rc.bottom - rc.top);
+}
+
+void CCarrierSlotGrid::DisableSystemScrollbars()
+{
+    // 鍘绘帀鏍峰紡
+    ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
+    // 闅愯棌锛堝吋瀹规�э級
+    ShowScrollBar(SB_HORZ, FALSE);
+    ShowScrollBar(SB_VERT, FALSE);
+    // 娓呯┖婊氬姩淇℃伅
+    SCROLLINFO si{ sizeof(SCROLLINFO) }; si.fMask = SIF_ALL; si.nMin = 0; si.nMax = 0; si.nPage = 0; si.nPos = 0;
+    SetScrollInfo(SB_HORZ, &si, TRUE);
+    SetScrollInfo(SB_VERT, &si, TRUE);
+    // 璁╅潪瀹㈡埛鍖洪噸绠�
+    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+    Invalidate(FALSE);
+}
+
+// 娉ㄦ剰锛氳繖閲岀敤鈥滄棤婊氬姩鏉℃牱寮忊�濇潵鎹㈢畻绐楀彛澶栨灏哄锛岀‘淇濆鎴峰尯=鍐呭灏哄 + 鎴戜滑瀹㈡埛鍖鸿竟妗�
+void CCarrierSlotGrid::ResizeWindowToFitAll(BOOL includeNonClient, int nSlotsOverride)
+{
+    // 璁$畻鍐呭鎵�闇�瀹㈡埛鍖猴紙CalcBestClientSize 鍐呭凡鍖呭惈鎴戜滑瀹㈡埛鍖�1px杈规鐨� +2锛�
+    CSize need = CalcBestClientSize(nSlotsOverride);
+
+    if (!includeNonClient) {
+        SetWindowPos(nullptr, 0, 0, need.cx, need.cy,
+            SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+        return;
+    }
+
+    RECT rc = { 0, 0, need.cx, need.cy };
+    DWORD style = GetStyle() & ~(WS_HSCROLL | WS_VSCROLL); // 鈫� 鐢ㄢ�滄棤婊氬姩鏉♀�濈殑鏍峰紡鏉ユ崲绠�
+    DWORD exStyle = GetExStyle();
+    ::AdjustWindowRectEx(&rc, style, FALSE, exStyle);
+
+    int w = rc.right - rc.left;
+    int h = rc.bottom - rc.top;
+
+    SetWindowPos(nullptr, 0, 0, w, h,
+        SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+}
+
+void CCarrierSlotGrid::SetNoScrollbarsMode(BOOL enable)
+{
+    m_noScrollbars = !!enable;
+
+    if (m_noScrollbars) {
+        // 1) 鍋忕Щ娓呴浂
+        m_scrollX = 0;
+        m_scrollY = 0;
+
+        // 2) 鍘绘帀鏍峰紡骞堕殣钘忔潯
+        ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
+        ShowScrollBar(SB_BOTH, FALSE);
+
+        // 3) 娓呯┖婊氬姩淇℃伅锛堝嵆渚胯祫婧愰噷鍘熸湰甯︿簡鏍峰紡锛屼篃涓嶅啀褰卞搷锛�
+        SCROLLINFO si{ sizeof(SCROLLINFO) }; si.fMask = SIF_ALL;
+        SetScrollInfo(SB_HORZ, &si, TRUE);
+        SetScrollInfo(SB_VERT, &si, TRUE);
+
+        // 4) 閫氱煡绯荤粺闈炲鎴峰尯鍒锋柊锛岀‘淇濇潯琚交搴曠Щ闄�
+        SetWindowPos(nullptr, 0, 0, 0, 0,
+            SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+
+        Invalidate(FALSE);
+    }
+    else {
+        // 閫�鍑烘棤婊氬姩鏉℃ā寮忥細浠呮仮澶嶆牱寮忥紝瀹為檯鑼冨洿浼氬湪 UpdateScrollRange 涓噸鏂拌缃�
+        ModifyStyle(0, WS_HSCROLL | WS_VSCROLL, 0);
+        SetWindowPos(nullptr, 0, 0, 0, 0,
+            SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+        UpdateScrollRange();
+        Invalidate(FALSE);
+    }
+}
+
+void CCarrierSlotGrid::FitWindowToContentNoScroll(BOOL includeNonClient, int nSlotsOverride)
+{
+    // 纭繚宸插浜庘�滄棤婊氬姩鏉℃ā寮忊�濓紝闃叉绯荤粺鍦� AdjustWindowRectEx 鏃堕鐣欐粴鍔ㄦ潯闈炲鎴峰尯
+    SetNoScrollbarsMode(TRUE);
+
+    // 浣犺嚜宸辩殑 CalcBestClientSize 宸插寘鍚鎴峰尯 1px 杈规(+2)鐨勪慨姝�
+    CSize needCli = CalcBestClientSize(nSlotsOverride);
+
+    if (!includeNonClient) {
+        SetWindowPos(nullptr, 0, 0, needCli.cx, needCli.cy,
+            SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+        return;
+    }
+
+    RECT rc{ 0, 0, needCli.cx, needCli.cy };
+    // 娉ㄦ剰锛氭鏃剁獥鍙f牱寮忓凡缁忔病鏈� WS_H/VSCROLL 浜嗏�斺�旂敤鐪熷疄鏍峰紡鎹㈢畻鍗冲彲
+    ::AdjustWindowRectEx(&rc, GetStyle(), FALSE, GetExStyle());
+    const int w = rc.right - rc.left;
+    const int h = rc.bottom - rc.top;
+
+    SetWindowPos(nullptr, 0, 0, w, h,
+        SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+}
+
+// ---------- 鍑犱綍 ----------
+CRect CCarrierSlotGrid::GetClientRectNoSB() const
+{
+    CRect rc; GetClientRect(&rc); return rc;
+}
+CRect CCarrierSlotGrid::GetHeaderRect() const
+{
+    CRect rc = GetClientRectNoSB(); rc.bottom = rc.top + m_headerCY; return rc;
+}
+int CCarrierSlotGrid::GetTotalContentWidth() const
+{
+    int w = m_slotColCX;
+    for (int cx : m_portColCXs) w += cx;
+    return w;
+}
+CRect CCarrierSlotGrid::GetHeaderItemRect(int iItem) const
+{
+    CRect rcHeader = GetHeaderRect();
+    int x = rcHeader.left - m_scrollX;
+    if (iItem == 0)
+        return CRect(x, rcHeader.top, x + m_slotColCX, rcHeader.bottom);
+
+    x += m_slotColCX;
+    for (int c = 1; c < iItem; ++c) x += m_portColCXs[c - 1];
+    int w = m_portColCXs[iItem - 1];
+    return CRect(x, rcHeader.top, x + w, rcHeader.bottom);
+}
+BOOL CCarrierSlotGrid::GetCellRect(int row, int sub, CRect& rc) const
+{
+    CRect cli = GetClientRectNoSB();
+    int y0 = cli.top + m_headerCY - m_scrollY;
+    int top = y0 + row * m_rowHeight;
+    int bottom = top + m_rowHeight;
+    if (bottom <= cli.top + m_headerCY || top >= cli.bottom) return FALSE;
+
+    int x = cli.left - m_scrollX;
+    if (sub == 0) { rc = CRect(x, top, x + m_slotColCX, bottom); return TRUE; }
+    x += m_slotColCX;
+    for (int c = 1; c < sub; ++c) x += m_portColCXs[c - 1];
+    int w = m_portColCXs[sub - 1];
+    rc = CRect(x, top, x + w, bottom);
+    return TRUE;
+}
+CRect CCarrierSlotGrid::GetHeaderCheckboxRect(int iItem) const
+{
+    CRect rItem = GetHeaderItemRect(iItem);
+    const int box = 16;
+    const int padR = 6;
+    int vpad = max(0, (rItem.Height() - box) / 2);
+    return CRect(rItem.right - padR - box, rItem.top + vpad, rItem.right - padR, rItem.bottom - vpad);
+}
+CRect CCarrierSlotGrid::GetCheckboxRect(const CRect& cell) const
+{
+    int sz = max(14, min(int(m_rowHeight * 0.70), 20));
+    int leftPad = 8;
+    int top = cell.top + (cell.Height() - sz) / 2;
+    return CRect(cell.left + leftPad, top, cell.left + leftPad + sz, top + sz);
+}
+CRect CCarrierSlotGrid::GetMaterialTagRect(const CRect& cell) const
+{
+    int h = max(14, min(int(m_rowHeight * 0.65), m_rowHeight - 8));
+    int w = 32;
+    int gap = 6;
+    int rightPadForDot = 16;
+    int top = cell.top + (cell.Height() - h) / 2;
+    int right = cell.right - rightPadForDot - gap;
+    return CRect(right - w, top, right, top + h);
+}
+CRect CCarrierSlotGrid::GetStatusDotRect(const CRect& cell) const
+{
+    int d = max(8, min(int(m_rowHeight * 0.42), 12));
+    int rightPad = 6;
+    int top = cell.top + (cell.Height() - d) / 2;
+    return CRect(cell.right - rightPad - d, top, cell.right - rightPad, top + d);
+}
+
+void CCarrierSlotGrid::UpdateScrollRange()
+{
+    if (m_noScrollbars) {
+        // 纭繚鍋忕Щ涓�鐩翠负 0锛屼笉璁句换浣曟粴鍔ㄤ俊鎭�
+        m_scrollX = 0;
+        m_scrollY = 0;
+        return;
+    }
+
+    CRect rc; GetClientRect(&rc);
+
+    // 鍨傜洿
+    const int contentH = m_headerCY + m_nSlots * m_rowHeight;
+    const int pageY = max(1, rc.Height());
+    const int maxPosY = max(0, contentH - pageY);
+    m_scrollY = max(0, min(m_scrollY, maxPosY));
+    SCROLLINFO siY = { sizeof(SCROLLINFO) };
+    siY.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
+    siY.nMin = 0; siY.nMax = contentH - 1; siY.nPage = pageY; siY.nPos = m_scrollY;
+    SetScrollInfo(SB_VERT, &siY, TRUE);
+
+    // 姘村钩
+    const int contentW = GetTotalContentWidth();
+    const int pageX = max(1, rc.Width());
+    const int maxPosX = max(0, contentW - pageX);
+    m_scrollX = max(0, min(m_scrollX, maxPosX));
+    SCROLLINFO siX = { sizeof(SCROLLINFO) };
+    siX.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
+    siX.nMin = 0; siX.nMax = contentW - 1; siX.nPage = pageX; siX.nPos = m_scrollX;
+    SetScrollInfo(SB_HORZ, &siX, TRUE);
+}
+
+// ---------- 琛ㄥご鍒嗛殧绾垮懡涓� ----------
+int CCarrierSlotGrid::HitHeaderEdge(CPoint pt) const
+{
+    if (!m_bAllowResize) return -1;  // 鈫� 鏂板
+    if (!GetHeaderRect().PtInRect(pt)) return -1;
+    const int tol = 4;
+    int x = GetHeaderRect().left - m_scrollX + m_slotColCX;
+    if (abs(pt.x - x) <= tol) return 0;
+
+    int cum = GetHeaderRect().left - m_scrollX + m_slotColCX;
+    for (int i = 0; i <= GetPortCount() - 2; ++i) {
+        cum += m_portColCXs[i];
+        if (abs(pt.x - cum) <= tol) return i + 1;
+    }
+    return -1;
+}
+
+// ---------- 缁樺埗 ----------
+BOOL CCarrierSlotGrid::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; }
+
+void CCarrierSlotGrid::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);
+
+    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);
+}
+
+void CCarrierSlotGrid::PaintTo(CDC* pDC)
+{
+    EnsureGdiplus();
+
+    CRect cli = GetClientRectNoSB();
+    pDC->FillSolidRect(cli, m_colBg);
+
+    // Header
+    CRect rh = GetHeaderRect();
+    pDC->FillSolidRect(rh, ::GetSysColor(COLOR_BTNFACE));
+    CPen penSep(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
+    CPen* pOldPen = pDC->SelectObject(&penSep);
+    pDC->MoveTo(rh.left, rh.bottom - 1); pDC->LineTo(rh.right, rh.bottom - 1);
+    pDC->SelectObject(pOldPen);
+
+    for (int i = 0; i <= GetPortCount(); ++i)
+    {
+        CRect rItem = GetHeaderItemRect(i);
+
+        // 淇敼涓猴細
+        if (i < GetPortCount()) { // 鈽� 鏈�鍚庝竴鍒椾笉鐢诲垎闅旂嚎
+            CPen pen(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
+            pOldPen = pDC->SelectObject(&pen);
+            pDC->MoveTo(rItem.right - 1, rItem.top);
+            pDC->LineTo(rItem.right - 1, rItem.bottom);
+            pDC->SelectObject(pOldPen);
+        }
+
+        CString text;
+        if (i == 0) {
+            text = _T("Slot");
+            CRect rt = rItem; rt.DeflateRect(6, 0, 6, 0);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SelectObject(&m_fntBold);
+            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
+            pDC->DrawText(text, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+        }
+        else {
+            const auto& pc = m_ports[i - 1];
+            int selected = 0; bool any = false, all = true;
+            for (const auto& cell : pc.slots) {
+                if (cell.hasGlass) { any = true; if (cell.checked) ++selected; else all = false; }
+            }
+            if (!any) all = false;
+
+            CString leftTitle = pc.carrierName.IsEmpty()
+                ? pc.portName
+                : (pc.portName + _T(" (") + pc.carrierName + _T(")"));
+
+            // 鍕鹃�夋闈犲彸
+            CRect rcCb = GetHeaderCheckboxRect(i);
+            DrawFlatCheckbox(pDC, rcCb, all, pc.allocated);
+
+            // 璁℃暟璐磋繎鍕鹃�夋宸︿晶
+            CString cnt; cnt.Format(_T("%d/%d"), selected, m_nSlots);
+            SIZE szCnt{ 0,0 };
+            { CFont* o = pDC->SelectObject(&m_fntBold);
+            GetTextExtentPoint32(pDC->GetSafeHdc(), cnt, cnt.GetLength(), &szCnt);
+            pDC->SelectObject(o); }
+            const int gap = 6;
+            CRect rcCnt(rcCb.left - gap - szCnt.cx, rItem.top, rcCb.left - gap, rItem.bottom);
+
+            // 宸︿晶鏍囬
+            CRect rt = rItem; rt.DeflateRect(6, 0, (rItem.right - rcCnt.left) + 6, 0);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SelectObject(&m_fntBold);
+            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
+            pDC->DrawText(leftTitle, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+
+            // 璁℃暟
+            pDC->SelectObject(&m_fntBold);
+            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
+            pDC->DrawText(cnt, rcCnt, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
+        }
+    }
+
+    // Cells
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        for (int s = 0; s <= GetPortCount(); ++s)
+        {
+            CRect rc;
+            if (!GetCellRect(r, s, rc)) continue;
+
+            COLORREF bk = m_colBg;
+            if (s >= 1) {
+                int port = s - 1;
+                if (port % 2 == 0) bk = m_colAlt;
+                if (SAFE_PORT(port) && m_ports[port].allocated) bk = m_colLock;
+            }
+            pDC->FillSolidRect(rc, bk);
+
+            CPen penMajor(PS_SOLID, (s >= 1 && ((s - 1) % 2 == 0)) ? 2 : 1, m_gridMajor);
+            CPen* pOld = pDC->SelectObject(&penMajor);
+            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(pOld);
+
+            CPen penMinor(PS_SOLID, 1, m_gridMinor);
+            pOld = pDC->SelectObject(&penMinor);
+            if (s == GetPortCount()) { pDC->MoveTo(rc.right - 1, rc.top); pDC->LineTo(rc.right - 1, rc.bottom); }
+            if (r == m_nSlots - 1) { pDC->MoveTo(rc.left, rc.bottom - 1); pDC->LineTo(rc.right, rc.bottom - 1); }
+            pDC->SelectObject(pOld);
+
+            if (s == 0) {
+                CString sl; sl.Format(_T("Slot %d"), r + 1);
+                CRect rt = rc; rt.DeflateRect(8, 0, 8, 0);
+                pDC->SelectObject(&m_fntBold);
+                pDC->SetBkMode(TRANSPARENT);
+                pDC->SetTextColor(RGB(60, 60, 64));
+                pDC->DrawText(sl, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
+            }
+            else {
+                int port = s - 1;
+                const auto& pc = m_ports[port];
+                const auto& cell = pc.slots[r];
+
+                CRect rChk = GetCheckboxRect(rc);
+                DrawFlatCheckbox(pDC, rChk, cell.checked, pc.allocated || !cell.hasGlass);
+
+                CString t = GetDisplayId(port, r);
+                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);
+
+                pDC->SelectObject(&m_fntText);
+                pDC->SetBkMode(TRANSPARENT);
+                pDC->SetTextColor(cell.hasGlass ? m_text : m_textDim);
+                pDC->DrawText(t, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+
+                if (m_bShowMatToggle)
+                {
+                    CRect rT = GetMaterialTagRect(rc);
+                    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);
+                    CString tx; tx.Format(_T("G%d"), (cell.material == MAT_G2) ? 2 : 1);
+                    pDC->SelectObject(&m_fntSmall);
+                    pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(crText);
+                    pDC->DrawText(tx, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
+                }
+
+                // 鐘舵�佺偣锛圙DI+ 鎶楅敮榻匡級
+                {
+                    Graphics g(pDC->GetSafeHdc());
+                    g.SetSmoothingMode(SmoothingModeAntiAlias);
+                    COLORREF c = cell.hasGlass ? (pc.allocated ? RGB(215, 160, 60) : RGB(60, 170, 80)) : RGB(160, 160, 160);
+                    SolidBrush brush(Color(255, GetRValue(c), GetGValue(c), GetBValue(c)));
+                    Pen outline(Color(255, 120, 120, 120), 1.f);
+                    g.FillEllipse(&brush, rDot.left, rDot.top, rDot.Width(), rDot.Height());
+                    g.DrawEllipse(&outline, (REAL)rDot.left, (REAL)rDot.top, (REAL)rDot.Width(), (REAL)rDot.Height());
+                }
+            }
+        }
+    }
+
+
+
+// ===== 鍦ㄦ瘡涓凡鍒嗛厤(allocated)鐨勫垪涓ぎ缁樺埗鍗婇�忔槑 LOCK 姘村嵃锛堢敤 HDC+LOGFONT 鏋勯�犲瓧浣擄級=====
+    {
+        Gdiplus::Graphics g(pDC->GetSafeHdc());
+        g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
+
+        CRect cli = GetClientRectNoSB();
+        CRect rh = GetHeaderRect();
+
+        // 鍙栧綋鍓� UI 瀛椾綋锛堜紭鍏堢矖浣擄級
+        LOGFONT lf{};
+        if ((HFONT)m_fntBold)      m_fntBold.GetLogFont(&lf);
+        else if ((HFONT)m_fntText) m_fntText.GetLogFont(&lf);
+
+        for (int i = 0; i < GetPortCount(); ++i)
+        {
+            if (!m_ports[i].allocated) continue;
+
+            // 鍒楃煩褰紙闄ゅ幓琛ㄥご锛岃�冭檻姘村钩婊氬姩锛�
+            CRect rCol = GetHeaderItemRect(i + 1);
+            rCol.top = rh.bottom;
+            rCol.bottom = cli.bottom;
+            if (rCol.right <= cli.left || rCol.left >= cli.right || rCol.Height() <= 0) continue;
+
+            // 鑷�傚簲涓�涓悎閫傜殑鍍忕礌楂樺害
+            int availW = rCol.Width() - 12;
+            int availH = rCol.Height() - 12;
+            int emPx = max(16, min(min(availW / 3, availH / 5), 72));
+            if (emPx < 16) emPx = 16;
+            // 瀛楀彿鍑忓崐锛堝苟缁欎釜鏇翠綆鐨勫厹搴曪紝閬垮厤澶皬锛�
+            emPx = max(12, emPx / 2);
+
+            // 鐢� LOGFONTW + HDC 鏋勯�� GDI+ 瀛椾綋
+            LOGFONTW lfw{};
+#ifdef UNICODE
+            lfw = *reinterpret_cast<LOGFONTW*>(&lf);
+#else
+            lfw.lfHeight = lf.lfHeight;
+            lfw.lfWidth = lf.lfWidth;
+            lfw.lfEscapement = lf.lfEscapement;
+            lfw.lfOrientation = lf.lfOrientation;
+            lfw.lfWeight = lf.lfWeight;
+            lfw.lfItalic = lf.lfItalic;
+            lfw.lfUnderline = lf.lfUnderline;
+            lfw.lfStrikeOut = lf.lfStrikeOut;
+            lfw.lfCharSet = lf.lfCharSet;
+            lfw.lfOutPrecision = lf.lfOutPrecision;
+            lfw.lfClipPrecision = lf.lfClipPrecision;
+            lfw.lfQuality = lf.lfQuality;
+            lfw.lfPitchAndFamily = lf.lfPitchAndFamily;
+            MultiByteToWideChar(CP_ACP, 0, lf.lfFaceName, -1, lfw.lfFaceName, LF_FACESIZE);
+#endif
+            lfw.lfHeight = -emPx;           // 璐熷��=鎸夊儚绱犻珮搴�
+            lfw.lfWeight = FW_BOLD;
+
+            Gdiplus::Font gdifont(pDC->GetSafeHdc(), &lfw);    // 鈽� 鍔犱笂 Gdiplus::
+            Gdiplus::StringFormat fmt;
+            fmt.SetAlignment(Gdiplus::StringAlignmentCenter);
+            fmt.SetLineAlignment(Gdiplus::StringAlignmentCenter);
+            Gdiplus::Color col(140, 120, 100, 60);             // 鍗婇�忔槑
+            Gdiplus::SolidBrush brush(col);
+            Gdiplus::RectF box((Gdiplus::REAL)rCol.left, (Gdiplus::REAL)rCol.top,
+                (Gdiplus::REAL)rCol.Width(), (Gdiplus::REAL)rCol.Height());
+
+            if (gdifont.GetLastStatus() == Gdiplus::Ok) {
+                g.DrawString(L"LOCK", -1, &gdifont, box, &fmt, &brush);
+            }
+            else {
+                Gdiplus::Font fallback(L"Arial", (Gdiplus::REAL)emPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
+                g.DrawString(L"LOCK", -1, &fallback, box, &fmt, &brush);
+            }
+        }
+    }
+
+    // === 瀹㈡埛鍖哄唴 1px 鐏拌壊杈规锛堜笉鍖呮粴鍔ㄦ潯锛屼絾涓嶄細缂鸿/鎶㈢粯鍒讹級===
+    {
+        CRect cli; GetClientRect(&cli);
+
+        // 鐢� FrameRect 鏇寸ǔ锛堥伩鍏嶅彸涓嬭涓㈢嚎锛�
+        CBrush br; br.CreateSolidBrush(::GetSysColor(COLOR_3DSHADOW));
+        CRect r = cli;
+        // 娉ㄦ剰锛氬鎴峰尯鍧愭爣鏄� [0..width, 0..height]锛汧rameRect 浼氬湪鍐呬晶鐢� 1px
+        pDC->FrameRect(&r, &br);
+        br.DeleteObject();
+    }
+}
+
+void CCarrierSlotGrid::OnPaint()
+{
+    CPaintDC dc(this);
+    CRect rc; GetClientRect(&rc);
+
+    CDC mem; mem.CreateCompatibleDC(&dc);
+    CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
+    HGDIOBJ ob = mem.SelectObject(bmp);
+
+    PaintTo(&mem);
+    dc.BitBlt(0, 0, rc.Width(), rc.Height(), &mem, 0, 0, SRCCOPY);
+
+    mem.SelectObject(ob);
+}
+
+void CCarrierSlotGrid::OnSize(UINT nType, int cx, int cy)
+{
+    CWnd::OnSize(nType, cx, cy);
+    UpdateScrollRange();
+    Invalidate(FALSE);
+    RedrawWindow(nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW);
+}
+
+void CCarrierSlotGrid::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
+{
+    if (m_noScrollbars) return; // 鈫� 鏂板
+
+    UNREFERENCED_PARAMETER(pBar);
+
+    SCROLLINFO si = { sizeof(SCROLLINFO) };
+    si.fMask = SIF_ALL;
+    GetScrollInfo(SB_HORZ, &si);
+
+    int pos = m_scrollX;
+    const int maxPos = max(0, (int)si.nMax - (int)si.nPage + 1);
+
+    switch (nSBCode)
+    {
+    case SB_LINELEFT:  pos -= 30; break;
+    case SB_LINERIGHT: pos += 30; break;
+    case SB_PAGELEFT:  pos -= (int)si.nPage; break;
+    case SB_PAGERIGHT: pos += (int)si.nPage; break;
+    case SB_THUMBTRACK:
+    case SB_THUMBPOSITION:
+        pos = (int)si.nTrackPos;   // 鈽� 32 浣嶆嫋鍔ㄤ綅缃�
+        break;
+    default:
+        return;
+    }
+
+    pos = max(0, min(pos, maxPos));
+    if (pos != m_scrollX) {
+        m_scrollX = pos;
+        SetScrollPos(SB_HORZ, m_scrollX);
+        Invalidate(FALSE);
+    }
+}
+
+void CCarrierSlotGrid::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
+{
+    if (m_noScrollbars) return; // 鈫� 鏂板
+
+    UNREFERENCED_PARAMETER(pBar);
+
+    SCROLLINFO si = { sizeof(SCROLLINFO) };
+    si.fMask = SIF_ALL;
+    GetScrollInfo(SB_VERT, &si);
+
+    int pos = m_scrollY;
+    const int maxPos = max(0, (int)si.nMax - (int)si.nPage + 1);
+
+    switch (nSBCode)
+    {
+    case SB_LINEUP:    pos -= m_rowHeight; break;
+    case SB_LINEDOWN:  pos += m_rowHeight; break;
+    case SB_PAGEUP:    pos -= (int)si.nPage; break;
+    case SB_PAGEDOWN:  pos += (int)si.nPage; break;
+    case SB_THUMBTRACK:
+    case SB_THUMBPOSITION:
+        pos = (int)si.nTrackPos;   // 鈽� 32 浣嶆嫋鍔ㄤ綅缃�
+        break;
+    default:
+        return;
+    }
+
+    pos = max(0, min(pos, maxPos));
+    if (pos != m_scrollY) {
+        m_scrollY = pos;
+        SetScrollPos(SB_VERT, m_scrollY);
+        Invalidate(FALSE);
+    }
+}
+
+BOOL CCarrierSlotGrid::OnMouseWheel(UINT, short zDelta, CPoint)
+{
+    if (m_noScrollbars) return FALSE; // 鈫� 鏂板锛氬交搴曚笉婊�
+
+    int delta = (zDelta > 0 ? -1 : +1) * (m_rowHeight * 3);
+    m_scrollY = max(0, m_scrollY + delta);
+    UpdateScrollRange();
+    Invalidate(FALSE);
+    return TRUE;
+}
+
+void CCarrierSlotGrid::OnShowWindow(BOOL bShow, UINT nStatus)
+{
+    CWnd::OnShowWindow(bShow, nStatus);
+    if (bShow) { UpdateScrollRange(); Invalidate(FALSE); }
+}
+
+void CCarrierSlotGrid::OnWindowPosChanged(WINDOWPOS* wp)
+{
+    CWnd::OnWindowPosChanged(wp);
+    if (wp && (wp->flags & SWP_SHOWWINDOW)) { UpdateScrollRange(); Invalidate(FALSE); }
+}
+
+void CCarrierSlotGrid::OnLButtonDown(UINT nFlags, CPoint pt)
+{
+    // 鏄惁鎷栧姩鍒楀
+    int edge = m_bAllowResize ? HitHeaderEdge(pt) : -1;  // 鈫� 淇敼
+    if (edge >= 0)
+    {
+        m_bResizing = true;
+        m_resizeEdge = edge;
+        m_resizeStartX = pt.x;
+        m_slotColCXStart = m_slotColCX;
+        m_portColCXsStart = m_portColCXs;
+        SetCapture();
+        return;
+    }
+
+    // Header 鐐瑰嚮锛堜粎澶嶉�夋鍖哄煙锛�
+    if (GetHeaderRect().PtInRect(pt))
+    {
+        for (int i = 1; i <= GetPortCount(); ++i)
+        {
+            CRect rItem = GetHeaderItemRect(i);
+            if (!rItem.PtInRect(pt)) continue;
+
+            CRect rcCb = GetHeaderCheckboxRect(i);
+            if (!rcCb.PtInRect(pt)) return;
+
+            int port = i - 1;
+            if (!SAFE_PORT(port) || m_ports[port].allocated) return;
+
+            bool any = false, all = true;
+            for (auto& cell : m_ports[port].slots) {
+                if (!cell.hasGlass) continue;
+                any = true; if (!cell.checked) { all = false; break; }
+            }
+            if (!any) return;
+            CheckAllInPort(port, all ? FALSE : TRUE, TRUE);
+            return;
+        }
+        return;
+    }
+
+    // Cell 鐐瑰嚮
+    CRect cli = GetClientRectNoSB();
+    if (pt.y < cli.top + m_headerCY) return;
+    int yIn = pt.y - (cli.top + m_headerCY) + m_scrollY;
+    int row = yIn / m_rowHeight;
+    if (!SAFE_SLOT(row)) return;
+
+    int x = pt.x + m_scrollX - cli.left;
+    int sub = 0;
+    if (x < m_slotColCX) sub = 0;
+    else {
+        x -= m_slotColCX;
+        sub = 1;
+        for (size_t i = 0; i < m_portColCXs.size(); ++i) {
+            if (x < m_portColCXs[i]) { sub = (int)i + 1; break; }
+            x -= m_portColCXs[i];
+            sub = (int)i + 2;
+        }
+        if (sub < 1 || sub > GetPortCount()) return;
+    }
+
+    if (sub == 0) return;
+    int port = sub - 1;
+    if (!SAFE_PORT(port)) return;
+
+    auto& pc = m_ports[port];
+    auto& cell = pc.slots[row];
+
+    CRect rc; if (!GetCellRect(row, sub, rc)) return;
+
+    if (pc.allocated || !cell.hasGlass) return;
+
+    if (GetCheckboxRect(rc).PtInRect(pt))
+    {
+        cell.checked = !cell.checked;
+        NotifySelectionChanged(port, row, cell.checked);
+        Invalidate(FALSE);
+        return;
+    }
+
+    if (m_bShowMatToggle && GetMaterialTagRect(rc).PtInRect(pt))
+    {
+        cell.material = (cell.material == MAT_G1) ? MAT_G2 : MAT_G1;
+        NotifyMaterialChanged(port, row, cell.material);
+        Invalidate(FALSE);
+        return;
+    }
+
+    CWnd::OnLButtonDown(nFlags, pt);
+}
+
+void CCarrierSlotGrid::OnLButtonUp(UINT nFlags, CPoint pt)
+{
+    if (m_bResizing)
+    {
+        ReleaseCapture();
+        m_bResizing = false;
+        m_resizeEdge = -1;
+        UpdateScrollRange();
+        Invalidate(FALSE);
+    }
+    CWnd::OnLButtonUp(nFlags, pt);
+}
+
+void CCarrierSlotGrid::OnMouseMove(UINT nFlags, CPoint pt)
+{
+    if (m_bResizing)
+    {
+        int dx = pt.x - m_resizeStartX;
+        if (m_resizeEdge == 0)
+        {
+            int nw = max(m_slotColMin, m_slotColCXStart + dx);
+            if (nw != m_slotColCX) { m_slotColCX = nw; UpdateScrollRange(); Invalidate(FALSE); }
+        }
+        else
+        {
+            int idx = m_resizeEdge - 1; // 璋冩暣 Port idx 鐨勫搴�
+            int nw = max(m_portColMin, m_portColCXsStart[idx] + dx);
+            if (nw != m_portColCXs[idx]) { m_portColCXs[idx] = nw; UpdateScrollRange(); Invalidate(FALSE); }
+        }
+        return;
+    }
+
+    int edge = HitHeaderEdge(pt);
+    if (edge != m_hitEdgeHover)
+    {
+        m_hitEdgeHover = edge;
+        if (m_hitEdgeHover >= 0) ::SetCursor(::LoadCursor(nullptr, IDC_SIZEWE));
+        else ::SetCursor(::LoadCursor(nullptr, IDC_ARROW));
+    }
+}
+
+BOOL CCarrierSlotGrid::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
+{
+    CPoint pt; ::GetCursorPos(&pt); ScreenToClient(&pt);
+    if (m_bAllowResize && (m_bResizing || HitHeaderEdge(pt) >= 0))
+    {
+        ::SetCursor(::LoadCursor(nullptr, IDC_SIZEWE));
+        return TRUE;
+    }
+    return CWnd::OnSetCursor(pWnd, nHitTest, message);
+}
+
+void CCarrierSlotGrid::NotifySelectionChanged(int port, int slot, BOOL checked)
+{
+    // 鍏煎鏃х殑 WM_COMMAND锛堝彲鐣欙紝涔熷彲娉ㄩ噴鎺夛級
+    if (GetParent()) {
+        const int code = 0x2001;
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+    // 鏂扮殑 WM_NOTIFY锛屽甫涓婄储寮曚笌鐘舵��
+    if (GetParent()) {
+        CSG_SEL_CHANGE nm{};
+        nm.hdr.hwndFrom = m_hWnd;
+        nm.hdr.idFrom = (UINT)GetDlgCtrlID();
+        nm.hdr.code = CSGN_SEL_CHANGED;
+        nm.port = port;
+        nm.slot = slot;
+        nm.checked = checked;
+        GetParent()->SendMessage(WM_NOTIFY, nm.hdr.idFrom, (LPARAM)&nm);
+    }
+}
+
+void CCarrierSlotGrid::NotifyMaterialChanged(int port, int slot, int material)
+{
+    if (GetParent()) {
+        const int code = 0x2002;
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+    if (GetParent()) {
+        CSG_MAT_CHANGE nm{};
+        nm.hdr.hwndFrom = m_hWnd;
+        nm.hdr.idFrom = (UINT)GetDlgCtrlID();
+        nm.hdr.code = CSGN_MAT_CHANGED;
+        nm.port = port;
+        nm.slot = slot;
+        nm.material = material;
+        GetParent()->SendMessage(WM_NOTIFY, nm.hdr.idFrom, (LPARAM)&nm);
+    }
+}
+

--
Gitblit v1.9.3