mrDarker
2025-08-26 43c7dc211f10851480352b12bd01f3443079bb01
SourceCode/Bond/Servo/CExpandableListCtrl.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,313 @@
#include "stdafx.h"
#include "CExpandableListCtrl.h"
IMPLEMENT_DYNAMIC(CExpandableListCtrl, CListCtrl)
CExpandableListCtrl::CExpandableListCtrl() {}
CExpandableListCtrl::~CExpandableListCtrl() {}
BEGIN_MESSAGE_MAP(CExpandableListCtrl, CListCtrl)
    ON_WM_CREATE()
    ON_NOTIFY_REFLECT(NM_CLICK, &CExpandableListCtrl::OnClick)
    ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &CExpandableListCtrl::OnCustomDraw)
END_MESSAGE_MAP()
int CExpandableListCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CListCtrl::OnCreate(lpCreateStruct) == -1)
        return -1;
    // æŠ¥è¡¨é£Žæ ¼åˆ—举例
    SetExtendedStyle(GetExtendedStyle()
        | 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)
{
    auto n = std::make_unique<Node>((int)max(1, (int)cols.size()));
    for (size_t i = 0; i < cols.size(); ++i) n->cols[i] = cols[i];
    n->level = 0;
    Node* raw = n.get();
    m_roots.emplace_back(std::move(n));
    return raw;
}
CExpandableListCtrl::Node* CExpandableListCtrl::InsertChild(Node* parent, const std::vector<CString>& cols)
{
    ASSERT(parent);
    auto n = std::make_unique<Node>((int)max(1, (int)cols.size()));
    for (size_t i = 0; i < cols.size(); ++i) n->cols[i] = cols[i];
    n->parent = parent;
    n->level = parent->level + 1;
    Node* raw = n.get();
    parent->children.emplace_back(std::move(n));
    return raw;
}
void CExpandableListCtrl::appendVisible(Node* n)
{
    m_visible.push_back(n);
    if (n->expanded) {
        for (auto& ch : n->children) {
            appendVisible(ch.get());
        }
    }
}
void CExpandableListCtrl::RebuildVisible()
{
    // 1) é‡å»ºå¯è§åºåˆ—
    m_visible.clear();
    for (auto& r : m_roots) appendVisible(r.get());
    // 2) é‡ç»˜/重填数据
    SetRedraw(FALSE);
    DeleteAllItems();
    // æ’入可见行
    for (int i = 0; i < (int)m_visible.size(); ++i) {
        Node* n = m_visible[i];
        LVITEM lvi{};
        lvi.mask = LVIF_TEXT;
        lvi.iItem = i;
        lvi.iSubItem = 0;
        lvi.pszText = const_cast<LPTSTR>((LPCTSTR)(n->cols.empty() ? _T("") : n->cols[0]));
        InsertItem(&lvi);
        for (int col = 1; col < GetHeaderCtrl()->GetItemCount(); ++col) {
            CString txt = (col < (int)n->cols.size()) ? n->cols[col] : _T("");
            SetItemText(i, col, txt);
        }
    }
    SetRedraw(TRUE);
    Invalidate();
}
void CExpandableListCtrl::Expand(Node* n)
{
    if (!n || n->children.empty()) return;
    if (!n->expanded) { n->expanded = true; RebuildVisible(); }
}
void CExpandableListCtrl::Collapse(Node* n)
{
    if (!n || n->children.empty()) return;
    if (n->expanded) { n->expanded = false; RebuildVisible(); }
}
void CExpandableListCtrl::Toggle(Node* n)
{
    if (!n || n->children.empty()) return;
    n->expanded = !n->expanded;
    RebuildVisible();
}
CExpandableListCtrl::Node* CExpandableListCtrl::GetNodeByVisibleIndex(int i) const
{
    if (i < 0 || i >= (int)m_visible.size()) return nullptr;
    return m_visible[i];
}
CRect CExpandableListCtrl::expanderRectForRow(int row) const
{
    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);
    if (!n || n->children.empty())
        return CRect(0, 0, 0, 0); // å¶å­ä¸å ä½ï¼Œæ–‡æœ¬å°±ä¸ä¼šè¢«å¤šæŽ¨ä¸€æ ¼
    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)
{
    LPNMITEMACTIVATE pia = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
    if (pia->iItem >= 0) {
        CPoint pt = pia->ptAction;
        // å‘½ä¸­å±•开按钮?
        CRect expRc = expanderRectForRow(pia->iItem);
        if (expRc.PtInRect(pt)) {
            Node* n = GetNodeByVisibleIndex(pia->iItem);
            if (n && !n->children.empty()) {
                Toggle(n);
            }
        }
    }
    *pResult = 0;
}
void CExpandableListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
    LPNMLVCUSTOMDRAW pCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
    switch (pCD->nmcd.dwDrawStage)
    {
    case CDDS_PREPAINT:
        *pResult = CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYSUBITEMDRAW;
        return;
    case CDDS_ITEMPREPAINT:
        *pResult = CDRF_NOTIFYSUBITEMDRAW;
        return;
    case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
    {
        const int row = (int)pCD->nmcd.dwItemSpec;
        const int col = pCD->iSubItem;
        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
        if (col == 0)
        {
            CRect rc; GetSubItemRect(row, 0, LVIR_LABEL, rc);
            Node* n = GetNodeByVisibleIndex(row);
            if (!n) { *pResult = CDRF_DODEFAULT; return; }
            // 1) èƒŒæ™¯/前景颜色:按是否选中
            const bool selected = (GetItemState(row, LVIS_SELECTED) & LVIS_SELECTED) != 0;
            const bool focusOnCtrl = (GetSafeHwnd() == ::GetFocus());
            COLORREF bk = selected ? GetSysColor(focusOnCtrl ? COLOR_HIGHLIGHT : COLOR_3DFACE)
                : ListView_GetBkColor(m_hWnd);
            COLORREF txt = selected ? GetSysColor(focusOnCtrl ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)
                : ListView_GetTextColor(m_hWnd);
            // ä»…在需要时填充背景(避免“黑一片”)
            CBrush bkBrush(bk);
            pDC->FillRect(rc, &bkBrush);
            // 2) å±•å¼€/折叠指示(参考旧项目的右对齐坐标法,做像素对齐,纯GDI)
            if (!n->children.empty())
            {
                CRect box = expanderRectForRow(row);
                // ---- å¯è°ƒå‚数:与旧代码命名一致 ----
                // å³ä¾§ç•™ç™½ï¼ˆä¸Žæ–‡æœ¬é—´éš™/网格线保持距离)
                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 {
                    // â–¶
                    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;
                }
                // ä»…填充,不描边(描边会加重台阶感);颜色用 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 * 14;
            const int baseLeft = rc.left + m_expanderPadding + indentPx;
            CRect textRc = rc;
            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;
        }
        // å…¶ä»–列默认绘制
        *pResult = CDRF_DODEFAULT;
        return;
    }
    }
    *pResult = CDRF_DODEFAULT;
}