1. 树形控件和树形视图:
1) 树形控件在Microsoft Windows95中就已经引入,如文件资源管理器中的树形文件列表就是使用树形控件实现的,这是树形控件最常见的应用;
2) 树形视图就是将整个树形控件作为视图模板的视图,就和之前做过的Phones列表视图一样,直接将CXXXView继承自CTreeCtrl即可;
3) 我们看到的资源管理器的左侧其实就是一个子视图,该视图中只包含树形控件,其本质就是一个树形视图;
!!树形视图在多视图中应用非常多,经常将树形视图作为最左侧的子视图用以展示工作空间中各个资源的从属层级关系;
2. 树形视图的本质:
1) 其实树形视图CTreeView里面就只有两个成员,一个是其构造函数,另一个就是GetTreeCtrl来获取隐藏的(private控制的)的属性控件对象引用;
2) CTreeView其实就只包含了一个CTreeCtrl成员对象,所有的树形操作都是有该成员对象实现的,所有的相关操作都是调用CTreeCtrl的操作实现的,CTreeView仅仅就是对CTreeCtrl的包装而已;
3) 想要操作树形控件就必须先使用GetTreeCtrl获得控件引用,然后再调用控件的函数来实现相关操作:CTreeCtrl& CTreeView::GetTreeCtrl() const;
3. 树形视图的初始化:
1) 树形视图初始化的主要是其外观和功能,而总共有6种样式会影响树形视图的外观和功能;
2) 6种样式:都以TVS_打头,即Tree View Style的缩写
i. TVS_HASLINES:具有线段,将子项目和父项目用线连起来;
ii. TVS_LINESATROOT:将各个根目录用线段连起来(在拿资源管理器来说就是用线段将所有表示盘符的根目录用线连起来),前提是TVS_HASLINES开启时才有效;
iii. TVS_HASBUTTONS:给具有子项目的项目添加带有加减号的按钮,+表示正处于折叠状态,点击后可以展开,-表示正处于展开状态,点击后可以折叠;
iv. TVS_EDITLABELS:开启项目文本编辑通知,有些应用(特别是Windows资源管理器)直接允许在树形空间中修改文件名,可以是双击图标将文件名设置成可编辑状态,编辑完成之后再双击图标或者点击其它任意位置就可以保存并结束编辑,随即文件名修改成功;
!!开始编辑时会发出TVN_BEGINLABELEDIT通知,结束时会发出TVN_ENDLABELEDIT通知;
!!而TVS_EDITLABELS样式决定这两种通知是否可以发送,如果添加则表示可以发送这两种通知,如果不添加则关闭这两种通知的发送功能;
v. TVS_DISABLEDRAGDROP:关闭拖放效果,默认情况下拖放效果是一直打开着的,有时可以将一些东西用鼠标拖放到树形控件的项目中去,比如在Windows资源管理器中,可以将某个文件的图标拖放到树形控件的某个文件夹上,就可以达到将文件移动到该文件夹的目的;
vi. TVS_SHOWSELALWAYS:让加亮显示的选中项目始终被加亮显示(树形控件默认当前选中的项目被加亮显示),默认状态下,当树形控件失去输入焦点时加亮显示会被取消;
3) 在哪里初始化呢?在CTreeView的PreCreateWindow的CREATESTRUCT的style字段中使用位或添加,例如:
BOOL CMyTreeView::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CTreeView::PreCreateWindow (cs))
return FALSE;
cs.style |= TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS |
TVS_SHOWSELALWAYS;
return TRUE;
}
3. 树形控件和图形列表控件简介:
1) 树形控件是MFC的公用控件的一种(即高级控件),其它公用控件还有微调杆、进度条、IP地址栏等;
2) 树形控件最大的特点就是用文本字符串和图形来表示每个项目,文本字符串非常好设定,可以直接在InsertItem中传给字符串参数后者直接使用SetItemText等函数设置,但是图形的设置就比较麻烦了;
3) 图形的设置:由于树形控件中的项目通常很多,项目的类型通常也很多,因此也需要很多不同的图形来表示不同的项目,如果这些众多的项目图形分别用单个的位图资源来表示则工程中的位图资源会很多很乱,而且不好管理,而树形控件的项目图形通常面积非常的小,没必要为每个图形创建一个位图资源;
!!CTreeCtrl使用图形列表控件CImageList来管理项目图标;
4) 图形列表控件:
i. 是Windows95中引入的高级图形控件,也成为可选图形列表控件;
ii. 该控件中只加载一个位图,而这个位图是由各个小位图连续拼接而成的,例如:
iii. 该控件要求,里面的每个小图标的宽度必须相等(不用精确相等,但也得90%以上相等),每个小图标按照这个宽度分割后以0开始索引,在提取某个小图标时就直接用其索引值提取,提取的本质其实就是从这个大位图中将目标小位图切割出来;
iv. 可选图形列表控件的优点:小巧节省资源,可以将同一类型的小图标集合在一起,方便管理,是一种小图标的非常优秀而巧妙的管理方式;
5) CImageList的创建和绑定:
i. 先定义对象,然后调用其Create函数完成创建;
ii. Create:BOOL CImageList::Create(UINT nBitmapID, int cx, int nGrow, COLORREF crMask);
! nBitmapID:将位图列表资源画好后赋予一个UINT型ID,Create通过此ID将资源加载进列表对象中;
!cx:小图标的平均宽度,列表以这个作为分割小图标的一句;
!nGrow:当列表需要扩增时每次扩增的空间大小,这里的单位是图标个数,如果是2,就表示扩增时就扩增两个小图标空间的大小;
!crMask:即图形列表的背景色,在上面的示例中就是品红色,该参数表示出了该参数指定的颜色以外的其它颜色都显示,也叫掩盖色;
!!为什么要这样做呢?
a. 因为显示小图标时会一块儿显示其配景色(CImageList还没只能到可以自动识别图标的边界的地步,因此只能连着背景一块儿被显示);
b. 那么有人就问了,问什么不在画图时直接将背景色指定为白色呢?为什么还要那么麻烦呢?
c. 当然,如果视图的背景色也是白色的话那没什么问题,那如果视图的背景色不是白色那怎么办呢?比如一个蓝色的视图背景,而小图标的背景色却是白色那岂不是非常不美观;
d. 因此需要使用掩盖色crMask,该参数可以去掉小图标的背景色,这样小图标的背景色在显示是就能跟随视图的背景色了;
e. 那小图标的背景色选取有没有什么要求?那就是图标背景色一定要和图标本身含有的颜色反差大,说白了就是不要和图标本身含有的颜色相同,否则图标本身中包含的该颜色岂不是在显示时也被掩盖掉了吗?其次就是图标背景色一定要和图标的边界反差大,否则画图时背景和边界分不清岂不是很难画吗?
!在上图中,品红没有出现在图标本身中,并且品红鲜艳,可以和图标的边界明显地分开;
iii. 接下来用CTreeCtrl的SetImageList将树形控件和相应的图形列表联系起来:
a. 原型:CImageList* CTreeCtrl::SetImageList(CImageList* pImageList, int nImageListType);
b. 返回的是上一个图形列表的指针;
c. pImageList就是要绑定的图形列表的指针;
d. nImageListType是图形列表的类型,共有两种类型,都已TVSIL_打头,即Tree View Set Image List的缩写:
TVSIL_ NORMAL:正常类型,即每个项目的图形只有两种状态,一种是双击打开的状态,另一种是关闭的状态,要事先准备好这两种状态的图标,在上面的图片中,最后两个图标分别是文件夹关闭和打开是的图标;
TVSIL_STATE:用户可以自定义项目图形的多种状态,需要事先为每种项目准备各个状态的图标;
6) 图形列表和树形控件绑定后不能在树形控件销毁之前销毁图形列表,否则图形会从树形控件中消失,因此在使用树形视图时一定要将用到的图形列表作为视图类的数据成员保存,这样就可以和CTreeView里封装的树形控件同生死!!!
4. 向树形控件中插入项目:InsertItem系列重载函数
1) 插入项目,但不指定项目对应的图标:
i. 原型:HTREEITEM CTreeCtrl::InsertItem(LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertLast = TVI_LAST);
ii. 每个树形控件的项目都用HTREEITEM句柄来标识,返回值就是插入的新项目的句柄;
iii. lpszItem:新项目的文本
iv. hParent:指定插入的新项目的父结点,如果为TVI_ROOT(TVI即Tree View Insert的缩写)标识该项目本身就是一个根结点;
v. hInsertLast:表示新项目应该插入在哪一个项目之后,如果为TVI_LAST就表示插入到当前指定的父结点之下的最后一个子结点之后,如果为TVI_SORT就表示按照字母升序插入到当前指定的父结点的相应子结点处;
vi. hInsertLast可以指定三个值:TVI_FIRST、TVI_LAST、TVI_SORT,其中TVI_FIRST肯定是插入到当前父结点下的第一个子结点的位置;
2) 插入项目并指定项目的图形(图形是来自绑定好的图形列表控件):
i. 原型:HTREEITEM CTreeCtrl::InsertItem(LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertLast = TVI_LAST);
ii. nImage:表示项目未被选中时的默认图形;
iii. nSelectedImage:表示项目被选中时的(即打开时的图形)
!!注意:通常树形控件上的图形都由打开和未打开两种状态,比如Windows的资源管理器,文件夹在打开时图形是一个开口的公文袋,没被打开(选中)时是一个闭合的公文袋;
a. 这里的选中就是指鼠标左击项目文本或图标,单击就行!选中并不是将该结点展开(展开会显示其所有子项目),选中仅仅就是让该项目的文本背景高亮同时图标变成打开样式的图标而已,如果一个项目被选中后按键盘的上下键,项目会随着家加亮条的达到和离开而选中和未选中(打开和关闭);
b. 而双击项目文本或图标不仅具有选中的效果,同时也将触发展开功能;
!!这两个参数都是图标在图形列表控件中的下标索引;
5. 项目的选中和展开:
1) 这两个动作很容易混淆,这里需要厘清,否则在编程过程中造成不必要的困扰;
2) 选中:
i. 效果:项目文本被高亮,图标从未打开状态变为打开状态;
ii. 触发动作:鼠标单击项目图标或文本,也可以通过键盘上下键从一个选中项目跳到另一个选中项目;
3) 展开:
i. 效果:项目的按钮(即加减号小方框)从加号变为减号,同时项目的所有子项目也被全部显示出来;
ii. 触发动作:鼠标单击按钮(加减号小方框),或者双击项目的图标或文本,但不过双击同时也会触发选中效果;
!!因此可以发生这种现象,即选中一个项目但不展开,但另一个隔了很远的项目被展开;
6. 获取树形控件的相关信息:Get系列函数
1) 获取整个控件中项目的总数:UINT CTreeCtrl::GetCount();
2) 获取控件的图形列表指针:CImageList* CTreeCtrl::GetImageList(UINT nImage); // nImage为TVI_NORMAL或TVI_STATE,绑定什么类型就获取什么类型,一定要一致
3) 判断某个项目是否有子项目:BOOL CTreeCtrl::ItemHasChildren(HTREEITEM hItem);
4) 获取指定项目的第一个子项目:HTREEITEM CTreeCtrl::GetChildrenItem(HTREEITEM hItem); // 如果没有子项目则返回NULL
5) 获取指定项目的父项目:HTREEITEM CTreeCtrl::GetParentItem(HTREEITEM hItem);
6) 获取指定项目的下一个兄弟项目:HTREEITEM CTreeItem::GetNextSiblingItem(HTREEITEM hItem); // 兄弟项目即同一个父项目底下的所有子项目相互之间都是兄弟
7) 获取指定项目的前一个兄弟项目:HTREEITEM CTreeItem::GetPrevSiblingItem(HTREEITEM hItem);
8) 获取当前选中的项目:HTREEITEM CTreeItem::GetSelectedItem();
9) 获取某个项目(必须要指定该项目和传入的参数指定的项目之间的关系):HTREEITEM CTreeItem::GetNextItem(HTREEITEM hItem, UINT nCode);
!nCode需要指明要返回的项目和hItem之间的关系,都已TVGN_打头,即Tree View Get Next的缩写;
!!以上所有的Get都可以用GetNextItem来实现,只不过上面的函数都是nCode在特定值时的例子而已;
TVGN_CARET:返回当前选中的项目,如果nCode是这个,那么hItem则无效,直接传一个NULL即可;
TVGN_CHILD:返回hItem的第一个子项目,和GetChildrenItem等价
TVGN_PARENT:返回hItem的父项目,和GetParentItem等价
TVGN_NEXT:返回hItem的下一个兄弟项目,和GetNextSiblingItem等价
TVGN_PREVIOUS:返回hItem的上一个兄弟项目,和GetPrevSiblingItem等价
TVGN_ROOT:如果hItem在某个根结点所代表的树中,则返回该根结点的第一个子结点
!!由于MFC没有直接提供返回整个树形控件的第一个根结点的函数,因此就使用TVGN_ROOT选项来获取:GetNextItem(NULL, TVGN_ROOT)即可;
!因为NULL所在的树就是整个树形控件,而整个树形控件的第一个子结点就是第一个根结点!!
7. 获取和修改项目文本:
1) 获取指定项目的文本:CString CTreeCtrl::GetItemText(HTREEITEM hItem) const;
2) 修改指定项目的文本:BOOL CTreeCtrl::SetItemText(HTREEITEM hItem, LPCTSTR lpszItem); // 返回值表示成功与否
8. 删除项目:
1) 删除指定项目:BOOL CTreeCtrl::DeleteItem(HTREEITEM hItem); // 返回值表示成功与否
2) 将整个控件中所有的项目删除:BOOL CTreeCtrl::DeleteAllItems();
9. 对指定项目的所有子项目进行排序:
1) 原型:BOOL CTreeCtrl::SortChildren(HTREEITEM hItem);
2) 返回值表示成功与否;
3) 默认是按照项目文本字符串升序排列;
4) 只会对指定项目的所有子项目排序,而不会递归地向下排序(即只排序一层);
5) 如果想对根项目排序则hItem要传NULL,因为根项目的父结点就是NULL;
10. 树形控件的通知消息:
1) 树形控件的消息响应同样是通过通知进行的,其通知ID都以TVN_打头,即Tree View Notify的缩写;
2) 最常用的有一下几个:
TVN_BEGINDRAG/TVN_BEGINRDRAG:用鼠标左/右键开始实施拖放操作,前提是要开启TVS_DISABLEDRAGDROP
TVN_BEGINLABELEDITE/TVN_ENDLABELEDIT:开始/结束编辑编辑标签操作(即项目文本),前提是必须要开启TVS_EDITLABELS样式
TVN_DELETEITEM:一个项目被删除
TVN_ITEMEXPANDING/TVN_ITEMEXPANDED:一个项目正要/已经展开
TVN_KEYDOWN:当控件具有输入焦点时一个键被按下
11. 树形控件默认的键盘接口:
1) 即使你不为树形控件添加任何键盘接口树形控件也具有一些基本的键盘功能,这是MFC默认实现的,而且功能非常全、非常够用,几乎不许要你额外添加键盘功能;
2) 上下键:选种高亮垂直向下移动;
3) 左键:折叠选种项;
4) 右键:展开选中项;
5) 字母键:在当前全部展开的所有项中立即跳转到以按下字母开头文本的项目,如果连续按就在相同字母开头的几个项之间不停循环;
12. 直接使用函数强制展开和选中某个项目:
1) 展开某个项目:BOOL CTreeCtrl::Expand(HTREEITEM hItem, int nCode); // 展开hItem的所有子结点,nCode有TVE_EXPAND(展开)和TVE_COLLAPSE(折叠)两种
2) 选中某个项目:BOOL CTreeCtrl::Select(HTREEITEM hItem, int nCode); // nCode一般为TVGN_CARET,表示将hItem高亮选中,如果选中了一个未展开的项目的子项目,则该项目会被自动展开
13. 项目展开消息的处理:
!!提一句,所有以控件为视图的消息处理中,如果想让视图处理控件消息(通知)则这些通知必须通过ON_NOTIFY_REFLECT进行反射!!
1) 一般对展开消息只需要处理TVN_ITEMEXPANDING即可,即只处理即将打开消息,已经打开的消息一般没有处理的必要;
2) 该消息的处理函数具有规定的形式:void CTreeView::SelfDefProcName(NMNHDR*, LRESULT*);
!!!其实,如果MFC没有预定义一个通知的处理函数,则通用的通知处理程序的形式就是:void SelfDefProcName(NMHDR*, LRESULT*);
!!这就跟窗口消息通用处理函数ON_MESSAGE的void SelfDefProcName(WPARAM wParam, LPARAM lParam)的道理一样,只不过通用通知处理函数的形式是这样滴!
!这里NMHDR中的信息会用到,里面保存着触发动作的项目的相关状态;
!!!第二个参数LRESULT*的作用,一般形参名取为pResult:
a. 我们并不是在响应函数中调用Expand来展开或是折叠项目的,这个函数不用在响应函数中调用;
b. 我们只要在响应函数中处理其它和展开/折叠有关的逻辑即可,Expand动作是由MFC自动调用的;
c. 但是pResult的值可以控制MFC是否调用Expand来执行展开/折叠动作;
d. pResult的值可以在响应函数中设定,响应函数执行完后MFC会根据pResult的值来决定是否执行Expand;
e. 如果pResult为FALSE,则MFC会调用Expand执行展开/折叠动作,如果pResult为TRUE则MFC不执行Expand;
f. MFC执行Expand时会根据NMHDR的action来决定到底是折叠还是展开;
!!那既然MFC收到展开/折叠消息时会自动调用Expand那该响应函数还有什么必要存在呢?
a. 该函数用来优化展开/折叠操作;
b. 比如是展开动作触发了该函数,那么就可以在函数判断触发动作的项目是否具有子项目,如果没有子项目就没有必要让MFC调用Expand展开了,此时就可以为pResult赋一个TRUE;
3) NMHDR即Notify Message Handler的缩写,它是一种通知消息句柄结构,里面保存了和通知有关的信息,该结构的指针会作为WM_NOTIFY的lParam参数;
4) 只不过NMHDR是所有各种类型的通知的抽象,在不同通知处理中需要转换成相应类型的通知消息句柄结构,在CTreeView中就要转化成树形视图的通知结构NM_TREEVIEW;
5) NM_TREEVIEW:
i. 是树形视图通知的消息结构,这里只介绍里面几个最常用的树形;
ii. UINT action:表示当前树形控件的发出的动作,主要有一下几项,都是以TVE_打头的,即Tree View Expend的缩写,MFC将所有树形控件的动作都归结为展开动作,比如展开结点、折叠结点都总结为展开动作,所以以TVE_打头;
TVE_EXPEND:展开
TVE_COLLAPSE:折叠
!!可以通过该树形来决定接下来是要执行展开操作还是折叠操作;
iii. TV_ITEM itemOld/TV_ITEM itemNew:被操作的项目在发生动作前/后的状态,TV_ITEM是一个指示项目的状态信息的结构体,在本程序中只需要用到TV_ITEM的hItem属性,它是发生动作的那个项目的句柄;
6) 处理函数的用法示例:
void CMyView::OnItemExpending(NMHDR* pNMHDR, LRESULT* lResult)
{
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
HTREEITEM hItem = pNMTreeView->itemNew.hItem; // 获取有动作触发的那个项的句柄
// 通常是由发生动作后的状态
if (pNMTreeView->action == TVE_EXPAND) {
...
}
else if (pNMTreeView->action == TVE_COLLAPSE) {
...
}
...
}
14. 关于加减号按钮:
1) 首先必须要开启TVS_HASBUTTONS样式才会显示按钮;
2) 其次就是按钮显示的条件:只有具有子项目的项目才会显示出按钮,没有子结点的项目不会显示出按钮,即使开启了TVS_HASBUUTONS也无济于事
3) 按钮出现的时机:
i. 当项目无子项目时无按钮;
ii. 此时如果使用InsertItem给该项目添加至少一个子项目,则添加完的一瞬间就会立即在视图中显示出加减号按钮;
iii. 一个有按钮的项目在使用DeleteItem删除光其所有的子项目的一瞬间按钮立即从视图中消失;
!!因此,是否具有子项目就直接影响项目是否会显示按钮;
!!一个典型的应用——Windows资源管理器:
a. 由于文件系统中文件多而庞大,因此不可能直接将整个文件系统中所有的文件都添加到树形控件的结点中(以没有那么大的内存,二是如果全部添加将导致系统效率大大降低,甚至是系统崩溃);
b. 因此标准的做法是只有要展开结点的时候才为该结点添加子项目,而在结点折叠的时候在把其子项目都删除,这样就能保证整棵树的结点数量不会过于庞大,效率会大大增加;
c. 但是有些结点是肯定有子结点的,如果在折叠这些结点时将其全部子结点都删除那么它的加减号按钮就会消失,这就和实际情况不符了,它明明有子结点怎么能不显示其加减号按钮呢?
d. 因此,标准的做法是在折叠时(即关闭的时候)给它只保留一个子结点,那么不久能保留住其加减号按钮了吗?通常的做法都是先一次性删除其所有子结点(用一个DeleteAllChildren来实现),然后再立即为其添加一个空的子结点,这样就可以保留其按钮了;
!!其实初始化时也是这么做的,即刚开始只有一些根结点(还未展开),但是为了保留其按钮,就会先给其添加一个空的子项目保留住其按钮,等到真正展开的时候再添加子结点;