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

多功能树控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (15投票s)

2013 年 1 月 30 日

CPOL

12分钟阅读

viewsIcon

83269

downloadIcon

6037

带有自定义复选框和其他功能的树控件。

引言

很久以前,我需要为树控件项添加复选框。那又如何?树控件具有启用复选框的功能,我还需要什么?我以不同的方式需要它。

  • 我希望能够仅为特定项添加复选框。
  • 能够动态添加/删除复选框。
  • 能够启用/禁用项。

除了上述功能外,我还添加了一些我认为会非常有用的功能。

  • 使用橡皮筋选择多选树控件项
  • 将多选项拖放到目标上。
  • 指定项是否可被视为有效的放置目标。
  • 如果需要,限制项的重命名功能。
  • 指定项是否为放置操作的有效目标。
  • 为每个树项附加菜单的功能。

背景

为特定项添加复选框

CTreeControl 具有为项添加复选框的功能,但启用该样式会为所有项创建复选框。我只想为指定的项添加复选框。

网络上有很多自定义控件实现了上述功能。它们大多使用图像来模仿复选框项。会有一个类似复选框的图像来模仿复选框。单击该图像将切换复选框的状态 [使用两个图像:选中图像和未选中图像]。

我发现上述实现可能无法满足我,因为我希望项有图像 [如果项属于一个公司,那么公司的徽标必须与该项一起] 以及复选框。如果复选框是用图像模仿的,那么我如何表示项的图像?

解决方案是,将项的图像 [例如 16x16] 与复选框图像 [16/16] 结合起来 -> [32x16]。我们可以生成所有项的组合图像,并将其添加到图像列表中。

但在我的情况下,并非所有项都会有复选框。所以有些图像可能没有组合图像 [在这种情况下,图像将是 16x16],而有些可能有 [图像将是 32x16]。但是图像列表不支持可变大小的图像。

解决方案

为了克服可变大小图像列表的问题,我为所有项生成了组合图像,而不管该项是否有复选框。

  1. 对于具有复选框的项,会生成组合图像 [复选框图像 + 项图像] 并添加。
  2. 对于没有复选框的项,会创建一个空白图像并与其项图像组合,以便图像的大小为 32x16。

动态添加/删除复选框

我们如何在运行时添加复选框?

解决方案

在此之前,我想先了解用于表示项的各种图像。我将项的相同图像用于表示其选中状态。我的意思是,我没有单独的图像来表示选中状态。假设如此,每个项可以有六个关联的图像。让我们看看它们是什么

  1. 普通图像:用于表示已折叠且没有复选框的项的图像 []。
  2. 展开图像:用于表示已展开且没有复选框的项 []。
  3. 复选框已选中且已折叠:用于表示已折叠且复选框已选中的项 []。
  4. 复选框未选中且已折叠:用于表示已折叠且复选框未选中的项 [] 。
  5. 复选框已选中且已展开:用于表示已展开且复选框已选中的项 []。
  6. 复选框未选中且已展开:用于表示已展开且复选框未选中的项 []。

所有六个图像都表示一个启用的项。这些图像在项插入树中并添加到图像列表时生成。启用复选框时,设置图像列表中组合图像 [复选框 + 项图像] 的索引。

动态启用/禁用项

现在的问题是如何动态启用/禁用树控件项。

解决方案

在禁用项时,会创建一个新的位图,通过将当前项图像变灰来实现,同时保持透明度,并添加到图像列表中,例如 []。此图像被添加到图像列表,并将索引设置为树控件的图像。

这就是事情的总体情况!详细信息将在呈现代码时给出。

实现细节

我们在查看代码时深入探讨了更多实现细节。我从 `CTreeCtrl` 派生了一个树控件,名为 `CCustomTreeCtrl`。我有一个对话框,树控件就放置在该对话框上。此对话框有两个编辑框,用于列出选定的项和勾选的项。因此,每当选定项/勾选项列表发生更改时,树控件会通知对话框,对话框会填充编辑框。我将在此处解释重要函数和消息处理程序的概述。代码包含所有函数的详细注释。我们现在将更多地了解 `CCustomTreeCtrl`。

如何插入项?

首先,让我们看看如何将项插入到树中。由于我们在插入项时可以定义项的各种属性,例如项的颜色、粗体字体、放置操作的有效目标、项是否有复选框、限制重命名功能等,因此我们需要一个自定义插入结构。

//CUSTOM DATA WHICH IS ASSOCIATED WITH EACH TREE ITEM.THIS CONTAIN LOT OF INFORMATION
//OF THE ITEMS SUCH AS ITS NORMAL IMAGE,EXPANDED IMAGE,MASK FOR THEM,CHECKBOX IS REQUIRED ETC
typedef struct __CUSTOMITEMDATA
{
 
 //EXPOSED DATA: TO BE SET BY THE USER.
 //####################################

  //Resource of the images for normal and expanded states.Remember these are the bitmap
  //ID's and not the indice.
  UINT    m_uNormalImage;      //Image given when the item is in normal state
  UINT    m_uNormalMaskImage;   //Mask Image for the normal state
  UINT    m_uExpandedImage;     //Image Given when the item is expanded
  UINT    m_uExpandedMaskImage;  //Mask image for the expanded state
  COLORREF  m_cMaskColor;       //If the mask image is not given this can be considered as the mask color
  
  bool     m_bEditable;      //Flag to indicate whether the item is editable or not
  bool     m_bDropTarget;    //Is the item is valid target for dropping operation
  bool     m_bChecked;      //Whether the item has to have CHECKBOX or NOT
  bool     m_bCheckState;     //If checkbox is needed, state of it [CHECKED/UNCHECKED]
  COLORREF  m_cItemColor;     //Color of the text of the item
  bool     m_bIsBold;       //Item to be displayed in bold font
  UINT    m_uMenuID;      //Context menu id given to the item
  bool     m_bEnable;       //Enable or disable tree item
  bool     m_bExpStateBefDisable; //True if the item is in expanded state before disabling, false otherwise

 
 //INTERNAL DATA: DATA NOT EXPOSED TO USER.INERNALLY USED BY THE TREE
 //####################################################################

  //Indice of the images representing various state from the image list
  //-------------------------------------------------------------------
   //Normal State with NO CHECKBOX
   int m_iNormalIndex;
 
   //Expanded State with NO CHECKBOX
   int m_iExpandedIndex;
 
   //Normal State with CHECKBOX
   int   m_iCheckedNormalIndex;   //Index of the checked/collapsed(normal) image
   int   m_iUnCheckedNormalIndex;  //Index of the unchecked/collapsed(normal) image
   
   //Expanded State: In Some case item wont have expanded image.So the following
   //are equal to the above 2. i.e, m_iCheckedExpandedIndex = m_iCheckedNormalIndex
   //and m_iUnCheckedExpandedIndex = m_iUnCheckedNormalIndex.
   //Expaned State with CHECKBOX
   int   m_iCheckedExpandedIndex;  //Index of the checked/expanded image
   int   m_iUnCheckedExpandedIndex; //Index of the checked/expanded image

   //Disabled image index
   int   m_iDisableIndex; //Index of the image for the disable state

 
}CUSTOMITEMDATA,*PCUSTOMITEMDATA;

上述结构保存了关于项的所有信息。因此,在插入项时,不是使用 `TVITEMINSERTSTRUCT`,而是创建一个 `CUSTOMINSERTSTRUCT` 的实例,进行填充并传递。此结构内部包含一个 `CUSTOMITEMDATA`。通过 `SetItemData` 方法将 `CUSTOMITEMDATA` 的实例与项关联。

让我们看一些成员

  • m_uNormalImage:指定用于表示项的普通状态的位图 ID。
  • m_uExpandedImage:指定用于表示项的展开状态的位图 ID。
  • m_cMaskColor:位图的哪个颜色需要绘制成透明。我使用了颜色 (0,128,128),这是创建图标时获得的默认背景颜色。
  • m_bEditable:如果项可以重命名,则为 True,否则为 False。
  • m_bDropTarget:如果项可被视为放置操作的有效目标,则为 True,否则为 False。
  • m_bChecked:如果项有复选框,则为 True,否则为 False。
  • m_bCheckState:如果复选框已开启,则为 True,否则为 False。此标志仅在 `m_bChecked` 为 True 时有效。
  • m_cItemColor:项标签的颜色。
  • m_bIsBold:如果项标签的字体为粗体,则为 True,否则为 False。
  • m_uMenuID:右键单击项时显示的菜单的资源 ID。
  • m_bEnable:如果项要启用,则为 True,否则为 False。
CUSTOMINSERTSTRUCT tvIS;

//First Insert the project item
//-----------------------------------------
tvIS.m_tvIS.hParent      = hRoot;   //As the project under the root
tvIS.m_tvIS.hInsertAfter = TVI_FIRST; 

//Valid members of this item are: text,image and the selected image
tvIS.m_tvIS.item.mask  = TVIF_IMAGE | TVIF_TEXT | TVIF_SELECTEDIMAGE; 

//Name of the project is the item text
tvIS.m_tvIS.item.pszText = (LPSTR)( (pProj->m_sTitle).operator LPCTSTR());

//Create the custom data
PCUSTOMITEMDATA pCustomData = new CUSTOMITEMDATA;

//Images representing the normal and expanded
pCustomData->m_uNormalImage   = IDB_BITMAP_FOLDER_16;
pCustomData->m_uExpandedImage  = IDB_BITMAP_FOLDER_OPEN_16;

//Enable the label editing 
pCustomData->m_bEditable = true;

//Need checkbox or not
pCustomData->m_bChecked  = true;

//If needed state of the check box [ CHECKED/UNCHECKED]
pCustomData->m_bCheckState = true;

//Item color
pCustomData->m_cItemColor = RGB(255,40,148);

//Menu resource for this item
pCustomData->m_uMenuID  = IDR_CONTEXT_MENU;

//Custom data
tvIS.m_pCustomData          = pCustomData;
pProj->m_pCustomData = pCustomData;

//Insert the project into the tree
HTREEITEM hPRoject = m_ctrlTree.InsertItem(&tvIS);

正如我之前所说,对于每个项,将生成 6 个图像,并且基于用户请求的状态 [在 `CUSTOMITEMDATA` 实例中填充],将设置图像的索引。

创建组合图像很简单。假设我有一个大小为 16x16 的复选框和一个大小为 16x16 的项图像。我创建一个大小为 32x16 的图像,并将复选框和项图像复制到其中,然后将其添加到图像列表中。

如何处理 LButton down 消息?

由于组合图像在单个位图中同时包含复选框和项图像,我们如何区分单击复选框和单击项图像?单击复选框应切换其状态,单击项图像应切换展开状态。

当用户单击图像时,我们必须找出单击发生在图像的左半部分 [单击复选框] 还是右半部分。如果是左半部分,则生成 `TVN_STATEICON_CLICKED` 消息并发送给树本身。`TVN_STATEICON_CLICKED` 的消息处理程序会根据复选框的状态更改项的图像,并通知父项关于已勾选项列表的 `TVN_ITEM_CHECK_TOGGLE` 消息。此消息的 `WPARAM` 包含已勾选的项列表。

如果鼠标单击发生在图像的右半部分或项标签上,则必须通过调用 `CTreeCtrl` 的默认 `lbuttondown` 处理程序来选择该项。

还有另一种情况是单击发生在树项之外。在这种情况下,会启动橡皮筋选择。

void CCustomTreeCtrl::OnLButtonDown(UINT nFlags, CPoint point) 
{
 // TODO: Add your message handler code here and/or call default
 TV_HITTESTINFO tvHitInfo;
 tvHitInfo.pt = point;
 HTREEITEM hClickedItem = HitTest(&tvHitInfo );
 
 m_bAllowExpand = true;
 
 //Handling the click on the check box. 
 //If the click is ON the item icon find out whethere it is ON the check box
 //or on the image
 if(  hClickedItem )
 {
  PCUSTOMITEMDATA pCustData = (PCUSTOMITEMDATA) GetItemData(hClickedItem);
 
  if( pCustData && pCustData->m_bChecked && pCustData->m_bEnable )
  {   
   if( tvHitInfo.flags & TVHT_ONITEMICON )
   {
    CPoint pt = point;
 
    //Convert the point from client to screen coordinate
    ClientToScreen(&pt );
 
    //Get the full rect area
    CRect fullRect;
    GetItemRect( hClickedItem,&fullRect,FALSE );
 
    //Convert the rect into screen ordinates
    ClientToScreen( &fullRect );
 
    //Get the rect area of the label
    CRect labelRect;
    GetItemRect( hClickedItem,&labelRect,TRUE );
 
    //Convert the rect into screen ordinates
    ClientToScreen( &labelRect );
 
    //Get the rect of the image
    CRect imgRect;
    imgRect.left   = fullRect.left;
    imgRect.top    = fullRect.top;
    imgRect.right  = imgRect.left + ( labelRect.left - fullRect.left );
    imgRect.bottom = fullRect.bottom;
 
    //ImgRect contains both the check box image and image of the button
    //We want to know whether the check box is clicked or not. So get
    //the left half of the ImgRect which is nothing but the check box rect.
    int imgW,imgH;
    ImageList_GetIconSize( m_pImgList->GetSafeHandle(),&imgW,&imgH);
    imgRect.right -= imgW/2;
 
    //Now check the click point is there in this rect. If so the click is on the
    //check box of the item
    if( imgRect.PtInRect( pt ) )
    {
     m_bAllowExpand = false;
     CUSTNMHDR chdr;
     chdr.m_hdr.hwndFrom =  m_hWnd;
     chdr.m_hdr.idFrom  =  ::GetDlgCtrlID(m_hWnd);
     chdr.m_hdr.code   =  TVN_STATEICON_CLICKED;
     chdr.m_hItem     =  hClickedItem;
     chdr.m_data      =  (PCUSTOMITEMDATA) GetItemData(hClickedItem);
     SendMessage(TVN_STATEICON_CLICKED, (WPARAM)&chdr, (LPARAM)chdr.m_hdr.idFrom );              
     return;     
    }
   }  
  }
 }
 
 
 //If the click is not on the item and not on the item button means enable the
 //banding
 if( !(tvHitInfo.flags & TVHT_ONITEM) && !(tvHitInfo.flags & TVHT_ONITEMBUTTON) )
 {
  if( GetEditControl( ) )
  {
   CTreeCtrl::OnLButtonDown(nFlags, point);
   return;
  }
 
  //if there is no selected item then ON the banding
  m_bIsBandingON = true;
 
  //Store the starting point
  m_startPt = m_endPt = point;
 
  //Remove the selection
  int i = 0;
  for( ; i < m_vecSelectedItems.size(); i++ )
  {
   HTREEITEM hItem = m_vecSelectedItems[i];
   SetItemState( m_vecSelectedItems[i],~TVIS_SELECTED,TVIS_SELECTED );
  }
 
  //This is important.If you dont select the NULL item, the GetSelectedItem will
  //always return the previous selected item.For example,select item1.Click the mouse
  //button somewhere on the tree and not on any of the tree item. So in previous loop
  //we have removed the selection flag of item1 [ highlighting of the items goes].
  //Now again click on item1.But the tree gives the item1 as selected [eventhough it is
  //not highlighted ]item1 and because of this we are not able to select item1.So make
  //forcefully that no item is selected by calling SelectItem(NULL ).Now it works.Great!!!!
  SelectItem( NULL );
   
  //Send the notification to the parent dialog about the selected items.
  //Collect the selected items. May be parent may seek this data.
  CollectSelectedItems();
  GetParent()->SendMessage( TREE_SELCHANGED,(WPARAM)(&m_vecSelectedItems),NULL ); 
  SetCapture();
  return;  
 } 
 //Capture the mouse
 SetCapture();
 Invalidate();
 CTreeCtrl::OnLButtonDown(nFlags, point);
 
}

处理 RButtonDown 消息

右键单击可能发生在:

  1. 一个项
    • 项已选中:仅显示右键单击项的上下文菜单。
    • 未选中的项:丢弃所有选中的项,仅选择右键单击的项,并显示与之关联的上下文菜单。
  2. 项之外:目前未处理。

注意:右键单击菜单时显示的命令特定于右键单击的项。我的意思是,如果选中了三个项,并且其中一个选中的项被右键单击,则菜单命令仅应用于右键单击的项,而不应用于所有选中的项。我的要求是这样的,如果需要,可以更改为对所有选中的项进行操作。

void CCustomTreeCtrl::OnRButtonDown(UINT nFlags, CPoint point) 
{
 TV_HITTESTINFO tvHitInfo;
 tvHitInfo.pt = point;
 m_hRClickItem = NULL;
 m_hRClickItem = HitTest(&tvHitInfo );
 
 //Get the custom data of the item
 if( m_hRClickItem )
 {
  //If the item was already in selected state, then show only the context menu
  if( GetItemState( m_hRClickItem,TVIS_SELECTED ) & TVIS_SELECTED )
  {
   //Dont do any thing. Just show the context menu
  }
  else
  {
   //If there are some already selected items, deselect them and select the right clicked item
   //and show the context menu
   int iSelItemCtr = 0;
   for( ; iSelItemCtr < m_vecSelectedItems.size( ); iSelItemCtr++ )
   {
    SetItemState(m_vecSelectedItems[iSelItemCtr],~TVIS_SELECTED,TVIS_SELECTED );
   }
   SelectItem(NULL);
 
   //Select the right clicked item
   SetItemState(m_hRClickItem,TVIS_SELECTED,TVIS_SELECTED );   
 
   //As this is the only selected item, have only this in the m_vecSelectedItems
   m_vecSelectedItems.clear( );
   m_vecSelectedItems.push_back( m_hRClickItem );
   GetParent()->SendMessage( TVN_SELECTION_CHANGED,(WPARAM)(&m_vecSelectedItems),NULL ); 
  }
 
  //Show Context menu
 } 
}

处理鼠标移动消息

  1. 拖动模式开启:如果拖动模式开启,则基于选定的项准备一个拖动图像列表,并在鼠标位置显示。我们知道 `CTreeCtrl::CreateDragImage` 用于创建项的拖动图像。但是这个树控件允许你拖动多个树项,我们必须准备自己的拖动图像列表。请参阅 `OnBeginDrag` 中准备的 `GetImageListForDrag` 函数,这是 `TVN_BEGINDRAG` 的处理程序。`TVN_BEGINDRAG` 消息由树控件本身通过消息反射机制处理。如果鼠标位于项上,则将该项存储为放置操作的目标。
  2. 橡皮筋模式开启:当橡皮筋模式开启时,通过使用在左键按下时存储的点和当前鼠标点来绘制一个矩形。
void CCustomTreeCtrl::OnMouseMove(UINT nFlags, CPoint point) 
{
 //If dragging is enabled then drag image
 if( m_bIsDragging )
 {
  m_hDragTargetItem = NULL;
  if( m_pDragImageList )
  {
   // Move the drag image to the next position 
   m_pDragImageList->DragMove(point);
   
   //DragShowNolock:Shows or hides the drag image during a drag operation,
   //without locking the window.
   //As the window is locked during the BeginDrag,if you dont unlock
   //using DragShowNoLock(), the previosly highlighted target wont be
   //refreshed.So it will remain in highlighted state.
   m_pDragImageList->DragShowNolock(false );
 
   //Based on the mouse position keep updating the target node for the drop operation
   UINT flags;
   m_hDragTargetItem = HitTest( point,&flags );
 
   //If the target is not a valid one for dropping the show the nocursor
   PCUSTOMITEMDATA pTargetData = NULL;
 
   ::SetCursor( m_defaultCursor);
 
   if( m_hDragTargetItem )
   {
    pTargetData = (PCUSTOMITEMDATA) GetItemData(m_hDragTargetItem);
   
    //if both source and target are same,show invalid cursor
    if( m_hDragSourceItem == m_hDragTargetItem )
    {
     ::SetCursor( m_noCursor );        
    }
 
    if( !pTargetData->m_bDropTarget )
     ::SetCursor( m_noCursor );        
   } 
        
   if( m_hDragTargetItem )
   {
    //Highlight the target item
    SelectDropTarget( m_hDragTargetItem );
 
    //Expand the target item if it is not in the disbaled state
    if( pTargetData )
     if( pTargetData->m_bEnable )
      Expand( m_hDragTargetItem,TVE_EXPAND);
 
    //Again lock the window
    m_pDragImageList->DragShowNolock(true );    
   
   }   
  }
 }
 else if( m_bIsBandingON )
 {
  CClientDC dc(this);
  InvertRectangle( &dc,m_startPt,m_endPt );
  InvertRectangle( &dc,m_startPt,point );
  m_endPt = point;
 }
 CTreeCtrl::OnMouseMove(nFlags, point);
}

处理 LButtonUp 消息

  1. 如果拖动模式开启,则检查有效的被拖动的项和有效的目标项 [在鼠标移动期间更新]。如果既有被拖动的项又有目标项,则通过检查与目标项关联的 `PCUSTOMDATA` 的 `m_bDropTarget` 成员来检查目标项是否为放置操作的有效目标。如果是,则将所有被拖动的项复制到目标项下。
  2. 如果橡皮筋模式开启,则查找矩形内的所有项并选择它们。

处理 TVN_BEGINDRAG 消息

如前所述,此处理程序为选定项列表准备拖动图像。此消息由树控件本身通过消息反射机制处理。

处理 TVN_BEGINLABELEDIT 消息

此消息由树控件本身通过消息反射机制处理。此处理程序通过检查与项关联的 `CUSTOMITEMDATA` 的 `m_bEditable` 来检查是否允许标签编辑。对于禁用的项不允许标签编辑。

处理 TVN_ITEMEXPANDING 消息

仅当项已启用并且单击发生在项图像上而不是复选框上 [如果项有复选框] 时,才允许展开。

如果允许展开,则获取表示项展开状态的图像并将其设置给该项。

处理 TVN_SELCHANGING 和 TVN_SELCHANGED 消息

这些处理程序基本上处理使用 CTRL 键和箭头键的扩展选择。

处理 ON_WM_PAINT 消息

基本上,我们对所有项进行自定义绘制。为什么不使用默认绘制?因为每个项都有自己的颜色、文本粗细等。因此,我们处理项的绘制。除此之外,我们还做了一件事,为项提供更好的外观和感觉。还记得吗,如果项没有复选框,我们就附加一个空白位图,以便图像列表中使用的图像大小相同。这个空白图像在进行默认绘制时会产生以下外观

您看到项图像和标签之间的空间了吗?我正在绘制一个带有合适背景颜色的矩形来隐藏默认绘制,并通过偏移左侧来调整用于绘制标签的矩形,以获得以下输出

我正在使用一个名为 `IteateItems` 的函数,它基本上用于遍历树的所有项,并为每个项调用传递给 `IterateItems` 的回调函数。我将在许多地方使用 `IterateItems`,例如在橡皮筋矩形内查找项、在绘制所有项时、查找选定项列表时等。

此函数以要为每个项调用的回调函数、要从中开始迭代的项、迭代的结束项以及回调函数的任何特定信息作为参数。此函数内部调用 `ScanItem`,`ScanItem` 会递归调用,直到达到迭代的结束项。

void CCustomTreeCtrl::IterateItems( ScanCallBackFunc func,
       HTREEITEM hIterStart /*= NULL*/,
       HTREEITEM hIterEnd, /*= NULL*/
       void* pInfo /*=NULL*/
       )
{
 //If there is no start then take the root item
 HTREEITEM hStart = GetRootItem(); 
 if( !hIterStart )
  hIterStart = hStart;
 
 m_bContinueScan = true;
 m_bStartFound = false;
 ScanItems( func,hStart,hIterStart,hIterEnd,pInfo );
}

改进空间

我发现代码存在一些问题,可以在将来的版本中修复。

  1. 例如,如果您拖动多个图像,则拖动图像显示的是堆叠视图而不是树视图。这是因为我只是将所有项的图像堆叠在一个更大的位图中,然后像这样添加到拖动图像列表中
  2. 进行橡皮筋选择时,橡皮筋矩形必须在垂直方向上完全包围该项才能被选中。我的意思是,该项的顶部和底部矩形必须位于橡皮筋矩形内。否则,该项将不会被选中。
  3. 尽管如此,可能还有我未注意到的许多问题。

实际应用

此树控件可用于各种目的,例如列出整个目录结构,其中勾选的项表示具有只读权限的文件夹,禁用的项表示隐藏的文件夹/文件。或者表示一个公司及其进行的项目的关联成员,如本示例所示。可以为可计费项目提供复选框,禁用项目可以表示已放弃的项目,或者不再活跃在该项目中的人员等。

© . All rights reserved.