#include "stdafx.h" #include "CCarrierSlotGrid.h" #include #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 }; // 注意:此时窗口样式已经没有 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); } // 状态点(GDI+ 抗锯齿) { 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(&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];FrameRect 会在内侧画 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); } }