8.2 表格控件

本节先介绍表格控件 QTableWidget 的功能特性,表格控件一般有多行多列,它的单元格条目为 QTableWidgetItem,单元格条目可以有丰富的图标、文字、工具提示,支持设置显示的字体、字体颜色、背景色等。另外表格控件拥有水平表头和垂直表头,可以设 置表头的丰富特性,表头设置通常会影响表格控件整体外观。由于表格控件的具有多行多列,它的选择区域和选择行为也变得复杂起来,本节会依次讲述这些内容,最后通过 两个示例展示表格控件相关知识运用。

8.2.1 QTableWidget

在 Qt 设计师可以拖动表格控件到界面窗口,下图可以直观看到表格控件的大致组成:
tablewidget
表格控件最上面一排是只读的水平表头,最左边一列是只读的垂直表头。表头又可以细分为多个分段(section),水平表头的分段就是表格各个列的列首,垂直表头 分段就是表格各个行的行首。表格控件的实体区域是按行、列排布的单元格,单元格内容一般用 QTableWidgetItem 条目填充,单元格如果不填充任何东西,那么默认是 NULL。本小节先介绍表格控件基本的内容,并且讲解基类中比较实用的内容;后面 8.2.2 小节介绍单元格条目 QTableWidgetItem;控件表头的内容也比较丰富,单独放在 8.2.3 小节介绍;然后详细介绍关于表格控件的选中区域、选中行为、选中模式、单次选中命令等内容,选中区域是表格控件独有的特性,而选中行为、选中模式、单次选中命令是本章所有 基于条目的控件都具有的特性,放在 8.2.4 节一块讲解了。

先来看看表格控件的构造函数:
    QTableWidget(QWidget * parent = 0)
    QTableWidget(int rows, int columns, QWidget * parent = 0)
构造函数里最重要的参数是行数 rows 和列数 columns,表格控件必须指定行数、列数,才能进行后续操作。第一个构造函数没指定行数、列数,必须用下面 两个函数设置行数和列数:
void    setRowCount(int rows) //设置行数
void    setColumnCount(int columns) //设置列数
行数、列数对应的获取函数为:
int    rowCount() const //获取行数
int    columnCount() const //获取列数
设置好行数、列数之后就可以进行后续的操作。表格控件在设置行数、列数之后,就有排布好的单元格,默认这些单元格是空的,即 NULL。程序员可以通过 setItem() 函数可以设置各个单元格条目;程序运行时,表格控件默认所有单元格都可以双击编辑,用户的编辑操作会使表格控件自动生成条目存储用户输入的内容。setItem() 函数和用户编辑都可以填充单元格。

这里提前说明一下,表格控件的有很多名字相似的两套函数或信号,比如设置当前高亮单元格函数 setCurrentCell(int row, int column) 和设置当前高亮条目 ​setCurrentItem(QTableWidgetItem * item),这两个函数名字很类似,二者最主要的区别就是:名字带 Cell 的函数是基于单元格的,对 NULL 空单元格也可以使用;而名字带 Item 的函数,必须是填充了实体条目的单元格,针对实际存在的单元格条目操作。下面大致按函数功能介绍表格控件的相关内容。

(1)设置条目
之前 8.1 节 QListWidget 有很多添加条目或设置条目的函数,但是本节表格控件因为设置单元格必须同时知道行号、列号,所以没有 Add** 和 Insert** 之类的添加函数,表格只有如下设置条目函数:
void QTableWidget::​setItem(int row, int column, QTableWidgetItem * item)
一般用 new 新建条目,然后设置到指定行号、列号的单元格里面,比如下面一段示范代码:
    QTableWidgetItem *newItem = new QTableWidgetItem(tr("newitem");
    tableWidget->setItem(row, column, newItem);
从代码而言,就只有 setItem() 函数设置单元格条目;当然,程序运行时用户可以双击编辑单元格,表格控件可以自动新建条目保存用户编辑的内容存到单元格里面。单元格条目不仅支持文本,还能设置图标、丰富 字体、字体颜色、工具提示等信息,后面还会介绍 QTableWidgetItem 类。
关于 setItem() 函数需要注意的问题是:一个条目只能设置给唯一的一个单元格,不能重复设置给多个单元格!如果希望把条目换一个单元格放置,必须用下面移除条目 函数把条目从表格控件卸载下来,再重新设置给新的单元格。

(2)移除条目
QTableWidgetItem * QTableWidget::​takeItem(int row, int column)
​takeItem() 函数会把指定行号、列号的条目从表格控件卸载下来,但不会完全删除。返回的指针如果非空,那么该指针会指向内存中的条目,如果希望把条目 彻底删除,还需要自己手动 delete 删掉内存空间。如果不彻底删除,那么还可以把非空指针重新设置给表格控件。
表格控件还有关于整行删除、整列删除、清空表格的几个槽函数,等会再介绍。

(3)条目访问函数
表格控件可以根据行号、列号获取条目对象指针:
QTableWidgetItem * QTableWidget::​item(int row, int column) const
注意判断返回的指针是否为空,非空指针才能进行后续操作。
另外还能根据表格控件内部的坐标点位来获取位于该坐标位置的条目:
QTableWidgetItem * QTableWidget::​itemAt(const QPoint & point) const
QTableWidgetItem * QTableWidget::​itemAt(int ax, int ay) const
这两个 itemAt() 函数等价的,坐标是指表格控件内部的相对坐标,表格控件内部左上角是 (0,0) 原点。itemAt() 函数也是可能返回空指针的,一定要判断返回值是否为 NULL。
如果知道了条目 item 非空指针,可以用条目的函数获取行号、列号,如 item->row() 是行号,item->column() 是列号。根据条目的非空指针,也可以获取条目的可视矩形(条目在表格可见范围内的显示矩形):
QRect QTableWidget::​visualItemRect(const QTableWidgetItem * item) const

(4)当前选中条目的操作
表格控件一般是多行多列的,实际的选中操作是比较复杂的,我们在这里先介绍最简单的内容,就是不按键盘,只用鼠标左边点击选中当前条目的情况,就是简单的单选操 作。
获取当前选中条目的函数为:
QTableWidgetItem * QTableWidget::​currentItem() const
返回的值如果非空那就是实际存在的条目,如果返回了 NULL,说明单元格是空的。
不管选中的单元格内部是否为空,都可以获取当前选中单元格的行号、列号:
int QTableWidget::​currentRow() const //当前选中行号
int QTableWidget::​currentColumn() const //当前选中列号
除了鼠标点击选中当前高亮的单元格,也可以用函数设置当前单元格:
void QTableWidget::​setCurrentCell(int row, int column)
void QTableWidget::​setCurrentCell(int row, int column, QItemSelectionModel::SelectionFlags command)
单元格不管是不是空的,上面两个函数都可以设置当前高亮选中状态,第二个 ​setCurrentCell() 多了个单次选中命令的参数,等到 8.4.4 节再 详细说。
如果知道了非空条目指针,可以设置该条目为当前选中条目:
void QTableWidget::​setCurrentItem(QTableWidgetItem * item)
void QTableWidget::​setCurrentItem(QTableWidgetItem * item, QItemSelectionModel::SelectionFlags command)
第二个函数的 command 参数等到 8.4.4 节再 详细说。

无论是通过代码改变当前选中条目还是用户点击操作改变当前选中条目,都会触发当前单元格变化的信号:
void    currentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn)
//单元格无论是否为空都起作用,当前选中单元格的变化信号,参数指出了新当前单元格行列号和旧单元格的行列号
void    currentItemChanged(QTableWidgetItem * current, QTableWidgetItem * previous)
//当前选中条目变化信号,新、旧当前选中单元格至少有一个非空才会触发这个信号

(5)条目查找和排序
表格控件也可以根据模版字符串查找匹配的条目:
QList<QTableWidgetItem *> QTableWidget::​findItems(const QString & text, Qt::MatchFlags flags) const
参数里的 text 是模板字符串,第二个 flags 是匹配标志,匹配标志 Qt::​MatchFlags 在 8.1.1 节列举过了,一模一样,比如 Qt::MatchContains 是包含子串,Qt::MatchWildcard 是通配符(*、?)匹配,Qt::MatchRegExp 是正则表达式匹配,等等。

表格控件的排序操作复杂一些,因为有很多列,开启自动排序后需要指定按照哪一列来排序。开启自动排序函数为:
void    setSortingEnabled(bool enable) //设置是否自动排序
bool    isSortingEnabled() const //判断是否开启了自动排序
开启自动排序之后,需要指定按照哪一列来排序:
void QTableView::​sortByColumn(int column, Qt::SortOrder order)
排序的函数是从基类 QTableView 继承的,指定自动排序的列 column 和 order (升序 Qt::AscendingOrder 或降序 Qt::DescendingOrder)。
除了上面自动排序的函数,还有一个手动排序函数,可以在不开启自动排序时手动调整排序:
void QTableWidget::​sortItems(int column, Qt::SortOrder order = Qt::AscendingOrder)

开启自动排序之后,会对 ​setItem(int row, int column, QTableWidgetItem * item) 函数产生重要影响:新设置的条目如果正好在指定的自动排序列,那么新条目会自动排序,被挪到按序排列好的行号,​setItem() 函数指定的行号不一定有效。
如果用代码设置某一行的条目,该行的条目是联系在一起的,比如是某人的个人信息,那么就需要特别注意自动排序的问题:比如预期一行条目的行号是 therow,自动排序列为 sortcol,对于前几个列的条目,使用 therow 设置条目给表格,该行位置不变;当设置到 therow 行、sortcol 列的条目时,问题来了,自动排序列会根据新条目内容调整该行序号,很有可能把该行移动到了新的行号 newrow,那么如果后面代码继续用 旧的 therow 行号设置后面几列的条目,后面几列条目就会错位。这是很严重的问题。
因此在调用 setItem() 函数设置整行的多列条目时,一定要提前关闭自动排序,把新条目都设置完毕后再考虑开启自动排序!

(6)单元格控件和运行时条目编辑
单元格除了设置条目,还可以设置独立的单元格控件:
void QTableWidget::​setCellWidget(int row, int column, QWidget * widget)
需要注意的问题是单元格控件 widget 会完全覆盖住单元格条目 item,默认情况下单元格条目 item 与单元格控件 widget 是完全没关联的,比如设置给单元格一个组合框,组合框显示的东西与单元格原本条目没关系,修改了组合框内容不会影响单元格条目内容。如果希望单元格控件与单元格条 目有关,需要自己编代码。
获取单元格控件使用如下函数:
QWidget * QTableWidget::​cellWidget(int row, int column) const
注意返回的指针可能为空。
单元格控件也可以被删除掉:
void QTableWidget::​removeCellWidget(int row, int column)
删除单元格控件后,单元格就会显示原来的条目内容(如果条目原本是 NULL ,就剩下空单元格)。

表格控件的单元格也可以开启持续编辑器和关闭持续编辑器(必须配对使用):
void QTableWidget::​openPersistentEditor(QTableWidgetItem * item) //开启持续编辑器
void QTableWidget::​closePersistentEditor(QTableWidgetItem * item) //关闭持续编辑器
因为表格控件默认的条目全都带了可以编辑标志,所以一般用不到持续编辑器,如果想用代码让单元格进行编辑状态,可以用更简单的函数:
void QTableWidget::​editItem(QTableWidgetItem * item)
对于可编辑的条目,都可以用 ​editItem() 开启编辑器,这个临时编辑器可以自动关闭的,不需要调用其他函数。

程序运行时,表格控件默认所有单元格都可以双击编辑,但如果希望表格全部是只读的,那么可以用下面函数关闭表格控件的整体编辑触发器:
void    setEditTriggers(EditTriggers triggers) //设置编辑触发器
EditTriggers    editTriggers() const //获取编辑触发器
默认的编辑器触发器是双击、回车键等可以编辑单元格,修改为 QAbstractItemView::NoEditTriggers 就不会开启任何编辑器了,相当于整个表格只读了。如果不希望整个表格只读,而只是希望某部分的单元格只读,那么只有设置单元格条目自身的标志 位,item->setFlags(Qt::ItemFlags flags),把 flags 设置为不带可编辑标志的:
(item->flags())  & (~Qt::ItemIsEditable) 。

(7)信号
除了之前介绍的 currentCellChanged() 和 currentItemChanged() 信号,还有多个关于单元格和条目操作的信号,按照基于单元格操作触发还是实体条目操作触发,大致分为两类:
void    cellActivated(int row, int column) //单元格被激活
void    cellChanged(int row, int column) //单元格内部发生变化,如从NULL变成有条目,或者条目内部数据发生变化
void    cellClicked(int row, int column) //单元格被点击
void    cellDoubleClicked(int row, int column) //单元格被双击
void    cellEntered(int row, int column)
//鼠标进入单元格,只有在 mouseTracking 开启时或鼠标移动时点击了单元格,才触发该信号
void    cellPressed(int row, int column) //单元格被鼠标点击按下
第二类是基于实体条目触发的:
void    itemActivated(QTableWidgetItem * item) //条目被激活
void    itemChanged(QTableWidgetItem * item) //条目内部数据发生变化,如文本修改、图标变化、复选状态变化等
void    itemClicked(QTableWidgetItem * item) //条目被点击
void    itemDoubleClicked(QTableWidgetItem * item) //条目被双击
void    itemEntered(QTableWidgetItem * item)
//鼠标进入条目,只有在 mouseTracking 开启时或鼠标移动时点击了条目,才触发该信号
void    itemPressed(QTableWidgetItem * item) //条目被鼠标点击按下
因为表格条目是多选的,可以有很多高亮选中条目,高亮选中的状态发生变化时,会触发如下信号:
void    itemSelectionChanged()
上面信号参数没有指明哪些条目被选中,要获取所有选中的条目,可以用如下函数:
QList<QTableWidgetItem *> QTableWidget::​selectedItems() const
更多关于表格选中操作的内容后面再介绍。

(8)槽函数
表格有两个清空内容的槽函数,首先是 clear() 槽函数:
void QTableWidget::​clear()
​clear() 函数删除表格内所有条目内容,单元格全为 NULL,清除选中状态,水平表头和垂直表头的设置内容清空,但是会保留表格的行数、列数,表头清掉 之后,会使用默认的数字行号、列号。
第二个清除内容的槽函数:
void QTableWidget::​clearContents()
这个函数删除所有条目内容,单元格全为 NULL,清除选中状态,但是表头的设置内容都保留,表头文本等照旧显示,表格行数、列数也不变。
还有关于整行、整列插入删除操作的槽函数:
void QTableWidget::​insertColumn(int column) //插入新的一列,新列序号 column,新列的单元格默认 NULL
void QTableWidget::​insertRow(int row) //插入新的一行,新行序号 row,新行的单元格默认 NULL
void QTableWidget::​removeColumn(int column) //删除第 column 整列条目
void QTableWidget::​removeRow(int row) //删除第 row 整行条目
最后是关于条目滚动显示的函数,表格的行、列很多时,只有一部分的单元格显示在表格控件可视矩形里,如果希望滚动表格,让指定的条目显示出来,使用如下函数:
void QTableWidget::​scrollToItem(const QTableWidgetItem * item, QAbstractItemView::ScrollHint hint = EnsureVisible)
QAbstractItemView::ScrollHint 枚举常量在 8.1.1 节列举过,这里不重复介绍了。

(9)基类 QTableView 的函数
表格控件还从基类 QTableView 继承了很多函数,这里介绍一些比较实用的,更多关于 QTableView 视图的内容会等到模型/视图章节再详解。
表头控件的表头分段有分隔线,拖动分隔线可以调整各列的宽度,各行高度也可以拖动分隔线调整。如果通过函数代码调整列宽、行高,可以用下面的函数:
void QTableView::​setColumnWidth(int column, int width) //设置列宽
int QTableView::​columnWidth(int column) const //获取列宽
void QTableView::​setRowHeight(int row, int height) //设置行高
int QTableView::​rowHeight(int row) const //获取行高
用户在程序运行时可以双击表头分段的分隔线,那样表格控件会自动根据该列单元格内容最宽的条目,调整列宽,让该列条目内容都显示出来,这个操作也有对应的函数: (下面四个都是槽函数)
void    resizeColumnToContents(int column) //自动调整第 column 列列宽,将该列条目显示完整
void    resizeColumnsToContents() //自动调整所有列宽
void    resizeRowToContents(int row) //自动调整第 row 行行高,将该行条目显示完整
void    resizeRowsToContents() //自动调整所有行高
如果希望隐藏或显示指定列的所有条目或指定行的条目,可以用如下函数:
void QTableView::​setColumnHidden(int column, bool hide) //设置指定列隐藏或显示
bool QTableView::​isColumnHidden(int column) const //判断指定列是否被隐藏
void QTableView::​setRowHidden(int row, bool hide) //设置指定行隐 藏或显示
bool QTableView::​isRowHidden(int row) const //判断指定行是否被隐藏
关于显示或隐藏行列,还有四个快捷槽函数:hideColumn(int column),hideRow(int row),showColumn(int column),showRow(int row),这些函数都可以灵活使用,效果与 ​setColumnHidden() 和 ​setRowHidden() 函数是一样的。

QTableView 也有更抽象的基类 QAbstractItemView ,QAbstractItemView 里面也有一些实用函数,这里先介绍一个设置条目显示图标大小的函数:
void QAbstractItemView::setIconSize(const QSize & size)
单元格里图标默认显示为 16*16 的,可以用该函数设置图标显示尺寸为 24*24 或 32*32 。 QAbstractItemView 类还有关于条目选中模式、选中行为等重要函数,后面再介绍。基类的内容暂时介绍的到这,下面来学学单元格条目的知识。

8.2.2 QTableWidgetItem

表格控件条目 QTableWidgetItem 与 8.1.2 节 QListWidgetItem 有很多相似的地方,
QTableWidgetItem 也是一个纯数据类,不是控件,没有基类,也就没有信号和槽函数。QTableWidgetItem 可以直接用数据流 QDataStream 读写,表格条目不单单有文本,还可以有自己的图标、复选框等特性,表格控件会根据条目对象的丰富特性来呈现数据并进行交互操作。
(1)首先来看看条目的构造函数:
    QTableWidgetItem(int type = Type)
    QTableWidgetItem(const QString & text, int type = Type)
    QTableWidgetItem(const QIcon & icon, const QString & text, int type = Type)
    QTableWidgetItem(const QTableWidgetItem & other) //复制构造函数
参数里的 type 一般用于派生类,指定条目独特的分类,对于普通的表格控件条目,通常用默认的数值即可。第二个构造函数是指定了条目的文本,第三个构造函数同 时指定了条目图标和文本。
在构造函数里不能直接指定所属的表格控件,因为表格控件有行号、列号所以不能简单追加到表格里,而只能用表格控件的 QTableWidget::​setItem() 函数把条目设置到指定单元格里。注意一个条目只能设置给表格控件唯一的一个单元格,不能把一个条目设置给多个单元格。如果希望为多个单元格设置相似的条目数据内容,那么可 以用后面介绍的 clone() 函数制造大量新的复制体条目。
复制构造函数不会复制旧条目的 type() 类型值和tableWidget() 所属表格控件指针,条目其他的内部数据和条目标志都会复制。

(2)复制函数和运算符函数
除了复制构造函数,还有专门的克隆函数:
QTableWidgetItem * QTableWidgetItem::​clone() const
克隆函数会新建一个当前条目的复制体,然后返回新复制体条目的指针,如果要构造并添加大量重复内容的条目,这个函数就比较实用。clone() 函数内部会调用复制构造函数创建新条目并返回。
条目复制还可以用等于号函数:
QTableWidgetItem & QTableWidgetItem::​operator=(const QTableWidgetItem & other)
等于号函数复制的内容与复制构造函数差不多,type() 类型值和 tableWidget() 所属表格控件指针也不复制。
还有个小于号函数,可以比较条目文本的字典序:
QTableWidgetItem & QTableWidgetItem::​operator=(const QTableWidgetItem & other)
如果希望使用更多的比较函数,可以直接用 QString 的比较函数。

(3)QTableWidgetItem 的功能函数与内部数据
QTableWidgetItem 内部的数据也大致分为两类:第一类是以数据角色形式管理的通用数据,这些数据自动参与 QDataStream 数据流的读写;第二类是非通用数据,不参与数据流读写,与 QListWidgetItem 和 QListWidget 自身特性有关。QTableWidgetItem 主要的功能函数也是围绕这两类内部数据展开的。

● 第一类:通用数据及其处理函数
通用数据是以数据角色与数据变量一一对应的形式存储管理,比如设置文本 setText()、设置图标 setIcon() 等函数,其本质都是根据各自的角色调用通用设置数据的函数:
virtual void setData(int role, const QVariant & value)
也可以根据角色来获取各个数据变量:
virtual QVariant data(int role) const
查看表格控件的源代码文件可以看到关于图标操作函数的源码:
inline QIcon icon() const
   { return qvariant_cast<QIcon>(data(Qt::DecorationRole)); }
inline void QTableWidgetItem::setIcon(const QIcon &aicon)
   { setData(Qt::DecorationRole, aicon); }
QTableWidgetItem 的通用数据与 QListWidgetItem 的通用数据一模一样,函数名也一样,列表如下:

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

表格条目 QTableWidgetItem 可以直接用数据流 QDataStream 读写,是通过下面运算符重载函数实现的:
QDataStream &    operator<<(QDataStream & out, const QTableWidgetItem & item)
QDataStream &    operator>>(QDataStream & in, QTableWidgetItem & item)
使用 >> 和 << 运算符进行流输入输出比较常见,当然也可以用 QTableWidgetItem 类内部的读写函数进行输入输出:
void QTableWidgetItem::​read(QDataStream & in)
void QTableWidgetItem::​write(QDataStream & out) const

● 第二类:非通用数据及其操作函数
表格控件的条目也是在构造函数一次性指定条目类型,然后这个类型是只读的:
int QTableWidgetItem::​type() const
类型值一般用于派生类区分不同的条目类型,自定义的条目类型值需要大于等于 QTableWidgetItem::UserType 。
新条目通过表格控件 QTableWidget::​setItem() 函数设置给表格控件之后,条目会自动保存所属的表格控件指针,可以用如下函数获取:
QTableWidget * QTableWidgetItem::​tableWidget() const

程序运行时,除了可以通过表格控件的 QTableWidget::​selectedItems() 获取用户选中的条目列表,每个单元格条目自身也会记录是否 被用户选中,并且能设置自身是否被选中:
bool QTableWidgetItem::​isSelected() const //判断条目自身是否高亮选中
void QTableWidgetItem::​setSelected(bool select) //设置条目自身是 否被选中

表格控件条目初始化时也有默认的标志位,并且运行时可以修改条目的特性标志:
Qt::ItemFlags QTableWidgetItem::​flags() const //获取特性标志位
void QTableWidgetItem::​setFlags(Qt::ItemFlags flags) //设置特性标志位
QTableWidgetItem 构造时默认的标志位为:
    Qt::ItemIsEditable
    |Qt::ItemIsSelectable
    |Qt::ItemIsUserCheckable
    |Qt::ItemIsEnabled
    |Qt::ItemIsDragEnabled
    |Qt::ItemIsDropEnabled
因此程序代码生成的所有表格条目默认都是可编辑的,程序运行时用户双击空的单元格时,表格控件会自动创建新条目保存用户编辑内容,这些自动创建的条目也都是可编辑 的。如果希望条目是只读的,那么把标志位设置不带Qt::ItemIsEditable 的新标志位即可。

Qt::ItemFlags 所有的枚举常量在 8.1.2 节末尾有详细的表格描述,这里不重复贴了。表格条目默认就是支持复选状态的,只是默认没有显示出来,要让表格控件显示条目对应的复选框,可以用下面一句代码实现:
    item->setCheckState( Qt::Unchecked );  //显示复选框
对于表格控件和表格条目,自带单行编辑器和复选框功能,因此不需要为单元格添加自定义的单行编辑器或复选框,如果要用到其他更多的输入控件,那才需要定制单元格。

表格控件条目与 QListWidgetItem 最大的一个区别就是表格条目既有行号,也有列号:
int QTableWidgetItem::​row() const //获取行号
int QTableWidgetItem::​column() const //获取列号
行号和列号也是在 QTableWidget::​setItem() 函数里指定的,条目自身并不能修改行号或列号。表格控件条目与 QListWidgetItem 另一个区别是,表格控件条目没有隐藏函数,所有表格条目都是显示的(只要不被单元格控件QTableWidget::​ cellWidget() 遮挡)。

在 QtCreator 设计模式和 Qt 设计师界面,表格控件 QTableWidget 和其条目 QTableWidgetItem  都是可以可视化编辑的,上面介绍的通用数据和非通用数据都是可以在属性栏设置的,后面会介绍表格控件及其条目的可视化编辑。

8.2.3 表头设置

表格控件在指定了行号和列号之后,就会自动生成水平表头和垂直表头,默认的表头都是从数字 1 开始递增编号,如下图所示:
autoheader
虽然默认的表头文本看起来从 1 开始编号,但是内部的表头分段(section)都是从 0 开始编号的,代码里设置表头分段时,需要从 0 编号。
表格控件的既可以简单地设置表头分段文本,也可以利用 QTableWidgetItem 为表头分段设置功能更丰富的表头条目,设置表头文本的函数如下:
void QTableWidget::​setHorizontalHeaderLabels(const QStringList & labels) //设置水平表头文本
void QTableWidget::​setVerticalHeaderLabels(const QStringList & labels) //设置垂直表头文本
QStringList 内字符串列表,第 0 个字符串就是第 0 号表头分段的文本,一般要求 QStringList 字符串个数与表头分段数目一样,这样不会有漏的,也不会有多的。通常比较多的是设置水平表头(就是每列的列首),垂直表头一般可以不设置,用自动编号。

​setHorizontalHeaderLabels() 和 ​setVerticalHeaderLabels()  其实是两个快捷函数,本质是循环构建表头分段条目并设置给表头分段:
void QTableWidget::​setHorizontalHeaderItem(int column, QTableWidgetItem * item) //设置指定列的水平表头分段条目
QTableWidgetItem * QTableWidget::​horizontalHeaderItem(int column) const //获取指定列的水平表头分段条目
void QTableWidget::​setVerticalHeaderItem(int row, QTableWidgetItem * item) //设置指定行的垂直表头分段条目
QTableWidgetItem * QTableWidget::​verticalHeaderItem(int row) const //获取指定行的垂直表头分段条目
这里可以发现 QTableWidgetItem 既可以作为表格控件普通单元格的条目,也可以用来设置表头分段。
QTableWidgetItem 作为表头分段条目使用时,肯定是只读的,程序运行时不能双击编辑表头,而且也不能有复选框。表头分段条目支持显示文本、图标、工具提示信息,并能设置文本对齐、字体、前景 画刷和背景画刷等等,这样表头显示的功能就比较丰富,而不是单纯的文本。
使用 ​setHorizontalHeaderItem() 和 ​setVerticalHeaderItem() 函数时,一个 QTableWidgetItem 只能设置给唯一的一个表头分段,并且设置给表头分段之后,也不要把表头分段条目设置给表格控件普通的单元格。
表头分段的条目也可以从表格控件卸载卸载下来:
QTableWidgetItem * QTableWidget::​takeHorizontalHeaderItem(int column) //卸下指定列的水平表头条目
QTableWidgetItem * QTableWidget::​takeVerticalHeaderItem(int row) //卸下指定行的垂直表头条目
卸载下来的条目并不会自动删除,还存在内存中,如果希望彻底删除需要手动 delete,或者不删除的话,卸载下来的条目可以重新设置给另一个表头分段。

表头分段的条目与普通的单元格条目有重要的区别,普通单元格条目可编辑、可以有复选状态,但是表头分段的条目只有显示功能,不能双击编辑或复选。把 QTableWidgetItem 设置给表头分段后,QTableWidgetItem 条目会有额外的内部特性标志:
    if (item) {
        item->view = view;
        item->itemFlags = Qt::ItemFlags(int(item->itemFlags)|ItemIsHeaderItem);
    }
上面小段代码是从 QTableWidget 源代码里截取的,设置表头分段条目时,item 的标志位会额外设置 ItemIsHeaderItem 标志位(ItemIsHeaderItem 是内部专用标志位,并不公开),因此设置给表头的条目是不能再设置给普通单元格的。条目 item 设置给一个表头分段后,也不能设置给其他的表头分段,因为表格控件销毁时会 delete 每个表头条目,一个条目指针如果设置给多个表头分段,那么同一个条目指针最后会被 delete 多次,删除野指针会导致程序崩溃。

表格控件并不要求必须设置表头文本或表头条目,在不设置表头文本和表头条目的情况下,默认是所有的表头分段条目是 NULL ,所以要注意表头条目获取和卸载函数返回的指针可能是 NULL。不设置任何表头文本和表头条目时,表格控件依然是有表头的,默认的表头是按照前面截图的从 1 开始自动编号,自动编号表头是没有实体表头条目的,这些自动编号表头是从基类 QTableView 视图类继承而来的。

QTableWidget 本层类带有前面介绍 8 个表头分段操作的函数,各个表头分段设置从微观角度展示表头的功能,而基类 QTableView 有从宏观描述的整体表头,即:
QHeaderView * QTableView::​horizontalHeader() const //获取整个的水平表头
void QTableView::​setHorizontalHeader(QHeaderView * header) //设置整个的水平表头
QHeaderView * QTableView::​verticalHeader() const //获取整个的垂直表头
void QTableView::​setVerticalHeader(QHeaderView * header) //设置整个的垂直表头
因为表格控件自己就会带有水平表头和垂直表头,所以一般不需要调用 ​setHorizontalHeader() 和 ​setVerticalHeader() 函数,直接获取原本的表头对象指针,然后调用表头类 QHeaderView 的函数修改特性即可。
水平表头和垂直表头都是同一个类 QHeaderView ,水平表头和垂直表头可以通过如下函数判断:
Qt::Orientation QHeaderView::​orientation() const
返回值如果是 Qt::Horizontal 表示水平表头,Qt::Vertical 是垂直表头,这个方向特性是在构造函数时指定的,之后不能进行修改。
QHeaderView 功能是非常丰富的,这里介绍一些比较实用的,以后到模型视图章节还会介绍更多的。
获取当前表头的分段数目,使用函数:
int QHeaderView::​count() const
显示整个表头显示的像素点长度(水平表头是表头整体宽度,垂直表头是表头整体高度):
int QHeaderView::​length() const

在 Qt 设计师界面,选中表格控件,可以看到水平表头和垂直表头的属性,这里以水平表头的属性举例:
header2
当表格控件开启自动排序属性 sortingEnabled 之后,水平表头就会自动设置 horizontalHeaderShowSortIndicator 为勾选状态,这样程序运行时根据一个列自动排序时,该列水平表头分段显示三角形排序指示符。如果勾选水平表头的 horizontalHeaderStretchLastSection 属性,那么表格最后一列会自动拉伸占据剩下的空间。水平表头和垂直表头这些属性都有对应的 QHeaderView 类的函数,罗列描述如下:

属性 QHeaderView类设置 函数 QHeaderView类获取 函数 描述
**visible setVisible(bool v) isVisible() 是否显示表头。
**cascadingSectionResizes setCascadingSectionResizes(bool enable) cascadingSectionResizes() 调整分段尺寸时是否仅相邻分段调整。
**defaultSectionSize setDefaultSectionSize(int size) defaultSectionSize() 分段默认尺寸,水平表头是指分段宽度(默认100),垂直表头指分段高度(默认30)。
**highlightSections setHighlightSections(bool highlight) highlightSections() 条目高亮选中时,其行首、列首是否也有高亮效果。一般不需要手动设置。
**minimumSectionSize setMinimumSectionSize(int size) minimumSectionSize() 分段尺寸下限。水平表头分段默认最小宽度 21,垂直表头分段默认最小高度 21。
**showSortIndicator setSortIndicatorShown(bool show) isSortIndicatorShown() 表头是否显示排序指示符。开启自动排序后通常自动显示排序指示符,一般不需要手动设置。
**stretchLastSection setStretchLastSection(bool stretch) stretchLastSection() 是否自动拉伸末尾分段。

按照上表描述,如果希望隐藏垂直表头,那么就可以执行如下代码:
 ui->tableWidget->verticalHeader()->setVisible(false);
如果修改默认的列宽,让每列显示出更长的文本,可以把默认值 100 修改为 160 像素:
 ui->tableWidget->horizontalHeader()->setDefaultSectionSize(160);
其他属性的意义比较明确,关于 cascadingSectionResizes 属性是指用户拖动表头分段的分隔线调整列宽或行高时,是否仅相邻分段调整:
如果 cascadingSectionResizes 为 true,那么调整尺寸操作仅涉及分隔线相邻的两个分段,比如左边列变宽,那么右边一列就会变窄。如果 cascadingSectionResizes 为 false,就是默认的递推调整右侧所有列,左边一列变宽时,右边所有列列宽固定,并且右边所有列整体右移,表格整体拉大。

除了 Qt 设计师里能看到的属性,QHeaderView 还有 defaultAlignment 属性决定表头文本的对齐方式,maximumSectionSize 属性决定分段的尺寸上限。

表头的特性设置通常会影响表格控件整体的外观,之前介绍过表格控件基类 QTableView 调整列宽和隐藏/显示整列的函数,其实 QTableView 很多关于整行或整列调整的函数都是在内部调用表头 QHeaderView 的函数来实现,比如设置列宽的:
void QTableView::setColumnWidth(int column, int width)
{
    Q_D(const QTableView);
    d->horizontalHeader->resizeSection(column, width);
}
QHeaderView::resizeSection(int logicalIndex, int size),就是调整分段尺寸,对于水平表头是调整列宽, 对于垂直表头是调整行高。
QTableView 设置某列隐藏或显示的函数,内部代码为:
void QTableView::setColumnHidden(int column, bool hide)
{
    Q_D(QTableView);
    if (column < 0 || column >= d->horizontalHeader->count())
        return;
    d->horizontalHeader->setSectionHidden(column, hide);
}
QTableView 很多功能都是通过表头 QHeaderView 的函数实现的,如果表格控件需要设置特定的功能找不到直接的设置或获取函数,那么在 QHeaderView 类里面找函数是比较有效的方法。比 如 QHeaderView 类可以查询隐藏的分段计数:
int QHeaderView::​hiddenSectionCount() const
如果表格控件隐藏了一些列,水平表头的 ​hiddenSectionCount() 就是隐藏的列数目。
默认情况下,表格控件所有的列显示顺序是固定的,如果用户希望可以自己调整列的顺序,那么可以设置表头的分段是可移动的:
void QHeaderView::​setSectionsMovable(bool movable)

利用表头类的函数,我们可以通过调整水平表头和垂直表头的默认分段尺寸设置单元格默认尺寸:
    ui->tableWidget->horizontalHeader()->setDefaultSectionSize(150);
    ui->tableWidget->verticalHeader()->setDefaultSectionSize(50);
这样表格的单元格尺寸就会变大很多:
cellsize
现在单元格的尺寸是可以按自己意愿调整的,但是如果希望调整上面水平表头的高度或调整左侧垂直表头的宽度,那怎么办呢?
**SectionSize 只能指定表头分段延伸的一维尺寸,不会影响水平表头高度,也不影响垂直表头的宽度。 QHeaderView 表头类其实也是通用控件和窗口类 QWidget 的派生类,QWidget 设置控件或窗口最小尺寸的函数对 QHeaderView 也适用,表头 QHeaderView 相当于是表格控件内嵌的子控件,表头 QHeaderView 还可以有自己独立的 QSS 样式表。我们使用表头类的基类 QWidget 的函数可以设置水平表头的最小高度和垂直表头的最小宽度,这样可以调整表头自身尺寸:
    //水平表头高度设置
    ui->tableWidget->horizontalHeader()->setMinimumHeight(50);
    //垂直表头宽度设置
    ui->tableWidget->verticalHeader()->setMinimumWidth(100);
设置效果如下:
headersize

关于表头的内容大致介绍到这,QtCreator 设计模式和 Qt 设计师可以编辑表格控件的表头和单元格,后面再介绍。提前说一下,Qt 5.4.* 开发环境里的 QtCreator 设计模式和 Qt 设计师在添加表格控件表头时有 bug,需要第二次编辑并调整顺序,表头才能正常显示,因此更推荐用代码来 设置水平表头和垂直表头。

8.2.4 选中区域和选中行为

(1)选中区域
之前介绍过,获取选中的条目可以如下函数:
QList<QTableWidgetItem *> QTableWidget::​selectedItems() const
选中的单元格如果有实体条目,会记录在 ​selectedItems() 返回的条目列表里,但是如果选中了内部为 NULL 的单元格,那是无法存到选中条目列表的。为了能更完整地描述选中的单元格区域, Qt 专门提供了一个 QTableWidgetSelectionRange 类,记录表格控件的选中区 域,不管单元格有没有条目,都可以描述选中单元格范围。
表格控件可以同时有多个选中区域,获取表格当前选中区域列表的函数如下:
QList<QTableWidgetSelectionRange> QTableWidget::​selectedRanges() const
一般用 Shift 键选中的连续矩形区域对应一个选中区域,而使用 Ctrl 键多次点击选中的多个离散单元格对应多个选中区域。
QTableWidget 也能通过代码设置某个矩形区域是否为高亮选中:
void QTableWidget::​setRangeSelected(const QTableWidgetSelectionRange & range, bool select)

QTableWidgetSelectionRange 类的构造函数可以指定矩形区域,是以单元格的行列号标出矩形区域:
QTableWidgetSelectionRange(int top, int left, int bottom, int right)
QTableWidgetSelectionRange(const QTableWidgetSelectionRange & other) //复制构造函数
参数里的 top 是矩形区域最上面的行号,bottom 是最下面的行号;left 是矩形选择区域最左边的列号,right 是矩形最右边的列号。如果 top 和 bottom 数值一样,同时 left 与 right 数值一样,那么矩形区域就退化为一个单元格了。
QTableWidgetSelectionRange 的功能函数也比较简单,就是获取上下行号、左右列号、行列计数:
int    topRow() const //最上面的行号
int    bottomRow() const //最下面的行号
int    leftColumn() const //最左边的列号
int    rightColumn() const //最右边的列号
int    rowCount() const //区域内行计数
int    columnCount() const //区域内列计数
QTableWidgetSelectionRange 比较简单,只能在构造函数设置矩形范围,没有其他修改矩形范围的函数。如果要修改矩形选中区域,可以直接 新创建一个矩形选中区域。

QTableWidgetSelectionRange 是表格控件 QTableWidget 独有的东西,接下来介绍选中行为、选中模式和单次选中命令,后面三项内容对本章所有基于条目控件都适用的,在这里统一介绍。

(2)选中行为
表格控件默认情况下,点击一个单元格只会选中该单元格本身,如果表格的行内部数据关联性很紧密,比如是同一个人的各种身份信息(姓名、性别、住址等),这时候用户 可能希望点击一次选中整行的单元格,这就是选中行为决定的特性。表格控件从祖辈基类 QAbstractItemView 继承了 selectionBehavior 属性,其获取和设置函数如下:
QAbstractItemView::SelectionBehavior    selectionBehavior() const
void    setSelectionBehavior(QAbstractItemView::SelectionBehavior behavior)
调用设置函数就可以改变选中行为,具体的选中行为枚举常量如下表所示:

QAbstractItemView::SelectionBehavior 枚举常量 数值 描述
QAbstractItemView::SelectItems 0 鼠标点击时只选中该条目,这个是默认值。
QAbstractItemView::SelectRows 1 鼠标点击时选中整行的条目。
QAbstractItemView::SelectColumns 2 鼠标点击时选中整列的条目。

如果我们希望用户点击时自动选中整行,那么把选中行为设置为 QAbstractItemView::SelectRows 即可。
对于 8.1 节列表控件和 8.3 节树形控件,它们也有选中行为属性,但是它们每行只有一个条目,所以一般用不到选中行为属性。

(3)选中模式
对于表格控件,默认就支持使用 Ctrl 和 Shift 键进行多选操作,而 8.1 节的列表控件默认是单选模式,决定到底是单选还是多选,就是选中模式决定的特性。表格控件、列表控件以及后文的属性控件都从祖辈基类 QAbstractItemView 继承了 selectionMode 属性,其获取和设置函数如下:
QAbstractItemView::SelectionMode    selectionMode() const
void    setSelectionMode(QAbstractItemView::SelectionMode mode)
选中模式的枚举常量比较多,列举描述如下:

QAbstractItemView::SelectionMode 枚举常量 数值 描述
QAbstractItemView::SingleSelection 1 单选模式。用户点击选中新条目时,旧的选中条目取消选中状态。只用鼠标点击不能取消选中当前条目,鼠标点击总有一个条目是高亮选中的。
QAbstractItemView::ContiguousSelection 4 连续多选模式。平常点击与单选模式特性一样;但如果用户按下 Shift 键同时点击条目,可以进行连续区域的选中或取消选中。以按下 Shift键之前的当前条目为基准条目,基准条目与Shift键按下后最后一次点击的条目之间的连续区域会被选中,其他条目取消选中。
QAbstractItemView::ExtendedSelection 3 扩展多选模式。平常点击与单选模式特性一样;但如果用户按下Shift键同时点击条目可以选中连续区域;如果用户按下Ctrl键可以选中或 取消选中离散条目的高亮选中状态。另外,鼠标左键按下后连续拖动也可以选中连续的区域。
QAbstractItemView::MultiSelection 2 普通多选模式。用户点击某个条目时,该条目的选中状态取反:如果之前是选中就变为非选中,如果之前是非选中就变为选中。鼠标左键按下后连续 拖动可以选中或取消选中连续区域。
QAbstractItemView::NoSelection 0 无选模式。不能选中条目,这个很少用到。

对于 8.1 列表控件和 8.3 树形控件,默认选中模式为单选模式 SingleSelection ,本节的表格控件默认是扩展多选模式 ExtendedSelection ,操作系统的文件资源管理器一般都是扩展多选模式。程序中常用的就是单选模式、扩展多选模式。

(4)单次选中命令
通常使用上面介绍的选中行为和选中模式就能满足绝大多数情况下的选中特性需求,但也有少数例外的情况,比如我们不希望修改控件默认的选中行为和选中模式,但又想使 用默认选中行为和选中模式规定之外的选中操作,那就涉及到单次选中命令这一特性了。对于列表控件、表格控件以及后面的树形控件,都有类似下面的函数:
void setCurrentItem(QTableWidgetItem * item, QItemSelectionModel::SelectionFlags command)
setCurrentItem() 函数第一个参数是条目指针,第二个参数就是单次选中命令(可以是一个标志位或者多个选中标志位的或值),通过 setCurrentItem() 函数可以在控件默认的选中行为和选中模式之外进行一些特定的选中或取消选中的操作。

在解释单次选中命令之前,我们先明确一下“当前条目”和“选中条目”的区别:
① 当前条目通常是指鼠标最后一次点击的条目,这个条目是唯一的,在图形上用虚线框标记,当前条目可以处于非选中状态,也可以处于选中状态,如下面两个图所示(假定下面两个图 的所有单元格都有条目,非 NULL):
curItem1
curItem2
当前条目通常情况下都是用户鼠标最后一次点击的条目,但 setCurrentItem() 函数也可以把当前条目设置为参数里指定的非空条目。获取当前条目的函数是 currentItem() 。

②选中条目就是从图形界面上看到的有高亮背景的条目,选中条目可以没有,可以有一个,也可以有多个。对于没有选中、选中一个的情况,看上面两个图即可。
高亮选中的多个条目可以与当前条目有重叠,也可以不重叠,如下面两个图示范(假定下面两个图的所有单元格都有条目,非 NULL):
selected1
selected2
获取高亮选中的条目函数为 selectedItems(),设置某个条目选中或非选中,可以直接用条目自身的函数 item->setSelected( bool ) 。

归纳一下就是:图形界面上虚线框包裹的唯一条目,是当前条目;拥有高亮选中背景的是选中条 目,选中条目可以没有,也可以有一个或多个。当前条目与选中条目可以有重叠,也可以无重叠。

解释完当前条目和选中条目,下面介绍具体的选中标志位。一个选中标志位或者多个选中标志位的或值 构成单次选中命令,选中标志位的枚举常量如下表描述:

QItemSelectionModel::​ SelectionFlag 枚举常量 数值 描述
QItemSelectionModel::NoUpdate 0x0000 保持旧的选中状态,不更新选中条目。
QItemSelectionModel::Clear 0x0001 清除所有选中状态,没有选中条目了。
QItemSelectionModel::Select 0x0002 选中指定的条目。
QItemSelectionModel::Deselect 0x0004 指定的条目取消选中状态。
QItemSelectionModel::Toggle 0x0008 指定条目的选中状态取反,原先选中就不选中,原先不选中就选中。
QItemSelectionModel::Current 0x0010 将指定的条目设为当前条目。
QItemSelectionModel::Rows 0x0020 对指定条目所在的整行进行选中或非选中。
QItemSelectionModel::Columns 0x0040 对指定条目所在的整列进行选中或非选中。
QItemSelectionModel::SelectCurrent Select | Current 选中指定条目,并且将其设置为当前条目。
QItemSelectionModel::ToggleCurrent Toggle | Current 指定条目的选中状态取反,并将其设置为当前条目。
QItemSelectionModel::ClearAndSelect Clear | Select 先清除旧的选中状态,然后设置指定条目为选中状态。

选中标志位的按位或值,一般要考虑实际意义会不会冲突,比如 Select、Deselect、Toggle 三者是冲突的,不应该同时用。意义不冲突的标志位就可以进行位或,比如 Select | Rows 就是整行选中。
在不修改选中行为和选中模式的时候,下面一句代码就会选中指定条目所在的整行:
ui->tableWidget->setCurrentItem(pItem , QItemSelectionModel::Select | QItemSelectionModel::Rows);
setCurrentItem() 会自动把当前条目设置为参数里指定的非空条目。
如果需要清空所有旧的选中状态,可以用下面这句:
ui->tableWidget->setCurrentItem(NULL, QItemSelectionModel::Clear);
setCurrentItem() 参数里指定的条目是 NULL 时,控件自动把当前条目设置为打头的第 0 个条目,对应到表格就是第 0 行 0 列的单元格。

本小节的选中行为、选中模式、单次选中命令对列表控件、表格控件和树形控件都是通用的,而且以后讲到模型和视图章节时,这三小块内容对列表视图、表格视图、树形视 图也是通用的。
关于表格控件的知识点介绍就到这里,下面来通过两个例子操练一下。

8.2.5 个人信息表格示例

本小节展示利用表格控件显示个人信息,表格共有姓名、性别、生日、婚否和住址五列内容,可以对表格按行增加或删除个人信息记录。在开始例子之前,先下载如下网络文 件夹里的五个图片文件,作为个人信息里的头像使用:
https://lug.ustc.edu.cn/sites/qtguide/QtProjects/ch08/personinfo/images/

我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 personinfo,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
在编辑界面之前,我们在项目文件夹 D:\QtProjects\ch08\personinfo\ 里新建一个 images 子文件夹,把下载的五个图片文件放到 images 子文件夹里面。然后在 QtCreator 里,右击左边栏里的 personinfo 项目名,在右键菜单选择“添加新文件”:
qrc01
在新建文件对话框,左边选择“Qt”,中间选择“Qt Resource File”,点击右下角 Choose 按钮,进入下面界面:
qrc02
编辑文件名为 photos,点击“下一步”,进入如下界面:
qrc03
点击“完成”,然后左边栏 personinfo 项目管理的资源子文件夹里找到 photos.qrc 文件,右击 photos.qrc 文件名,右键菜单选择“添加现有文件”,把文件系统里 D:\QtProjects\ch08\personinfo\images\ 目录里五个图片文件都添加到资源里:
qrc04
这样图片都添加到资源文件 photos.qrc 里了。以后章节示例遇到需要添加 *.qrc 和添加图片到 *.qrc 里的时候,就简单描述,不再逐个步骤 截图示范了。
我们按 Ctrl+S 快捷键保存 photos.qrc 文件,并关闭该资源文件。
双击打开 widget.ui 界面文件,把主界面的窗口大小调整为 400*360,然后按照下图拖入控件:
ui
界面共五行控件,第一行是表格控件,对象名 tableWidget。
第二行,标签控件,文本为 "姓名";单行编辑器,对象名 lineEditName;标签控件,文本为 "头像";组合框,对象名 comboBoxPhotos 。第二行按照水平布局器布局。
第三行,标签控件,文本为 "性别";单行编辑器,对象名 lineEditGender;标签控件,文本为 "生日";日期编辑器控件,对象名为 dateEdit;复选框,文本为 "婚否",对象名为 checkBoxIsMarried,复选框勾选代表已婚,没勾选代表未婚。第三行也按水平布局器布局。
第四行,标签控件,文本为 "住址";单行编辑器,对象名为 lineEditAddress。第四行也按照水平布局器布局。
第五行两边的是水平空白条,中间是两个按钮,第一个按钮文本 "添加",对象名 pushButtonAdd,第二个按钮文本 "删除" ,对象名 pushButtonDel。第五行也是按照水平布局器布局。
最后对于主界面,点击空白位置,只选中主界面的窗口本身,然后点击上面的垂直布局按钮,为主界面设置主布局器,布局好之后的界面如下:
ui2
我们首先右击头像组合框,右键菜单选择“编辑项目”,为组合框添加五个条目,条目的文本为空格字符 " ",条目的图标就是资源里面的五个图片,如下图所示:
combo
设置好组合框的条目后,点击“OK”按钮回到主界面。
然后我们在主界面右击表格控件 tableWidget,右键菜单选择“编辑项目”,弹出表格控件的编辑界面:
table1
表格控件的编辑对话框上方是标签页选择,可以编辑列(列首,即水平表头)、行(行首,即垂直表头)、项目(单元格条目)。对话框下方,左边是增加条目、删除条目按 钮,中间是控制条目上移或下移,右边“属性”按钮点开后看到条目的详细内容设置界面。
这个示例中,我们只设置列首(水平表头),行首不设置(用自动行号),按照如下操作,先编辑列首,添加五个列:
table2
添加"姓名"、"性别"、"生日"、"婚否"、"住址" 五个列后,点击对话框右下角 OK 按钮,回到主界面,看到如下效果:
table3
我们刚才添加的五个列首,现在变成了 "新建列"、"姓名"、"性别"、"生日"、"住址" ,有一个列首被 QtCreator设计模式弄丢了,顺序也错位了,这是 QtCreator设计模式也就是 Qt 设计师的 bug 。所 以更推荐用代码设置表格控件的列首和行首,使用代码设置列首和行首是没问题的,只有 QtCreator设计模式和 Qt 设计师有这样的 bug。

为了修正表头,我们重新右击表格控件,在右键菜单选择“编辑项目”,再弹出表格控件的编辑界面。这次把错误的 "新建列" 文字修改为 "婚否",然后选中"婚否",点击下方中间向下按钮调整 "婚否" 列首的顺序,调整后如下图所示:
table4
经过第二次编辑后,点击对话框右下角 OK 按钮,然后我们回到主界面,可以看到正确的列首:
table5
表头设置正确之后,我们选中表格控件,在 QtCreator 右下角的属性栏可以看到这时候的 rowCount 为 0,columnCount 为 5。
对于 QtCreator 设计模式和 Qt 设计师,如果希望使用自动编号的数字表头,那么就修改 rowCount 或 columnCount 数值,自动生成垂直表头或水平表头。如果不希望使用自动编码的表头,那么就右击表格控件选择 “编辑项目”,然后手动编辑列首或行首。

注意,对于水平表头,如果决定使用自动编号,那么就不要在编辑表格控件的对话框里额外添加列首;如果决定在编辑表格控件的对话框里手动编辑列首(即水平表头),那 么就不要再修改列计数 columnCount 。对于同一方向表头,不要既使用自动编号, 又手动编辑表头,那样导致表头混乱而又无法修改自动表头文本,因为QtCreator设计模式和Qt设计师对自动表头和手动表头编辑二者不兼 容,也有 bug,就如下图错误示范:
tableerror
如上图示范的三个步骤,先设置手动列首,再增加自动列首,然后就发现自动增加的两个列首 "3"、"4" 根本无法编辑,表格编辑对话框是看不到自动编号表头文本的。(表格编辑对话框只能编辑 QTableWidget 的手动表头条目 QTableWidgetItem 对象,自动表头有文本但没有表头条目 QTableWidgetItem 对象,因为自动编号表头是从基类 QTableView 继承的,无法在设计师里编辑。)
最好的办法还是用代码设置表头,这样不会有这些莫名其妙的问题。

现在回到这个例子,我们的水平表头,也就是行首设置好之后,我们把表格控件的垂直表头设置自动的编号,就是修改 rowCount 属性为 2。这时候水平表头是手动编辑的,垂直表头是全自动编号的,二者不在一个维度,因此不会有冲突:
table6
这时候表格为 2 行 5 列的,既有行计数,也有列计数,就能编辑单元格条目了。我们右击表格控件,右键菜单选择“编辑项目”,第三次进入表格编辑对话框,在表 格编辑对话框点击“项目”标签页,就可以编辑单元格了:
table7
我们把姓名一列的单元格既设置文本,也设置图标作为头像,其他列的单元格如下图所示设置:
table8
婚否一列的单元格,文本是空的,然后把单元格 checkState 属性设置成 Checked,就会如上图所示,勾选代表已婚的。
设置好单元格之后,点击 OK 按钮,回到主界面:
table9
关于控件的编辑就是上面描述的,下面我们为控件添加信号对应的槽函数。
右击表格控件,右键菜单选择“转到槽”,为表格控件添加 currentItemChanged 信号对应的槽函数:
slot1
然后我们为 "添加"、"删除" 两个按钮都添加 clicked 信号对应的槽函数:
slot2-3
添加如上三个槽函数后,保存 widget.ui 界面文件,然后关闭该文件,回到代码编辑模式。
在代码编辑模式,我们先编辑头文件 widget.h,添加一行头文件包含:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTableWidgetItem> //头文件槽函数使用到了

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_tableWidget_currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous);

    void on_pushButtonAdd_clicked();

    void on_pushButtonDel_clicked();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
表格控件信号对应的槽函数里使用到了条目类,因此需要增加头文件包含 <QTableWidgetItem>,其他代码都是自动生成的,不需要修改。
接下来编辑源文件 widget.cpp,首先来看头文件包含和构造函数的内容:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>
#include <QHeaderView>  //表头视图类

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置日期编辑控件可以弹出日历
    ui->dateEdit->setCalendarPopup(true);
    //设置表格控件特性
    //每次选中整行
    ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
    //为方便删除按钮操作,把选中模式设为单选,即每次只选中一行,而不能选中多行
    ui->tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
    //开启自动排序
    ui->tableWidget->setSortingEnabled(true);
    ui->tableWidget->sortByColumn(0, Qt::AscendingOrder);
    //设置末尾一列自动拉伸
    ui->tableWidget->horizontalHeader()->setStretchLastSection(true);

    //设置默认行高,把图标设置更大一些
    ui->tableWidget->verticalHeader()->setDefaultSectionSize(36);
    ui->tableWidget->setIconSize( QSize(32,32) );

    //修改主界面窗口宽度,让表格末尾列自动拉伸的效果显示出来
    QSize sz = this->size();
    sz.setWidth(640);
    this->resize(sz);
}

Widget::~Widget()
{
    delete ui;
}
头文件包含增加了调试类、消息框类和表格控件的表头视图类。
构造函数先设置日期编辑器可以弹出日历 setCalendarPopup(true),方便用户选择日子。
然后对表格控件进行了如下设置:
设置选择行为 QAbstractItemView::SelectRows,每次点击都是按照整行选择;
设置选择模式为 QAbstractItemView::SingleSelection,即单选模式(方便删除按钮操作),单选与整行选择行为不冲突,单选加整 行选中的意思,就是每次点击只能选中一行,而不能用 Shift 或 Ctr l键选中多行;
开启自动排序,并且按照第 0 行升序排序;
设置表格末尾列自动拉伸,这样表格控件比较宽时,最后一列自动占据表格控件剩余水平空间;
设置默认的行高为 36 像素,然后把单元格的默认图标尺寸设置为 32*32 。
对表格控件进行如上设置后,我们把主界面的窗口宽度拉伸至640,方便显示拉伸表格末尾列的效果。

然后我们编辑监视表格控件当前条目的槽函数:
void Widget::on_tableWidget_currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous)
{
    if(current != NULL)//如果条目非空,打印信息
    {
        qDebug()<<tr("当前条目行号:%1,列号:%2,文本:%3")
                  .arg(current->row())
                  .arg(current->column())
                  .arg(current->text());
    }
}
这个槽函数就是打印非空的当前条目信息,对于涉及到指针的操作,一定要先判断指针非空。
对于非空的当前条目,打印它的行号、列号文本。

接下来我们编辑 "添加" 按钮对应的槽函数:
void Widget::on_pushButtonAdd_clicked()
{
    QString strName = ui->lineEditName->text().trimmed();   //剔除名字两端空格
    //判断姓名是否非空
    if(strName.isEmpty())
    {
        QMessageBox::warning(this, tr("添加行"), tr("姓名不能为空!"));
        return;
    }
    //先把自动排序关掉,才能正确添加整行的条目
    ui->tableWidget->setSortingEnabled(false);

    //获取旧的行计数,表格末尾加入一个新空行,有新的空行才能设置新行条目
    int nOldRowCount = ui->tableWidget->rowCount();
    ui->tableWidget->insertRow(nOldRowCount);

    //获取其他数据项,并逐个单元格添加
    //头像
    QIcon iconHead = ui->comboBoxPhotos->itemIcon( ui->comboBoxPhotos->currentIndex() );
    //添加姓名和头像的单元格条目
    QTableWidgetItem *itemName = new QTableWidgetItem(iconHead, strName);
    ui->tableWidget->setItem(nOldRowCount, 0, itemName);
    //性别条目
    QString strGender = ui->lineEditGender->text();
    QTableWidgetItem *itemGender = new QTableWidgetItem(strGender);
    ui->tableWidget->setItem(nOldRowCount, 1, itemGender);
    //生日条目
    QString strBirthday = ui->dateEdit->date().toString("yyyy/MM/dd");
    QTableWidgetItem *itemBirthday = new QTableWidgetItem(strBirthday);
    ui->tableWidget->setItem(nOldRowCount, 2, itemBirthday);
    //婚否条目
    Qt::CheckState cks = ui->checkBoxIsMarried->checkState();
    QTableWidgetItem *itemIsMarried = new QTableWidgetItem();
    itemIsMarried->setCheckState(cks);
    ui->tableWidget->setItem(nOldRowCount, 3, itemIsMarried);
    //住址条目
    QString strAddress = ui->lineEditAddress->text();
    QTableWidgetItem *itemAddress = new QTableWidgetItem(strAddress);
    ui->tableWidget->setItem(nOldRowCount, 4, itemAddress);

    //添加条目完毕后,重新开启排序
    ui->tableWidget->setSortingEnabled(true);
    ui->tableWidget->sortByColumn(0, Qt::AscendingOrder);

    //设置新的当前条目并滚动显示
    ui->tableWidget->setCurrentItem(itemName);
    ui->tableWidget->scrollToItem(itemName);
}
添加" 按钮对应的槽函数先对姓名字符串进行了判断,只有姓名非空才会进行后续操作。
在进行任何的设置条目操作之前,我们先把表格控件的自动排序关闭了,因为自动排序会影响整行条目的正确输入,关闭自动排序才能让多列条目正确地添加到同一行里。
然后我们获取表格控件的行计数 nOldRowCount ,然后在表格末尾插入一行,新的末尾行号就成了 nOldRowCount ,末尾行默认是全 NULL 的条目,有了新的空行才能进行后续的设置新条目操作。
接下来我们获取头像图标,并与姓名字符串一起构建姓名条目,添加到末尾行第 0 列;
获取性别文本,新建条目并添加到末尾行第 1 列;
获取生日文本,新建条目并添加到末尾行第 2 列;
获取婚否复选框的状态,新建条目并设置选状态,添加到末尾行第 3 列;
获取住址文本,新建条目并添加到末尾行第 4 列。
添加该行的所有条目之后,就可以开启自动排序,设置按照第 0 列升序排列。
最后设置新的当前条目为刚才新增的姓名条目,并且滚动显示出该姓名条目,在表格的行很多时,自动滚动到设定的条目就很有用了。

最后我们来看看删除 "按钮" 的代码:
void Widget::on_pushButtonDel_clicked()
{
    //目前仅支持删除选中的当前行
    //获取当前条目
    QTableWidgetItem *curItem = ui->tableWidget->currentItem();
    if(curItem != NULL) //当前条目非空
    {
        if(curItem->isSelected())
        {
            //当前条目被选中,那么确定是删除该行
            ui->tableWidget->removeRow( curItem->row()  );
        }
    }
}
"删除" 按钮首先获取了当前条目,然后判断当前条目是否非空。
对于非空的当前条目,如果当前条目处于高亮选中状态,就删除该行全部内容。
因为之前在主窗体构造函数设置了单选模式,所以只需要删除一行,而且单选模式的选中行默认都是和当前条目重叠的。

例子代码介绍完了,下面运行例子看看:
run1
经过主窗体构造函数的调整,单元格的图标明显变大了很多,表格末尾 "住址" 一列自动占据了剩余空间,表格内部显得比较充实。如果我们新加一个行,会变成下面这样,自动排序是生效的:
run2
"删除" 按钮读者可以自行测试,另外鼠标点击某个单元格时,当前单元格的信息会打印到应用程序输出面板。
最后说明一下,主窗体没有修改行或单元格的按钮,因为双击任意单元格都可以编辑文本内容,一般用不着编辑按钮。关于本小节的例子介绍到这,下面来看看第二个例子。

8.2.6 成绩信息表示例

本小节编写一个记录学生成绩的表格示例,学生的姓名放在每行的行首,列首是各课程名字(行首、列首均用代码添加),每个单元格就是某学生某课程的成绩,支持按照列 查询成绩并将符合条件的单元格高亮显示。
我们重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 scores,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
打开 widget.ui 文件,进行 QtCreator 设计模式,先把主窗体尺寸修改为 480*400,然后按照下图拖入控件:
ui1
界面第一行是表格控件,对象名 tableWidget。
第二行:标签控件,文本为 "指定列";组合框,对象名 comboBoxColumns;标签控件,文本 "分值";组合框,对象名 comboBoxCompares;单行编辑器,对象名 lineEditFindText;按钮,文本为 "查找",pushButtonFind 。第二行控件按照水平布局器布局。
第三行:标签控件,文本为 "姓名";单行编辑器,对象名 lineEditName;按钮,文本为 "增加行",对象名 pushButtonAddLine;按钮,文本为 "删除选中行",对象名 pushButtonDelLines;按钮,文本为 "删除选中条目",对象名 pushButtonDelSelectedItems 。第三行也按照水平布局器布局。
主窗体的主布局器是按照垂直布局器排布。

表格控件的内容等会我们用代码来填充,这里编辑一下两个组合框的内容,对于 "指定列" 右边相邻的组合框,右击组合框并在右键菜单选择 “编辑项目”,为 comboBoxColumns 组合框增加四个项:
ui2
然后我们右击 "分值" 右边的组合框 comboBoxCompares,为第二个组合框添加三个比较项:
ui3
设置好两个组合框之后,主界面如下图所示:
ui4
主窗体第二行的控件就是在指定列内,根据分值比较条件来查询符合条件的单元格,并高亮显示出来。
第三行控件是根据姓名添加新的空行,用户可以双击单元格编辑空单元格的成绩,因此没有编辑单元格的按钮;
"删除选中行" 是连行首的表头分段一起整行删除,而 "删除选中条目" 只删除选中区域的单元格条目。

我们现在添加控件信号对应的槽函数,首先右击表格控件,添加 itemSelectionChanged 信号对应的槽函数,这个槽函数用于监视选中条目和选中矩形区域的变化:
slot1
然后我们为四个按钮分别添加各自的 clicked 信号对应的槽函数:
slot2-5
添加完槽函数后,我们保存界面文件 widget.ui,然后关闭该文件,回到代码编辑模式。
首先编辑头文件 widget.h 的代码,增加两个功能函数声明:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_tableWidget_itemSelectionChanged();

    void on_pushButtonFind_clicked();

    void on_pushButtonAddLine_clicked();

    void on_pushButtonDelLines_clicked();

    void on_pushButtonDelSelectedItems_clicked();

private:
    Ui::Widget *ui;
    //初始化表格函数
    void InitScoresTable();
    //设置某行的行首和四个单元格
    void SetTableRow(int nRow, QString strName,
                     QString strChinese, QString strMath,
                     QString strForeignLanguage, QString strFood);
};

#endif // WIDGET_H
Widget 类声明末尾增加了两个私有函数,InitScoresTable() 是用于初始化表格控件,SetTableRow() 根据参数设置表格控件指定行号的行首文本以及四个单元格条目文本。
然后编辑 widget.cpp 源代码文件,先添加头文件包含和构造函数内容:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>
#include <QTableWidgetItem> //表格条目
#include <QTableWidgetSelectionRange>   //表格选中区域

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //初始化表格
    InitScoresTable();
    //本示例表格不能排序,因为行首是姓名,不能对行首同步排序
}

Widget::~Widget()
{
    delete ui;
}
头文件包含增加了调试类、消息框类、表格控件条目类和表格选中区域类。
构造函数里调用了 InitScoresTable() 函数对表格控件进行初始化,本小节示例没使用排序,也没有修改表格控件其他特性,是默认的表格控件属性。由于行首不参与实时排序,如果排序会导致行首姓名与单元格分数错 位,所以不能开启排序功能。
接下来看看表格初始化函数,这个函数是手动添加的:
//初始化成绩表格的函数
void Widget::InitScoresTable()
{
    //初始设置表格为 2  4 列的
    ui->tableWidget->setRowCount(2);
    ui->tableWidget->setColumnCount(4);
    //记录四个列首字符串
    QStringList listHeaders;
    //添加四个字符串到字符串列表
    listHeaders<<tr("语文")<<tr("数学")<<tr("外语")<<tr("美食");
    ui->tableWidget->setHorizontalHeaderLabels(listHeaders);

    //初始时添加两行学生成绩记录
    //第0行的行首和单元格
    SetTableRow(0, tr("小明"),
                tr("66"), tr("77"),
                tr("88"), tr("99"));
    //第1行的行首和单元格
    SetTableRow(1, tr("小萌"),
                tr("99"), tr("88"),
                tr("77"), tr("66"));
}
表格初始化函数先把表格的维度设成 2 行 4 列的,然后设置水平表头(就是四个列首)的文本为四门课程名。
然后调用函数 SetTableRow() 为第 0 行设置行首文本和四个单元格文本;
再调用函数 SetTableRow() 为第 1 行设置行首文本和四个单元格文本。
因为表格设置某行的操作比较多,而且代码类似,重复使用的多,因此将设置行内容单独编一个函数 SetTableRow() 方便调用。 SetTableRow() 函数的内容如下:
//设置某行的行首和四个单元格条目
void Widget::SetTableRow(int nRow, QString strName,
                         QString strChinese, QString strMath,
                         QString strForeignLanguage, QString strFood)
{
    //行首名字
    QTableWidgetItem *itemName = new QTableWidgetItem(strName);
    ui->tableWidget->setVerticalHeaderItem(nRow, itemName);
    //语文单元格
    QTableWidgetItem *itemChinese = new QTableWidgetItem(strChinese);
    ui->tableWidget->setItem(nRow, 0, itemChinese);
    //数学单元格
    QTableWidgetItem *itemMath = new QTableWidgetItem(strMath);
    ui->tableWidget->setItem(nRow, 1, itemMath);
    //外语单元格
    QTableWidgetItem *itemForeignLanguage = new QTableWidgetItem(strForeignLanguage);
    ui->tableWidget->setItem(nRow, 2, itemForeignLanguage);
    //美食单元格
    QTableWidgetItem *itemFood = new QTableWidgetItem(strFood);
    ui->tableWidget->setItem(nRow, 3, itemFood);
}
SetTableRow() 函数第一个参数是行号,第二个参数是行首的字符串,后面四个参数是四个单元格的文本。该函数先以 strName 构造一个表格条目,设置为垂直表头的条目(即行首);
然后分别根据 strChinese、strMath、strForeignLanguage、strFood 构造四个条目,设置到对应的单元格中。

接下来编写监视表格控件选中区域变化的槽函数:
void Widget::on_tableWidget_itemSelectionChanged()
{
    //选中的条目
    QList<QTableWidgetItem*> listItems = ui->tableWidget->selectedItems();
    int nItemsCount = listItems.count();    //选中条目计数
    //选中的矩形区域
    QList<QTableWidgetSelectionRange> listRanges = ui->tableWidget->selectedRanges();
    int nRangesCount = listRanges.count();
    //计算矩形区域包含的单元格统计
    int nCellsCount = 0;
    for(int i=0; i<nRangesCount; i++)
    {
        nCellsCount += (listRanges[i].rowCount()) * (listRanges[i].columnCount());
    }
    //打印信息
    qDebug()<<tr("选中条目数:%1,选中区域数:%2,选中单元格数:%3")
              .arg(nItemsCount)
              .arg(nRangesCount)
              .arg(nCellsCount);
}
这个槽函数先获取选中的条目列表 listItems ,获知选中条目计数 nItemsCount;
然后获取选中矩形区域的列表 listRanges ,获知选中矩形区域的块数 nRangesCount ;
接着根据矩形区域列表,计算每个矩形区域占据的的单元格数目,累加得到选中单元格的总数 nCellsCount 。
最后打印 nItemsCount、nRangesCount 、nCellsCount 。如果选中区域内没有 NULL 单元格,那么选中条目计数会与选中单元格数目一致,如果选中区域内有 NULL 单元格,那么 nCellsCount 会比 nItemsCount 大。等会程序运行时注意比较这些数值。

接下来是四个按钮对应的槽函数,先看 "查找" 按钮的代码,该槽函数代码较多,使用循环遍历指定列的条目进行查找,这里将代码拆成三块讲解,分为循环前、循环本身、循环后三块的代码,首先是循环前的代码:
void Widget::on_pushButtonFind_clicked()
{
    //比较的分值字符串
    const QString strDstScore = ui->lineEditFindText->text().trimmed();
    if(strDstScore.isEmpty())
    {
        return; //没有比较分值不查询
    }
    const int nDstScore = strDstScore.toInt();    //用于比较的分值

    //指定的列号
    int nTheColumn = ui->comboBoxColumns->currentIndex();
    //指定的比较运算符,0是等于,1是小于等于,2是大于等于
    int nCompare = ui->comboBoxCompares->currentIndex();

    //表格总行数
    int nRowCount = ui->tableWidget->rowCount();
    //记录符合条件的单元格数目、均值、总值
    int nFilteredCount = 0;
    double dblTotal = 0;
    double dblAverage = 0;
    //记录符合条件的第一个条目
    QTableWidgetItem *itemFilteredFirst = NULL;
    //清空旧的高亮选中
    ui->tableWidget->setCurrentItem(NULL, QItemSelectionModel::Clear);

"查找" 按钮代码开始获取用于比较的目标字符串 strDstScore ,如果字符串为空就不处理。
对于非空字符串计算其对应的数值 nDstScore 。
然后获取指定的查询列号 nTheColumn ,获取比较运算符的编号 nCompare。
接下来是循环之前的准备,计算总行数 nRowCount 用于遍历指定列单元格;
将统计量过滤后的符合条件数目 nFilteredCount 、总值 dblTotal 、均值 dblAverage 初始化为 0;
准备保存首个符合条件的单元格条目指针到 itemFilteredFirst ,这个指针初始为 NULL;
然后清空表格旧的选中状态,因为查询后会将符合条件的都进行高亮选中。

接下来看循环本身的代码:
    //循环查询该列条目
    for(int i=0; i<nRowCount; i++)
    {
        QTableWidgetItem *itemCur = ui->tableWidget->item(i, nTheColumn);
        if(NULL == itemCur)
        {
            continue;   //无条目
        }
        QString strCur = itemCur->text().trimmed();
        int nCurScore = strCur.toInt();    //分值
        //判断当前比较运算符
        if(0 == nCompare) //等于号
        {
            //比较字符串或分值是不是一样
            if( nCurScore != nDstScore )
            {
                continue;   //不符合
            }
        }
        else if(1 == nCompare) //小于等于
        {
            if( ! (nCurScore <= nDstScore))
            {
                continue;   //不符合
            }
        }
        else    //大于等于
        {
            if( ! (nCurScore >= nDstScore) )
            {
                continue;   //不符合
            }
        }// end if-elseif-else
        //符合条件的才会执行到这里
        nFilteredCount++;       //合格计数增加
        dblTotal += nCurScore;  //总分值增加
        itemCur->setSelected(true); //选中该条目
        if( NULL == itemFilteredFirst)
        {
            //保存第一个符合的条目
            itemFilteredFirst = itemCur;
            //设置为当前条目并滚动到这
            ui->tableWidget->setCurrentItem(itemFilteredFirst);
            ui->tableWidget->scrollToItem(itemFilteredFirst);
        }
    }//end for
循环内先获取第 i 行第 nTheColumn 列的单元格条目 itemCur ,如果条目非空才继续后续操作。
对非空条目,获取 itemCur 文本 strCur ,并转为数值 nCurScore 。
接下来是根据选定的比较运算符编号 nCompare,分别进行判断,对于不符合比较条件的都进行 continue 操作跳过后续代码,而对于符合条件的条目, 自动执行到循环末尾代码:
    将计数 nFilteredCount 增加;
    总分值累加;
    设置符合条件的条目 itemCur 为高亮选中;
    然后判断是否已经保存了第一个符合条件的条目,如果尚未保存第一个符合条件的条目,那么保存 itemCur 为第一个符合条件的条目,并设置当前条目为该条目,并滚动到该条目。

循环结束后,最后是循环后的代码:
    //计算均值
    if(nFilteredCount > 0)
    {
        dblAverage = dblTotal / nFilteredCount;
    }
    //构造消息字符串
    QString strMsg = tr("匹配条目数:%1,匹配条目的总值:%2,均值:%3")
            .arg(nFilteredCount).arg(dblTotal).arg(dblAverage);
    QMessageBox::information(this, tr("查找"), strMsg);
    //焦点切换到表格控件,这样能看清楚高亮背景
    ui->tableWidget->setFocus();
}
循环结束后,先判断符合条件的条目计数 nFilteredCount ,如果大于 0 就计算均值;
然后构造消息字符串,显示符合条件条目的计数、总分值、均分值。
最后把显示焦点切换到表格控件,方便用户看到高亮选中的条目。

讲完查找功能,接下来 "增加行" 的按钮槽函数代码如下:
void Widget::on_pushButtonAddLine_clicked()
{
    //判断名字是否为空
    QString strName = ui->lineEditName->text().trimmed();
    if(strName.isEmpty())
    {
        return;
    }
    //末尾添加新行
    int nOldRowCount = ui->tableWidget->rowCount();
    ui->tableWidget->insertRow( nOldRowCount );
    //设置该行行首和单元格
    SetTableRow(nOldRowCount, strName,
                tr("0"), tr("0"),
                tr("0"), tr("0"));
    ui->tableWidget->scrollToBottom();  //自动滚到底部
}
该槽函数获取名字 strName ,判断名字是否非空。
对于非空名字,在表格末尾增加一个新行,然后设置该行的行首和四个单元格内容,行首就是姓名,四个单元格初始化为 "0" ,最后将表格控件滚动到底部,显示出末尾行。QTableWidget 从基类继承了滚动到顶部的函数 scrollToTop() 和滚动到底部的函数 scrollToBottom(),这里调用的是滚动到底部函数。

接下来是 "删除选中行" 按钮对应的槽函数代码:
void Widget::on_pushButtonDelLines_clicked()
{
    //支持删除多行,对于多行的删除,要从末尾开始删除,保证前面的行号不错乱
    QList<int> listRowIndex;    //保存要删的行号
    //选中区域
    QList<QTableWidgetSelectionRange> listRanges = ui->tableWidget->selectedRanges();
    int nRangeCount = listRanges.count();
    if(nRangeCount < 1)
    {
        return; //没选中
    }
    //有选中区域
    for(int i=0; i<nRangeCount; i++)
    {
        //目前这一块矩形区域里的行计数
        int nCurRangeRowCount = listRanges[i].rowCount();
        for(int j=0; j<nCurRangeRowCount; j++ )
        {
            //选中行序号
            int nRowIndex = listRanges[i].topRow() + j;
            if( ! listRowIndex.contains(nRowIndex) )//没有重复的
            {
                listRowIndex.append(nRowIndex);//添加到行号列表
            }
        }
    }
    //对行号排序
    qSort(listRowIndex);
    //从末尾开始删除行
    int nRowIndexCount = listRowIndex.count();
    for(int i=nRowIndexCount-1; i>=0; i--)
    {
        ui->tableWidget->removeRow( listRowIndex[i] );
    }
    //清空列表
    listRanges.clear();
    listRowIndex.clear();
}
这个函数支持删除多个选中的行。函数开头定义了保存选中行序号的列表 listRowIndex,然后获取选中矩形区域列表 listRanges ,获知选中矩形区域块数 nRangeCount 。
然后循环遍历每个选中矩形区域,将选中区域覆盖的行号 nCurRangeRowCount 提取出来,判断列表 listRowIndex 是否已经包含该行序号,如果不包含就添加到列表里。这样遍历所有选中举行区域后,就统计出了不重复的选中行号列表 listRowIndex 。
因为删除行操作需要从末尾开始删除,所以需要对行序号列表进行排序  qSort(listRowIndex);
排序之后,从末尾的选中行号开始删除选中的行,直到删除所有选中的行为止。
函数末尾清空了用到的两个列表 listRanges 和 listRowIndex ,因为这些矩形区域和行都删除干净了。

例子最后一个函数是 "删除选中条目" 按钮对应的槽函数:
void Widget::on_pushButtonDelSelectedItems_clicked()
{
    //选中的条目
    QList<QTableWidgetItem*> listItems = ui->tableWidget->selectedItems();
    //计数
    int nCount = listItems.count();
    if(nCount < 1)
    {
        return;
    }
    //将选中条目一个个卸载下来并删除
    for(int i=0; i<nCount; i++)
    {
        int nItemRow = listItems[i]->row();
        int nItemColumn = listItems[i]->column();
        delete ui->tableWidget->takeItem(nItemRow, nItemColumn);
    }
    //清空列表
    listItems.clear();
}
这个槽函数比较简单,获取选中的条目列表 listItems ,获知选中条目计数 nCount 。
如果计数小于 1 说明没有选中的,就不处理。
对应有选中的条目,获取每个选中条目的行号、列号,将条目从表格控件卸载下来,直接 delete 删除,彻底删除选中的条目。
函数最后清空列表 listItems 。

示例的代码讲解到这,我们运行例子看看:
run1
在 QtCreator 下面的输出面板可以看到实时的选中条目、选中区域和选中单元格计数。
选择指定列、比较运算符并设置比较分值后,可以查找符合条件的列统计信息,比如:
run2
可以对表格增加行,删除一些条目,对于删除过条目的 NULL 单元格,如果包含在选中矩形区域内,再看 QtCreator 的输出面板:
run3
可以看到选中条目数是 2,而选中单元格数目是 4。在实际编程中需要注意区分基于单元格处理的函数和基于条目处理的函数,对于存在 NULL 单元格的时候尤其要留心。另外说明一下,这个示例没有 "编辑" 或 "修改" 单元格之类的按钮功能,因为可以直接双击编辑单元格内容,为了简化例子,才没有对 单元格文本内容进行筛选,如果输入汉字就会有不正常的统计数值出现,以后的示例再考虑在单元格 内容输入时做合法性判断。

本节的内容介绍到这,表格控件的内容较多,不需要全部记住,到用的时候可以再查文档,本节要注意两个示例代码的学习,多动手实践。下一节介绍树形控件。



prev
contents
next