第一节 建立工程-种树
不管你是用纯代码写的窗口,还是通过 Qt Creator 或者 Visual Studio 的可视化环境堆出来的东东,你必须保证:有一个窗口里面放了个树形列表(QTreeWidget),这个树形列表里已经长满了“枝丫和叶子”(项目和子项目),首先保证你这个东西在没搞成三态树之前能正常运行,否则后果自负,嘿!
我用的是Qt SDK V1.2.1(内含 Qt 4.8.1、Qt Creator 2.4.1,一个开发基础,一个开发工具,官方网站上有下)。
打开 Qt Creator,菜单“文件(F)”,选择“新建文件或工程...”,或者直接按“Ctrl+N”,弹出“新建”对话框。在“新建”对话框里选择“Qt 控件 项目”中的“Qt Gui 应用”,然后单击对话框右下角的“选择(C)...”按钮,弹出“Qt Gui 应用”设置向导对话框,在“名称”栏输入“TriStateTree”,当然你也可以用你喜欢名字,最后狂点“下一步(N)>”,并“完成(F)”生成预置工程。
参照一下你的工程,大概有下图中内容就行了,有些朋友用的老的English版 Qt Creator,自己“看着办”吧。
三态树三态树,当然要先“种”棵树咯,双击打开“mainwindow.ui”,来到窗口可视化编辑界面,在左边的控件栏中拖一个“Tree Widget”(注意不是“Tree View”,原因我不解释),到你的窗口中,随便拉拉吧,那玩意儿太小,或者用布局工具整整,鉴于这里不是重点,我就不说了,直接放图,给大家看成品。
接下来说说里面的那些“项目”、“属性”呀怎么添加的,还有为什么“树形列表”右边多了个“属性列表”。
对拖到窗口中的“树形列表”(QTreeWidget)单击“右键”,在弹出菜单中选择“编辑项目...”,弹出的“编辑树窗口部件”对话框如下图。
这么人性化的编辑界面,不用我说太多了吧,自己整吧。如果你不嫌烦,并且为了体验成功的喜悦,你可以像我一样整多些、复杂些,呵呵。重点说说那个复选框“CheckBox”是怎么出来的,看到“属性”栏中的“checkState”属性了么,你用鼠标动动它看看。嘿,看到效果了吧,对了,你可以在这里初始化复选框的状态,我全选的“UnChecked”。另外讲点儿小知识,如果你要取消复选框,就点一下“checkState”属性编辑框右边的红色小箭头按钮,这个按钮的意思是“恢复默认”。
好了,按“确定”回到你的工程界面吧,现在你的基础打好了,座子安好了,试着运行一下,看看有木有问题。当然,只要你认真照做了,问题是不会有的,唯一的问题就是你的树形列表是二态的,而且父、子节点不能联动,觉得不爽吧,不爽就继续阅读下文。
第二节 前期准备-嫁接
看到这篇文章的朋友除了碰巧就是有意的(废话),都应该知道Qt程序各对象间主要通过信号与槽(函数)的机制进行通信。树形列表(QTreeWidget)中第1列(程序内部的序号为第0列)中的复选框发生变化后会产生多种信号,这里我们用的是“itemChanged(QTreeWidgetItem*, int)”这个信号,它代表了任意的项目发生了,例如复选框状态改变、文字改变、增删图标等等——你所看得到的变化。接下来要做的就是将这个“信号”与它的“槽(函数)”连接起来,那么,“槽(函数)”在哪里捏?果断放图先:对“树形列表”(QTreeWidget)点击右键,在弹出菜单中选择“转到槽...”,在“转到槽”对话框中选择“itemChanged(QTreeWidgetItem*, int)”信号,然后点击“确定”。
完成上面操作后,程序会自动在头文件(mainwindow.h)和实现文件(mainwindow.cpp)中生成相应的,
函数声明:
------mainwindow.h---------------
private slots:
void on_treeWidget_itemChanged(QTreeWidgetItem *item, int column);
---------------------------------------------------------------------------------------------
框架代码:
------mainwindow.cpp-----------
void MainWindow::on_treeWidget_itemChanged(QTreeWidgetItem *item, int column)
{
}
--------------------------------------------------------------------------------------
以上代码是系统生成的,先不管它。到目前为止,你的三态树制作前期准备工作已经完成了。
第三节 算法设计和代码编写-修枝
先从直观的角度来想想,当某个节点的复选框状态被用户改变后,我们希望该节点的父节点和子节点都能同步更新自身的状态,但是父、子节点的状态被改变后又会触发新的信号,新的信号再调用槽(函数)从而形成的递归调用。如果控制不好,这个递归调用将只能“递”不能“归”,因为它会不停的触发父、子节点状态的改变,这样的情形借用核物理学中的话来说就是“连锁反应”,你的程序就会因为“核爆”而崩溃。那么在递归过程中的关键就是控制好递归调用的流向,让递归不重复。
为了控制好递归调用过程中的程序走向,我们先定义好几个用于控制程序流程的标志变量和刷新父、子节点状态的函数,它们都是MainWindow的私有成员,代码如下:
------mainwindow.h-----------------------------------
private:
bool changeFromUser; // true:这个状态变化是由用户触发的;false:由程序调用 setCheckState 函数触发的
bool changeToParent; // 某个节点 checkState 变化后,同步更新其它节点的方向,true:向父节点方向;false:向子节点方向
void changeParents(QTreeWidgetItem *item, Qt::CheckState state); // 朝父节点方向刷新 CheckState
void changeChildren(QTreeWidgetItem *item, Qt::CheckState state); // 朝子节点方向刷新 CheckState
---------------------------------------------------------------------------------------------
在 MainWindow 的构造函数中将 changeFromUser 初始化为 true,这表示首次触发的树形列表复选框状态改变肯定是由用户操作产生的,除非你的动作快得过计算机的运算。
------mainwindow.cpp--------------------------------------------------
changeFromUser = true; // 设置状态改变由用户触发(代码位于 MainWindow 构造函数中)
-----------------------------------------------------------------------------
现在,编写代码前的所有准备工作已经完成。接下来,我们要来实现以下三个函数了。
void on_treeWidget_itemChanged(QTreeWidgetItem *item, int column); // private slots 私有槽(函数)
void changeParents(QTreeWidgetItem *item, Qt::CheckState state); // 朝父节点方向刷新 CheckState
void changeChildren(QTreeWidgetItem *item, Qt::CheckState state); // 朝子节点方向刷新 CheckState
我把具体的算法思想体现在程序的注释语句中,这样就能边写代码边讲程序了。
------mainwindow.cpp------------------------------
void MainWindow::on_treeWidget_itemChanged(QTreeWidgetItem *item, int column)
{
if (column != 0) return; // 树形列表位于第0列,当然,不是第0列的其它地方我们就不管了
if (changeFromUser) // 当用户触发某项目 CheckState 状态变化时,首次调用它绑定的槽函数
{
changeFromUser = false; // 必须立即将这个标志置为 false,因为接下来的状态改变操作都用函数 setCheckState 触发
changeToParent = true; // 用户触发的第一次必须朝着父节点方向进行刷新,原因我解释一下
// 为什么要选朝父节点方向刷新?
// 因为我们要在朝某个方向刷新后的终结位置改变方向,每个项目只可能有一个父节点,因此它可以确定而方便的找到刷新终结位置
// 如果先选择朝子节点方向刷新,那么子节点的个数是不确定的,再加上分支会越来越多,所以它的终结位置很不好确定
// 因此,我们在用户第一次触发 CheckState 改变的操作时强制先朝父节点方向,然后再终结位置转变为子节点方向\
// 所以,这个转变是在函数:changeParents 中实现的,这里大家一定要思路清晰,不然很突然想不到它们是怎样构成回路的
changeParents(item, item->checkState(0)); // 函数里隐含了刷新方向的改变,所以在调用 changeChildren 前无需再强制设置
changeChildren(item, item->checkState(0)); // 朝子节点方向刷新
changeFromUser = true; // 真正的递归调用环节在两个函数里完成,只有用户触发的首次状态改变才朝两个方向刷新,而后由程序代码触发的状态改变只朝着一个方向
// 而这个 changeFromUser 就是用来区别这两种情况。大家可以在脑子里想想,简单说就是每次状态改变都会调用此函数(形成递归调用)
// 所以必须在这个函数里对各种情况进行区分,使其有正确的流向。
// 从全局的角度来看,这个 if 下面的语句就完成了所有节点的刷新,里面的递归调用就包含在 changeParents、changeChildren 两个函数里
}
else
{
// 只有递归调用时才可能执行到这里的代码,因为递归调用位于 changeFromUser 的 true 和 false 状态之间,呵呵,和上面讲的吻合了吧
// changeToParent 这个标志是用来在递归调用时控制方向的,不然……你会明白出现什么状况的,这里我不解释^_^
// 递归调用会根据当前的 changeToParent 状态选择正确的流向,哈哈
if (changeToParent) changeParents(item, item->checkState(0));
else changeChildren(item, item->checkState(0));
}
// 此处特别说明:在 if (changeFromUser) {...} 中的 item 其实是用户触发时对应的 item
// 而通过递归调用执行到 else {...} 中的代码时,这里的 item 可就是各个地方不同的 item 了哦,这里要想清楚
}
void MainWindow::changeParents(QTreeWidgetItem *item, Qt::CheckState state)
{
// 前面提到,在这个函数里,在刷新的终结点处会将 changeToParent 状态变为 false 即接下来朝着子节点方向进行刷新了
// 那么终结点在哪里呢,终结点啊终结点,呵呵,别急
QTreeWidgetItem *iParent = item->parent();
if (iParent == 0) // 这里不就是终结点么,嘿!因为要调用到这个函数,那么 item 所指向的节点已经被刷新才能触发信号
{
// 所以呢,节点本身已经被刷新,又没有父节点,你说接下来该做什么捏^_^
changeToParent = false; // 这就是所谓有隐藏剧情了
return; // 没你事儿了,递个归,回去吧
}
else // 当然,如果人家还有老爹,你就有事儿做咯
{
// 那怎样给老爹上状态呢?当然是看你们兄弟齐心不齐心咯,下面看我的小白解释法
// 所有兄弟节点为 Checked ,老爹也是 checked
// 所有兄弟节点为 UnChecked,老爹也是 unChecked
// 所有兄弟节点为 PartiallyChecked,老爹是神马呢?自己猜!
// 一旦有哪个兄弟节点不一样,那老爹就伤心咯(PartiallyChecked)
// 所以,看代码!!!
int i,k=iParent->childCount(); // k 表示兄弟的个数,i 用来点名的
for (i=0;ichild(i)->checkState(0) != state) // 某个兄弟和你的状态不一样了,一票否决制,不多说
break;
}
// 看你点名点完了没有,如果 i = k,就说明点完名都没发现不同的兄弟(这里有些巧用,仔细想想,看看上面的 for 循环)
if (i==k) iParent->setCheckState(0, state); // 注意,这里出现递归调用了,因为 setCheckState 触发发 itemChanged 信号
else iParent->setCheckState(0, Qt::PartiallyChecked); // 同上,而且这里是程序代码触发
// 在没到终结点之前 changeToParent 为 true,递归会继续朝着父节点的方向进行,再看看那个槽函数吧(为什么要叫槽呢,我没想清楚)
// 老爹照顾完了,再管管儿子闺女们些吧。太乱了,呵呵^_^
}
}
----------------------------------------------------------------------
void MainWindow::changeChildren(QTreeWidgetItem *item, Qt::CheckState state)
{
int i,k=item->childCount(); // k 你儿子闺女的个数(支持计划生育,养不起啊养不起);i 老蛋,老一,老二,老三……
for (i=0;ichild(i)->setCheckState(0, state); // 儿女跟爹走
// 这里触发 itemChanged,你儿子女儿自己会管它们的子女(递归调用),因为在照顾完父亲后,已经在终结点将 changeToParent 改成了朝子节点
// 所以此时的递归调用会朝着子节点的方向流动,不信看上面那个槽函数
// 如果到了最后子节点(只能说是某一分支的最后),它没有子女了(k=0),就没有这里的事儿了
// 至此结束,谢谢!
}
}
---------------------------------------------------------------------------------------------------------
来个效果图吧!