mrDarker
2025-09-08 6dc80508b1c0f431007f8a8c947c152ec00c3d15
SourceCode/Bond/Servo/CExpandableListCtrl.cpp
@@ -19,16 +19,18 @@
    // 报表风格列举例
    SetExtendedStyle(GetExtendedStyle()
        | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES);
    // 示例列(可在外部设置)
    if (GetHeaderCtrl() == nullptr || GetHeaderCtrl()->GetItemCount() == 0) {
        InsertColumn(0, _T("名称"), LVCFMT_LEFT, 260);
        InsertColumn(1, _T("状态"), LVCFMT_LEFT, 120);
        InsertColumn(2, _T("描述"), LVCFMT_LEFT, 260);
    }
        | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
    return 0;
}
void CExpandableListCtrl::PreSubclassWindow()
{
    // 报表风格列举例
    SetExtendedStyle(GetExtendedStyle()
        | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
    CListCtrl::PreSubclassWindow();
}
CExpandableListCtrl::Node* CExpandableListCtrl::InsertRoot(const std::vector<CString>& cols)
@@ -119,20 +121,25 @@
CRect CExpandableListCtrl::expanderRectForRow(int row) const
{
    CRect rc;
    // 取首列矩形
    if (!GetSubItemRect(row, 0, LVIR_BOUNDS, rc))
    CRect rcLabel;
    if (!const_cast<CExpandableListCtrl*>(this)->GetSubItemRect(row, 0, LVIR_LABEL, rcLabel))
        return CRect(0, 0, 0, 0);
    Node* n = const_cast<CExpandableListCtrl*>(this)->GetNodeByVisibleIndex(row);
    int indent = (n ? n->level : 0);
    if (!n || n->children.empty())
        return CRect(0, 0, 0, 0); // 叶子不占位,文本就不会被多推一格
    // 缩进:每级给 16px
    int left = rc.left + m_expanderPadding + indent * 16;
    CRect box(left, rc.CenterPoint().y - m_expanderSize / 2,
        left + m_expanderSize, rc.CenterPoint().y + m_expanderSize / 2);
    return box;
    const int indent = n->level;
    const int left = rcLabel.left + m_expanderPadding + indent * 16;
    return CRect(
        left,
        rcLabel.CenterPoint().y - m_expanderSize / 2,
        left + m_expanderSize,
        rcLabel.CenterPoint().y + m_expanderSize / 2
    );
}
void CExpandableListCtrl::OnClick(NMHDR* pNMHDR, LRESULT* pResult)
{
@@ -174,7 +181,7 @@
        if (col == 0)
        {
            CRect rc; GetSubItemRect(row, 0, LVIR_BOUNDS, rc);
            CRect rc; GetSubItemRect(row, 0, LVIR_LABEL, rc);
            Node* n = GetNodeByVisibleIndex(row);
            if (!n) { *pResult = CDRF_DODEFAULT; return; }
@@ -190,40 +197,107 @@
            CBrush bkBrush(bk);
            pDC->FillRect(rc, &bkBrush);
            // 2) 展开/折叠指示
            // 2) 展开/折叠指示(参考旧项目的右对齐坐标法,做像素对齐,纯GDI)
            if (!n->children.empty())
            {
                CRect box = expanderRectForRow(row);
                // 画三角(▶/▼),并恢复画笔/画刷
                HGDIOBJ oldPen = pDC->SelectObject(GetStockObject(BLACK_PEN));
                HGDIOBJ oldBrush = pDC->SelectObject(GetStockObject(BLACK_BRUSH));
                POINT tri[3];
                if (n->expanded) { // ▼
                    tri[0] = { box.left + 2, box.top + 2 };
                    tri[1] = { box.right - 2, box.top + 2 };
                    tri[2] = { box.CenterPoint().x, box.bottom - 2 };
                // ---- 可调参数:与旧代码命名一致 ----
                // 右侧留白(与文本间隙/网格线保持距离)
                const int ROFFSET = 2;
                // 闭合/展开的“宽度”设置:奇数更顺眼(9/11 都行)
                const int WIDE = max(9, min(min(box.Width(), box.Height()), 13)); // ▶ 的边长
                const int WIDE2 = WIDE / 2;                                        // 一半
                const int EXPANDED_WIDE = WIDE;                                           // ▼ 的边长
                // 轻微内缩,避免贴边(与你旧代码“按钮要刷一下”同效)
                box.DeflateRect(1, 1);
                // 统一做偶数对齐,减少半像素锯齿
                auto even = [](int v) { return (v & 1) ? (v - 1) : v; };
                // 计算“自下向上”的基准偏移,与旧 TreeCtrl 一致
                // 这里用 box 作为 pRect
                POINT pt[3];
                if (n->expanded) {
                    // ▼
                    int nBottomOffset = (box.Height() - EXPANDED_WIDE) / 2;
                    pt[0].x = box.right - ROFFSET - EXPANDED_WIDE;
                    pt[0].y = box.bottom - nBottomOffset;
                    pt[1].x = box.right - ROFFSET;
                    pt[1].y = box.bottom - nBottomOffset;
                    pt[2].x = box.right - ROFFSET;
                    pt[2].y = box.bottom - nBottomOffset - EXPANDED_WIDE;
                }
                else {           // ▶
                    tri[0] = { box.left + 2, box.top + 2 };
                    tri[1] = { box.right - 2, box.CenterPoint().y };
                    tri[2] = { box.left + 2, box.bottom - 2 };
                else {
                    // ▶
                    int nBottomOffset = (box.Height() - WIDE) / 2;
                    pt[0].x = box.right - ROFFSET - WIDE2;
                    pt[0].y = box.bottom - nBottomOffset - WIDE;
                    pt[1].x = box.right - ROFFSET - WIDE2;
                    pt[1].y = box.bottom - nBottomOffset;
                    pt[2].x = box.right - ROFFSET;
                    pt[2].y = box.bottom - nBottomOffset - WIDE2;
                }
                pDC->Polygon(tri, 3);
                // 仅填充,不描边(描边会加重台阶感);颜色用 txt 与主题一致
                HGDIOBJ oldPen = pDC->SelectObject(GetStockObject(NULL_PEN));
                HBRUSH   hBrush = CreateSolidBrush(txt);
                HGDIOBJ oldBrush = pDC->SelectObject(hBrush);
                pDC->Polygon(pt, 3);
                pDC->SelectObject(oldPen);
                pDC->SelectObject(oldBrush);
                DeleteObject(hBrush);
            }
            // 3) 文本:右移避免遮挡
            const int indentPx = n->level * 16;
            // 3) 文本:基于首列区域右移(区分是否有子节点)
            const int indentPx = n->level * 14;
            const int baseLeft = rc.left + m_expanderPadding + indentPx;
            CRect textRc = rc;
            textRc.left = rc.left + m_expanderPadding + indentPx + m_expanderSize + m_textGap;
            if (!n->children.empty()) {
                // 有子项:预留按钮位 + 文本间隙
                textRc.left = baseLeft + m_expanderSize + m_textGap;
            }
            else {
                // 叶子行:不预留按钮位,只给一点点叶子间隙(让层级缩进仍然生效)
                constexpr int kLeafGap = 2; // 你可调 0~4
                textRc.left = baseLeft + kLeafGap;
            }
            pDC->SetBkMode(TRANSPARENT);
            pDC->SetTextColor(txt);
            CString txt0 = n->cols.empty() ? _T("") : n->cols[0];
            pDC->DrawText(txt0, textRc, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
            // 首列自绘完毕
            // —— 画完三角与文本之后,补一条该行的底部横向网格线 ——
            // 仅当开启了 LVS_EX_GRIDLINES 才绘制
            if (GetExtendedStyle() & LVS_EX_GRIDLINES)
            {
                // 用整行 bounds,保证横线贯穿所有列的可见宽度
                CRect rcRow;
                GetSubItemRect(row, 0, LVIR_BOUNDS, rcRow);
                // 底边 y 坐标(与系统网格线对齐)
                const int y = rcRow.bottom - 1;
                // 颜色与系统风格接近;若觉得偏浅,可换 COLOR_3DSHADOW
                CPen pen(PS_SOLID, 1, GetSysColor(COLOR_3DLIGHT));
                CPen* oldPen = pDC->SelectObject(&pen);
                // 横线从行左到行右(当前可见区域)
                pDC->MoveTo(rcRow.left, y);
                pDC->LineTo(rcRow.right, y);
                pDC->SelectObject(oldPen);
            }
            *pResult = CDRF_SKIPDEFAULT;
            return;
        }
@@ -237,5 +311,3 @@
    *pResult = CDRF_DODEFAULT;
}