Win32 可编辑 TreeView 和 ListView 合二为一






4.93/5 (58投票s)
一个自定义的 Win32 树控件。
引言
有时需要将 TreeView 控件与 ListView 控件结合使用。一个控件可以展示展开的树形数据,同时带有网格线和可编辑的列。遗憾的是,这种控件并非 Win32 基本控件(包括 comctl32.lib 中的控件)的一部分。本文将展示如何扩展 TreeView 控件来满足这一需求。附带的源文件包含一个完整的 TreeList 控件源代码,可以轻松地用于许多项目。该项目完全用 C 语言编写,直接调用 Win32 API,不使用任何运行时库支持(如 MFC 等)。这样做的原因是希望尽可能快和独立。此外,它可以与用 C 语言编写的现有项目合并,而无需对代码进行特殊修改。
我假设您具备 Win32 原生 API 的先验知识,理解窗口的工作原理,以及自定义属主绘制控件的创建方式。由于我们要处理复杂的数据类型展示,因此还需要理解链表。
主要特点
- 多列
- 可编辑的节点
- 可以为每个节点设置背景色和文本颜色
- 可以为已修改的节点设置特殊的文本颜色
- 可以设置锚点,以便控件随父窗口自动调整大小
- API 只包含 4 个调用
此快照展示了四个控件实例附加到对话框窗口。
关于控件旧版本的注意事项
此版本可能将是最后一个版本。在开发过程中,我稍作了界面修改,因此请务必使用最新的源代码。如果发现任何关键错误,我将更新以反映修复,但界面将保持不变。
使用代码
控件编码在 TreeList.c 中,文档在 TreeList.h 中。包含的示例文件 Container.c 包含一个简单易懂的实现。API 是线程安全的,这意味着没有有问题性的静态元素,您可以创建多个实例。第一步是创建一个控件实例。这将分配其内部会话所需的内存,并创建控件所基于的 TreeView 和 Header 窗口。请注意,此调用将子类化您的父窗口并将所有消息路由到控件。
TREELIST_HANDLE TreeListCreate (
HINSTANCE Instance, // Application instance
HWND Hwnd, // The parent window handler
RECT *pRect, // the controls size, NULL will set it to full client size
DWORD dwFlags, // See TreeList.h 'Control creation flags' section
TREELIST_CB *pFunc); // a call back to validate the user edit requests (can be NULL)
返回值是控件的有效句柄。以下是编辑请求回调
typedef BOOL _stdcall TREELIST_CB(
const TREELIST_HANDLE, // The Handle to the control
const void *pAnyPtr, // Optional a user pointer that
// was added to the node (TreeListAddNode)
const char *NewData, // The data that is about to be added to the tree
char *Override); // Set this if you want to override
// the user request, and force something else
// If you return TRUE you accept the edit request, FALSE will reject it.
下一步是创建列;我们将通过调用来完成
TreeListError TreeListAddColumn (
TREELIST_HANDLE ListTreeHandle, // a valid handle (created with TreeListCreate)
char *szColumnName, // a null terminated string
int Width); // Column width in pixels
// Make sure to set TREELIST_LAST_COLUMN as the last column width parameter.
现在我们可以构建树。请注意,在第一次调用 TreeListAddNode
后,无法再添加更多列。我们将为添加的每个节点调用 TreeListAddNode
。
NODE_HANDLE TreeListAddNode (
TREELIST_HANDLE ListTreeHandle, // a valid handle (created with TreeListCreate)
NODE_HANDLE ParentHandle, // a handle to our parent node
// (NULL for a the first root node)
TreeListNodeData *RowOfColumns, // an array of TreeListNodeData
// structs (for each column)
int ColumnsCount); // The count of elements in RowOfColumns
// The return value is a handle to a node that will
// be used to create the next (siblings) nodes
以下是节点结构的描述;节点的所有属性,如数据编辑能力、颜色等,都通过填充此结构来设置。
struct tag_TreeListNodeData
{
char Data [TREELIST_MAX_STRING +1]; // The string to display
BOOL Editable; // Is it an editable cell?
BOOL Numeric; // Is it a numeric cell?
void *pExternalPtr; // a caller pointer, will be sent back
// along with the call back function.
BOOL Colored; // Are we using colors?
COLORREF BackgroundColor; // Cell background color
COLORREF TextColor; // Text color
COLORREF AltertedTextColor; // Color for edited text
BOOL Altered; // Internal - do not modify
long CRC; // Internal - do not modify
};
typedef struct tag_TreeListNodeData TreeListNodeData;
最后一个调用是 TreeListDestroy
;用正确的句柄调用它将释放为其分配的所有内存,并销毁与之关联的所有窗口对象(窗口、画刷等)。
int TreeListDestroy (TREELIST_HANDLE ListTreeHandle);
// a valid handle (created with TreeListCreate)
现在,在解释了控件的用法之后,我们可以安全地继续关注以下方面
在 TreeView 控件上绘制网格线
在用 CreateWindowEx
创建 TreeView 后,我们必须在 Windows 过程中处理其一些消息。大多数有趣的消息以 WM_NOTIFY
消息的形式传入。我们首先要做的是提取隐藏在 LPARAM
参数中的消息。为此,我们将 LPARAM
转换为 LPNMHDR
指针,并检查其 'code
' 成员,如下所示
case WM_NOTIFY:
{
lpNMHeader = (LPNMHDR)lParam;
switch(lpNMHeader->code)
{
下一步是响应 NM_CUSTOMDRAW
消息。通过这样做,我们可以干预控件的绘制过程并根据我们的需求进行扩展。再次,我们将 LPARAM
转换为 LPNMTVCUSTOMDRAW
指针,并检查其 nmcd.dwDrawStage
成员。控件的创建过程有几个阶段需要我们处理
CDDS_PREPAINT |
在绘制整个 ListView 控件之前。 |
CDDS_ITEMPREPAINT |
在绘制树中的某个项之前。 |
CDDS_ITEMPOSTPAINT |
在项绘制完成之后。 |
在每个阶段,我们都需要将 Windows 重定向到下一个阶段,目的是能够在 CDDS_ITEMPOSTPAINT
阶段执行一些工作。最后,当我们到达断点停在 CDDS_ITEMPOSTPAINT
的点时,我们可以添加水平和垂直网格线。nmcd
结构成员向我们提供了控件的 DC 和当前正在绘制的树项的句柄。通过结合使用 TreeView_GetItemRect()
、FillRect()
、DrawEdge()
、DrawText()
等调用,我们将绘制这些线条以及我们每个列的标签文本。
有关更多信息,请参阅 TreeLis.c\TreeListHandleMessages()
。
内部数据类型
此控件必须在内部存储一个动态树以及每个节点之间正确的关系。这是通过使用以下类型来实现的
static struct tag_TreeListNode
{
int NodeDataCount; // Count of items in pNodeData
HTREEITEM TreeItemHandle; // Handle to the tree item (windows)
struct tag_TreeListNode *pParennt; // Node pointer to the parent
struct tag_TreeListNode *pSibling; // Node pointer to a sibling
struct tag_TreeListNode *pBrother; // Node pointer to a brother
TreeListNodeData **pNodeData; // Array of NodeData structures
// for each column
};
typedef struct tag_TreeListNode TreeListNode;
每次添加新节点时,我们都会为该结构分配内存,并将其链接到其周围的节点(父节点和可能的兄弟节点)。每个节点代表树中的一个元素,但由于我们有列,它包含 **pNodeData
指针,该指针又被分配为保存我们节点所关联的列数组。
有关更多信息,请参阅 TreeList.c\TreeList_Internal_NodeAdd()
。
数据有效性检查
由于我们大量使用指针和动态分配的内存,因此我为数据类型添加了一个安全防护。每次将一个节点与另一个节点链接时,我都会使用 CRC 来验证其数据完整性。每次创建或修改节点时,都会计算其 CRC 值并将其附加到节点上。
有关更多信息,请参阅 TreeList.c\TreeList_Internal_CRCCreate()
和 TreeList.c\TreeList_Internal_CRCCheck()
。
多个实例
为了能够创建该控件的多个实例,我必须存储会话指针,并在 WNDPROC
函数中以某种方式访问它。这可以通过将指针附加到父窗口并使用 SetProp
和 GetProp
将其取回轻松实现;棘手的部分是设置多个实例并每次获取正确的实例。有关更多信息,请参阅内部函数 TreeList_Internal_DictGetPtr()
和 TreeList_Internal_DictUpdate()
。
示例代码(Container.c)和 API 用法
此示例文件创建一个对话框窗口,并通过在其窗口过程的 WM_INITDIALOG
消息中将其附加来将其放置在对话框之上,如下所示
INT_PTR CALLBACK WinWndProc(HWND hWndDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch (Msg)
{
case WM_INITDIALOG : // This is the place to start the control
{
// Control setup..
TreeListHandle = TreeListCreate(GetModuleHandle(NULL) ,hWndDlg,...);
如果您使用 CreateWidow()
创建宿主窗口,可以将 TreeList 调用放在 WM_CREATE
消息中。退出宿主窗口时,不要忘记通过调用 TreeListDestroy()
来释放控件的内存。
历史
- 版本 1.2:这是该控件的初始版本。
- 版本 1.3:添加:颜色和自动调整大小;修复:主要是小问题。
- 版本 1.4:添加:控件现在接受固定的
RECT
并锚定到其父窗口。 - 版本 1.5:添加:API 是线程安全的,并且可以创建该控件的多个实例。
- 版本 1.7:错误修复,界面更改,创建标志。