65.9K
CodeProject 正在变化。 阅读更多。
Home

Win32 可编辑 TreeView 和 ListView 合二为一

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (58投票s)

2011 年 4 月 6 日

CPOL

6分钟阅读

viewsIcon

150018

downloadIcon

12053

一个自定义的 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 函数中以某种方式访问它。这可以通过将指针附加到父窗口并使用 SetPropGetProp 将其取回轻松实现;棘手的部分是设置多个实例并每次获取正确的实例。有关更多信息,请参阅内部函数 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:错误修复,界面更改,创建标志。
© . All rights reserved.