多功能树控件






4.92/5 (15投票s)
带有自定义复选框和其他功能的树控件。
引言
很久以前,我需要为树控件项添加复选框。那又如何?树控件具有启用复选框的功能,我还需要什么?我以不同的方式需要它。
- 我希望能够仅为特定项添加复选框。
- 能够动态添加/删除复选框。
- 能够启用/禁用项。
除了上述功能外,我还添加了一些我认为会非常有用的功能。
- 使用橡皮筋选择多选树控件项
- 将多选项拖放到目标上。
- 指定项是否可被视为有效的放置目标。
- 如果需要,限制项的重命名功能。
- 指定项是否为放置操作的有效目标。
- 为每个树项附加菜单的功能。
背景
为特定项添加复选框
CTreeControl
具有为项添加复选框的功能,但启用该样式会为所有项创建复选框。我只想为指定的项添加复选框。
网络上有很多自定义控件实现了上述功能。它们大多使用图像来模仿复选框项。会有一个类似复选框的图像来模仿复选框。单击该图像将切换复选框的状态 [使用两个图像:选中图像和未选中图像]。
我发现上述实现可能无法满足我,因为我希望项有图像 [如果项属于一个公司,那么公司的徽标必须与该项一起] 以及复选框。如果复选框是用图像模仿的,那么我如何表示项的图像?
解决方案是,将项的图像 [例如 16x16] 与复选框图像 [16/16] 结合起来 -> [32x16]。我们可以生成所有项的组合图像,并将其添加到图像列表中。
但在我的情况下,并非所有项都会有复选框。所以有些图像可能没有组合图像 [在这种情况下,图像将是 16x16],而有些可能有 [图像将是 32x16]。但是图像列表不支持可变大小的图像。
解决方案
为了克服可变大小图像列表的问题,我为所有项生成了组合图像,而不管该项是否有复选框。
- 对于具有复选框的项,会生成组合图像 [复选框图像 + 项图像] 并添加。
- 对于没有复选框的项,会创建一个空白图像并与其项图像组合,以便图像的大小为 32x16。
动态添加/删除复选框
我们如何在运行时添加复选框?
解决方案
在此之前,我想先了解用于表示项的各种图像。我将项的相同图像用于表示其选中状态。我的意思是,我没有单独的图像来表示选中状态。假设如此,每个项可以有六个关联的图像。让我们看看它们是什么
- 普通图像:用于表示已折叠且没有复选框的项的图像 [
]。
- 展开图像:用于表示已展开且没有复选框的项 [
]。
- 复选框已选中且已折叠:用于表示已折叠且复选框已选中的项 [
]。
- 复选框未选中且已折叠:用于表示已折叠且复选框未选中的项 [
] 。
- 复选框已选中且已展开:用于表示已展开且复选框已选中的项 [
]。
- 复选框未选中且已展开:用于表示已展开且复选框未选中的项 [
]。
所有六个图像都表示一个启用的项。这些图像在项插入树中并添加到图像列表时生成。启用复选框时,设置图像列表中组合图像 [复选框 + 项图像] 的索引。
动态启用/禁用项
现在的问题是如何动态启用/禁用树控件项。
解决方案
在禁用项时,会创建一个新的位图,通过将当前项图像变灰来实现,同时保持透明度,并添加到图像列表中,例如 []。此图像被添加到图像列表,并将索引设置为树控件的图像。
这就是事情的总体情况!详细信息将在呈现代码时给出。
实现细节
我们在查看代码时深入探讨了更多实现细节。我从 `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 消息
右键单击可能发生在:
- 一个项
- 项已选中:仅显示右键单击项的上下文菜单。
- 未选中的项:丢弃所有选中的项,仅选择右键单击的项,并显示与之关联的上下文菜单。
- 项之外:目前未处理。
注意:右键单击菜单时显示的命令特定于右键单击的项。我的意思是,如果选中了三个项,并且其中一个选中的项被右键单击,则菜单命令仅应用于右键单击的项,而不应用于所有选中的项。我的要求是这样的,如果需要,可以更改为对所有选中的项进行操作。
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
}
}
处理鼠标移动消息
- 拖动模式开启:如果拖动模式开启,则基于选定的项准备一个拖动图像列表,并在鼠标位置显示。我们知道 `CTreeCtrl::CreateDragImage` 用于创建项的拖动图像。但是这个树控件允许你拖动多个树项,我们必须准备自己的拖动图像列表。请参阅 `OnBeginDrag` 中准备的 `GetImageListForDrag` 函数,这是 `TVN_BEGINDRAG` 的处理程序。`TVN_BEGINDRAG` 消息由树控件本身通过消息反射机制处理。如果鼠标位于项上,则将该项存储为放置操作的目标。
- 橡皮筋模式开启:当橡皮筋模式开启时,通过使用在左键按下时存储的点和当前鼠标点来绘制一个矩形。
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 消息
- 如果拖动模式开启,则检查有效的被拖动的项和有效的目标项 [在鼠标移动期间更新]。如果既有被拖动的项又有目标项,则通过检查与目标项关联的 `PCUSTOMDATA` 的 `m_bDropTarget` 成员来检查目标项是否为放置操作的有效目标。如果是,则将所有被拖动的项复制到目标项下。
- 如果橡皮筋模式开启,则查找矩形内的所有项并选择它们。
处理 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 );
}
改进空间
我发现代码存在一些问题,可以在将来的版本中修复。
- 例如,如果您拖动多个图像,则拖动图像显示的是堆叠视图而不是树视图。这是因为我只是将所有项的图像堆叠在一个更大的位图中,然后像这样添加到拖动图像列表中
- 进行橡皮筋选择时,橡皮筋矩形必须在垂直方向上完全包围该项才能被选中。我的意思是,该项的顶部和底部矩形必须位于橡皮筋矩形内。否则,该项将不会被选中。
- 尽管如此,可能还有我未注意到的许多问题。
实际应用
此树控件可用于各种目的,例如列出整个目录结构,其中勾选的项表示具有只读权限的文件夹,禁用的项表示隐藏的文件夹/文件。或者表示一个公司及其进行的项目的关联成员,如本示例所示。可以为可计费项目提供复选框,禁用项目可以表示已放弃的项目,或者不再活跃在该项目中的人员等。