#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;
|
}
|