8.3 树形控件

本节首先介绍树形控件 QTreeWidget 的内容,然后介绍它的树形节点条目 QTreeWidgetItem。 树形控件的节点可以有多层、多个子节点, 如果将子节点全部展开,那么每一行都是一个数据条目。QTreeWidgetItem 比较特殊,一个条目内部可以有多列数据信息,相当于表格控件一整行的表格单元集成为一个条目,所以树形条目要比前面两节的列表条目和表格条目都复杂。第三小节介绍迭代器和 递归遍历算法,因为树形控件每行的条目之间可以是兄弟关系或父子关系,含有子节点的条目可以折叠也可以展开,进行遍历时有专门的迭代器 QTreeWidgetItemIterator 实现,也可以自行编写递归算法遍历所有条目。
本节介绍完 QTreeWidget 、QTreeWidgetItem、QTreeWidgetItemIterator 等内容之后,通过两个例子展示树形控件的用法,如果读者提前学习一下树形遍历的递归算法,对本节的内容理解会有帮助。

8.3.1 QTreeWidget

在 Qt 设计师界面可以直接拖动树形控件到窗口里,下图展示树形控件的外观和构成:
treewidget
默认情况下,树形控件最上面是一个树头条目,树头条目也是 QTreeWidgetItem 对象,可以有多列内容。
树头下面是真正的树形控件所有条目,在折叠的情况下,如上图所示,每行一个顶级条目,顶级条目也是 QTreeWidgetItem 对象,顶级条目的父节点指针 QTreeWidgetItem::​parent() 为 NULL。
将所有节点展开之后,可以看到每个节点可以有多个子节点:
treewidget2
对于包含子节点的父节点,左边会有小的三角形指示器,用于控制折叠或展开父节点。子节点也可以拥有更低级别的子节点(孙节点),以此类推,树形控件没 有限定子节点 的层数。顶级节点和其子孙节点的数据结构一样,都可以有多列数据,只是添加的函数、父节点指针不一样。下面介绍树形控件的函数和功能,在 8.3.2 节介绍树形节点条目 QTreeWidgetItem,8.3.3 节介绍树形条目迭代器 QTreeWidgetItemIterator 和递归遍历算法,最后两个小节是示例程序的实践。

树形控件的构造函数很简单:
QTreeWidget(QWidget * parent = 0)
参数里只有指定父窗口或父控件的指针 parent 。树形控件在添加条目之前,必须要先设置列数:
void setColumnCount(int columns) //设置列数
int columnCount() const //获取列数
默认的列数是 1 列,如果涉及到多列数据,比如文件浏览树,有文件名、文件类型、大小、修改时间等等,就需要设置为多列数据的树。
树形控件设置好列数就可以添加相应的顶级条目,添加顶级条目是由树形控件自身的函数实现,而子条目则由 QTreeWidgetItem 的函数实现。本小节主要围绕树形控件和其基类的函数来讲,树形控件也可以设置和表格控件类似的表头,这里称为树头条目,放在本小节末尾再讲。

(1)添加和访问顶级条目
树形控件顶级条目的操作比较类似 QListWidget 的列表条目操作函数。新建条目之后,可以用如下函数把条目添加到树形控件的顶级条目列表末尾:
void QTreeWidget::​addTopLevelItem(QTreeWidgetItem * item) //添加一个顶级条目到末尾
void QTreeWidget::​addTopLevelItems(const QList<QTreeWidgetItem *> & items) //添加多个顶级条目到末尾
如果希望将条目插入到指定顶级条目列表的 index 序号位置,使用如下函数:
void QTreeWidget::​insertTopLevelItem(int index, QTreeWidgetItem * item)
void QTreeWidget::​insertTopLevelItems(int index, const QList<QTreeWidgetItem *> & items)
树形控件所有的顶级条目父节点指针都为 NULL (父节点是指树形层次中的节点关系,而条目的父控件依然是树形控件本身)。
添加了顶级条目之后,可以对顶级条目进行计数:
int QTreeWidget::​topLevelItemCount() const

(2)移除顶级条目
移除顶级条目的函数也是take*打头:
QTreeWidgetItem * QTreeWidget::​takeTopLevelItem(int index)
index是顶级条目的序号,该函数只是从树形控件卸下顶级条目,但不会删除条目的内存空间,如果希望彻底删除,那么手动 delete 该函数返回的条目。
如果要清空所有的顶级条目和子条目,使用槽函数:
void QTreeWidget::​clear()

(3)条目访问函数
对于顶级条目,如果知道顶级条目的序号获取对应的条目:
QTreeWidgetItem * QTreeWidget::​topLevelItem(int index) const
反过来,对于已知顶级条目对象,查看其顶级序号:
int QTreeWidget::​indexOfTopLevelItem(QTreeWidgetItem * item) const
如果条目不是顶级条目或者条目不属于该控件,那么会返回 -1。
树形控件实际运行时,可能既有顶级条目,也有展开后的子孙条目同时显示,所以某个条目上面或下面的相邻条目不一定是同级别的兄弟条目,有可能是叔辈祖 辈的条目,也 可能是子辈孙辈条目。获取某个条目的相邻条目函数为:
QTreeWidgetItem * QTreeWidget::​itemAbove(const QTreeWidgetItem * item) const //上面相邻条目
QTreeWidgetItem * QTreeWidget::​itemBelow(const QTreeWidgetItem * item) const //下面相邻条目

从屏幕控件显示角度,如果根据树形控件内部相对坐标获取条目(树形控件显示区域的左上角为原点),使用下面函数:
QTreeWidgetItem * QTreeWidget::​itemAt(const QPoint & p) const
QTreeWidgetItem * QTreeWidget::​itemAt(int x, int y) const
这两个函数是一个意思,一个用 QPoint 对象表示相对坐标,另一个直接用 x 和 y 数值表示坐标,​如果对应坐标没有条目,会返回 NULL,注意判断 返回值。
树形控件也是自带滚动条的,如果条目特别多,自动显示滚动条,对于树形控件在屏幕可见的条目,可以根据条目对象获取它的可视矩形(树形控件显示区域的 左上角为原 点):
QRect QTreeWidget::​visualItemRect(const QTreeWidgetItem * item) const

(4)当前条目的操作
树形控件的选中操作默认比较像 QListWidget,如果不手动设置,只能选中一个高亮条目。
获取当前高亮选中条目的函数为:
QTreeWidgetItem * QTreeWidget::​currentItem() const
树形控件可以有多列,当前条目被点击选中的列号为:
int QTreeWidget::​currentColumn() const
树形控件内的条目一般都没有固定行号,因为条目可以展开也可以折叠,行号是变化的,所以没有基于行号的操作函数。

如果要设置某个条目为当前选中的状态:
void QTreeWidget::​setCurrentItem(QTreeWidgetItem * item)
void QTreeWidget::​setCurrentItem(QTreeWidgetItem * item, int column)
void QTreeWidget::​setCurrentItem(QTreeWidgetItem * item, int column, QItemSelectionModel::SelectionFlags command)
第一个 ​setCurrentItem() 函数相当于设置该条目整行高亮选中,第二个是设置该条目行的 column 列高亮选中,第三个函数是单次选中命令,参考“8.2.4 选中区域和选中行为”的单次选中命令内容,只是树形控件是一整行为一个条目,定位到条目的某列数据,就 类似指定表格控件的单元格。
如果当前高亮选中的状态发生变化,会触发如下信号:
void QTreeWidget::​currentItemChanged(QTreeWidgetItem * current, QTreeWidgetItem * previous)
参数里分别是当前高亮选中的条目,和之前高亮选中的条目,注意指针可能是 NULL,使用指针前一定要判断指针非空。

(5)条目查找和排序
如果要根据模板子串查找某列文本匹配的条目,使用如下函数:
QList<QTreeWidgetItem *> QTreeWidget::​findItems(const QString & text, Qt::MatchFlags flags, int column = 0) const
 参数里text是模板子串,flags是匹配标志(参看“8.1.1 QListWidget”中的字符串匹配标志表格),第三个参数是指定查找的列。该函数只查找一列的文本,其他列的文本是不查找的。如果需要查找所有列数据,那么要根据不 同列号逐列查询。

类似表格控件,树形控件也可以按照列的文本进行自动排序,自动排序的设置函数为:
bool    isSortingEnabled() const          //设置是否自动排序
void    setSortingEnabled(bool enable)   //查看是否开启自动排序
指定排序的列号和升序降序,使用从基类继承的函数:
void QTreeView::​sortByColumn(int column, Qt::SortOrder order)
在没有开启自动排序的情况下,也可以调用该函数进行一次性的条目排序。

(6)条目显示和运行时条目编辑
可以为条目的某列“单元格”设置单独的控件来静态显示(控件不具有编辑功能):
void QTreeWidget::​setItemWidget(QTreeWidgetItem * item, int column, QWidget * widget) //设置条目列控件
QWidget * QTreeWidget::​itemWidget(QTreeWidgetItem * item, int column) const //获取条目列控件,不设置就是NULL
注意该函数只能在条目添加到树形控件之后才能调用,否则无效,并且条目列控件只能用于显示,无法编辑,如果要定制可编辑的“单元格”控件,必须用基类 QTreeView 并继承 QItemDelegate 做代理,这些内容到后面模型视图章节讲解。
再次强调:itemWidget 条目控件,在默认情况下是与条目本身数据完全无关的,是条目数据的替换品,而不是协作模式。只有手动设置信号与槽,它们才可能关联上。
QListWidget 和 QTreeWidget 的条目控件都是静态显示,不能编辑。
QTreeWidget 控件的条目列控件 widget 还必须把 autoFillBackground 属性设置为 true,如果不是自动填充背景,那么默认是透明背景,这样控件的内容和内部模型数据(就是条目的列数据)同时显示,文本会重影,效果就糟糕了。
删除条目的列控件使用如下函数:
void QTreeWidget::​removeItemWidget(QTreeWidgetItem * item, int column)
这个函数没有返回值,会自动地彻底删除条目列控件。

在大多数情况下都用不到 itemWidget ,因为能够为条目设置可编辑标志位,然后调用如下函数开启树形控件自带的文本编辑器:
void QTreeWidget::​editItem(QTreeWidgetItem * item, int column = 0)
参数 item 是指定的条目,column 是条目的列(类似“单元格”)。在没有为条目设置可编辑标志位的情况下,可以调用下面一对函数进行持续编辑器的开启和关闭:
void QTreeWidget::​openPersistentEditor(QTreeWidgetItem * item, int column = 0)
void QTreeWidget::​closePersistentEditor(QTreeWidgetItem * item, int column = 0)
注意这对函数一开一关,要成对调用,否则编辑完了不会自动关闭持续编辑器。

(7)信号
关于当前高亮选中变化的信号 currentItemChanged() 前面讲过了,这里先列几个常规的信号,然后再将树形控件独有的信号。常规信号就是下面这 几个:(条目列就类似表格控件的单元格)
void itemActivated(QTreeWidgetItem * item, int column) //条目列被激活
void itemChanged(QTreeWidgetItem * item, int column)   //条目列的数据发生变化,比如文本或图标修改了
void itemClicked(QTreeWidgetItem * item, int column)  //条目列被单击
void itemDoubleClicked(QTreeWidgetItem * item, int column) //条目列被双击
void itemEntered(QTreeWidgetItem * item, int column) //进入条目列
void itemPressed(QTreeWidgetItem * item, int column) //条目列被点 击按下
树形控件最独特的就是展开和折叠信号:
void QTreeWidget::​itemExpanded(QTreeWidgetItem * item)  //条目展开时发送信号
void QTreeWidget::​itemCollapsed(QTreeWidgetItem * item) //条目折叠时发送信号
如果调用槽函数  expandAll() 展开所有子孙条目,那么不会触发 ​itemExpanded() 信号,因为触发太多会非常影响性能。
类似地,如果用槽函数 collapseAll() 折叠所有子孙条目,也不会触发 ​itemCollapsed() 信号,以免影响性能。
举例来说,在文件夹浏览的时候,因为操作系统里的文件太多,没法一次性构建完整的文件树,那么就可以用展开和折叠信号实时枚举某一层次文件夹的内容, 而不是一次性 枚举文件系统所有文件,因为一次性枚举所有文件的性能太糟糕。
树形控件还有一个 itemSelectionChanged() 信号,一般在多选模式才会用到,稍后讲解。

(8)槽函数
树形控件的槽函数包括四个(基类的另算):
void clear() //清空整个树形控件
void collapseItem(const QTreeWidgetItem * item) //折叠指定的条目
void expandItem(const QTreeWidgetItem * item)  //展开指定 条目
void scrollToItem(const QTreeWidgetItem * item, QAbstractItemView::ScrollHint hint = EnsureVisible) //滚动到指定条目
滚动函数 scrollToItem() 第二个参数是滚到到该条目的显示方式,参考“8.1.1 QListWidget”QAbstractItemView:: ​ ScrollHint 枚举常量的表格。

(9)基类 QTreeView 的函数
QTreeView 的功能函数也很多,这里列举几个可能常用的,详细的内容等到模型视图章节讲解。关于列隐藏或显示、设置列宽的函数如下:
void QTreeView::​setColumnHidden(int column, bool hide) //设置列隐藏或显示
bool QTreeView::​isColumnHidden(int column) const //判断列是否隐藏
void QTreeView::hideColumn(int column) //槽函数,隐藏指定列
void QTreeView::showColumn(int column) //槽函数,显示指定列
void QTreeView::​setColumnWidth(int column, int width) //设置列宽
int QTreeView::​columnWidth(int column) const //获取指定列的宽度
void QTreeView::​resizeColumnToContents(int column) //槽函数,自动调整 指定列的宽度
属性 indentation 控制显示父子节点的缩进宽度:
int indentation() const    //获取父子节点的缩进宽度
void setIndentation(int i) //设置缩进宽度
void resetIndentation()    //重置缩进宽度为默认值
基类还有几个常用的折叠和展开槽函数:
void collapseAll() //折叠所有子孙节点,这样只能看到顶级节点
void expandAll() //展开所有子孙节点,完全展开的树
void expandToDepth(int depth) //展开 depth 层级的子节点
expandToDepth() 函数是指一直展开,直到将第 depth 层级的子节点都展开为止。以顶级条目为第 0 层级,顶级条目的直接子节点为第 1 层级,孙子节点为第 2 层级,依次类推。
例如 expandToDepth(0) 的效果如下:
expand0
如果调用 expandToDepth(1) 展开第1级的节点:
expand1
如果把 expandToDepth() 参数设置成负数,那么相当于展开无穷大级别,就是展开所有的子孙节点。

(10)树头条目
树形控件只有一个表头,就是显示在上面的水平表头,本节也叫树头条目。设置树头条目的函数为:
void QTreeWidget::​setHeaderItem(QTreeWidgetItem * item) //设置树头条目,树头条目可以有多列数据,相当于多列的表头一次性设置了
void QTreeWidget::​setHeaderLabel(const QString & label) //只设置第 0 列的表头
void QTreeWidget::​setHeaderLabels(const QStringList & labels) //设置多列的表头
QTreeWidgetItem * QTreeWidget::​headerItem() const //获取树头条目
树头条目本质其实也是由 QHeaderView 子控件来显示的,可以在基类找到相关函数:
QHeaderView * QTreeView::​header() const //获取表头视图控件
void QTreeView::​setHeader(QHeaderView * header) //设置表头视图, 一般树形控件不需要用这个函数
void QTreeView::setHeaderHidden(bool hide) //设置表头是否隐藏
bool QTreeView::isHeaderHidden() const //判断是否隐藏了表头
无论是 QTableWidget 还是 QTreeWidget 的表头,都是 QHeaderView 子控件显示,QHeaderView 参考“8.2.3 表头设置”的内容。

(11)选中行为和选中模式
与 QTableWidget 类似,QTreeWidget也从祖类 QAbstractItemView 继承了选中行为和选中模式的属性:
QAbstractItemView::SelectionBehavior  selectionBehavior() const //获取选中行为,按条目选中、整行或整列选中
void setSelectionBehavior(QAbstractItemView::SelectionBehavior behavior) //设置选中行为
QAbstractItemView::SelectionMode  selectionMode() const //获取选中模式,比如单选、多选、扩展选择
void setSelectionMode(QAbstractItemView::SelectionMode mode) //设置选中模式
关于选中模式和选中行为的枚举常量参看“8.2.4 选中区域和选中行为”小节中的枚举常量表格,单次选中命令的函数和枚举常量也参考该小节。

默认情况下,树形控件是按照整行选中,并且是单选模式,如果把选中模式改成多选的 QAbstractItemView::ExtendedSelection,那么树形控件也可以使多选的,这时候信号 itemSelectionChanged() 就能派上用场:
void QTreeWidget::​itemSelectionChanged()
多选状态变化时会触发该信号(单选模式也触发,只是不需要用这个信号),可以关联该信号,监视当前所有选中的条目:
QList<QTreeWidgetItem *> QTreeWidget::​selectedItems() const
注意,这里的选中条目仅仅是指实际显示的直接选中的条目,不包括折叠隐藏的子孙条目计数,因为选中父节点与选中其子孙节点没关系,不会递归选中所有子 孙:
select
树形控件及其基类没有递归选中子条目的属性或函数,如果希望递归选中某个节点的所有子孙节点,那么需要自行编写递归函数。关于树形控件类本身的内容介 绍到这,因为 涉及到父子节点隶属关系、节点展开和折叠,树形控件还有很大一部分功能都是由其条目类 QTreeWidgetItem 的函数实现的,下面来学习这个树形控件条 目类。

8.3.2 QTreeWidgetItem

树形控件条目的内容是最复杂的,因为每一个条目涉及到内部多列数据的操作、父子节点操作,这些都是之前列表控件条目和表格控件条目不具备的特性。我们 首先介绍树形 控件条目的构造函数,然后按父子节点操作、通用数据操作和非通用数据操作三方面介绍树形控件条目。
(1)构造函数和复制函数
QTreeWidgetItem 构造函数较多,首先看不带父对象指针的构造函数:
QTreeWidgetItem(int type = Type)
QTreeWidgetItem(const QStringList & strings, int type = Type)
QTreeWidgetItem(const QTreeWidgetItem & other)
类型 type 一般用于派生类的自定义条目类型,基本用不到。第二个构造函数字符串列表 strings 就是条目内多列的文本,类似把表格控件一整行的多列文本塞到一个条目内部了。第三个是复制构造函数,复制时除了 type()、treeWidget()、parent(),其他的都复制。
克隆函数:
QTreeWidgetItem * QTreeWidgetItem::​clone() const
 ​clone()是按照本条目一模一样造出一个新的条目,是深拷贝,与本条目(包括子孙节点)不共享内存,函数返回的新条目也没有复制 type()、treeWidget()、​parent() ,新条目是自由的,没归属。​ clone() 函数会克隆所有的子孙节点,并且新子孙节点之间的关系也一样,QTreeWidgetItem 源码中 使用压栈出栈 方式实现了子孙节点的遍历复制。
赋值=函数:
QTreeWidgetItem & operator=(const QTreeWidgetItem & other)
 等于号函数与克隆函数有本质区别,它只拷贝 other 这一个节点内的数据到本节点里,包括显示的字符串和多列数据、标志位等,type() 和 treeWidget()、​parent()的内容不会修改。等于号函数不涉 及任何子孙节 点,也不改变隶属的父节点。
顺便提一下小于号函数:
virtual bool operator<(const QTreeWidgetItem & other) const
 如果为树形控件指定了排序的列号sortColumn(),那么按该列的文本字典序比较大小,否则都按照第0列的文本比较。

(2)父子节点操作
条目查看父节点的指针使用函数:
QTreeWidgetItem * QTreeWidgetItem::​parent() const  //常量,子节点不能改父节点指针
注意子节点是不能修改父节点指针的,只能父节点换子节点,不能子节点换父节点。

最常用的是节点控制自己的直接子节点,添加子节点使用函数:
void addChild(QTreeWidgetItem * child) //添加一个子节点到末尾
void addChildren(const QList<QTreeWidgetItem *> & children) //添加多个子节点末尾
void insertChild(int index, QTreeWidgetItem * child) //插入子节点序号 index 序号位置
void insertChildren(int index, const QList<QTreeWidgetItem *> & children)//插入多个子节点到 index 位置
直接子节点的计数(与孙辈或更低辈分的节点数目无关)用如下函数:
int childCount() const

根据序号获取直接子节点的指针使用函数(如果序号超界返回NULL):
QTreeWidgetItem * child(int index) const  //序号查子节点指针
反过来,根据子节点指针查序号的函数如下(如果查不到序号返回-1):
int indexOfChild(QTreeWidgetItem * child) const
移除子节点使用如下函数:
void removeChild(QTreeWidgetItem * child)  //根据子节点指针解除父子关系
QTreeWidgetItem * takeChild(int index)  //根据子节点序号解除父子 关系,返回卸下后的自由节点指针
QList<QTreeWidgetItem *> takeChildren()  //卸下所有子节点
注意这几个函数只是解除父子关系,卸下的子节点还存在内存中,如果要完全删除需要手动 delete  每个节点。
当节点有隶属的树形控件时,可以使用下面函数对子节点排序:
void sortChildren(int column, Qt::SortOrder order)  // 根据指定列号column排序,升序或降序由order指定
如果条目不属于任何树形控件,那么该排序函数无效。

条目还可以控制自己的子节点指示器(条目显示时左边的加号 +)如何显示:
QTreeWidgetItem::ChildIndicatorPolicy childIndicatorPolicy() const //获取子节点指示器显示策略
void QTreeWidgetItem::​setChildIndicatorPolicy(QTreeWidgetItem::ChildIndicatorPolicy policy) //设置子节点指示器显示策略
子节点指示器显示策略 QTreeWidgetItem::ChildIndicatorPolicy 有三种:

ChildIndicatorPolicy 枚举常量 数值 描述
QTreeWidgetItem::ShowIndicator 0 无论有无子节点都显示指示符。
QTreeWidgetItem::DontShowIndicator 1 始终不显示指示符。
QTreeWidgetItem::DontShowIndicatorWhenChildless 2 条目有子节点就显示指示符,没子节点就不显示。

默认值是最后一个,有子节点就显示指示符,没有子节点就不显示指示符,这种方式也最为科学,一般不需要改指示符显示策略。

关于父子节点操作函数介绍到这,这些函数的特点就是只处理直接的子节点,与孙子辈、更低辈分节点无关,孙子辈由儿子辈去管理,以此类推,族谱树中各层 节点只管理亲 儿 子,其他辈分都不管。 递归操作 就是这样,只管处理儿子辈代码,孙子辈的由儿子辈去管,层层下推,就是递归的过程。

(3)通用数据操作
通用数据一般是用于 QDataStream 保存条目的信息到文件中,也可以从文件中加载通用数据生成以前的树。树形控件条目的通用数据函数与前面章节列表条目、表格条目类似,但是多了指定列号的参数,因为每个属 性条目有多列数据,每列数据有分多种角色,因此属性条目使用二维向量存储通用数据:
// One item has a vector of column entries. Each column has a vector of (role, value) pairs.
    QVector< QVector<QWidgetItemData> > values;
对于通用数据的设置和读取,也有 data() 和 setData() 函数,只是多了列号:
QVariant QTreeWidgetItem::​data(int column, int role) const
void QTreeWidgetItem::​setData(int column, int role, const QVariant & value)
其他针对各个角色的读写函数如下表所示:

获取函数 设置函数 数据角色 描述
text(int column) setText(int column, const QString &text) Qt::DisplayRole 条目显示的文本。
icon(int column) setIcon(int column, const QIcon &icon) Qt::DecorationRole 条目显示的图标。
statusTip(int column) setStatusTip(int column, const QString &statusTip) Qt::StatusTipRole 如果主界面有状态栏,鼠标悬停在该条目上时显示该状态信息到状态栏。
toolTip(int column) setToolTip(int column, const QString &toolTip) Qt::ToolTipRole 鼠标悬停在该条目上时显示的工具提示信息。
whatsThis(int column) setWhatsThis(int column, const QString &whatsThis) Qt::WhatsThisRole 如果主界面窗口标题栏有?帮助按钮,点击帮助按钮再点击该条目会显示该帮助信息。
font(int column) setFont(int column, const QFont &font) Qt::FontRole 显示条目文本用的字体。
textAlignment(int column setTextAlignment(int column, int alignment) Qt::TextAlignmentRole 文本的对齐方式。
backgroundColor(int column) setBackgroundColor(int column, const QColor &color) Qt::BackgroundColorRole 文本背景色。
textColor(int column) setTextColor(int column, const QColor &color) Qt::TextColorRole 文字颜色。
background(int column) setBackground(int column, const QBrush &brush) Qt::BackgroundRole 条目的背景画刷。
foreground(int column) setForeground(int column, const QBrush &brush) Qt::ForegroundRole 条目的前景画刷。
checkState(int column) setCheckState(int column, Qt::CheckState state) Qt::CheckStateRole 条目自带的复选框选中状态,可以是三态复选框。
sizeHint(int column) setSizeHint(int column, const QSize &size) Qt::SizeHintRole 条目显示的建议尺寸。

 树形条目也有相应的数据流读写函数,就是用于读取或保存这些通用数据:
QDataStream &    operator<<(QDataStream & out, const QTreeWidgetItem & item) //外部函数,将条目写入数据流
QDataStream &    operator>>(QDataStream & in, QTreeWidgetItem & item) //外部函数,读取数据流中的条目数据
void QTreeWidgetItem::​write(QDataStream & out) const //成员函数,将条目数据写入数据流
void QTreeWidgetItem::​read(QDataStream & in) //成员函数,从数据流中读取条目数据
 运算符重载函数 operator<<() 和 operator>>() 本质就是调用上面的 ​write() 和 ​read() 函数。

(4)非通用数据操作
条目在构造函数指定的类型可以用如下函数获取,这个类型是只读的:
int QTreeWidgetItem::​type() const
在添加到树形控件之后,都可以用如下函数查看条目隶属的树形控件:
QTreeWidget * QTreeWidgetItem::​treeWidget() const  //常量,节点不能自行更换隶属,要从树形控件增删节点

 程序运行时除了树形控件本身的 QTreeWidget::​selectedItems() 可以判断选中条目,每个条目对象自己也有函数获取高亮选中状态或设置是否高亮选中:(默认情况 下,树形条目自身是否高亮选中与子孙条目的情况无关)
bool QTreeWidgetItem::​isSelected() const
void QTreeWidgetItem::​setSelected(bool select)

树形条目初始化时也有默认的标志位,并且运行时可以修改标志位:
Qt::ItemFlags QTreeWidgetItem::​flags() const
void QTreeWidgetItem::​setFlags(Qt::ItemFlags flags)
树形条目构造时的默认标志位如下: 
 Qt::ItemIsSelectable
   |Qt::ItemIsUserCheckable
   |Qt::ItemIsEnabled
   |Qt::ItemIsDragEnabled
   |Qt::ItemIsDropEnabled
树形条目默认不能编辑,如果希望条目文本可以双击编辑,可以用下面一句代码:
    item->setFlags( (item->flags()) | Qt::ItemIsEditable ); //双击条目会自动 开启文本编辑器
这个标志会对树形条目所有列的文本编辑都生效,开启后该条目每个列的数据都能双击编辑。对于开启 Qt::ItemIsEditable 标志位的条目,除了用户双击等操作启用编辑器,也可以用函数代码指定开启条目的某列数据编辑器:
void QTreeWidget::​editItem(QTreeWidgetItem * item, int column = 0)

树形条目默认有 Qt::ItemIsUserCheckable 标志,可以复选,但是复选框默认却看不到,可以用下面代码真正地显示复选框:
item->setCheckState(0, Qt::Unchecked);  //显示第0列的复选框,要指定列号

树形条目的复选框状态很特殊,如果不使用三态复选(默认情况),那么当前条目的复选状态与子孙条目复选状态无关。
如果开启树形条目的三态复选,那么当前条目的复选状态与子孙有关:
    如果所有子孙勾选,那么父节点勾选 Qt::Checked;
    如果部分子孙勾选,那么父节点部分勾选 Qt::PartiallyChecked;
    如果所有子孙不勾选,那么父节点不勾选 Qt::Unchecked。
三态勾选其实是真正反映父子勾选关系的,如果用复选框,那么有子孙的节点应该用三态的,无子孙的叶子节点用二态的,并且应当将所有树形控件的条目都显 示复选框:
   // 迭代器
    QTreeWidgetItemIterator it(ui->treeWidget);
    while (*it)
    {
        //取出当前条目
        QTreeWidgetItem *item = *it;
        if(item->childCount() > 0 )//有子节点开启三态复选,没子节点是二态复选
        {
            item->setFlags( item->flags() | Qt::ItemIsTristate );
        }
        item->setCheckState(0, Qt::Unchecked);  //正常应该只用第0列的复选框,代表一整行条目
        //找下一个条目
        ++it;
    }
迭代器 QTreeWidgetItemIterator 专门用于遍历树形控件或某个父节点的所有子孙条目,因为树形结构是分叉结构,不同于列表控件的一维遍历,也不同于表格控件的二维遍历,因此需要迭代器或递归算法来穷举子孙 条目,下面小节专门介绍这些内容。

8.3.3 迭代器和递归遍历

我们拿一棵经典的二叉树做例子:
tree
在上面树图中,A是总树的根,总树有两棵子树,分别隶属B、C。没有子孙的是叶子节点:D、E、F、G。
对于树的遍历,存在多种方式:
①先序遍历( pre-order traversal ):根节点->左子树->右子树;对于每棵子树内的遍历顺序也一样类推。
以上图为例,先序遍历为:A  ->B->D->E  ->  C->F->G。
对于多叉树,先序遍历规则就是:根节点->第一棵子树->第二棵子树->第三棵子树 等等。

②后序遍历( post-order traversal ):左子树->右子树->根节点;对于每棵子树内的遍历顺序也一样类推。后序遍历时父节点和根节点一定是在后面出现的,所以遍历时打头的是叶子。
以上图为例,后序遍历为:D->E->B  ->  F->G->C  ->A。
对于多叉树,后序遍历规则就是:第一棵子树->第二棵子树->第三棵子树....->根节点。

③中序遍历( in-order traversal ):左子树->根节点->右子树;对于每棵子树内的遍历顺序也一样类推。这种遍历仅对二叉树有意义,二叉树的父节点正好在左右子树中间,但多叉树没有中间的概 念。
以上图为例,中序遍历为:D->B->E  ->A  ->F->C->G。

④按层遍历( level-order traversal ):第0层->第1层->第2层 ……。这种很直观,比如上面的树就是 A->B->C->D->E->F->G 。

通常遍历树的节点需要写较为复杂的递归代码或者用压栈出栈代码实现,这些代码都比较麻烦,因此 Qt 专门为树形控件提供了迭代器 QTreeWidgetItemIterator ,这个迭代器以先序遍历方式访问树形控件每个节点,大多数情况下就不需要我们自己去编写遍历算法了。我们可以将 QTreeWidgetItemIterator 想象成为一个保存节点指针的一维链表,链表每个元素指向树形控件一个节点,顺序是按照先序遍历。
QTreeWidgetItemIterator 构造函数如下:
    QTreeWidgetItemIterator(const QTreeWidgetItemIterator & it)
    QTreeWidgetItemIterator(QTreeWidget * widget, IteratorFlags flags = All)
    QTreeWidgetItemIterator(QTreeWidgetItem * item, IteratorFlags flags = All)
第一个是复制构造函数,按照参数 it 指定的迭代器一样构造新的迭代器,构造时新迭代器的当前条目也与 it 的当前条目一样。
第二个构造函数是根据树形控件指针,获取该控件内所有节点,形成遍历的链表,参数 flags 是迭代器标志位,可以筛选节点类型,等会列举迭代器标志位。
第三个构造函数是以某个节点作为根,遍历以这个节点为根的子树。参数 flags 是迭代器标志位,用于筛选节点类型,如下表所示:

IteratorFlags 枚举常量 数值 描述
QTreeWidgetItemIterator::All 0x00000000  默认值,枚举所有节点。
QTreeWidgetItemIterator::Hidden 0x00000001  枚举隐藏节点。
QTreeWidgetItemIterator::NotHidden 0x00000002  枚举非隐藏节点。
QTreeWidgetItemIterator::Selected 0x00000004  枚举高亮选中节点。
QTreeWidgetItemIterator::Unselected 0x00000008  枚举未选中节点。
QTreeWidgetItemIterator::Selectable 0x00000010  枚举可以选中的节点。
QTreeWidgetItemIterator::NotSelectable 0x00000020  枚举不可选中的节点。
QTreeWidgetItemIterator::DragEnabled 0x00000040  枚举能够拽出的节点。
QTreeWidgetItemIterator::DragDisabled 0x00000080  枚举不能拽出去的节点。
QTreeWidgetItemIterator::DropEnabled 0x00000100  枚举可接收拖进来的节点。
QTreeWidgetItemIterator::DropDisabled 0x00000200  枚举不能接收拖进来的节点。
QTreeWidgetItemIterator::HasChildren 0x00000400  枚举所有父节点。
QTreeWidgetItemIterator::NoChildren 0x00000800  枚举所有叶子节点。
QTreeWidgetItemIterator::Checked 0x00001000  枚举复选框勾选的节点。
QTreeWidgetItemIterator::NotChecked 0x00002000  枚举复选框没有勾选的节点。
QTreeWidgetItemIterator::Enabled 0x00004000  枚举所有启用的节点。
QTreeWidgetItemIterator::Disabled 0x00008000  枚举所有禁用的节点。
QTreeWidgetItemIterator::Editable 0x00010000  枚举可以编辑的节点。
QTreeWidgetItemIterator::NotEditable 0x00020000  枚举不能编辑的节点。
QTreeWidgetItemIterator::UserFlag 0x01000000  枚举自定义的用户节点。

这些枚举标志位大部分与节点条目自身的标志位 Qt::​ItemFlags 枚举常量相对应,比如是否可编辑,是否能选中,用于筛选指定标志位的节点;还有一部分标志位,如是否折叠隐藏,是否高亮选中,与用户实时的操作相关。

QTreeWidgetItemIterator 重载了多个运算符函数,方便程序员使用,比如 Qt 文档中的示例 代码:
    QTreeWidgetItemIterator it(treeWidget);
    while (*it) {
        if ((*it)->text(0) == itemText)  //查找匹配文本的条目
            (*it)->setSelected(true);  //文本找到了就高亮显示
        ++it;
    }
 这段代码根据树形控件 treeWidget 构造了迭代器 it,it内部有个游标,实时指向遍历的属性条目,在未遍历的时候,指向先序遍历第一个条目,运算符函数 operator*() 用于取出当前条目的指针,比如 *it 就是遍历过程中的当前条目;
迭代器支持 ++it 和 it++,第一个前加加是在执行语句前移到下一条目,第二个后加加是执行语句之后移到下一个条目;
还有 --it 和 it-- ,是逆向遍历,与加加的遍历顺序是反的;
迭代器也支持跳跃式遍历,如即 it += 5, it-=5 ,这是跳到后面第 5 个或跳到前面第 5 个。
QTreeWidgetItemIterator 类似链表,与数组不同,它没有 [] 运算符函数,不能按照序号取节点指针,在穷举完所有节点之前也是不知道总数目多少。遍历过程中,*it 数值为 NULL 时,就是穷举完了,后面已经没有节点了。可以用 while 循环单独对节点数目计数,或者指定 IteratorFlags 去穷举父节点多少个,叶子节点多少个等。关于迭代器的内容介绍到这,我们在下面小节专门用例子来解释递归算 法。

8.3.4 省市行政区示例

本小节示范使用设计师添加编辑树形条目,树形控件设置三列信息,第一列是省市名称,第二列是经度,第三列是纬度。行政区划分正好是树形结构,省级的经 纬度用省会城 市的经纬度代替,城市经纬度用自身的数值。省市经纬度举例如下:
安徽省 合肥市 117.250 31.833
安徽省 安庆市 117.050 30.533
广东省 广州市 113.267 23.133
广东省 深圳市 114.050 22.550
湖南省 长沙市 112.933 28.233
湖南省 株洲市 113.133 27.833
我们以省级作为树形控件顶级节点,城市作为子节点。下面开始示例,打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 citytree,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
界面拖入三行控件,第一行:树形控件,默认对象名 treeWidget。
第二行:三个标签,文本分别为“省市名称”、“经度”、“纬度”,三个单行编辑器,对象名分别为 lineEditName、lineEditLon、lineEditLat,第二行使用水平布局。
第三行:四个按钮,文本、对象名分别为“添加顶级节点”pushButtonAddTop、“添加子节点”pushButtonAddChild、 “删除叶子节 点”pushButtonDelLeaf、“删除节点子树”pushButtonDelSubtree,第三行也是用水平布局。
窗口整体使用垂直布局,尺寸 440*330。

布局完成后,我们选中树形控件,在右下角设置 columnCount 为 3,然后右击树形控件:
ui
在右键菜单选择编辑项目。弹出如下编辑对话框:
ui
首先看到的“列”标签页,指的是树头条目,有三列内容,分别是 1、2、3,我们点击“属性”按钮,展开右边的树头属性编辑,把树头的三个列 text 值修改为 “省市名称”、“经度”、“纬度”,修改后如下图所示:
ui
然后我们点击上面的“项目”标签页,即树形节点的编辑界面:
ui
绿色加号按钮就是添加节点的按钮,我们点击该按钮,添加一个新条目:
ui
我们编辑第一列文本为 “安徽省”,第二列经度 117.250,第三列纬度 31.833 ,使用的是省会经纬度。每列信息都可以双击编辑文本。添加后如下图所 示:
ui
然后我们选中“安徽省”条目,点击左下角第二个按钮添加子节点,是转折箭头和加号的图标,添加安徽省的子节点:
ui
我们编辑子节点名字为 “合肥市 ”,第二列  117.250,第三列 31.833 。然后如法炮制,为安徽省添加安庆市节点,添加两个子节点后显 示如下:
ui
注意,左下角第一个按钮(加号图标)意为添加选中节点的同级别节点;
第二个按钮(转折箭头和小加号)意为添加选中节点的子节点;
第三个按钮(减号)是删除选中节点。添加节点功能和添加子节点功能都是相对于选中节点而言的。

上面示范的是先建立父节点,后建立子节点。下面示范反过来的情况,先创建子节点,后添加到父节点。

我们选中“安徽省”节点,点击左下角第一个添加同级别节点按钮,添加一个 “广州市 ” 113.267 23.133 ,
然后接着点击该按钮,再添加一个 “深圳市” 114.050 22.550 ,如下图所示:
ui
然后我们继续点击加号按钮,添加节点“广东省” 113.267 23.133:
ui
广东的省市节点新建后,下一步是建立父子节点关系,我们选中广东省上面一个节点“深圳市”:
ui
我们点击中间第二个按钮,即将选中节点设置为下一个同级节点的首个子节点,点击后的效果如下图所示:
ui
这样深圳市就变成广东省的子节点了。然后我们再选中“广州市”节点,也点击中间第二个按钮,使得广州市也成为广东省的子节点:
ui
这样就完成广东省两个子节点设置了。上面对话框中间四个按钮意义为:
中间第一个按钮(转折箭头向上):将选中节点提高级别,升格为父节点同级别,并移动到父节点前面;
中间第二个按钮(转折箭头向右):将选中节点设置为下一个同级别节点的首个子节点,是降级操作;
中间第三个按钮(向上):将选中节点上移,不改变级别,就是排到上面亲兄弟节点之前;
中间第三个按钮(向下):将选中节点下移,不改变级别,就是排到下面亲兄弟节点之后。

安徽省和广东省的节点设置完后,我们点击“OK”按钮,回到设计师界面。湖南省的节点留着程序运行时用。
我们在设计师界面,右击各个按钮,为每个按钮添加槽函数:
ui
添加四个按钮的槽函数之后,我们开始代码的编辑。首先是 widget.h 头文件代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTreeWidgetItem> //条目类的头文件
#include <QTreeWidget> //树形控件头文件

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonAddTop_clicked();

    void on_pushButtonAddChild_clicked();

    void on_pushButtonDelLeaf_clicked();

    void on_pushButtonDelSubtree_clicked();

private:
    Ui::Widget *ui;
    //递归删除节点子树
    void RemoveSubtree(QTreeWidgetItem *curLevelItem);

};

#endif // WIDGET_H
头文件新增内容主要是三块:
添加了树形条目和树形控件的头文件引用;
之前添加了四个按钮的槽函数;
最后添加了递归删除节点子树的函数 void RemoveSubtree(QTreeWidgetItem *curLevelItem) ,这个函数后面详细讲解。
然后我们分块来讲解源码文件 widget.cpp 的内容:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
}

Widget::~Widget()
{
    delete ui;
}
上面部分,添加了消息框和调试输出的头文件包含,构造函数和析构函数没有改动,是 QtCreator 自动生成的代码。

下面来看第一个“添加顶级节点”按钮的槽函数:
void Widget::on_pushButtonAddTop_clicked()
{
    //获取省市名称、经度、纬度三个文本
    QString strName = ui->lineEditName->text();
    QString strLon = ui->lineEditLon->text();
    QString strLat = ui->lineEditLat->text();
    //检查字符串都不空才继续
    if( strName.isEmpty() || strLon.isEmpty() || strLat.isEmpty() )
    {
        QMessageBox::information(this, tr("输入检查"), tr("三个编辑框均需要输入信息!"));
        return;
    }
    //新建树形条目,设置三列文本,名称、经度、纬度
    QTreeWidgetItem *itemNew = new QTreeWidgetItem();
    itemNew->setText(0, strName);
    itemNew->setText(1, strLon);
    itemNew->setText(2, strLat);
    //添加顶级条目
    ui->treeWidget->addTopLevelItem(itemNew);
    ui->treeWidget->setFocus(); //设置树形控件显示焦点
}
该函数首先获取省市名称、经度、纬度三个编辑框的内容,判断是否都有字符串,如果有空的编辑框,提示消息框并返回;
如果三个编辑框都有内容,然后新建树形条目,设置省市名称、经度、纬度三列文本;
最后添加新条目为顶级节点,并将显示焦点设置给树形控件,方便用户查看新增的顶级节点。

下面来看第二个“添加子节点”按钮的代码:
void Widget::on_pushButtonAddChild_clicked()
{
    //获取当前选中节点
    QTreeWidgetItem *curItem = ui->treeWidget->currentItem();
    if( NULL == curItem )
    {
        QMessageBox::information(this, tr("无选中节点"), tr("请先选中一个节点,然后为其添加子节点!"));
        return;
    }
    //获取省市名称、经度、纬度三个文本
    QString strName = ui->lineEditName->text();
    QString strLon = ui->lineEditLon->text();
    QString strLat = ui->lineEditLat->text();
    //检查字符串都不空才继续
    if( strName.isEmpty() || strLon.isEmpty() || strLat.isEmpty() )
    {
        QMessageBox::information(this, tr("输入检查"), tr("三个编辑框均需要输入信息!"));
        return;
    }
    //新建子节点,添加三列文本
    QTreeWidgetItem *itemChild = new QTreeWidgetItem();
    itemChild->setText(0, strName);
    itemChild->setText(1, strLon);
    itemChild->setText(2, strLat);
    //添加到选中节点
    curItem->addChild( itemChild );
    //自动展开显示新加的子节点
    ui->treeWidget->expandItem( curItem );
    ui->treeWidget->setFocus(); //设置树形控件显示焦点
}
该函数首先获取当前选中的节点,如果没有选中任何节点,那么不能添加子节点;如果有选中的节点,那么继续:
获取三个编辑框的内容,并判断是否 非空;
三个文本非空的时候,新建一个条目,并设置三列文本信息;
然后调用  curItem->addChild( itemChild ),添加新节点为选中节点的子节点;
自动展开当前选中节点,并设置树形控件为显示焦点,方便用户查看新增的子节点。
添加子节点的函数代码与添加顶级节点比较类似,需要注意的是,凡是指针都要判断是否为空指针,避免访问野指针导致程序崩溃。

下面来看第三个“删除叶子节点”按钮的代码:
void Widget::on_pushButtonDelLeaf_clicked()
{
    //获取当前选中节点
    QTreeWidgetItem *curItem = ui->treeWidget->currentItem();
    if( NULL == curItem )
    {
        QMessageBox::warning(this, tr("未选中节点"), tr("未选中节点,没东西删除。"));
        return;
    }
    //判断是否为叶子节点
    if( curItem->childCount() > 0 )
    {
        //该节点还有子节点,不能直接删除,因为子孙节点尚未删除
        QMessageBox::warning(this, tr("不是叶子节点"), tr("不是叶子节点,不能删除!"));
        return;
    }
    //是叶子节点可以删除
    //凡是知道指针的叶子节点,可以直接 delete
    //QTreeWidgetItem 析构函数会自动将条目从树形控件卸载掉
    delete curItem; curItem = NULL;
}
该函数首先获取当前选中节点,如果没有选中就提示未选中消息框并返回;
有选中节点时,我们获取选中节点的子节点数目,如果存在子节点,那么不是叶子节点,不能直接删除,提示消息框后返回;
如果没有子节点,那么直接 delete 并将指针置空。
两点需要说明:
第一,非叶子节点不能直接删除,因为它的子孙节点没有删除,会一直占用内存,删除非叶子节点会导致内存泄漏;
第二,对于叶子节点,在知道指针的情况下,可以直接 delete 并将指针置空,~QTreeWidgetItem() 析构函数会自动将自己从树形控件卸载掉并删除内存空间。注意析构函数只能删除自己一个节点,不会删除儿孙节点。

删除节点和其子树,需要手动编写代码,从最底层叶子节点开始删除,自底向上删除,最后删除选中节点。
第四个按钮“删除节点子树”就是完成删除选中节点和其子树的功能:
void Widget::on_pushButtonDelSubtree_clicked()
{
    //获取当前选中节点
    QTreeWidgetItem *curItem = ui->treeWidget->currentItem();
    if( NULL == curItem )
    {
        QMessageBox::warning(this, tr("未选中节点"), tr("未选中节点,没东西删除。"));
        return;
    }
    //递归删除节点子树
    RemoveSubtree( curItem );
}
该函数先获取当前选中节点,如果是空指针就提示消息框并返回;
如果不是空指针,存在选中节点,那么使用递归函数 RemoveSubtree( curItem ) 删除选中节点的儿孙和该节点本身。
实际干活的是 RemoveSubtree( curItem )  函数,我们下面来看看它的代码:
void Widget::RemoveSubtree(QTreeWidgetItem *curLevelItem)
{
    //凡是指针,都要判断是否为空
    if( NULL == curLevelItem )
    {
        return;
    }
    //儿子节点个数
    int nCount = curLevelItem->childCount();

    //截止判断代码
    if( nCount < 1)//一个子节点都没有,已经是叶子节点
    {
        //直接删除
        delete curLevelItem;    curLevelItem = NULL;
        return; //递归层数截止
    }

    //踢皮球代码,对各个子节点调用本函数,称为递归
    for(int i=0; i<nCount; i++)
    {
        //逐个卸载子节点
        QTreeWidgetItem *curChild = curLevelItem->takeChild(i);
        //递归调用本函数,即踢皮球代码,叫儿子们自己去处理
        RemoveSubtree(curChild);
    }
    //儿孙们都删除干净了,儿孙们先做苦工,自己后做苦工,叫后序递归

    //苦工代码,自己动手删除自己
    delete curLevelItem;    curLevelItem = NULL;
    return; //返回父辈
}
递归的意思就是函数内部调用函数本身,递归函数的代码是对当前节点和其所有子孙节点通用的,一般包括三部分:
递归层级截止判断代码;
递归调用代码(踢皮球代码);
本节点工作代码(苦工代码)。
踢皮球代码和苦工代码顺序可能互换,根据情况调整顺序。

上面函数首先判断指针是否 非空,非空后继续;
计算当前节点的子节点数目;
然后判断是否为叶子节点,叶子节点是最低级的节点,干完活后返回,是递归调用最深的节点;
接着是踢皮球代码,对每个子节点,删除时先卸载父子关系,然后得到各个儿子节点;
儿子节点还有多少孙子我们不知道,那么就对每个儿子调用 RemoveSubtree(curChild),这就是递归的意义;
最后等儿孙们干完活了,子孙节点删干净了,再删除本节点,就是最后的苦工代码。
当前节点活也干完了,就返回父辈函数。
本例子代码先讲解到这,递归函数下面小节专门探讨。我们构建这个示例代码,运行:
run
填写三个编辑框内容,添加湖南省顶级节点,然后选中湖南省顶级节点,修改三个编辑框,添加长沙市和株洲市的节点:
run
删除叶子节点和删除节点子树的功能读者自行测试一下,这里不截图了。我们下面小节专门讲解递归算法。

8.3.5 二叉树示例

本小节专门讲解递归算法的原理,然后以二叉树为例,编写递归算法代码。典型的二叉树如下图所示:
bintree
递归的思想就是使用同一个函数对树结构所有节点都调用一遍,我们假设有一个省级公司,下属两个市级公司,每个市级公司各有两个县级公司。比如省级公司总经理想知道本级和所有下属公司总人数,那么怎么统计人数呢?
我们假定有一个函数叫:  统计人数(?级公司)。
首先调用:
①统计人数(省级公司A);
假定省级公司目前仅知道自己总部级别人数,但是不知道下属公司人数,那么省级总经理需要通知市级公司的经理,叫他们算一下自己管理的人数:
②统计人数(市级公司B);
③统计人数(市级公司C);
然后市级公司目前也仅知道自己级别人数,也不知道下属公司人数,那么市级公司经理通知县级公司经理,叫他们算一下自己管理的人数:
④统计人数(县级公司D);
⑤统计人数(县级公司E);
⑥统计人数(县级公司F);
⑦统计人数(县级公司G);
县级公司没有子公司了,自己把人数点清,返回给市级公司 B、C;
市级公司 B、C把下属公司人数和本级人数加起来,返回给A;
A把下属公司人数和本级公司人数求和,得到公司在本省的总人数。
上面标号 7 个函数调用,其实只需要用同一段代码来实现,针对树形结构 7 个节点都执行即可。统计人数的函数伪码如下:
int  统计人数( ?级公司)
{
    //判断自己是否为最底层叶子节点,即递归层级截止代码
    if(  县级公司 ==  ?级公司 )
    {
         点清当前县级公司人数 curEmployees ;
         return    curEmployees ;
    }
    //否则不是最底层,还有子公司
    计算子公司数目 CompanyCount;
    定义本级别和下属公司统计总数  totalEmployees = 0;

    //对每个子公司调用本函数,下属公司多少人叫他们自己去数,即踢皮球代码
    for(int i=0;  i<CompanyCount; i++)
    {
         totalEmployees  +=  统计人数( 下属公司 i );
    }
    //苦工代码,自己级别的人数自己清点
    清点本级别人数 curLevelEmployees;
    //计算下属公司人数和本级别人数的总和,并返回给上级公司
    totalEmployees += curLevelEmployees;
    return totalEmployees ;   
}
递归函数是先递推,后回归,比如:
A 叫 B 去点人数,B 叫 D、E 去点人数,这是自顶向下的推进过程,即上面红色代码的工作;
D、E点清人数,返回报给 B, B 将下属公司人数加上本级别人数,求和,将求和数值报给 A,即从自底向上的回归过程,这是上面蓝色代码的工作。
所有子节点都反馈给 A,A将下属公司人数和本级别人数求和,就得到公司在本省的总人数了。
对于求省级公司人数,那么调用一句 :
 
 int nCount = 统计人数( 省级公司A);

剩下的就靠递推和回归的过程自动进行统计了,递归函数会对树形结构每个节点都会执行一遍!
递归函数代码一般由三个部分组成:
第一部分是递归层级截止代码,到了叶子节点,是递推层级的最底层,叶子节点直接做工作,然后返回结果;
第二部分是踢皮球代码,计算儿子节点数,对每个儿子节点调用递归函数本身,就是将工作拆分,叫儿子们去干活;
第三部分是苦工代码,儿子们工作都做完了,自己动手将本级别的工作做了,然后将儿子们的工作和自己做的工作统计总和,返回给父级节点。


第一部分递归层级截止代码通常是固定在递归函数最前面;后面两部分的顺序可能会互调:
如果踢皮球代码在前,苦工代码在后,这是后序递归,比如统计人数例子,必须先统计底层公司人数,然后向上级回馈统计;
如果苦工代码在前,踢皮球代码在后,这叫先序递归,比如公司发工资了,肯定从省级公司开始发钱,最后发钱给县级公司。
中序递归一般只在二叉树里面使用,即:调用大儿子干活(踢皮球代码1);自己干活(苦工代码);调用小儿子干活(踢皮球代码2)。


下面我们就以上图的二叉树例子,建立树形控件,然后对树节点的文本进行打印,分别为先序遍历、后序遍历、中序遍历、迭代器遍历、按层遍历五个功能实现按钮代码。
打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 bintree,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
窗口第一行是树形控件,就用默认对象名 treeWidget;
第二行是五个按钮,文本和对象名分别为:“先序遍历”pushButtonPreorder、“后序遍历”pushButtonPostorder、
“中序遍历”pushButtonMidorder、“迭代器遍历”pushButtonIterator、“按层遍历”pushButtonLevels,
第二行按钮使用水平布局,然后窗体整体使用垂直布局,窗体大小 440*330。
布局设置完成后,我们右击每个按钮,为每个按钮添加槽函数:
ui

这个示例我们用代码创建树形控件的七个节点,下面直接开始代码的编辑。
首先是 widget.h 头文件的代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTreeWidgetItem> //树形条目
#include <QTreeWidget>  //树形控件
#include <QTreeWidgetItemIterator> //树形迭代器

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonPreorder_clicked();

    void on_pushButtonPostorder_clicked();

    void on_pushButtonMidorder_clicked();

    void on_pushButtonIterator_clicked();

    void on_pushButtonLevels_clicked();

private:
    Ui::Widget *ui;
    //先序遍历递归函数,只打印字符,不需要返回值
    void PreorderTraversal(QTreeWidgetItem *curItem);
    //后序遍历递归函数
    void PostorderTraversal(QTreeWidgetItem *curItem);
    //中序遍历递归函数
    void MidorderTraversal(QTreeWidgetItem *curItem);
    //迭代器遍历
    void IteratorTraversal(QTreeWidgetItem *curItem);
    //按层遍历
    void LevelsTraversal(QTreeWidgetItem *curItem);
};

#endif // WIDGET_H
widget.h 首先添加了树形条目、树形控件、树形迭代器的头文件引用;
之前添加的五个按钮的槽函数;
最后是手动添加的五个功能函数,对应每个遍历按钮的功能实现。
下面来看源码文件 widget.cpp 的内容,首先是头文件包含、构造函数、析构函数代码:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置树形控件只有 1 
    ui->treeWidget->setColumnCount( 1 );
    //创建A条目,添加到顶级条目
    QTreeWidgetItem *itemA = new QTreeWidgetItem();
    itemA->setText(0, "A");
    ui->treeWidget->addTopLevelItem(itemA);
    //创建 B、C条目,添加给 A
    QTreeWidgetItem *itemB = new QTreeWidgetItem();
    itemB->setText(0, "B");
    itemA->addChild( itemB );
    QTreeWidgetItem *itemC = new QTreeWidgetItem();
    itemC->setText(0, "C");
    itemA->addChild( itemC );
    //创建D、E条目,构造函数指定父条目为 B,自动设置父子关系
    QTreeWidgetItem *itemD = new QTreeWidgetItem(itemB);
    itemD->setText(0, "D");
    QTreeWidgetItem *itemE = new QTreeWidgetItem(itemB);
    itemE->setText(0, "E");
    //创建F、G条目,构造函数指定父条目为 C,自动设置父子关系
    QTreeWidgetItem *itemF = new QTreeWidgetItem(itemC);
    itemF->setText(0, "F");
    QTreeWidgetItem *itemG = new QTreeWidgetItem(itemC);
    itemG->setText(0, "G");
    //展开所有子孙节点
    ui->treeWidget->expandAll();
}

Widget::~Widget()
{
    delete ui;
}
构造函数里首先设置树形控件的列计数为 1,只有 1 列文本;
新建条目 itemA,设置文本,添加为树形控件的顶级条目;
然后创建条目 itemB、itemC,设置文本,并添加给 itemA;
接着新建了 itemD、itemE,注意构造函数里设置了父节点为 itemB,构造时就确立了父子关系,不需要添加代码,只需设置文本;
然后新建了 itemF、itemG,构造函数指定了父节点为 itemC,设置各自的文本。
最后展开树形控件的所有子孙节点,方便查看树形结构。

下面来看“先序遍历”按钮及其功能函数代码:
void Widget::on_pushButtonPreorder_clicked()
{
    //获取A条目
    QTreeWidgetItem *itemA = ui->treeWidget->topLevelItem(0);
    //先序遍历打印
    qDebug()<<tr("先序遍历:");
    PreorderTraversal( itemA );
}
//先序遍历递归函数
void Widget::PreorderTraversal(QTreeWidgetItem *curItem)
{
    if(NULL == curItem) return; // 空指针判断
    //子节点数目
    int nChildCount = curItem->childCount();
    //递归层级截止判断
    if( nChildCount < 1 )//叶子节点,直接打印
    {
        qDebug()<<curItem->text(0);
        return; //干完活返回
    }
    //存在子节点,先序遍历是苦工代码在前
    //苦工代码,打印本节点文本
    qDebug()<<curItem->text(0);

    //踢皮球代码
    for(int i=0; i<nChildCount; i++)
    {
        QTreeWidgetItem *curChild = curItem->child( i );
        PreorderTraversal( curChild );//递归调用
    }
    //返回
    return;
}
该按钮槽函数代码简单,获取 itemA 条目指针,打印“先序遍历:”字样,然后调用功能函数 PreorderTraversal( itemA ) 。
在先序遍历功能函数里,首先对指针是否为空进行判断,空指针不处理直接返回。
为非空指针时,先获取 curItem 子节点数目 nChildCount;
然后进行递归层级截止判断,当 curItem 为叶子节点时,直接打印文本并返回;
当 curItem 拥有子节点时,对于先序遍历,苦工代码在前,打印 curItem 条目文本;
然后是踢皮球代码,针对每个子节点调用本递归函数;
最后返回。

下面来看“后序遍历”按钮代码及其功能函数代码:
void Widget::on_pushButtonPostorder_clicked()
{
    //获取A条目
    QTreeWidgetItem *itemA = ui->treeWidget->topLevelItem(0);
    //后序遍历打印
    qDebug()<<tr("后序遍历:");
    PostorderTraversal( itemA );
}
//后序遍历递归函数
void Widget::PostorderTraversal(QTreeWidgetItem *curItem)
{
    if( NULL == curItem )  return;  //空指针判断
    //子节点数目
    int nChildCount = curItem->childCount();
    //递归层级截止判断
    if( nChildCount < 1 )//叶子节点,直接打印后返回
    {
        qDebug()<<curItem->text(0);
        return; //干完活返回
    }
    //存在子节点,后序递归是苦工代码在后,踢皮球代码在前
    //踢皮球代码
    for(int i=0; i<nChildCount; i++)
    {
        QTreeWidgetItem *curChild = curItem->child( i );
        PostorderTraversal(curChild);//递归调用
    }

    //苦工代码,打印本节点文本
    qDebug()<<curItem->text(0);
    //返回
    return;
}
按钮槽函数简单,就是获取 itemA 指针,然后打印提示信息,调用功能函数 PostorderTraversal( itemA )。
后序遍历的功能函数代码,首先判断 curItem 指针是否为空,空指针不处理。
对于非空指针,获取子节点数目 nChildCount ;
然后进行递归层级截止判断,对于叶子节点直接打印信息并返回;
对于拥有子节点的情况,后序遍历是苦工代码在后,踢皮球代码在前,
踢皮球代码对每个子节点调用本递归函数;
所有子节点干完活,开始苦工代码,本级节点打印文本,最后返回。

先序遍历和后序遍历的函数非常相似,仅仅是把苦工代码和踢皮球代码互换顺序而已。
苦工代码在先就是先序,苦工代码在后就是后序。

下面来看看“中序遍历”按钮及其功能函数代码:
void Widget::on_pushButtonMidorder_clicked()
{
    //获取A条目
    QTreeWidgetItem *itemA = ui->treeWidget->topLevelItem(0);
    //中序遍历打印
    qDebug()<<tr("中序遍历:");
    MidorderTraversal( itemA );
}
//中序遍历递归函数
void Widget::MidorderTraversal(QTreeWidgetItem *curItem)
{
    if( NULL == curItem ) return;   //空指针判断
    //子节点数目
    int nChildCount = curItem->childCount();
    //递归层级截止判断
    if( nChildCount < 1)//叶子节点,直接干活并返回
    {
        qDebug()<<curItem->text(0);
        return;//干完活返回
    }
    //存在子节点,中序是大儿子在前,苦工代码居中,小儿子在后
    //踢皮球代码 1 ,叫大儿子干活
    QTreeWidgetItem *bigSon = curItem->child(0);
    MidorderTraversal( bigSon );//递归调用 1

    //苦工代码
    //自己打印本节点文本
    qDebug()<<curItem->text(0);

    //踢皮球代码 2,叫剩下的儿子干活,剩下的儿子都算小儿子
    for(int i=1; i<nChildCount; i++)
    {
        QTreeWidgetItem *litteSon = curItem->child( i );
        MidorderTraversal( litteSon );//递归调用 2
    }
    //大儿子、自己、其他小儿子活都干完了,返回
    return;
}
按钮槽函数简单,就是获取 itemA 指针,打印提示信息,调用功能函数  MidorderTraversal( itemA )。
中序遍历的功能函数首先也是判断空指针,如果是空指针就不处理。
对于非空指针,先获取  curItem 子节点数目;
进行递归层级截止判断,如果是叶子节点,直接打印文本并返回;
中序递归会将踢皮球代码拆成两份,苦工代码放在两份踢皮球代码中间:
如果拥有子节点,那么先获取大儿子指针 bigSon,第一部分踢皮球代码叫大儿子干活,即递归调用 1;
然后中间是苦工代码,自己打印本节点文本信息;
接着是第二部分踢皮球代码,将其他儿子枚举,其他儿子都算小儿子 litteSon ,逐个踢皮球给其他小儿子,即递归调用 2。
最后返回。
中序遍历一般用于二叉树,先序遍历和后序遍历应用的场景更多。

Qt 为树形控件专门提供了迭代器 QTreeWidgetItemIterator ,属于先序遍历,下面来看看“迭代器遍历”按钮及其功能函数代码:
void Widget::on_pushButtonIterator_clicked()
{
    //获取A条目
    QTreeWidgetItem *itemA = ui->treeWidget->topLevelItem(0);
    //迭代器遍历打印
    qDebug()<<tr("迭代器遍历:(同先序)");
    IteratorTraversal( itemA );

}
//迭代器遍历打印,迭代器使用的是先序遍历
void Widget::IteratorTraversal(QTreeWidgetItem *curItem)
{
    if( NULL == curItem ) return;   //空指针判断
    //定义迭代器
    QTreeWidgetItemIterator  it( curItem );
    //循环遍历
    while( NULL != (*it) )//it相当于是指向条目指针的指针
    {
        //直接打印条目文本
        qDebug()<< (*it)->text(0);
        ++it;   //用 ++ 获取下一个条目,遍历过程迭代器自动控制
        //当 *it 为空时,表示树节点遍历完毕
    }
    //返回
    return;
}
按钮槽函数代码三行,获取 itemA 指针,打印提示信息,调用功能函数 IteratorTraversal( itemA )。
迭代器遍历功能函数的代码非常简洁,就像普通数组一样循环遍历。
首先检查指针是否为空,空指针不处理;
然后对非空指针定义迭代器 it;
开始 while 循环,it 相当于是指向条目指针的指针,如果 (*it) 非空就一直循环:
      (*it) 就是当前枚举的条目指针,打印条目文本;
     递增 it 数值,找寻下一个节点。
当 *it 为 NULL 时退出循环,就枚举完所有节点了。
有了树形迭代器,省了许多递归代码的编写,一般先序递归功能都可以用该迭代器实现。

最后是“按层遍历”按钮及其功能函数代码,注意按层遍历是自顶向下的单向推进,到了最底层节点之后就结束了,
没有回归的特性,所以不适合递归函数的调用方法。这种单向性,从顶层到最底层,有先来后到的特点,适合用队列。
void Widget::on_pushButtonLevels_clicked()
{
    //获取A条目
    QTreeWidgetItem *itemA = ui->treeWidget->topLevelItem(0);
    //按层遍历打印
    qDebug()<<tr("按层遍历:(没有回归的特性,使用队列实现)");
    LevelsTraversal( itemA );
}
//按层遍历打印
void Widget::LevelsTraversal(QTreeWidgetItem *curItem)
{
    if( NULL == curItem )  return;  //空指针判断
    //建立队列
    QList<QTreeWidgetItem *> list;
    list.append( curItem ); //添加子树根节点
    //循环处理
    while( list.count() > 0 ) //队列非空
    {
        //取出队列头一个节点
        QTreeWidgetItem *itemFirst = list.takeFirst();
        //打印文本
        qDebug()<<itemFirst->text(0);
        //枚举 itemFirst 子节点数
        int nChildCount = itemFirst->childCount();
        //把子节点添加到队列
        for(int i=0; i<nChildCount; i++)
        {
            list.append( itemFirst->child( i ) );
        }
        //处理下一个打头节点
    }
    //返回
    return;
}
按钮槽函数代码三行,获取节点 itemA 指针,打印提示信息,调用功能函数 LevelsTraversal( itemA )。
按层遍历的功能函数里,先判断是否为空指针,如果是空指针就不处理。
对于非空指针,建立一个保存条目指针的队列 list,将子树根节点 curItem 添加到队列;
然后开始 while 循环,只要队列不为空,那么一直处理:
        取出队头节点 itemFirst ,打印该节点文本;
        获取该节点子节点数,把每个子节点都添加到队列末尾,然后继续下轮循环。
如果队头节点有子节点,队头节点出队之后,有子节点补充,队列长度不会变短,子节点越多,队列越长;
等到了最底层叶子节点排到队头时,由于没有子节点,那么只有出队的,没有入队的,
每轮循环队列会逐步缩短,最后队列空了,所有树节点就遍历完成了。
树形结构常见的遍历上述例子都一一示范了,内容比较多,建议读者多花费点时间慢慢理解。
代码讲解完了,我们构建并运行示例:
run
上图示范了先序遍历和迭代器遍历,可以看到打印的序列是一模一样的。其他遍历按钮请读者自行测试,与 8.3.3 小节手工遍历的序列对比看看。
下面小节的示例我们研究一下如何保存树形结构和根据文件加载树形结构。

8.3.6 树形节点读写示例

本小节示例 将树形控件的节点写入到 *.tree 文件中。树形结构创建时一般先有父节点,后有子节点,所以我们采用先序遍历方式保存和加载节点。我们翻到上一个示例  PreorderTraversal(QTreeWidgetItem *curItem) 函数的实现代码,数一数该函数使用的变量数目,发现只有三个:curItem、nChildCount、i 。而 i 是由 nChildCount 变量限定的,取值为 0 至 nChildCount - 1 。该函数里的独立变量只有两个,即 curItem、nChildCount,我们能通过两个独立变量递归遍历所有节点,那么针对每个节点,保存节点自身数据 (*curItem) 及其子节点数 nChildCount,就能重构出整个树。我们以最简化的树举例:
minitree
我们将节点自身数据和子节点数配对存储,使用先序遍历,父子节点按照先序遍历顺序紧密相邻地存储,得到:
"A"  2  "B"  0  "C"  0
然后读取的时候,也按照父子节点先序遍历关系加载:
我们先读到 "A"节点,子节点数 2;
知道有2个子节点,那么逐个创建子节点,并设置它们的父节点为 "A":
        读到 "B", 子节点数 0;
        再读到"C" ,子节点数 0。
读到叶子节点时,因为没有下一层子节点,就自动返回到父辈节点了。

因为父子节点是严格按照先序关系紧密相邻存储的,读的时候顺序也一样,那么就能完整地从文件中加载所有子节点,并根据子节点数值确立父子关系,恢复成原本的树形结构。
下面我们学习保存和加载 *.tree 文件的示例,打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 savetree,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
第一行是树形控件,默认对象名 treeWidget。
第二行是 标签“条目文本”、单行编辑器 lineEditItemText、按钮“添加顶级条目”pushButtonAddTop、按钮“添加子条目”pushButtonAddChild、按钮“修改树头条目”pushButtonEditHeader,第二行使用水平布局。
第三行是 标签“文件路径”、单行编辑器 lineEditFileName、按钮“保存文件” pushButtonSaveFile、按钮“清空树”pushButtonClearTree、按钮“加载文件”pushButtonLoadFile,第三行使用水平布局。
窗体使用垂直布局,尺寸 440*330。
设置布局后,我们为六个按钮分别添加槽函数:
slot
添加六个按钮槽函数后,我们进入代码编辑,这个示例的树形节点也是使用代码创建,并且为节点开启双击编辑的功能。
首先是头文件 widget.h 的代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTreeWidgetItem>//树形条目
#include <QTreeWidget> //树形控件
#include <QFile> //文件类
#include <QDataStream> //用数据流保存树

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonAddTop_clicked();

    void on_pushButtonAddChild_clicked();

    void on_pushButtonEditHeader_clicked();

    void on_pushButtonSaveFile_clicked();

    void on_pushButtonClearTree_clicked();

    void on_pushButtonLoadFile_clicked();

private:
    Ui::Widget *ui;
    //文件对象,用于保存或打开
    QFile m_file;
    //数据流对象
    QDataStream m_ds;
    //保存树的先序递归函数,自顶向下保存
    void SaveTree( QTreeWidgetItem *curItem );
    //加载树的先序递归函数,自顶向下创建树形结构
    void LoadTree( QTreeWidgetItem *curItem );
    //加载时的列数限制
    static const int MAX_COLS = 1000;
};

#endif // WIDGET_H
widget.h 首先添加头文件引用,树形条目类、树形控件类、文件类和数据流类;
然后是6个按钮的槽函数;
接着添加了 文件对象 m_file 、数据流 m_ds,递归保存和递归加载时需要用到这两个对象;
添加了递归保存函数、递归加载函数;
最后的常量用于加载时数据异常判断,我们示例设计只能加载 不超过1000 列的树形结构。

下面分段编辑源码文件widget.cpp,首先是头文件包含、构造函数、析构函数:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置树的列数和默认树头文本
    ui->treeWidget->setColumnCount( 1 );
    ui->treeWidget->headerItem()->setText(0, "TreeHeader");
    //构建顶级节点 A
    QTreeWidgetItem *itemA = new QTreeWidgetItem();
    itemA->setText(0, "A");
    //设置条目可以双击编辑
    itemA->setFlags( itemA->flags() | Qt::ItemIsEditable );
    ui->treeWidget->addTopLevelItem( itemA );
    //新建 A 的两个子节点 B、C,并设置可以双击编辑
    QTreeWidgetItem *itemB = new QTreeWidgetItem(itemA);//itemA为父节点
    itemB->setText(0, "B");
    itemB->setFlags( itemB->flags() | Qt::ItemIsEditable );
    QTreeWidgetItem *itemC = new QTreeWidgetItem(itemA);//itemA为父节点
    itemC->setText(0, "C");
    itemC->setFlags( itemC->flags() | Qt::ItemIsEditable );
    //展开所有节点
    ui->treeWidget->expandAll();

    //设置默认文件名
    ui->lineEditFileName->setText( "s.tree" );
}

Widget::~Widget()
{
    delete ui;
}
构造函数里先设置了树形控件的列数为 1,设置树头文本为 "TreeHeader";
然后构造节点 itemA,设置文本,并开启双击可编辑的标志位 Qt::ItemIsEditable,添加为树形控件顶级节点;
接着构造 itemA 的两个子节点 itemB、itemC,分别设置文本和双击编辑标志位;
设置树形控件展开所有节点;
最后设置了默认的文件名为 "s.tree" 。
需要注意的是 双击编辑标志位  Qt::ItemIsEditable  必须对每个条目都设置,不能漏了,如果漏了会导致部分可编辑,部分不可编辑,条目操作的一致性没了,就是 bug。

接下来是“添加顶级条目”和“添加子条目”两个按钮槽函数的代码:
void Widget::on_pushButtonAddTop_clicked()
{
    //获取编辑框条目文本
    QString strText = ui->lineEditItemText->text();
    if(strText.isEmpty()) //空文本不处理
    {
        QMessageBox::warning(this, tr("添加"), tr("条目文本为空不能添加。"));
        return;
    }
    //新建节点
    QTreeWidgetItem *itemNew = new QTreeWidgetItem();
    itemNew->setText(0, strText);
    //设置可以双击编辑
    itemNew->setFlags( itemNew->flags() | Qt::ItemIsEditable );
    ui->treeWidget->addTopLevelItem( itemNew );
    ui->treeWidget->setFocus(); //设置显示焦点
}

void Widget::on_pushButtonAddChild_clicked()
{
    //获取当前选中节点
    QTreeWidgetItem *curItem = ui->treeWidget->currentItem();
    if(NULL == curItem)
    {
        QMessageBox::warning(this, tr("添加子节点"), tr("未选中节点,无法添加子节点。"));
        return;
    }
    //获取编辑框条目文本
    QString strText = ui->lineEditItemText->text();
    if(strText.isEmpty()) //空文本不处理
    {
        QMessageBox::warning(this, tr("添加子节点"), tr("条目文本为空不能添加。"));
        return;
    }
    //新建节点,父节点为 curItem
    QTreeWidgetItem *itemChild = new QTreeWidgetItem( curItem );
    itemChild->setText(0, strText);
    //设置双击可以编辑
    itemChild->setFlags( itemChild->flags() | Qt::ItemIsEditable );
    //展开条目并设置显示焦点
    ui->treeWidget->expandItem( curItem );
    ui->treeWidget->setFocus();
}
这两个槽函数的代码,我们在本节第一个示例里面有类似的,已经学习过了,不同点主要是每次 new 一个条目,都要注意设置双击可编辑的标志位 Qt::ItemIsEditable ,这样才能保证所有条目都是可编辑的。

我们下面来看看“修改树头条目”按钮槽函数的代码:
void Widget::on_pushButtonEditHeader_clicked()
{
    //获取编辑框条目文本
    QString strText = ui->lineEditItemText->text();
    if(strText.isEmpty()) //空文本不处理
    {
        QMessageBox::warning(this, tr("修改树头"), tr("条目文本为空,不能修改树头文本。"));
        return;
    }
    //修改树头文本
    ui->treeWidget->headerItem()->setText(0, strText);
}
该函数获取编辑框 lineEditItemText 的文本字符串,判断是否为空,空文本不处理;
文本非空时设置树头条目的第 0 列文本为编辑框字符串。

下面来看看“保存文件”按钮槽函数的代码:
void Widget::on_pushButtonSaveFile_clicked()
{
    //获取文件名编辑框内容
    QString strFileName = ui->lineEditFileName->text().trimmed();
    //判断是否为空
    if( strFileName.isEmpty() )
    {
        QMessageBox::warning(this, tr("保存"), tr("文件名为空,无法保存。"));
        return;
    }
    //尝试打开文件,只写模式
    m_file.setFileName( strFileName );
    if( ! m_file.open( QIODevice::WriteOnly ) )
    {
        QMessageBox::warning(this, tr("打开"), tr("要写入的文件无法打开,请检查文件名或权限。"));
        return;
    }
    //已正常打开,设置数据流
    m_ds.setDevice( &m_file );

    //先写入树头条目,列数目就是树头条目的 columnCount(),不需要单独保存列数
    QTreeWidgetItem *iHeader = ui->treeWidget->headerItem();
    m_ds<< (*iHeader); //树头是孤立的,没有子节点,不需要写子节点计数

    //获取隐形根条目,虽然顶级条目 parent 指针是 NULL,但是隐形根条目的子节点是顶级条目
    //隐形根条目可以像普通条目递归使用,为隐形根条目添加或删除子条目,就是添加或删除顶级条目
    QTreeWidgetItem *iroot = ui->treeWidget->invisibleRootItem();

    //对隐形根条目递归保存整个树
    SaveTree(iroot);
    //保存完毕
    QMessageBox::information(this, tr("保存完毕"), tr("保存节点到文件完毕。"));

    //关闭文件,数据流置空
    m_file.close();
    m_ds.setDevice(NULL);
    //返回
    return;
}
该函数先获取文件名,判断是否非空;
尝试以只写模式打开非空文件名,判断文件是否正常打开;
正常打开后设置数据流的设备为该文件;
获取树头条目指针,将树头条目对象的数据写入文件中,树头条目内置了列计数,就是树形控件的列计数。
然后获取树形控件的隐形根条目,这个隐形根条目专门用于递归函数,虽然顶级条目的 parent 指针为空,但是隐形根条目的子节点全是顶级条目,是单向设置的父子关系。我们对隐形根条目 iroot 调用递归保存函数 ,将树形节点全都存入文件中,隐形根条目也会写入到文件中。
递归函数执行完成后,显示消息框,关闭设备,将数据流置空。
递归遍历并存储节点的工作都由下面函数实现:
//先有父,后有子,我们按照先序递归遍历保存节点
//每个节点苦工代码保存自己数据和儿子节点数,然后对儿子节点递归调用本函数
//父子节点是紧密相邻存储的,知道儿子节点数就能恢复父子关系
void Widget::SaveTree(QTreeWidgetItem *curItem)
{
    if(NULL == curItem) return; //空指针不处理
    //获取子节点数目
    int nChildCount = curItem->childCount();
    //递归层级截止判断,叶子节点是递归最底层
    if( nChildCount < 1 )
    {
        //所有节点都保存自身数据和子节点数,叶子的子节点数为 0
        m_ds<< (*curItem) << nChildCount;
        return;
    }

    //先序递归,苦工代码在前
    //苦工代码,保存自己条目数据和儿子节点数,儿子节点数用于恢复父子关系
    m_ds<< (*curItem) << nChildCount;

    //踢皮球代码,枚举所有儿子,叫他们干活
    for(int i=0; i<nChildCount; i++)
    {
        QTreeWidgetItem *curChild = curItem->child( i );
        SaveTree( curChild );//递归调用
    }
    //返回
    return;
}
递归保存函数先判断空指针,执行非空时才继续;
获取子节点数;
判断是否为叶子节点,到叶子节点是递归的截止层级,对于叶子节点,直接输出条目对象数据和子节点数(0),并返回;
对于非叶子节点,还有子节点,按照先序递归,先执行苦工代码:
即输出条目对象和子节点数;
然后是踢皮球代码,枚举每个子节点,对每个子节点递归调用本函数。
最后返回。
保存树结构的代码是比较简单的,而加载时就有很多问题需要注意了。

我们先来看看“清空树”按钮槽函数的代码:
void Widget::on_pushButtonClearTree_clicked()
{
    //清空树形结构
    ui->treeWidget->clear();
    //设置树头文本为空
    ui->treeWidget->headerItem()->setText(0, "");
}
只有两句代码,调用树形控件的 clear() 函数删除所有子孙节点,设置树头条目的文本为空。
注意树头条目还是占用内存空间的,隐形根条目也还是存在的,只是没子孙节点了。

接下来我们看看“加载按钮”槽函数的代码:
void Widget::on_pushButtonLoadFile_clicked()
{
    //获取文件名编辑框内容
    QString strFileName = ui->lineEditFileName->text().trimmed();
    //判断是否为空
    if( strFileName.isEmpty() )
    {
        QMessageBox::warning(this, tr("文件名"), tr("文件名为空,无法加载。"));
        return;
    }
    //尝试打开文件,只读模式
    m_file.setFileName( strFileName );
    if( ! m_file.open( QIODevice::ReadOnly ) )
    {
        QMessageBox::warning(this, tr("打开"), tr("无法打开目标文件,请检查文件名或权限。"));
        return;
    }
    //已正常打开,设置数据流
    m_ds.setDevice( &m_file );

    //清空树形结构,槽函数可以像普通函数一样调用
    on_pushButtonClearTree_clicked();

    //获取树头条目指针
    QTreeWidgetItem *iHeader = ui->treeWidget->headerItem();
    //读取数据到树头,树头数据里自带了树的列计数 columnCount()
    m_ds >> (*iHeader);
    int nColCount = iHeader->columnCount();
    qDebug()<<"Header columns: "<<nColCount; //调试信息方便排查问题
    //每读取一次数据,都进行异常判断
    if( ( nColCount < 0)
        || (nColCount > MAX_COLS) )
    {
        //异常状态
        QMessageBox::critical(this, tr("树头加载异常"),
            tr("树头条目加载异常,列计数小于 0 或大于 1000。"));

        ui->treeWidget->setColumnCount( 1 ); //列数还原为 1

        //关闭文件,数据流置空
        m_file.close();
        m_ds.setDevice(NULL);
        //返回
        return;
    }

    //获取隐形根条目,隐形根条目是一直存在的,隐形根条目的列数为 0
    QTreeWidgetItem *iroot = ui->treeWidget->invisibleRootItem();

    //隐身根条目已有内存空间,不需要分配
    //调用递归函数,先序递归创建树结构
    LoadTree( iroot );

    //加载结束
    QString strMsg = tr("加载文件中树形节点结束。");
    //数据流状态测试
    if( m_ds.status() != QDataStream::Ok )
    {
        strMsg += tr("\r\n文件读取异常,只加载了合格的部分数据。");
    }
    if( ! m_ds.atEnd() )
    {
        int nres = m_file.size() - m_file.pos();
        strMsg += tr("\r\n文件内容未全部加载,后面数据不合格或与该树无关。\r\n剩余未读数据: %1 B").arg( nres );
    }
    //显示消息框
    QMessageBox::information(this, tr("加载文件结束"), strMsg);
    //全部展开
    ui->treeWidget->expandAll();

    //关闭文件,数据流置空
    m_file.close();
    m_ds.setDevice(NULL);
    //返回
    return;
}
该函数首先获取文件名,检查非空后,尝试以只读模式打开该文件,如果失败就提示消息框并返回;
打开成功后,设置数据流的设备为该文件;
调用“清空树”按钮的槽函数 on_pushButtonClearTree_clicked(),手动调用槽函数与调用普通函数是一样的;
然后获取树形控件的树头条目指针,从文件中加载数据到树头条目对象里,

注意读取与保存操作最大的不同是:读取的数据源存在损坏或污染的情况。

每个读取函数都应该尽量考虑数据异常导致代码出错的情况,读取函数里要尽可能多地对输入数据进行判断。
我们这里限定了树头条目的列计数 为 0~MAX_COLS (即1000)之间,超出范围就报错,
如果出错就将树形控件的列数还原为 1,关闭文件,设置数据流为空,并返回。

树头条目不出错的情况下,继续下面代码,我们获取树形控件的隐形根条目,
隐形根条目一直存在,不需要我们分配空间,获取指针后使用即可;
我们对隐形根条目调用递归加载函数 ,实现树形结构的还原;

LoadTree( iroot ) 函数递归调用时也可能读取异常数据,因此我们在该函数后,
判断数据流的状态是否正常,如果异常就添加提示信息;
然后数据文件也可能没有全部读完,还有剩余的字节,因此对文件读取位置做了判断,
如果有未读取的部分,就显示未读取的字节数目;
消息字符串设置完后,弹窗显示消息;
最后我们展开树形所有节点,关闭文件,数据流置空,返回。

读取函数代码要比保存函数复杂,因为需要对输入进行判断,这在实际项目开发时尤为重要,因为输入处理不当,容易导致程序崩溃。我们要加强程序的健壮性,就必须多考虑异常数据污染的情况,并进行鉴别处理。

最后是递归加载各个节点的函数代码:
//先序递归加载树的节点
void Widget::LoadTree(QTreeWidgetItem *curItem)
{
    if(NULL == curItem) return; //空指针不处理
    //递归层级截止判断,树没有创建完,不能用叶子节点判断
    //用数据流的结束标志作为截止判断
    if( m_ds.atEnd() )
    {
        //没有数据了,直接返回
        qDebug()<<"File end. This is an empty node.";
        return;
    }
    //子节点数目
    int nChildCount = 0;
    //先序递归,苦工代码在先
    //苦工代码,读取节点数据和子节点数
    m_ds>> (*curItem) >> nChildCount;
    //列计数
    int nColCount = curItem->columnCount();
    //注意递归函数和循环体里面不要弹出任何消息框,因为如果出问题容易一直弹窗弹到死机。
    //打印信息
    qDebug()<<"curItem text: "<<curItem->text(0)
            <<" , colCount: "<<nColCount
            <<" , childCount"<<nChildCount;
    //进行异常判断
    if( (nChildCount < 0)
            || (nColCount < 0)
            || (nColCount > MAX_COLS) )
    {
        //子节点数是负数,或条目的列计数不是 0 ~ 1000
        qDebug()<<"This is an error node.";
        return;
    }

    //如果数据正常
    //踢皮球代码
    for(int i=0; i<nChildCount; i++)
    {
        //分配子节点空间,构造函数自动设置父节点为 curItem
        QTreeWidgetItem *curChild = new QTreeWidgetItem( curItem );
        //双击可编辑的标志位不是通用数据,不会写入文件
        //手动设置双击可编辑
        curChild->setFlags( curChild->flags() | Qt::ItemIsEditable );

        //叫儿子们去干活,递归调用
        LoadTree( curChild );
    }
    //返回
    return;
}
函数先进行空指针判断,指针非空才继续下面代码;
注意这里的递归层级截止代码与之前的都不一样,我们这个函数正在构建树形结构,树是不完整的,
不能用之前的叶子节点判断,因为叶子节点可能都没加载。
我们使用的是数据流末尾的判断,当数据流处于末尾时,没数据可以加载,
那就直接返回,此时的 curItem 是新建存在的条目,但是没数据提供给它,所以该条目的文本全是空的。

当数据流还有数据时,继续后面的代码:
初始子节点数 nChildCount  为 0 ;
从数据流加载数据给 条目对象  (*curItem)  和 子节点数变量 nChildCount  ,
注意每次从数据流加载数据都要进行鉴别判断,获取节点的列计数,
我们打印了条目的文本、列计数、子节点数,方便调试问题时查看;
然后对子节点数合法性进行判断,并检查列计数是否在 0~MAX_COLS 区间,
如果 nChildCount 、nColCount 有一个不合法,我们就打印错误节点信息并返回。

如果两个计数变量正常,那么进入循环:
        为每个子节点分配内存空间,构造函数指定父节点为 curItem ;
        手动设置条目的双击可编辑标志位,因为这个标志位不会存到文件里,我们每 new 一个条目,就需要设置一下标志位;
        然后对每个子节点调用本函数,即踢皮球给子节点们。
最后返回。

我们在递归函数里专门对计数变量进行判断,如果出错就返回;并且打印了错误信息。
注意在所有的递归函数和循环体里面,不要使用弹窗函数,因为一旦出问题,很可能一直弹窗弹到死机。
在递归函数和循环体里面,建议只用 qDebug() 等打印错误信息,不要弹窗。

代码讲解到这,本示例有专门准备的异常数据文件,请读者下载了测试,链接目录为:
https://qtguide.ustclug.org/QtProjects/ch08/savetree/error/
五个文件:
crush.bmp、errorChild.tree、errorExtra.tree、errorHeader.tree、normal.tree。
只有最后一个文件可以正常加载,其他的都是损坏文件,第一个压根不是我们程序保存的,是随便找的图片文件。

我们构建示例程序并运行测试:
run1
像添加功能、双击条目编等功能读者自行测试,我们下面把从网站下载的五个文件放到构建目录:
D:\QtProjects\ch08\build-savetree-Desktop_Qt_5_4_0_MinGW_32bit-Debug

然后将文件路径分别设为五个文件的名字,进行加载测试,第一个 crush.bmp 加载:
run
直接就崩溃了,而且是加载树头条目时就崩溃了。调试时可以发现是执行下面这句代码时崩溃的:
void Widget::on_pushButtonLoadFile_clicked()
{
    ...
    //读取数据到树头,树头数据里自带了树的列计数 columnCount()
    m_ds >> (*iHeader);
    ...
}
这算 Qt 库自己崩溃的,是 QVector类的代码分配内存出错崩溃了。我们这个示例代码不好修正这个bug。

我们重新运行示例程序,加载 errorChild.tree 文件:
run
出现5个空文本节点,是因为 C 节点原本没有子节点,把数据污染了,用二进制编辑器修改文件内容,设置 C 的子节点数为5,但实际没有子节点数据,到了文件末尾没数据,就出来了 5个空文本节点。

我们再加载 errorExtra.tree 文件,查看效果:
run
该文件长度被拉长了 16 字节,后面 16字节是无用数据,递归函数结束了,但是还没读到文件末尾,所以提示还有 16 字节剩余的数据。

最后我们加载 errorHeader.tree,在操作系统的任务管理器可以看到一个 CPU 核心长时间保持满载(双核CPU有50%占用),是一直在加载数据:
run
上图看到输出窗口显示了许多错误类型数据的加载提示,然后老爷机双核CPU的一颗核心跑了好几分钟,才出来下面弹窗:
run
打印输出窗口里面提示树头条目的列数为 1001,而我们设定的范围是 0~1000,越界提示了错误信息弹窗。
跑了好几分钟,其实一直在读取树头条目的 1001 列数据,但是数据不合法,程序就开始胡乱读了。
所以读取函数的输入源污染一定要重视,针对异常数据要尽可能鉴别和处理,否则轻着卡死,重则崩溃。
最后一个文件 normal.tree 是正常的,就不截图了。本节的知识介绍到这,下一节学习对控件进行简单的界面定制。


prev
contents
next