CRHTree - 一个拥有右侧展开/折叠和复选框的自绘 CTreeCtrl






4.76/5 (10投票s)
一个自绘的 CTreeCtrl,它在树的右侧边缘垂直对齐了复选框和展开/折叠控件,以便于查看,无论水平滚动条的位置如何。

引言
最近我需要一个树形展示,但标准的 CTreeCtrl
无法满足我的需求。我希望展开/折叠控件和复选框能够沿着树的一侧垂直对齐,而不是随着每个项目的缩进而前后曲折。为什么?嗯,如果你在做某种监控应用程序,我认为所有复选框都排成一条直线会更好,这样你就可以一目了然地评估当前状态。
我去了 The Code Project,查看了树形和列表控件,但没有找到符合我要求的,所以我意识到我必须自己写一个。通常,当这种情况发生时,我会先尝试改编一个标准的通用控件,但最终会从头开始编写整个控件,因为我不得不与默认行为进行过多的斗争。不过,在我最初搜索时,我遇到了 Jim Alsup 的 Vivid Tree。虽然不适合我的需求,但他的文章确实向我展示了,你只需劫持 OnPaint
处理程序并做自己的事情就可以完成很多工作。所以我深吸一口气,创建了 CTreeCtrl
的一个子类,并开始工作。
结果是 CRHTree
,它具有以下特点:
- 展开/折叠按钮已自定义,出现在树的最右侧,并具有微妙的悬停效果。
- 复选框 - 如果存在 - 也绘制在右侧。它们在可用时遵循 Vista/XP 的主题外观,否则会恢复到“经典”外观。
- 连接子节点到父节点的线按 usual 方式绘制。
- 水平滚动已进行调整,仅影响项目标题、图标和线条 - 复选框和展开/折叠控件始终保持在屏幕上,垂直对齐在右侧。
必备组件
在我们开始之前,请注意该控件使用 GDI+ 来处理 alpha 混合和其他绘图效果。XP 及以上的所有系统都默认支持此功能,但 Windows 2000 不支持。幸运的是,你可以从 Microsoft 这里 下载最新版本的 GDI+ DLL。如果该链接不起作用(感谢 Microsoft 再次重新组织其网站!),只需搜索“gdiplus redistributable”即可找到它。GDI+ DLL 的优点是安装非常简单。只需将其放入可执行文件所在的文件夹即可。请注意,为了减小下载大小,我没有在下载中包含 DLL。
此外,为了减轻 Vista/XP 主题的麻烦,我使用了 Rail Jon Rogut 文章 CHoverBitmapButton 中提供的非常有用的 CTheme
类。该类已包含在下载中。
最后,请注意,CRHTree
类及其示例项目是在 Visual Studio 2003 (VC 7.1?) 中编写的。我已经使用了 Stephane Rodriguez 的 VC7 -> VC6 转换器来提供 VC6 项目文件,但我不能保证该项目在 VC6 下实际可以构建。
Using the Code
使用 CRHTree
与使用标准 CTreeCtrl
的区别不大。你只需要在项目中添加一些额外的文件,如果你想让展开/折叠控件拥有与提供的不同的外观,还可以进行一些图形编辑。以下是涉及步骤的快速概述。
步骤 1 - 将文件和资源添加到项目中
你需要将 RHTree.h/.cpp、ResourceUtils.h/.cpp、Theme.h/.cpp 和 ThemeLib.h 添加到你的项目中。
你还需要将四个展开/折叠 PNG 文件(两个标准,两个“热”)作为自定义 PNG 资源导入,ID 分别为 IDR_COLLAPSE
、IDR_COLLAPSEHOT
、IDR_EXPAND
和 IDR_EXPANDHOT
。要了解我为什么选择使用 PNG 文件,请查看下面的“兴趣点”部分。
步骤 2 - 连接到 GDI+
在你的 stdafx.h 中包含 gdiplus.h,并在项目的链接器输入部分添加 gdiplus.lib。
此外,请务必在你的应用程序的 InitInstance
和 ExitInstance
方法中初始化和取消初始化该库。
// Initialize GDI+ Library
Gdiplus::GdiplusStartup(&m_gdiToken, &m_gdiStartup, NULL);
取消初始化
// Shutdown GDI+ Library
if ( 0 != m_gdiToken )
Gdiplus::GdiplusShutdown( m_gdiToken );
步骤 3 - 使用 CRHTree 代替 CTreeCtrl
在你的项目中,根据需要将 CTreeCtrl
改为 CRHTree
。例如,如果你已将树控件添加到对话框资源,请使用类向导将树连接到 CTreeCtrl
变量,然后编辑对话框的头文件,将 CTreeCtrl
替换为 CRHTree
(并在此过程中添加 #include "RHTree.h"
)。
你可以通过更改树本身的样式来打开/关闭线条和复选框,就像使用普通的 CTreeCtrl
一样。但请注意,CRHTree
会忽略 TVS_HASBUTTONS
样式 - 它总是绘制展开/折叠控件。
同样,项目图标的处理方式与 CTreeCtrl
类似 - 创建并填充图像列表,将图像列表分配给树,并在插入到树中的每个项目时指定图像索引。
步骤 4 - 可选自定义
CRHTree
的大部分绘图被分解成小的、虚拟的函数,因此可以轻松更改树的外观。展开/折叠按钮(控件)目前的外观来自四个 PNG 文件(带有用于透明度的 alpha 通道),但你也可以轻松地切换使用图标,甚至在代码中绘制整个控件。
要考虑更改或覆盖的关键函数包括:
IsGroup |
当前,它只返回 TRUE 如果被检查的项目是一个父项。但在我自己的项目中,我覆盖了它,以便即使没有子项的项目也带有展开/折叠按钮。 |
GetPartColor |
项目和组(父项)的标题和背景颜色在此处选择。 |
DrawBackground |
绘制控件的主背景。 |
DrawWidget |
绘制展开/折叠按钮。 |
DrawGroupWash |
绘制组(父项)项目的背景。 |
DrawGroupTitle |
绘制组(父项)项目的标题。 |
DrawItemTitle |
绘制标准(非父项)项目的标题。 |
关注点
本节讨论了我在构建该类时遇到的一些问题,并指出了我使用的一些编码技巧和技术,这些技巧和技术可能对其他项目有所帮助,无论你是否想使用 CRHTree
。
调整项目矩形
当树正确绘制时,我遇到的第一个问题是 CTreeCtrl::GetItemRect
返回的文本矩形在水平方向上通常是错误的。这不足为奇,因为展开/折叠按钮和复选框已经换了位置!
我写了一个函数来解决这个问题,名为 OffsetTextRect
。基本上,它测量树中第一个根节点的文本矩形和整个项目矩形之间的水平差异。它使用此值作为树中每个项目的基本向左偏移量,但也会考虑当前的水平滚动位置。
void CRHTree::OffsetTextRect( CRect& rText )
// Depending on the styles that are set, the text rectangle might need
// to have its left edge adjusted, since the underlying control will make
// room for items that are normally on the left (+/- button, checkbox)
{
CRect rRoot;
CRect rRootText;
//
if ( this->GetItemRect(this->GetRootItem(), rRoot, FALSE) &&
this->GetItemRect(this->GetRootItem(), rRootText, TRUE) )
{
rText.left = m_nHSpacer +
max(0, rText.left - (rRootText.left - rRoot.left))
- this->GetScrollPos(SB_HORZ);
}
if ( this->GetImageList(TVSIL_NORMAL) )
rText.left += ::GetSystemMetrics(SM_CXSMICON) + m_nHSpacer;
}
修复水平滚动/大小调整后的绘图
当我在示例应用程序中添加了许多项目并使窗口可调整大小时,下一个主要问题变得显而易见。标准的树控件执行绘图优化,其基础是认为每个树项目的整个区域在滚动时都会受到影响。但在我的树中,我希望复选框和展开/折叠按钮保持在控件的右侧,而树的其余部分正常滚动。
我尝试了各种“聪明”的方法来解决这个问题,但最终还是暴力破解获胜。
void CRHTree::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
// Since we've arranged our tree differently, the standard ctrl won't
// get things right...
{
// Inhibit standard drawing...
this->SetRedraw( FALSE );
// Handle the scroll normally...
CTreeCtrl::OnHScroll(nSBCode, nPos, pScrollBar);
// Re-enable drawing..
this->SetRedraw( TRUE );
// Redraw the whole thing thoroughly in one pass...
CRect rClient;
this->GetClientRect( rClient );
this->RedrawWindow( rClient, NULL,
RDW_NOCHILDREN | RDW_UPDATENOW | RDW_INVALIDATE);
}
OnSize
的处理程序非常相似。
使用 GDI+
这属于“对其他项目有用”的类别。
只要你拥有必要的头文件和库(对于 VC6 用户来说可能是一个问题),GDI+ 就是以最小的努力获得复杂效果的绝佳方式。在 CRHTree
中,它用于提供漂亮的 alpha 混合选择效果,并处理具有 alpha(透明度)通道的 PNG 的绘制。
这是 CRHTree::DrawItem
的一个片段,展示了选择效果的绘制。
if ( bSelected )
{
//
// GDI+ lets us easily paint a transparent colored layer over the item
//
// Let's have our GDI+ graphics wrapper...
Gdiplus::Graphics gfx( pDC->GetSafeHdc() );
// Define the hilite color, but reduce the alpha for a blended look
COLORREF crHi = ::GetSysColor(COLOR_HIGHLIGHT);
// Note the alpha - 100 (255 is opaque)
Gdiplus::Color clrSel( 100,
GetRValue(crHi),
GetGValue(crHi),
GetBValue(crHi) )
// Define a simple brush with this color...
Gdiplus::Rect rGPItem( rDraw.left, rDraw.top,
rDraw.Width(), rDraw.Height());
Gdiplus::SolidBrush brushFill( clrSel );
// Apply it
gfx.FillRectangle( &brushFill, rGPItem );
}
所有这些 PNG 图标是怎么回事?
嗯,就在我写下这句话的时候,我预感会有人评论说:“你为什么要这样做?你只需要……”
无论如何,这是关于 PNG 文件的故事。
我希望我的展开/折叠控件拥有漂亮的光滑边缘。这意味着抗锯齿,而抗锯齿基本上需要 alpha 通道。现在,XP 支持带有 alpha 通道的图标,但更早的操作系统不支持。至少我这么认为。所以一个简单的图标不够。
现在,我已经决定使用 GDI+,而 GDI+ 可以加载和绘制多种格式,包括带 alpha 通道的 PNG。所以,我没有使用图标,而是可以使用 PNG。太棒了。但 PNG 在大型列表和大量父项中绘制可能会有点慢。
我找到的解决方案是:
- 创建一个屏幕外 DC,并将其绘制成与父项(组)项目当前使用的背景色相匹配。
- 让 GDI+ 加载并将抗锯齿图像绘制到屏幕外画布上。
- 将生成的位图放入图像列表中,并由此创建图标。
- 使用该图标进行快速漂亮的绘制,但如果颜色选择发生变化则重新构建它。
这段代码在 CRHTree::PrepareWidgetIcons
中。
将资源解压到文件
当我刚开始处理绘制展开/折叠按钮时(见上文),我只将 *.png 文件放在可执行文件旁边。这也可以,但我认为将它们直接放在可执行文件中会更整洁。将它们作为自定义类型“PNG”导入资源非常简单,但 GDI+ 似乎对此类形式的它们不太感兴趣。所以我写了一组 static
例程在 CR
中将资源解压到一个临时文件。我认为这些本身就非常有用。esourceUtils
致谢
再次感谢 Rail Jon Rogut 提供的有用的 CTheme
类,以及 Jim Alsup,他的“Vivid Tree”文章给了我信心继续将 CTreeCtrl
作为基础类。
历史
- 2007 年 8 月 6 日:第一个版本
- 2007 年 8 月 22 日:第二个版本
主要更改
- 添加了
OnMouseWheel
和EnsureVisible
重载,以确保在发生水平滚动的其他情况下进行绘制。 - 添加了工具提示功能,用于消除复选框和展开/折叠控件的文本。
- 添加了