LAPTOP-SNT8I5JK\Boounion
2025-09-18 fd381da2f50420d8c861ad9cf213d7b1586f0188
1.Port物料选择控件完善;
2.继续处理CControlJobManagerDlg的功能;
已修改12个文件
803 ■■■■ 文件已修改
SourceCode/Bond/Servo/CCarrierSlotGrid.cpp 293 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCarrierSlotGrid.h 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage2.cpp 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage2.h 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage3.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage3.h 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPageBase.h 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobManagerDlg.cpp 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobManagerDlg.h 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CLoadPort.cpp 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.rc 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ServoDlg.cpp 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCarrierSlotGrid.cpp
@@ -1,4 +1,4 @@
#include "stdafx.h"
#include "stdafx.h"
#include "CCarrierSlotGrid.h"
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
@@ -26,7 +26,7 @@
    ON_WM_CREATE()
    ON_WM_SIZE()
    ON_WM_HSCROLL()
    ON_WM_VSCROLL()          // ★ 新增
    ON_WM_VSCROLL()          // ★ 新增
    ON_WM_LBUTTONDOWN()
    ON_WM_LBUTTONUP()
    ON_WM_MOUSEWHEEL()
@@ -50,8 +50,7 @@
    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
    EnsureFonts();
    EnsureGdiplus();
    // 确保样式包含滚动条
    ModifyStyle(0, WS_HSCROLL | WS_VSCROLL, 0);
    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
@@ -61,7 +60,7 @@
    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
    EnsureFonts();
    EnsureGdiplus();
    ModifyStyle(0, WS_HSCROLL | WS_VSCROLL, 0);
    return 0;
}
@@ -142,7 +141,7 @@
    Invalidate(FALSE);
}
void CCarrierSlotGrid::SetSlotChecked(int portIndex, int slotIndex, BOOL checked)
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];
@@ -150,7 +149,7 @@
    auto& cell = pc.slots[slotIndex];
    if (!cell.hasGlass) return;
    cell.checked = !!checked;
    NotifySelectionChanged(portIndex, slotIndex, cell.checked);
    if(bNotify) NotifySelectionChanged(portIndex, slotIndex, cell.checked);
    Invalidate(FALSE);
}
@@ -183,7 +182,7 @@
CString CCarrierSlotGrid::GetDisplayId(int portIndex, int slotIndex) const
{
    CString s(_T("—"));
    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;
@@ -218,24 +217,172 @@
    return CSize(w, h);
}
CSize CCarrierSlotGrid::CalcBestWindowSize(BOOL includeNonClient, int nSlotsOverride) const
CSize CCarrierSlotGrid::CalcBestWindowSize(BOOL includeNonClient,
    int nSlotsOverride,
    int extraPadX,
    int extraPadY) const
{
    CSize cli = CalcBestClientSize(nSlotsOverride);
    if (!includeNonClient) return cli;
    // 1) 基础客户区尺寸(含我们在客户区画的 1px 边框:左右+2/上下+2)
    const CSize content = CalcBestClientSize(nSlotsOverride);
    RECT rc = { 0, 0, cli.cx, cli.cy };
    // 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
    // 目标是“刚好不出现滚动条”的窗口外框大小:
    // 用当前样式去掉 WS_HSCROLL/WS_VSCROLL 再做 AdjustWindowRectEx
    DWORD style = GetStyle();
    // 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();
    style &= ~(WS_HSCROLL | WS_VSCROLL);
    ::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;
@@ -313,9 +460,16 @@
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);
@@ -325,7 +479,7 @@
    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);
@@ -336,10 +490,10 @@
    SetScrollInfo(SB_HORZ, &siX, TRUE);
}
// ---------- 表头分隔线命中 ----------
// ---------- 表头分隔线命中 ----------
int CCarrierSlotGrid::HitHeaderEdge(CPoint pt) const
{
    if (!m_bAllowResize) return -1;  // ← 新增
    if (!m_bAllowResize) return -1;  // ← 新增
    if (!GetHeaderRect().PtInRect(pt)) return -1;
    const int tol = 4;
    int x = GetHeaderRect().left - m_scrollX + m_slotColCX;
@@ -353,7 +507,7 @@
    return -1;
}
// ---------- 绘制 ----------
// ---------- 绘制 ----------
BOOL CCarrierSlotGrid::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; }
void CCarrierSlotGrid::DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled)
@@ -395,8 +549,8 @@
    {
        CRect rItem = GetHeaderItemRect(i);
        // 修改为:
        if (i < GetPortCount()) { // ★ 最后一列不画分隔线
        // 修改为:
        if (i < GetPortCount()) { // ★ 最后一列不画分隔线
            CPen pen(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
            pOldPen = pDC->SelectObject(&pen);
            pDC->MoveTo(rItem.right - 1, rItem.top);
@@ -425,11 +579,11 @@
                ? 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);
@@ -438,14 +592,14 @@
            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);
@@ -527,7 +681,7 @@
                    pDC->DrawText(tx, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
                }
                // 状态点(GDI+ 抗锯齿)
                // 状态点(GDI+ 抗锯齿)
                {
                    Graphics g(pDC->GetSafeHdc());
                    g.SetSmoothingMode(SmoothingModeAntiAlias);
@@ -543,7 +697,7 @@
// ===== 在每个已分配(allocated)的列中央绘制半透明 LOCK 水印(用 HDC+LOGFONT 构造字体)=====
// ===== 在每个已分配(allocated)的列中央绘制半透明 LOCK 水印(用 HDC+LOGFONT 构造字体)=====
    {
        Gdiplus::Graphics g(pDC->GetSafeHdc());
        g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
@@ -551,7 +705,7 @@
        CRect cli = GetClientRectNoSB();
        CRect rh = GetHeaderRect();
        // 取当前 UI 字体(优先粗体)
        // 取当前 UI 字体(优先粗体)
        LOGFONT lf{};
        if ((HFONT)m_fntBold)      m_fntBold.GetLogFont(&lf);
        else if ((HFONT)m_fntText) m_fntText.GetLogFont(&lf);
@@ -560,21 +714,21 @@
        {
            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 + HDC 构造 GDI+ 字体
            LOGFONTW lfw{};
#ifdef UNICODE
            lfw = *reinterpret_cast<LOGFONTW*>(&lf);
@@ -594,14 +748,14 @@
            lfw.lfPitchAndFamily = lf.lfPitchAndFamily;
            MultiByteToWideChar(CP_ACP, 0, lf.lfFaceName, -1, lfw.lfFaceName, LF_FACESIZE);
#endif
            lfw.lfHeight = -emPx;           // 负值=按像素高度
            lfw.lfHeight = -emPx;           // 负值=按像素高度
            lfw.lfWeight = FW_BOLD;
            Gdiplus::Font gdifont(pDC->GetSafeHdc(), &lfw);    // ★ 加上 Gdiplus::
            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::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());
@@ -616,14 +770,14 @@
        }
    }
    // === 客户区内 1px 灰色边框(不包滚动条,但不会缺角/抢绘制)===
    // === 客户区内 1px 灰色边框(不包滚动条,但不会缺角/抢绘制)===
    {
        CRect cli; GetClientRect(&cli);
        // 用 FrameRect 更稳(避免右下角丢线)
        // 用 FrameRect 更稳(避免右下角丢线)
        CBrush br; br.CreateSolidBrush(::GetSysColor(COLOR_3DSHADOW));
        CRect r = cli;
        // 注意:客户区坐标是 [0..width, 0..height];FrameRect 会在内侧画 1px
        // 注意:客户区坐标是 [0..width, 0..height];FrameRect 会在内侧画 1px
        pDC->FrameRect(&r, &br);
        br.DeleteObject();
    }
@@ -654,6 +808,8 @@
void CCarrierSlotGrid::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
{
    if (m_noScrollbars) return; // ← 新增
    UNREFERENCED_PARAMETER(pBar);
    SCROLLINFO si = { sizeof(SCROLLINFO) };
@@ -671,7 +827,7 @@
    case SB_PAGERIGHT: pos += (int)si.nPage; break;
    case SB_THUMBTRACK:
    case SB_THUMBPOSITION:
        pos = (int)si.nTrackPos;   // ★ 32 位拖动位置
        pos = (int)si.nTrackPos;   // ★ 32 位拖动位置
        break;
    default:
        return;
@@ -687,6 +843,8 @@
void CCarrierSlotGrid::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
{
    if (m_noScrollbars) return; // ← 新增
    UNREFERENCED_PARAMETER(pBar);
    SCROLLINFO si = { sizeof(SCROLLINFO) };
@@ -704,7 +862,7 @@
    case SB_PAGEDOWN:  pos += (int)si.nPage; break;
    case SB_THUMBTRACK:
    case SB_THUMBPOSITION:
        pos = (int)si.nTrackPos;   // ★ 32 位拖动位置
        pos = (int)si.nTrackPos;   // ★ 32 位拖动位置
        break;
    default:
        return;
@@ -720,6 +878,8 @@
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();
@@ -741,8 +901,8 @@
void CCarrierSlotGrid::OnLButtonDown(UINT nFlags, CPoint pt)
{
    // 是否拖动列宽
    int edge = m_bAllowResize ? HitHeaderEdge(pt) : -1;  // ← 修改
    // 是否拖动列宽
    int edge = m_bAllowResize ? HitHeaderEdge(pt) : -1;  // ← 修改
    if (edge >= 0)
    {
        m_bResizing = true;
@@ -754,7 +914,7 @@
        return;
    }
    // Header 点击(仅复选框区域)
    // Header 点击(仅复选框区域)
    if (GetHeaderRect().PtInRect(pt))
    {
        for (int i = 1; i <= GetPortCount(); ++i)
@@ -780,7 +940,7 @@
        return;
    }
    // Cell 点击
    // Cell 点击
    CRect cli = GetClientRectNoSB();
    if (pt.y < cli.top + m_headerCY) return;
    int yIn = pt.y - (cli.top + m_headerCY) + m_scrollY;
@@ -856,7 +1016,7 @@
        }
        else
        {
            int idx = m_resizeEdge - 1; // 调整 Port idx 的宽度
            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); }
        }
@@ -883,20 +1043,41 @@
    return CWnd::OnSetCursor(pWnd, nHitTest, message);
}
// ---------- ֪ͨ ----------
void CCarrierSlotGrid::NotifySelectionChanged(int /*port*/, int /*slot*/, BOOL /*checked*/)
void CCarrierSlotGrid::NotifySelectionChanged(int port, int slot, BOOL checked)
{
    if (GetParent())
    {
    // 兼容旧的 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*/)
}
void CCarrierSlotGrid::NotifyMaterialChanged(int port, int slot, int material)
{
    if (GetParent())
    {
    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);
}
}
SourceCode/Bond/Servo/CCarrierSlotGrid.h
@@ -7,6 +7,25 @@
#include <afxwin.h>
#endif
// 放到 CCarrierSlotGrid 类定义前或内部 public: 区都可
enum { CSGN_SEL_CHANGED = 1, CSGN_MAT_CHANGED = 2 };
struct CSG_SEL_CHANGE {
    NMHDR hdr;   // hdr.code = CSGN_SEL_CHANGED
    int   port;  // 0..GetPortCount()-1
    int   slot;  // 0..GetSlotCount()-1
    BOOL  checked;
};
struct CSG_MAT_CHANGE {
    NMHDR hdr;   // hdr.code = CSGN_MAT_CHANGED
    int   port;
    int   slot;
    int   material; // 1=G1, 2=G2
};
class CCarrierSlotGrid : public CWnd
{
public:
@@ -57,7 +76,7 @@
    BOOL IsPortAllocated(int portIndex) const;
    void SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material);
    void SetSlotChecked(int portIndex, int slotIndex, BOOL checked);
    void SetSlotChecked(int portIndex, int slotIndex, BOOL checked, BOOL bNotify = FALSE);
    BOOL GetSlotChecked(int portIndex, int slotIndex) const;
    int  GetSlotMaterialType(int portIndex, int slotIndex) const;
@@ -73,9 +92,24 @@
// - CalcBestClientSize:内容区域(不含滚动条/非客户区)刚好容纳表头+所有行、全部列
// - CalcBestWindowSize:在当前窗口样式下,将“内容大小”转换为窗口外框大小(会考虑 WS_BORDER/CLIENTEDGE 等)
//   默认按“隐藏滚动条”的目标来算(即不把 WS_HSCROLL/WS_VSCROLL 计入调整)
// 计算最佳大小(支持可选安全边距,默认按 DPI 约等于 2px)
    CSize CalcBestClientSize(int nSlotsOverride = -1) const;
    CSize CalcBestWindowSize(BOOL includeNonClient = TRUE, int nSlotsOverride = -1) const;
    CSize CalcBestWindowSize(BOOL includeNonClient = TRUE,
        int nSlotsOverride = -1,
        int extraPadX = -1,  // -1 表示按 DPI 自动
        int extraPadY = -1) const;
    // 永久禁用系统滚动条(去掉样式并刷新非客户区)
    void DisableSystemScrollbars();
    // 把窗口尺寸调到正好容纳所有内容(不出现滚动条)
    void ResizeWindowToFitAll(BOOL includeNonClient = TRUE, int nSlotsOverride = -1);
    // 进入/退出无滚动条模式(去样式、清滚动、忽略滚动消息)
    void SetNoScrollbarsMode(BOOL enable);
    // 在“无滚动条模式”下,把窗口尺寸调到刚好容纳所有内容(不出现滚动条)
    void FitWindowToContentNoScroll(BOOL includeNonClient = TRUE, int nSlotsOverride = -1);
protected:
    // 数据
@@ -115,6 +149,7 @@
    std::vector<int> m_portColCXsStart;
    int  m_hitEdgeHover = -1;
    bool m_bAllowResize = true; // ← 新增:是否允许拖动列宽
    bool m_noScrollbars = false;   // ← 新增:无滚动条模式
    // 工具
    void EnsureFonts();
SourceCode/Bond/Servo/CCjPage2.cpp
@@ -15,7 +15,7 @@
CCjPage2::CCjPage2(CWnd* pParent /*=nullptr*/)
    : CCjPageBase(IDD_CJ_PAGE2, pParent)
{
    m_nSelRadioId = 0;
}
CCjPage2::~CCjPage2()
@@ -36,6 +36,8 @@
    ON_BN_CLICKED(IDC_RADIO2, &CCjPage2::OnBnClickedRadio2)
    ON_BN_CLICKED(IDC_RADIO3, &CCjPage2::OnBnClickedRadio3)
    ON_BN_CLICKED(IDC_RADIO4, &CCjPage2::OnBnClickedRadio4)
    ON_NOTIFY(CSGN_SEL_CHANGED, IDC_GRID1, &CCjPage2::OnGridSelChanged)
    ON_NOTIFY(CSGN_MAT_CHANGED, IDC_GRID1, &CCjPage2::OnGridMatChanged)
END_MESSAGE_MAP()
@@ -60,15 +62,29 @@
    m_grid.SubclassDlgItem(IDC_GRID1, this);
     m_grid.InitGrid(4, 8);
    m_grid.SetColumnWidths(100, 220);
    m_grid.SetRowHeight(28);
    m_grid.SetHeaderHeight(32);
    m_grid.SetRowHeight(32);
    m_grid.SetHeaderHeight(36);
    m_grid.EnableColumnResize(FALSE); // 禁止拖动列宽
    m_grid.SetShowMaterialToggle(TRUE);
    m_grid.DisableSystemScrollbars();
    m_grid.ResizeWindowToFitAll(TRUE); // TRUE=包含非客户区(边框、标题栏)
    m_grid.SetNoScrollbarsMode(TRUE);           // 彻底禁用滚动条
    m_grid.FitWindowToContentNoScroll(TRUE);    // 窗口尺寸刚好容纳全部内容(不出现滚动条)
    m_grid.SetPortInfo(0, _T("Port 1"), _T(""));
    m_grid.SetPortInfo(1, _T("Port 2"), _T(""));
    m_grid.SetPortInfo(2, _T("Port 3"), _T(""));
    m_grid.SetPortInfo(3, _T("Port 4"), _T(""));
    // 测试数据
    char szBuffer[256];
    for (int port = 0; port < 4; port++) {
        for (int slot = 0; slot < 8; slot++) {
            sprintf_s(szBuffer, 256, "Gls%04d%04d", port + 1, slot + 1);
            m_grid.SetSlotGlass(port, slot, TRUE, szBuffer, CCarrierSlotGrid::MAT_G1);
        }
    }
    UpdatePjData();
@@ -98,16 +114,16 @@
    // 让控件窗口尺寸自动匹配当前列宽/行数(不出现滚动条)
    if (::IsWindow(m_grid.m_hWnd)) {
        CSize best = m_grid.CalcBestWindowSize(TRUE); // 计算到含非客户区的最终窗口大小
        CSize best = m_grid.CalcBestWindowSize(TRUE, -1, 2, 2);
        pItem->MoveWindow(rcItem.left, rcItem.top, best.cx, best.cy);
        pItem->Invalidate();
    }
}
void CCjPage2::OnApply()
int CCjPage2::OnApply()
{
    //SERVO::CProcessJob*
    if (m_pContext == nullptr) return;
    if (m_pContext == nullptr) return -1;
    PJWarp* pPjWarp = (PJWarp*)m_pContext;
    SERVO::CProcessJob* pProcessJob = (SERVO::CProcessJob*)pPjWarp->pj;
@@ -116,7 +132,7 @@
    char szBuffer[256];
    GetDlgItemText(IDC_EDIT_PJ_ID, szBuffer, 256);
    for (auto item : m_pjWarps) {
        if (item.pj != m_pContext) {
        if (item.pj != pProcessJob) {
            SERVO::CProcessJob* temp = (SERVO::CProcessJob*)item.pj;
            if (temp->id().compare(std::string(szBuffer)) == 0) {
                bOkName = FALSE;
@@ -126,7 +142,7 @@
    }
    if (!bOkName) {
        AfxMessageBox("不能使用和其它Process Job相同的ID");
        return;
        return -1;
    }
@@ -149,11 +165,31 @@
    }
    // 更新Port
    int port = -1;
    static int ids[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4 };
    for (int i = 0; i < 4; i++) {
        int state = ((CButton*)GetDlgItem(ids[i]))->GetCheck();
        if (state == BST_CHECKED) port = i;
    }
    pPjWarp->port = port;
    if (pPjWarp->port != -1) {
        for (int i = 0; i < 8; i++) {
            pPjWarp->checkSlot[i] = m_grid.GetSlotChecked(pPjWarp->port, i);
            pPjWarp->material[i] = m_grid.GetSlotMaterialType(pPjWarp->port, i);
        }
    }
    ContentChanged(1);
    return 0;
}
void CCjPage2::UpdatePjData()
{
    if (m_pContext == nullptr) return;
    m_bContentChangedLock = TRUE;
    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_RECIPE);
@@ -163,24 +199,51 @@
        pComboBox->AddString(CString(recipe.c_str()));
    }
    if (m_pContext) {
    // ComboBox
        PJWarp* pPjWarp = (PJWarp*)m_pContext;
        SERVO::CProcessJob* pProcessJob = (SERVO::CProcessJob*)pPjWarp->pj;
        SetDlgItemText(IDC_EDIT_PJ_ID, pProcessJob->id().c_str());
        int idx = pComboBox->FindStringExact(-1, pProcessJob->recipeSpec().c_str());
        if (idx != CB_ERR) pComboBox->SetCurSel(idx);
    // 4个checkbox
    static int ids[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4};
    static char* pszUsed[] = { "Port1(已占用)", "Port2(已占用)", "Port3(已占用)", "Port4(已占用)" };
    static char* pszUnUsed[] = { "Port1(可用)", "Port2(可用)", "Port3(可用)", "Port4(可用)" };
    int portIndex = -1;
    bool enable[] = {true, true, true, true};
    bool checked[] = { false, false, false, false };
    for (auto item : m_pjWarps) {
        if (0 <= item.port && item.port <= 4 && item.pj != ((PJWarp*)m_pContext)->pj) {
            enable[item.port] = false;
        }
    }
    if (0 <= ((PJWarp*)m_pContext)->port && ((PJWarp*)m_pContext)->port <= 3) {
        checked[((PJWarp*)m_pContext)->port] = true;
        portIndex = ((PJWarp*)m_pContext)->port;
        m_nSelRadioId = ids[((PJWarp*)m_pContext)->port];
    }
    // 示例:设置Port信息、锁列、填充Glass
    /*
    m_grid.SetPortInfo(0, _T("Port 1"), _T(""));
    m_grid.SetPortInfo(1, _T("Port 2"), _T(""));
    m_grid.SetPortInfo(2, _T("Port 3"), _T("Carrier C"));
    m_grid.SetPortInfo(3, _T("Port 4"), _T("Carrier D"));
    m_grid.SetPortAllocated(2, TRUE, _T("ProcessJob 1"));
    m_grid.SetSlotGlass(0, 0, TRUE, _T("A00123"), CCarrierSlotGrid::MAT_G1);
    m_grid.SetSlotGlass(0, 1, TRUE, _T("A00124"), CCarrierSlotGrid::MAT_G1);
    */
    for (int i = 0; i < 4; i++) {
        CButton* pButton = (CButton*)GetDlgItem(ids[i]);
        pButton->SetCheck(checked[i] ? BST_CHECKED : BST_UNCHECKED);
        pButton->SetWindowText(enable[i] ? pszUnUsed[i] : pszUsed[i]);
        pButton->EnableWindow(enable[i]);
        m_grid.SetPortAllocated(i, !checked[i], _T(""));
    }
    // 设置勾选数据
    if (portIndex != -1) {
        for (int i = 0; i < 8; i++) {
            m_grid.SetSlotChecked(portIndex, i, ((PJWarp*)m_pContext)->checkSlot[i]);
            m_grid.SetSlotMaterialType(portIndex, i, ((PJWarp*)m_pContext)->material[i]);
        }
    }
    m_bContentChangedLock = FALSE;
}
@@ -197,32 +260,127 @@
void CCjPage2::OnBnClickedRadio1()
{
    m_grid.SetPortAllocated(0, FALSE, _T(""));
    m_grid.SetPortAllocated(1, TRUE, _T(""));
    m_grid.SetPortAllocated(2, TRUE, _T(""));
    m_grid.SetPortAllocated(3, TRUE, _T(""));
    BOOL lock[] = {TRUE, TRUE, TRUE, TRUE};
    if (IDC_RADIO1 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO1);
        m_nSelRadioId = IDC_RADIO1;
        lock[0] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
    }
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio2()
{
    m_grid.SetPortAllocated(0, TRUE, _T(""));
    m_grid.SetPortAllocated(1, FALSE, _T(""));
    m_grid.SetPortAllocated(2, TRUE, _T(""));
    m_grid.SetPortAllocated(3, TRUE, _T(""));
    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
    if (IDC_RADIO2 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO2);
        m_nSelRadioId = IDC_RADIO2;
        lock[1] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
    }
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio3()
{
    m_grid.SetPortAllocated(0, TRUE, _T(""));
    m_grid.SetPortAllocated(1, TRUE, _T(""));
    m_grid.SetPortAllocated(2, FALSE, _T(""));
    m_grid.SetPortAllocated(3, TRUE, _T(""));
    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
    if (IDC_RADIO3 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO3);
        m_nSelRadioId = IDC_RADIO3;
        lock[2] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
    }
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio4()
{
    m_grid.SetPortAllocated(0, TRUE, _T(""));
    m_grid.SetPortAllocated(1, TRUE, _T(""));
    m_grid.SetPortAllocated(2, TRUE, _T(""));
    m_grid.SetPortAllocated(3, FALSE, _T(""));
    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
    if (IDC_RADIO4 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO4);
        m_nSelRadioId = IDC_RADIO4;
        lock[3] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
    }
    ContentChanged(0);
}
void CCjPage2::OnGridSelChanged(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* nm = reinterpret_cast<CSG_SEL_CHANGE*>(pNMHDR);
    const int port = nm->port;
    const int slot = nm->slot;
    const BOOL chk = nm->checked;
    // 这里写你的业务逻辑
    // 例如:更新状态栏 / 同步其它控件 / 统计数量
    ContentChanged(0);
    /*
    if (m_pContext != nullptr) {
        PJWarp* pjWarp = (PJWarp*)m_pContext;
        for (int i = 0; i < 8; i++) {
            pjWarp->checkSlot[i] = m_grid.GetSlotChecked(port, i);
            pjWarp->material[i] = m_grid.GetSlotMaterialType(port, i);
        }
    }
    */
    *pResult = 0;
}
void CCjPage2::OnGridMatChanged(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* nm = reinterpret_cast<CSG_MAT_CHANGE*>(pNMHDR);
    const int port = nm->port;
    const int slot = nm->slot;
    const int mat = nm->material; // 1/2
    // 例如:即刻刷新右侧预览/记录日志等
    ContentChanged(0);
    /*
    if (m_pContext != nullptr) {
        PJWarp* pjWarp = (PJWarp*)m_pContext;
        for (int i = 0; i < 8; i++) {
            pjWarp->checkSlot[i] = m_grid.GetSlotChecked(port, i);
            pjWarp->material[i] = m_grid.GetSlotMaterialType(port, i);
        }
    }
    */
    *pResult = 0;
}
SourceCode/Bond/Servo/CCjPage2.h
@@ -6,9 +6,11 @@
struct PJWarp {
    BOOL addToCj;
    void* pj;
    int port;
    bool checkSlot[8];
    BOOL checkSlot[8];
    int material[8];
};
// CPjPage1 对话框
@@ -26,7 +28,7 @@
protected:
    void Resize();
    virtual void OnApply();
    virtual int OnApply();
    virtual void OnSetContext(void* pContext);
private:
@@ -35,6 +37,7 @@
private:
    CCarrierSlotGrid m_grid;
    std::vector<PJWarp> m_pjWarps;
    int m_nSelRadioId;
// 对话框数据
#ifdef AFX_DESIGN_TIME
@@ -54,4 +57,6 @@
    afx_msg void OnBnClickedRadio2();
    afx_msg void OnBnClickedRadio3();
    afx_msg void OnBnClickedRadio4();
    afx_msg void OnGridSelChanged(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnGridMatChanged(NMHDR* pNMHDR, LRESULT* pResult);
};
SourceCode/Bond/Servo/CCjPage3.cpp
@@ -67,7 +67,7 @@
        */
}
void CCjPage3::OnApply()
int CCjPage3::OnApply()
{
    return 0;
}
SourceCode/Bond/Servo/CCjPage3.h
@@ -14,7 +14,7 @@
protected:
    void Resize();
    virtual void OnApply();
    virtual int OnApply();
// 对话框数据
#ifdef AFX_DESIGN_TIME
SourceCode/Bond/Servo/CCjPageBase.h
@@ -18,7 +18,7 @@
public:
    void SetTitle(CString strTitle);
    virtual void OnApply() {};
    virtual int OnApply() { return 0; };
    void SetOnContentChanged(ONCONTENTCHANGED onContentChanged);
    void SetContext(void* pContext, int type);
    void* GetContext();
SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
@@ -51,9 +51,14 @@
BEGIN_MESSAGE_MAP(CControlJobManagerDlg, CDialogEx)
    ON_WM_SIZE()
    ON_WM_GETMINMAXINFO()
    ON_NOTIFY(TVN_SELCHANGED, IDC_TREE1, &CControlJobManagerDlg::OnTvnSelchangedTree1)
    ON_NOTIFY(TVN_ITEMCHANGED, IDC_TREE1, &CControlJobManagerDlg::OnTvnItemChangedTree)
    ON_NOTIFY(NM_CLICK, IDC_TREE1, &CControlJobManagerDlg::OnTreeClick)          // 新增
    ON_NOTIFY(TVN_KEYDOWN, IDC_TREE1, &CControlJobManagerDlg::OnTreeKeyDown)        // 新增
    ON_MESSAGE(WM_AFTER_TVCHECK, &CControlJobManagerDlg::OnAfterTvCheck)                // 新增
    ON_WM_DESTROY()
    ON_BN_CLICKED(IDC_BUTTON_APPLY, &CControlJobManagerDlg::OnBnClickedButtonApply)
    ON_NOTIFY(TVN_SELCHANGING, IDC_TREE1, &CControlJobManagerDlg::OnTvnSelchangingTree1)
    ON_BN_CLICKED(IDC_BUTTON_BATH_COMPLETION, &CControlJobManagerDlg::OnBnClickedButtonBathCompletion)
END_MESSAGE_MAP()
@@ -184,8 +189,7 @@
void CControlJobManagerDlg::UpdateCtrlState()
{
    auto& master = theApp.m_model.getMaster();
    GetDlgItem(IDC_BUTTON_BATH_COMPLETION)->EnableWindow(false);
    GetDlgItem(IDC_BUTTON_BATH_COMPLETION)->EnableWindow(true);
}
void CControlJobManagerDlg::UpdateControlJob()
@@ -199,7 +203,7 @@
    for (auto& item : m_pjWarps) {
        HTREEITEM hItem = m_tree.InsertItem(((SERVO::CProcessJob*)item.pj)->id().c_str(), 0, 0, hRoot);
        m_tree.SetItemData(hItem, (DWORD_PTR)&item);
        m_tree.SetItemState(hItem, INDEXTOSTATEIMAGEMASK(false ? 2 : 1), TVIS_STATEIMAGEMASK);
        m_tree.SetItemState(hItem, INDEXTOSTATEIMAGEMASK(item.addToCj ? 2 : 1), TVIS_STATEIMAGEMASK);
    }
    m_tree.Expand(hRoot, TVE_EXPAND);
}
@@ -216,35 +220,127 @@
    return m_pControlJob->removePjPointer(pj->id());
}
void CControlJobManagerDlg::OnTvnSelchangedTree1(NMHDR* pNMHDR, LRESULT* pResult)
void CControlJobManagerDlg::OnTvnItemChangedTree(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* p = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
    UINT oldState = p->itemOld.state, newState = p->itemNew.state;
    HTREEITEM hItem = p->itemNew.hItem;
    if (((oldState ^ newState) & TVIS_STATEIMAGEMASK) != 0) {
        const int idx = (newState & TVIS_STATEIMAGEMASK) >> 12; // 1=未选,2=已选
        const bool checked = (idx == 2);
        PJWarp* pjWarp = (PJWarp*)m_tree.GetItemData(hItem);
        if (pjWarp != nullptr) {
            CString s; s.Format("%s %d", ((SERVO::CProcessJob*)pjWarp->pj)->id().c_str(),
                checked ? "" : "");
            AfxMessageBox(s);
        }
    }
    *pResult = 0;
}
// 命中复选框:用 NM_CLICK 做命中测试,然后“滞后”读取新状态
void CControlJobManagerDlg::OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult)
{
    *pResult = 0;
    DWORD pos = ::GetMessagePos();
    CPoint pt(GET_X_LPARAM(pos), GET_Y_LPARAM(pos));
    m_tree.ScreenToClient(&pt);
    TVHITTESTINFO ht{}; ht.pt = pt;
    HTREEITEM hItem = m_tree.HitTest(&ht);
    if (hItem && (ht.flags & TVHT_ONITEMSTATEICON)) {
        // 让 TreeView 先切换,再异步读取最终状态
        PostMessage(WM_AFTER_TVCHECK, (WPARAM)hItem, 0);
    }
}
// 空格键也会切换复选框
void CControlJobManagerDlg::OnTreeKeyDown(NMHDR* pNMHDR, LRESULT* pResult)
{
    *pResult = 0;
    auto* p = reinterpret_cast<LPNMTVKEYDOWN>(pNMHDR);
    if (p->wVKey == VK_SPACE) {
        HTREEITEM hItem = m_tree.GetSelectedItem();
        if (hItem) PostMessage(WM_AFTER_TVCHECK, (WPARAM)hItem, 0);
    }
}
// 统一处理(读最终状态 + 你的业务)
LRESULT CControlJobManagerDlg::OnAfterTvCheck(WPARAM wParam, LPARAM /*lParam*/)
{
    HTREEITEM hItem = (HTREEITEM)wParam;
    if (!hItem) return 0;
    // 只处理第二层:根的直接子节点(可选)
    auto getLevel = [&](HTREEITEM h) {
        int lv = 0; for (HTREEITEM p = m_tree.GetParentItem(h); p; p = m_tree.GetParentItem(p)) ++lv; return lv;
    };
    if (getLevel(hItem) != 1) return 0;
    BOOL checked = m_tree.GetCheck(hItem);
    // 你的业务逻辑(修正了 CString::Format 的参数类型)
    auto* pjWarp = reinterpret_cast<PJWarp*>(m_tree.GetItemData(hItem));
    if (pjWarp) {
        pjWarp->addToCj = checked;
    }
    return 0;
}
void CControlJobManagerDlg::OnTvnSelchangingTree1(NMHDR* pNMHDR, LRESULT* pResult)
{
    LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
    bool allow = FALSE;
    HTREEITEM hOldSel = pNMTreeView->itemOld.hItem;
    HTREEITEM hSel = pNMTreeView->itemNew.hItem;
    if (hSel != nullptr) {
        HTREEITEM hParent = m_tree.GetParentItem(hSel);
        if (hParent == nullptr) {
            SERVO::CControlJob* cj = (SERVO::CControlJob*)m_tree.GetItemData(hSel);
            ASSERT(m_pages.size() == 3);
            m_pages[0]->ShowWindow(SW_HIDE);
            m_pages[1]->ShowWindow(SW_HIDE);
            m_pages[2]->ShowWindow(SW_SHOW);
            if (0 == ShowPage(2)) {
            }
        }
        else if (m_tree.GetParentItem(hParent) == nullptr) {
            if (0 == ShowPage(1)) {
            PJWarp* pjWarp = (PJWarp*)m_tree.GetItemData(hSel);
            m_pages[0]->ShowWindow(SW_HIDE);
            m_pages[1]->ShowWindow(SW_SHOW);
            m_pages[2]->ShowWindow(SW_HIDE);
            m_pages[1]->SetContext(pjWarp, 1);
            ((CCjPage2*)m_pages[1])->SetPjWarps(m_pjWarps);
                m_pages[1]->SetContext(pjWarp, 1);
            }
            else {
                allow = TRUE;
            }
        }
        else {
            // 有祖先 → 第三层及以下 → Glass
        }
    }
    *pResult = 0;
    *pResult = allow;
}
int CControlJobManagerDlg::ShowPage(int index)
{
    ASSERT(0 <= index && index <= 2);
    for (int i = 0; i < 3; i++) {
        if (m_pages[i]->IsWindowVisible()) {
            int ret = m_pages[i]->OnApply();
            if (ret != 0) return -1;
        }
        m_pages[i]->ShowWindow(index == i ? SW_SHOW : SW_HIDE);
    }
    return 0;
}
void CControlJobManagerDlg::OnDestroy()
@@ -269,7 +365,7 @@
    for (int i = 0; i < 4; i++) {
        sprintf_s(szBuffer, 256, "PJ%03d", i + 1);
        SERVO::CProcessJob* pj = new SERVO::CProcessJob(std::string(szBuffer));
        PJWarp pjWarp;
        PJWarp pjWarp = {};
        pjWarp.pj = pj;
        pjWarp.port = -1;
        m_pjWarps.push_back(pjWarp);
@@ -279,9 +375,12 @@
void CControlJobManagerDlg::OnBnClickedButtonApply()
{
    for (auto item : m_pages) {
        item->OnApply();
    }
        if (item->IsWindowVisible()) {
            if (0 == item->OnApply()) {
    GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(FALSE);
            }
        }
    }
}
void CControlJobManagerDlg::UpProcessJobId(PJWarp* pjWarp)
@@ -321,3 +420,75 @@
    m_state.pjWarps = m_pjWarps;
    m_bHasState = true;
}
void CControlJobManagerDlg::OnBnClickedButtonBathCompletion()
{
    // 先应用
    for (int i = 0; i < 3; i++) {
        if (m_pages[i]->IsWindowVisible()) {
            int ret = m_pages[i]->OnApply();
            if (ret != 0) return ;
        }
    }
    GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(FALSE);
    // 先检查数据正确性
    int checkCount = 0;
    for (auto item : m_pjWarps) {
        if (!item.addToCj) continue;
        checkCount++;
    }
    if (checkCount == 0) {
        AfxMessageBox(_T("您没有选择要进行工艺处理的Process Job!\n请在要进行工艺处理的Process Job前打勾。"));
        return;
    }
    auto& master = theApp.m_model.getMaster();
    std::vector<SERVO::CProcessJob*> pjs;
    for (auto item : m_pjWarps) {
        if (!item.addToCj) continue;
        if (item.port == -1) continue;
        BOOL bCheck = FALSE;
        for (int i = 0; i < 8; i++) {
            if (item.checkSlot[i]) {
                bCheck = TRUE;
                break;
            }
        }
        if (!bCheck) continue;
        SERVO::CProcessJob* pScr = (SERVO::CProcessJob*)item.pj;
        SERVO::CProcessJob * pj = new SERVO::CProcessJob(pScr->id());
        pj->setRecipe(SERVO::RecipeMethod::NoTuning, pScr->recipeSpec());
        std::vector<SERVO::CarrierSlotInfo> carriers;
        SERVO::CarrierSlotInfo csi;
        csi.carrierId = "Port" + std::to_string(item.port + 1);
        for (int i = 0; i < 8; i++) {
            if (item.checkSlot[i]) {
                csi.slots.push_back(i);
            }
        }
        carriers.push_back(csi);
        pj->setCarriers(carriers);
        pjs.push_back(pj);
        m_pControlJob->addPJ(pScr->id());
    }
    if (pjs.empty()) {
        AfxMessageBox(_T("没有需要进行工艺处理的Process Job!\n可能未选择Port或选择任何物料。"));
        return;
    }
    m_pControlJob->setPJs(pjs);
    int nRet = master.setProcessJobs(pjs);
    master.setControlJob(*m_pControlJob);
}
SourceCode/Bond/Servo/CControlJobManagerDlg.h
@@ -5,6 +5,8 @@
#include "ApredTreeCtrl2.h"
#define WM_AFTER_TVCHECK (WM_USER + 1000)
// CControlJobManagerDlg 对话框
class CControlJobManagerDlg : public CDialogEx
@@ -26,6 +28,7 @@
    void InitData();
    void LoadState();
    void SaveState();
    int ShowPage(int index);
private:
    std::vector<CCjPageBase*> m_pages;
@@ -54,7 +57,12 @@
    virtual BOOL OnInitDialog();
    afx_msg void OnSize(UINT nType, int cx, int cy);
    afx_msg void OnGetMinMaxInfo(MINMAXINFO* lpMMI);
    afx_msg void OnTvnSelchangedTree1(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnTvnItemChangedTree(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnDestroy();
    afx_msg void OnBnClickedButtonApply();
    afx_msg void OnTvnSelchangingTree1(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnBnClickedButtonBathCompletion();
    afx_msg void OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnTreeKeyDown(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg LRESULT OnAfterTvCheck(WPARAM wParam, LPARAM lParam);
};
SourceCode/Bond/Servo/CLoadPort.cpp
@@ -505,6 +505,9 @@
    void CLoadPort::setIndex(unsigned int index)
    {
        m_nIndex = index;
        std::string id = "Port" + std::to_string(index + 1);
        m_portStatusReport.setCassetteId(id.c_str());
    }
    unsigned int CLoadPort::getIndex()
SourceCode/Bond/Servo/Servo.rc
Binary files differ
SourceCode/Bond/Servo/ServoDlg.cpp
@@ -1039,14 +1039,16 @@
        }
    }
    else if (id == IDC_BUTTON_JOBS) {
        static int i = 0; i++;
        if (i % 2 == 0) {
        CControlJobManagerDlg dlg;
        dlg.DoModal();
        /*
        }
        else {
        CControlJobDlg dlg;
        dlg.SetControlJob(theApp.m_model.m_master.getControlJob());
        dlg.DoModal();
        */
        }
    }
    else if (id == IDC_BUTTON_PORT_CONFIG) {
        CPortConfigurationDlg dlg;