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

另一个自定义树控件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.92/5 (9投票s)

2002年12月3日

7分钟阅读

viewsIcon

123405

downloadIcon

1782

允许“每个项目样式” 且不需要位图数组的树控件

Sample Image - CustomTreeImage.png

引言

我正在开发一个项目,需要一个“复选树”类型的控件。然而,在这个应用程序中,在每个树节点上都放置一个复选框似乎不是最佳方案。因此,在几次尝试失败后,我最终实现了一个完全由所有者绘制的树控件。我发现这样的控件并不多见。而且,一旦开始,我就开始对我的创作有些“得寸进尺”了。最终,我选择的方法允许“混合元素”同时存在于树中。这个类似乎足够新颖,值得快速提交给Code Project。所以,开始了。

入门

我包含了一个简单的基于对话框的应用程序来演示如何使用这个控件类。这并不复杂,所以我不会详细介绍,除了解释它是如何设置树控件本身的。首先,参考CCustomTreeDlg::OnInitDialog()(在文件:CustomTreeDlg.cpp 中),你可以看到树是通过调用以下方法来初始化的:

  CreateTreeControl();
  FillTreeControl();

现在,深入研究这两个方法,你会发现CreateTreeControl()所做的就是创建控件本身。我选择这种方法是为了让我能够有更多的控制权,可以在不依赖资源编辑器的情况下尝试各种树控件的窗口样式。这个方法中唯一不明显的地方是我们测量了对话框上一个隐藏的静态控件的矩形,并使用它的矩形作为树控件的放置位置。

接下来,在CCustomTreeDlg::FillTreeControl()方法中,你会看到有几个对HTREEITEM CCustomTreeCtrl::AddNode( HTREEITEM hParent, HTREEITEM hInsertAfter, const char* pchLabelText, int iStyle );的调用。这个方法中唯一不完全自解释的参数是iStyle参数。这个参数的值决定了将被插入到树中的节点的类型,因此,很好地理解这个参数的含义非常重要。参考CustomTreeControl.h,你会发现定义了以下常量:

// item styles
//
const int ciEditable    = 1;  // means the element will be editable
const int ciCheckedNode = 2;  // means the element 
    //will be rendered as a check box
const int ciRadioNode   = 4;  // means the element will 
    //be rendered as a radio button
const int ciSelectable  = 8;  // means the element can be 
    //rendered as a selected node

这些样式可以进行逻辑“或”运算,以获得所需的节点类型。请注意,在此版本中,没有进行健全性检查,并且有可能将一个节点同时定义为单选按钮和复选框。这个问题我留给感兴趣的人去解决。

所以,回到CCustomTreeDlg::FillTreeControl(),你会看到几种不同的节点类型被添加到树中。其结果正如你在上面的截图中所见。

占用数据空间

我选择将每个元素的状体和样式数据存储在每个元素的ItemData值中。这种方法对于当前方案来说非常快捷,但如果你需要为每个树节点存储应用程序数据,那么你需要重新考虑这种方法,要么想出另一个数据映射,要么修改我如何使用ItemData来存储节点样式和状态。我通过调用MAKEWPARAM( state, style )将每个树元素的样式和状态混合到一个单独的DWORD中。现在,要处理元素的样式和/或状态,我需要解压这些值。因此,你会看到这样的代码:

        DWORD dwData    = GetItemData( hTreeItem );
        UINT  iStyle    = HIWORD( dwData );
        UINT  iState    = LOWORD( dwData );

CCustomTreeCtrl类的几个方法中。此外,如果我需要更改状态值,则会调用如下代码:

        DWORD dwData    = GetItemData( hTreeItem );
        UINT  iStyle    = HIWORD( dwData );
        UINT  iState    = LOWORD( dwData );

        // mess around with the state data

        // store it
        SetItemData( hTreeItem, MAKEWPARAM( iState, iStyle ) );

现在,所有对元素样式进行的特定更改都只是基于这个主题的简单变体。

模拟通用控件。

这个树可以包含“复选框”和“单选按钮”节点。复选框很简单,它们只是选中和取消选中。然而,由于我选择在这个树中包含“单选按钮节点”,我需要一种“有意义”的方式来处理它们。所以,我选择了以下方法:

  1. 所有作为兄弟节点的单选按钮节点将互斥。换句话说,在一组具有共同父节点的单选按钮节点中,一次只能选中一个。如果一个单选按钮节点被选中,那么它所有的兄弟单选按钮都必须被取消选中。
  2. 如果一个单选按钮节点被取消选中,那么它所有的子节点都会变得不活跃。

执行此操作的代码从方法:CCustomTreeCtrl::ChangeItemState( HTREEITEM hItem )开始。此方法首先检查/取消选中节点。然后,如果节点被选中,则所有子节点都会被启用。

这项工作是通过递归方法:CCustomTreeCtrl::EnableSubTree( HTREEITEM hTreeItem, bool bEnable )完成的。在此工作完成后,将访问新选中节点的所有兄弟节点。如果一个兄弟节点也是单选按钮,那么它将被取消选中,并且它的所有子节点都将被禁用,同样通过调用CCustomTreeCtrl::EnableSubTree(HTREEITEM hTreeItem, bool bEnable)方法。

看!没有位图数组

出于各种原因,我选择使用操作系统图标来渲染树元素。这为我省去了创建一套用于各种元素状态和样式的位图的麻烦。它还允许将此控件集成到任何项目中,而用户无需记住将位图添加到项目中。它也使得渲染元素变得非常容易,因为我可以调用DrawFrameControl来放置图标,而不是手动放置位图和/或处理大量的CDC方法。很简单。要了解其工作原理,请参阅OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)方法中的以下片段:

   // if this node has kids, then must put up an indicator for that
   //
   if ( ItemHasChildren( hTreeItem ) )
   {
     if ( !bExpanded )
     {
       poDC->DrawFrameControl( oButton, 
                               DFC_SCROLL,
                               DFCS_SCROLLRIGHT ); 
     }
     else
     {
       poDC->DrawFrameControl( oButton, 
                               DFC_SCROLL,
                               DFCS_SCROLLDOWN | DFCS_PUSHED ); 
     }
   }

        .......

   if ( iStyle & ciCheckedNode || iStyle & ciRadioNode )
   {
     CRect oCtrlRect( oRect.left, 
                      oRect.top, 
                      oRect.left + oRect.Height(), 
                      oRect.bottom );
     bool  bChecked  = ( 0 != ( iState & ciChecked ) );

     DWORD dwButton   = ( iStyle & ciCheckedNode ) 
                        ? DFCS_BUTTONCHECK : DFCS_BUTTONRADIO;
     DWORD dwChecked  = ( bChecked )               ? DFCS_CHECKED     : 0;
     DWORD dwInactive = ( iState & ciDisabled )    ? DFCS_INACTIVE    : 0;
        
     poDC->DrawFrameControl( oCtrlRect, 
                             DFC_BUTTON,
                             dwButton | dwChecked | dwInactive ); 
     pxRects->m_oCheckRect = oCtrlRect;
     oRect.left += oCtrlRect.Width() + 4;
   }

在这些方法中,最有可能引起争议的是我选择使用滚动按钮来指示节点的展开状态。我喜欢它的外观,但这纯粹是主观的,我确信其他人的意见可能不同。但对我来说,它起作用了。

所有者绘制的树

现在,渲染节点元素非常简单。只需从左到右工作,根据需要绘制每个“子部分”元素。实际上,这非常简单。但自定义绘制树的真正技巧是在OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)CDDS_ITEMPREPAINT处理程序中返回CDRF_SKIPDEFAULT。我看到的大多数示例都返回CDRF_DODEFAULT。然而,要实现完全的所有者绘制,你需要禁用基类的所有绘制,为此,你必须返回CDRF_SKIPDEFAULT。所以,如果你需要在树中进行任何类型的超额绘制,这是你需要了解的关键点。

待办事项...

没有代码是真正“完成”的,直到它被丢弃。话虽如此,如果你决定使用这个控件,还有一些待解决的问题可能需要处理。

  1. 有时,就地编辑控件的位置似乎与正在编辑的项目相对于不太好。
  2. 在这个版本中没有渲染“树线”。我不需要它,所以我没有尝试解决这个问题。然而,其他人可能没那么幸运。
  3. 如果你需要存储每个树元素的应用程序数据,那么你需要修改我存储项状态和样式的方式,和/或想出另一种方法来将应用程序数据映射到树元素。
  4. 对元素样式的健全性检查将是一个简单而好的修复。
  5. 修复我尚未意识到的bug。

继续

这是一个有趣的项目,实现所有者绘制的树并非易事。因此,我希望我在这里介绍的一些技巧和技术可能对Code Project空间中的一些人有所帮助。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.