| | |
| | | if (CListCtrl::OnCreate(lpCreateStruct) == -1) |
| | | return -1; |
| | | |
| | | // 报表风格列举例 |
| | | SetExtendedStyle(GetExtendedStyle() |
| | | | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER); |
| | | |
| | |
| | | |
| | | void CExpandableListCtrl::PreSubclassWindow() |
| | | { |
| | | // 报表风格列举例 |
| | | SetExtendedStyle(GetExtendedStyle() |
| | | | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER); |
| | | |
| | | CListCtrl::PreSubclassWindow(); |
| | | } |
| | | |
| | | // ===== 树 API ===== |
| | | CExpandableListCtrl::Node* CExpandableListCtrl::InsertRoot(const std::vector<CString>& cols) |
| | | { |
| | | auto n = std::make_unique<Node>((int)max(1, (int)cols.size())); |
| | |
| | | |
| | | 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.pszText = const_cast<LPTSTR>((LPCTSTR)(n->cols.empty() ? _T("") : n->cols[0])); |
| | | InsertItem(&lvi); |
| | | |
| | | for (int col = 1; col < GetHeaderCtrl()->GetItemCount(); ++col) { |
| | | const int colCount = GetHeaderCtrl() ? GetHeaderCtrl()->GetItemCount() : 1; |
| | | for (int col = 1; col < colCount; ++col) { |
| | | CString txt = (col < (int)n->cols.size()) ? n->cols[col] : _T(""); |
| | | SetItemText(i, col, txt); |
| | | } |
| | | } |
| | | |
| | | // 重建后,按行号颜色数组对齐 |
| | | m_rowColors.resize(GetItemCount()); |
| | | |
| | | SetRedraw(TRUE); |
| | | Invalidate(); |
| | | } |
| | |
| | | return m_visible[i]; |
| | | } |
| | | |
| | | // ===== 颜色 API ===== |
| | | void CExpandableListCtrl::SetNodeColor(Node* n, COLORREF text, COLORREF bk) |
| | | { |
| | | if (!n) return; |
| | | RowColor rc{}; |
| | | rc.text = text; rc.bk = bk; |
| | | rc.hasText = (text != CLR_DEFAULT); |
| | | rc.hasBk = (bk != CLR_DEFAULT); |
| | | m_colorByNode[n] = rc; |
| | | |
| | | for (int i = 0; i < (int)m_visible.size(); ++i) { |
| | | if (m_visible[i] == n) { |
| | | RedrawItems(i, i); |
| | | UpdateWindow(); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | |
| | | void CExpandableListCtrl::ClearNodeColor(Node* n) |
| | | { |
| | | if (!n) return; |
| | | auto it = m_colorByNode.find(n); |
| | | if (it != m_colorByNode.end()) { |
| | | m_colorByNode.erase(it); |
| | | for (int i = 0; i < (int)m_visible.size(); ++i) { |
| | | if (m_visible[i] == n) { |
| | | RedrawItems(i, i); |
| | | UpdateWindow(); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | void CExpandableListCtrl::ClearAllColors() |
| | | { |
| | | m_colorByNode.clear(); |
| | | m_rowColors.clear(); |
| | | Invalidate(FALSE); |
| | | } |
| | | |
| | | // 兼容旧接口:按“可见行号”着色 |
| | | void CExpandableListCtrl::SetItemColor(DWORD_PTR iItem, COLORREF TextColor, COLORREF TextBkColor) |
| | | { |
| | | SetItemColorByVisibleIndex((int)iItem, TextColor, TextBkColor); |
| | | } |
| | | void CExpandableListCtrl::SetItemColorByVisibleIndex(int row, COLORREF text, COLORREF bk) |
| | | { |
| | | if (row < 0) return; |
| | | if (row >= (int)m_rowColors.size()) |
| | | m_rowColors.resize(row + 1); |
| | | |
| | | RowColor rc{}; |
| | | rc.text = text; rc.bk = bk; |
| | | rc.hasText = (text != CLR_DEFAULT); |
| | | rc.hasBk = (bk != CLR_DEFAULT); |
| | | m_rowColors[row] = rc; |
| | | |
| | | RedrawItems(row, row); |
| | | UpdateWindow(); |
| | | } |
| | | |
| | | CRect CExpandableListCtrl::expanderRectForRow(int row) const |
| | | { |
| | | CRect rcLabel; |
| | |
| | | |
| | | Node* n = const_cast<CExpandableListCtrl*>(this)->GetNodeByVisibleIndex(row); |
| | | if (!n || n->children.empty()) |
| | | return CRect(0, 0, 0, 0); // 叶子不占位,文本就不会被多推一格 |
| | | return CRect(0, 0, 0, 0); |
| | | |
| | | const int indent = n->level; |
| | | const int left = rcLabel.left + m_expanderPadding + indent * 16; |
| | |
| | | ); |
| | | } |
| | | |
| | | // 颜色计算:优先 Node*,其次行号;若需要则让系统高亮覆盖 |
| | | void CExpandableListCtrl::computeColorsForRow(int row, COLORREF& outText, COLORREF& outBk) const |
| | | { |
| | | outText = ListView_GetTextColor(const_cast<CExpandableListCtrl*>(this)->m_hWnd); |
| | | outBk = ListView_GetBkColor(const_cast<CExpandableListCtrl*>(this)->m_hWnd); |
| | | |
| | | const bool selected = (const_cast<CExpandableListCtrl*>(this)->GetItemState(row, LVIS_SELECTED) & LVIS_SELECTED) != 0; |
| | | const bool focusOnCtrl = (const_cast<CExpandableListCtrl*>(this)->GetSafeHwnd() == ::GetFocus()); |
| | | |
| | | if (m_preserveSelHighlight && selected) { |
| | | outBk = GetSysColor(focusOnCtrl ? COLOR_HIGHLIGHT : COLOR_3DFACE); |
| | | outText = GetSysColor(focusOnCtrl ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT); |
| | | return; |
| | | } |
| | | |
| | | // Node* 颜色 |
| | | if (Node* n = const_cast<CExpandableListCtrl*>(this)->GetNodeByVisibleIndex(row)) { |
| | | auto it = m_colorByNode.find(n); |
| | | if (it != m_colorByNode.end()) { |
| | | if (it->second.hasText) outText = it->second.text; |
| | | if (it->second.hasBk) outBk = it->second.bk; |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 行号颜色 |
| | | if (row >= 0 && row < (int)m_rowColors.size()) { |
| | | const RowColor& rc = m_rowColors[row]; |
| | | if (rc.hasText) outText = rc.text; |
| | | if (rc.hasBk) outBk = rc.bk; |
| | | } |
| | | } |
| | | |
| | | 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); |
| | |
| | | return; |
| | | |
| | | case CDDS_ITEMPREPAINT: |
| | | { |
| | | const int row = (int)pCD->nmcd.dwItemSpec; |
| | | COLORREF txt, bk; |
| | | computeColorsForRow(row, txt, bk); |
| | | pCD->clrText = txt; |
| | | pCD->clrTextBk = bk; |
| | | *pResult = CDRF_NOTIFYSUBITEMDRAW; |
| | | return; |
| | | } |
| | | |
| | | case CDDS_ITEMPREPAINT | CDDS_SUBITEM: |
| | | { |
| | |
| | | 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; |
| | | // 如果没有树节点(纯平列表),首列也走默认绘制 |
| | | Node* n = GetNodeByVisibleIndex(row); |
| | | if (col != 0 || !n) { |
| | | *pResult = CDRF_DODEFAULT; |
| | | return; |
| | | } |
| | | |
| | | // 其他列默认绘制 |
| | | *pResult = CDRF_DODEFAULT; |
| | | // 首列自绘(树模式) |
| | | CRect rc; GetSubItemRect(row, 0, LVIR_LABEL, rc); |
| | | |
| | | COLORREF bk, txt; |
| | | computeColorsForRow(row, txt, bk); |
| | | |
| | | CBrush bkBrush(bk); |
| | | pDC->FillRect(rc, &bkBrush); |
| | | |
| | | if (!n->children.empty()) |
| | | { |
| | | CRect box = expanderRectForRow(row); |
| | | const int ROFFSET = 2; |
| | | 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); |
| | | |
| | | 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; |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | | 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; |
| | | 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); |
| | | |
| | | if (GetExtendedStyle() & LVS_EX_GRIDLINES) |
| | | { |
| | | CRect rcRow; GetSubItemRect(row, 0, LVIR_BOUNDS, rcRow); |
| | | const int y = rcRow.bottom - 1; |
| | | 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; |
| | | } |
| | | |
| | | // 兼容行为:同步 SetItemText 到 Node->cols;维护行号颜色数组的插入/删除 |
| | | LRESULT CExpandableListCtrl::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) |
| | | { |
| | | // 同步 SetItemText 到 Node(A/W 兼容) |
| | | if (message == LVM_SETITEMTEXT |
| | | #ifdef LVM_SETITEMTEXTA |
| | | || message == LVM_SETITEMTEXTA |
| | | #endif |
| | | #ifdef LVM_SETITEMTEXTW |
| | | || message == LVM_SETITEMTEXTW |
| | | #endif |
| | | ) |
| | | { |
| | | int row = static_cast<int>(wParam); |
| | | LVITEM* p = reinterpret_cast<LVITEM*>(lParam); |
| | | if (p) { |
| | | Node* n = GetNodeByVisibleIndex(row); |
| | | if (n) { |
| | | int sub = p->iSubItem; |
| | | if (sub >= (int)n->cols.size()) |
| | | n->cols.resize(sub + 1); |
| | | CString newText = p->pszText ? p->pszText : _T(""); |
| | | n->cols[sub] = newText; |
| | | } |
| | | } |
| | | // 继续交给基类处理 |
| | | } |
| | | |
| | | LRESULT nRet = CListCtrl::WindowProc(message, wParam, lParam); |
| | | |
| | | // 维护行号颜色数组(兼容旧 SetItemColor) |
| | | if (message == LVM_INSERTITEM) { |
| | | if (nRet != -1) { |
| | | LVITEM* p = (LVITEM*)lParam; |
| | | int pos = p ? p->iItem : (int)nRet; |
| | | if (pos < 0) pos = (int)nRet; |
| | | if (pos > (int)m_rowColors.size()) pos = (int)m_rowColors.size(); |
| | | m_rowColors.insert(m_rowColors.begin() + pos, RowColor{}); // 默认色 |
| | | } |
| | | } |
| | | else if (message == LVM_DELETEITEM) { |
| | | if (nRet != 0) { |
| | | int pos = (int)wParam; |
| | | if (pos >= 0 && pos < (int)m_rowColors.size()) |
| | | m_rowColors.erase(m_rowColors.begin() + pos); |
| | | } |
| | | } |
| | | else if (message == LVM_DELETEALLITEMS) { |
| | | if (nRet != 0) { |
| | | m_rowColors.clear(); |
| | | } |
| | | } |
| | | |
| | | return nRet; |
| | | } |
| | | |
| | | // CExpandableListCtrl.cpp 里 |
| | | void CExpandableListCtrl::ClearTree() |
| | | { |
| | | // 清数据 |
| | | m_roots.clear(); |
| | | m_visible.clear(); |
| | | |
| | | // 清可见项(务必!否则旧页的行会残留) |
| | | SetRedraw(FALSE); |
| | | DeleteAllItems(); |
| | | SetRedraw(TRUE); |
| | | |
| | | Invalidate(); |
| | | } |
| | | |
| | | |