8.1 列表控件

之前第 7 章有多个示例都用到了列表控件,本节再详细介绍一下列表控件 QListWidget 和它的数据条目 QListWidgetItem,列表控件用于显示单列多行数据,而且可以用程序代码编写能够手动编辑的动态列表,列表的条目可以有图标,并有多种显示模式,本节会通过两 个示例学习列表控件丰富的功能。

8.1.1 QListWidget

在 Qt 设计师,左边的控件工具箱可以看到本章三个基于条目控件:
item-based widgets
QListWidget 的基类是 QListView 视图类,关于基类的内容本节不介绍,等到模型视图章节再讲,本节只关注 QListWidget 本身的函数。
QListWidget 的构造函数很简单:
QListWidget(QWidget * parent = 0)
parent 是父窗口指针,如果用设计师拖控件,一般也不用手动调用构造函数。QListWidget 主要的函数是围绕条目添加、删除、选中条目、显示模式等功能以及相关的信号和槽,下面大致对这些功能函数分类来讲解:
(1)添加条目的函数
void QListWidget::​addItem(QListWidgetItem * item)
这个添加函数需要实现 new 一个 QListWidgetItem 条目对象,然后添加到列表控件末尾,下面小节会专门讲 QListWidgetItem 类。
void QListWidget::​addItem(const QString & label)
这第二个添加函数其实更常用,因为更简便,功能是将字符串 label 添加到列表控件末尾显示,其实该函数内部会自动根据字符串 new 一个条目对象添加到列表控件。
如果有设置好的字符串列表,那么可以通过如下函数把多个字符串全部添加到列表控件:
void QListWidget::​addItems(const QStringList & labels)

add* 函数是将条目添加到末尾,如果要将条目插入到指定行 row 位置,则使用 insert* 函数:
void QListWidget::​insertItem(int row, QListWidgetItem * item) //插入条目到第 row 行
void QListWidget::​insertItem(int row, const QString & label) //插入字符串到第 row 行
void QListWidget::​insertItems(int row, const QStringList & labels) //插入多个字符串到从 row 行开始的多个行

获取列表控件里面的条目计数使用如下函数:
int QListWidget::count() const

(2)删除函数
因为是基于条目的控件,所以列表控件的删除单个条目函数名字是 takeItem():
QListWidgetItem * QListWidget::​takeItem(int row)
takeItem() 根据行号从列表控件移除一个条目,并返回该条目指针,如果行号不合法,返回 NULL 指针。
如果返回的是实际存在的条目,那么需要注意,返回的条目指针需要手动 delete 掉,因为列表控件不再拥有该条目,该条目不会由列表控件析构时自动删除。
​takeItem() 函数还有第二个用途,因为列表控件没有直接的调整条目前后顺序的函数,可以先将要调整顺序的条目移出来 ​takeItem(),然后再调用 ​insertItem() 把这个条目插入到新的位置。

如果要清空整个列表控件,删除之前添加的所有条目,可以调用槽函数:
void QListWidget::​clear()

(3)条目访问函数
根据行号获取条目对象的指针,使用如下函数:
QListWidgetItem * QListWidget::​item(int row) const
如果已知列表控件含有的条目对象指针,反查当前行号,使用如下函数:
int QListWidget::​row(const QListWidgetItem * item) const

在图形界面,如果希望根据列表控件在屏幕显示的相对坐标位置(以列表控件内部左上角为原点)来获取条目,使用如下函数:
QListWidgetItem * QListWidget::​itemAt(const QPoint & p) const
QListWidgetItem * QListWidget::​itemAt(int x, int y) const
反过来,如果根据已知条目,获取这个条目占据的矩形区域,使用如下函数:
QRect QListWidget::​visualItemRect(const QListWidgetItem * item) const

(4)当前选中条目的操作
获取列表控件当前选中条目的函数如下:
QListWidgetItem * QListWidget::​currentItem() const //当前选中 条目
int QListWidget::​currentRow() const  //当前选中的行号
如果当前没有选中的条目,那么返回的指针为 NULL,返回的序号为 -1 ,代码里要注意判断返回值。

设置已存在的某个条目为选中状态,使用函数:
void QListWidget::​setCurrentItem(QListWidgetItem * item)    //设置当前选中条目为 item
void QListWidget::​setCurrentItem(QListWidgetItem * item, QItemSelectionModel::SelectionFlags command)
void QListWidget::​setCurrentRow(int row, QItemSelectionModel::SelectionFlags command)//设置当前选中行为 row
void QListWidget::​setCurrentItem(QListWidgetItem * item, QItemSelectionModel::SelectionFlags command)
第二个和第四个设置函数有个 SelectionFlags 类型的参数 command,这个参数决定选中的方式,是要选中QItemSelectionModel::Select ,还是取消选中 QItemSelectionModel::Deselect,还有其他选中方式,等到表格控件一节再详细列出来。
如果当前选中的条目发生变化,会触发如下三个信号,可以根据实际用途选择合适的信号:
void QListWidget::​currentItemChanged(QListWidgetItem * current, QListWidgetItem * previous)
void QListWidget::​currentRowChanged(int currentRow)
void QListWidget::​currentTextChanged(const QString & currentText)
注意参数里的指针有可能为空值,序号可能为 -1,字符串也可能是空串,一定要注意判断非法的参数值。

本章的三个控件都可以设置选中模式,比如单选模式,一次只能选中一个条目,多选模式,一次可以选中多个条目等等,详细的选中模式在放到表格控件一节讲解,因为表格 中涉及的选中模式较多,一块讲解。本节的列表控件默认情况下,选中模式为单选 QAbstractItemView::SingleSelection,以上的 *current* 等函数都是基于默认的单选模式的。

如果需要用到多选模式,可以设置 selectionMode 属性为 QAbstractItemView::ExtendedSelection,这种扩展选中模式类似常见的文件资源管理器的选中模式,可以使用 Ctrl 或 Shift 加鼠标点击实现多选:
void setSelectionMode(QAbstractItemView::SelectionMode mode)  //设置选中模式
QAbstractItemView::SelectionMode    selectionMode() const     //获取选中模式
QAbstractItemView 是本章所有控件的抽象基类,本章后面控件也有类似的选中模式设置,如果把列表控件设置成多选模式,那么可以用如下函数获取同 时选中的多个条目:
QList<QListWidgetItem *> QListWidget::​selectedItems() const
不论单选还是多选模式,条目选中情况有任何变化时,会触发如下信号:
void QListWidget::​itemSelectionChanged()

(5)条目查找和排序
如果需要根据文本查找匹配的条目,使用如下函数:
QList<QListWidgetItem *> QListWidget::​findItems(const QString & text, Qt::MatchFlags flags) const
该函数第一个参数 text 是要查找的模板子串,第二个参数是匹配标志,Qt::MatchFlags 是非常通用的枚举类型,不仅可用于字符串匹配,还能用于其他类型变量的匹配,Qt::MatchFlags 包含关于查找匹配的多种方式的枚举值:

Qt::​MatchFlags 枚举常量 数值 描述
Qt::MatchExactly 0 精确匹配,执行基于 QVariant 的匹配。
Qt::MatchFixedString 8 执行基于字符串的匹配,如果不指定 MatchCaseSensitive,默认是大小写不敏感。
Qt::MatchContains 1 条目包含要查找的模板子串。
Qt::MatchStartsWith 2 条目以要查找的模板子串打头。
Qt::MatchEndsWith 3 条目以要查找的模板子串结尾。
Qt::MatchCaseSensitive 16 查找时大小写敏感。
Qt::MatchRegExp 4 根据正则表达式模板子串匹配字符串。
Qt::MatchWildcard 5 根据通配符模板子串(如 *.txt)匹配字符串。
Qt::MatchWrap 32 执行回绕查找,当查找到最后一个条目时返回到第一个的条目继续查找,直到所有的条目都检查一遍。
Qt::MatchRecursive 64 递归查找,遍历所有子条目。

后面两节的表格控件和树形控件也有类似的查找函数 ​findItems() ,第二个匹配标志参数也是一样的类型。

列表控件的自动排序是通过 sortingEnabled 属性来控制,获取和设置函数如下:
bool isSortingEnabled() const
void setSortingEnabled(bool enable)
列表控件条目默认是不排序的,如果希望自动按照字典序排序,调用 setSortingEnabled(true) 即可。另外还可以手动对列表控件现有条目排 序:
void QListWidget::​sortItems(Qt::SortOrder order = Qt::AscendingOrder)
Qt::AscendingOrder 是按升序排列,Qt::DescendingOrder 是按降序排列。

(6)条目显示和运行时条目编辑
关于列表控件 QListWidget 和里面的条目 QListWidgetItem,需要注意条目 QListWidgetItem 仅仅是数据,不是控件或子控件,列表控件根据多个 QListWidgetItem 对象,来呈现条目里的数据,只有列表控件自己是控件实体。
列表控件默认以自己的方式呈现条目数据,比如白底黑字的普通条目显示,如果要按照特殊的子控件来显示字符串,比如用 QLabel 对象显示条目数据,可以用如下函 数:
void QListWidget::​setItemWidget(QListWidgetItem * item, QWidget * widget)
列表控件会同时拥有 item 数据条目和用于显示 item 的子控件 widget。注意这里的子控件 widget 只有静态显示功能,如果用按钮作为显示子控件,那么按钮是不可点击的。如果希望自己定制一个能交互操作的子条目显示控件,需要使用  QListView 并子类化 QItemDelegate 类,这些复杂的等到模型视图章节再讲。

对于本节列表控件,如果要获取 ​setItemWidget() 函数指定某个条目的显示子控件,使用如下函数:
QWidget * QListWidget::​itemWidget(QListWidgetItem * item) const
如果要删除上面 ​setItemWidget() 指定的特殊显示控件,使用函数:
void QListWidget::​removeItemWidget(QListWidgetItem * item)
特殊的显示子控件移除后,该条目就还按照列表控件原来的普通条目显示。

如果希望在程序运行时编辑列表控件的条目,有两种方式,本小节先讲第一种(第二种是设置条目自身的特性标志),手动打开条目的文本编辑器(这个编辑器是列表控件自 带功能,与 ​​setItemWidget 设置的显示子控件没关系):
void QListWidget::​openPersistentEditor(QListWidgetItem * item)
这个函数名是打开条目的持续编辑器,持续的意思是如果不调用关闭函数,该条目的编辑器会一直开启,关闭这个持续编辑器使用如下函数:
void QListWidget::​closePersistentEditor(QListWidgetItem * item)
一般可以在检测到条目激活信号(itemActivated)时调用打开函数 ​openPersistentEditor() ,在当前条目变化( currentItemChanged)时调用关闭函数 ​closePersistentEditor() 。 使用这一对开关持续编辑器函数涉及到编写多个信号的槽函数,使用比较麻烦,建议用后面第二小节介绍的条目标 志位和 QListWidget::​editItem(QListWidgetItem * item) 实现条目的可编辑功能。

(7)其他信号和槽函数
除了上面关于当前条目变化和选中条目变化的信号,条目还有激活、单击、双击等信号,罗列如下:
void QListWidget::​itemActivated(QListWidgetItem * item)  //激活信号
当用户点击或双击条目时,条目会被激活,具体哪些操作会激活条目,要根据操作系统设置来定,比如 Windows 一般是双击打开激活,KDE 桌面通常是单击打开激活。另外,系统的快捷键也可以激活条目,如 Windows 和 Linux X11 桌面是回车键激活,Mac OS X 是 Ctrl+0 ,一般激活信号用于开启编辑等操作。
void QListWidget::​itemChanged(QListWidgetItem * item)   //条目内容发生变化
注意这是条目内容变化的信号,不是选中状态变化,程序如果在运行时改变了条目文本内容,比如持续编辑器修改了文本,会触发这个信号。
剩下几个单击、双击、进入、按压等信号意义比较直白,不详细解释了:
void QListWidget::​itemClicked(QListWidgetItem * item)   //条目单击信号
void QListWidget::​itemDoubleClicked(QListWidgetItem * item)  //条目双击信号
void QListWidget::​itemEntered(QListWidgetItem * item)  //鼠标追踪时进入条目的信号,一般用不着
void QListWidget::​itemPressed(QListWidgetItem * item)  //鼠标按键在条目上处于按下状态时发的信号

列表控件的槽函数除了前面讲过的 clear() 槽函数用于清空所有条目,还有个比较实用的条目滚动函数,列表控件自带滚动条,当条目总数超出控件矩形能呈现的数目时,滚动条自动出现,通过滚动条支持更多的条目显示。使 用如下槽函数可 以让列表控件滚动到想显示的某个条目位置:
void QListWidget::​scrollToItem(const QListWidgetItem * item, QAbstractItemView::ScrollHint hint = EnsureVisible)
第一个参数 item 就是想显示出来的条目,第二个参数是滚动显示方式,默认是 QAbstractItemView::EnsureVisible,即保证指定条目显示出来,还有其他的滚动显示方式:

QAbstractItemView::​ ScrollHint 枚举常量 数值 描述
QAbstractItemView::EnsureVisible 0 滚动到指定条目能显示出来即可。
QAbstractItemView::PositionAtTop 1 滚动直到将指定条目显示到可视区域的顶部。
QAbstractItemView::PositionAtBottom 2 滚动直到将指定条目显示到可视区域的底部。
QAbstractItemView::PositionAtCenter 3 滚动直到将指定条目显示到可视区域的中间。

关于列表控件的函数介绍这些,下面讲解与之配合使用的条目类 QListWidgetItem。

8.1.2 QListWidgetItem

QListWidgetItem 专门用于表示列表控件 QListWidget 的数据条目,注意 QListWidgetItem 是一个纯数据类,不是控件,没有基类,也就没有信号和槽函数。QListWidgetItem 可以直接用数据流 QDataStream 读写。
QListWidgetItem 不单单有字符串,还可以有自己的图标、复选框等特性,列表控件会根据条目对象的丰富特性来呈现数据并进行交互操作。
(1)首先来看看列表控件条目的构造函数:
QListWidgetItem(QListWidget * parent = 0, int type = Type)
QListWidgetItem(const QString & text, QListWidget * parent = 0, int type = Type)
QListWidgetItem(const QIcon & icon, const QString & text, QListWidget * parent = 0, int type = Type)
那第三个构造函数的参数来讲,icon 是条目显示的图标,text 是条目文本,parent 是条目隶属的列表控件,type 是条目的自定义类型。
如果在构造函数指定了条目隶属的列表控件,那么这个条目会自动添加到列表控件末尾,而不需要调用列表控件的 add*() 和 insert*()函数,比如:
    new QListWidgetItem(tr("Hazel"), listWidget);
只需要上面一句代码,"Hazel" 条目就自动显示在列表控件 listWidget 里面了,只要指定所隶属的列表控件,新建的条目自动添加并显示在末尾。

(2)条目的复制方式,有三个函数可以实现:
QListWidgetItem(const QListWidgetItem & other)  //复制构造函数
QListWidgetItem & operator=(const QListWidgetItem & other) // = 赋值函数
virtual QListWidgetItem *  clone() const  //克隆函数
复制构造函数和 = 赋值函数 函数原理是一样的,它们除了 type() 和 listWidget() 函数指定的两个数据不复制,其他的数据都复制。type() 是条目的自定义类型,listWidget() 是该条目所隶属的列表控件。
而克隆函数 clone() 会精确复制所有数据,如果原条目隶属某个列表控件,克隆出来的也会自动隶属该列表控件,自定义条目类型也一样。
顺便说一下列表条目的比较函数:
virtual bool operator<(const QListWidgetItem & other) const
只有一个小于号函数,是比较列表条目文本的字典序先后关系,没有等于号函数和大于号函数,如果确实用到条目文本比较,建议直接用 QString 变量进行比较。

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

● 第一类:QListWidgetItem 的通用数据
通用数据是以数据角色与数据变量一一对应的形式存储管理,比如设置文本 setText()、设置图标 setIcon() 等函数,其本质都是根据各自的角色调用通用设置数据的函数:
virtual void setData(int role, const QVariant & value)
也可以根据角色来获取各个数据变量:
virtual QVariant data(int role) const

查看列表控件源码文件:
 C:\Qt\Qt5.4.0\5.4\Src\qtbase\src\widgets\itemviews\qlistwidget.h
可以看到关于文本变量的函数 text() 和 setText() 源代码:
inline QString text() const
  { return data(Qt::DisplayRole).toString(); }
inline void QListWidgetItem::setText(const QString &atext)
  { setData(Qt::DisplayRole, atext); }
关于 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 条目显示的建议尺寸。

QListWidgetItem 可以直接用数据流 QDataStream 读写,涉及到读写的内部数据就是上表所列举的以角色形式表述的数据,QListWidgetItem 通过两个外部全局运算符重载函数支持 QDataStream 数据流读写:
QDataStream &operator<<(QDataStream &out, const QListWidgetItem &item)
QDataStream &operator>>(QDataStream &in, QListWidgetItem &item)
使用运算符的形式比较方便,当然也可以用 QListWidgetItem 内部的读写函数:
void QListWidgetItem::​read(QDataStream & in)
void QListWidgetItem::​write(QDataStream & out) const
 
QListWidgetItem 内部采用私有的 QVector 向量存储通用数据的 role 和 value 对:
QVector<QWidgetItemData> values;
条目通用数据的读写就是读写私有向量 values,其中 QWidgetItemData 内部包含 role 和 value 成员变量:
class QWidgetItemData
{
public:
    inline QWidgetItemData() : role(-1) {}
    inline QWidgetItemData(int r, QVariant v) : role(r), value(v) {}
    int role;
    QVariant value;
    inline bool operator==(const QWidgetItemData &other) const { return role == other.role && value == other.value; }
};
在 8.2 节表格控件的单元格条目也是类似的私有向量存储 role 和 value 对。

● 第二类:QListWidgetItem 的非通用数据
条目自定义类型:
int QListWidgetItem::​type() const
这个条目类型只能在构造函数指定,指定之后不能修改,默认值为 QListWidgetItem::Type (数值 0),如果程序员希望自己区分列表控件条目的类型,那么可以自己定义大于 QListWidgetItem::UserType (数值 1000)的类型值,一般用在 QListWidgetItem 派生类里面。

条目所隶属的列表控件:
QListWidget * QListWidgetItem::​listWidget() const
条目自身不能修改所隶属的列表控件,要通过列表控件的删除函数 QListWidget::​takeItem(int row) 才能解除隶属关系。

列表条目本身的选中状态(与复选框无关,是用户在列表控件点击条目的高亮选中状态):
void QListWidgetItem::setSelected(bool select)
bool QListWidgetItem::isSelected() const
一般是用户在列表控件图形界面点击哪个条目,哪个条目就处于高亮选中状态,这里可以用条目自身的函数设置高亮选中状态。

条目在列表控件里面显示或者隐藏:
void QListWidgetItem::​setHidden(bool hide)
bool QListWidgetItem::​isHidden() const

条目的特性标志:
void QListWidgetItem::​setFlags(Qt::ItemFlags flags)
Qt::ItemFlags QListWidgetItem::​flags() const
flags 会决定条目的工作特性,比如是否有三态复选框,是否在用户双击该条目时开启文本编辑器等等,Qt::ItemFlags 枚举值见下面表格:

Qt::​ItemFlags 枚举常量 数值 描述
Qt::NoItemFlags 0 不设置任何特性,条目会处于完全的不可用状态。
Qt::ItemIsSelectable 1 条目本身可以被高亮选中。
Qt::ItemIsEditable 2 条目可以被编辑,比如用户双击条目时自动启用文本编辑器。
Qt::ItemIsDragEnabled 4 条目可以被拖拽出去。
Qt::ItemIsDropEnabled 8 条目可以作为拖拽的目的地。
Qt::ItemIsUserCheckable 16 条目可以有复选框,用户能勾选复选框。
Qt::ItemIsEnabled 32 条目处于可用状态。
Qt::ItemIsTristate 64 条目的复选框可以有三种勾选状态:选中、非选中、部分选中。
Qt::ItemNeverHasChildren 128 条目不能有子条目(指树形控件)。

上表中的 Qt::ItemFlags 也适用于后面 8.2 节的表格控件条目和 8.3 节的树形控件条目,表格条目和树形条目也都有一样的条目标志位设置函数,功能与本节列表条目一样,参数也一样。
列表控件条目默认的特性标志为同时启用四个:
Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled

按照默认标志位,列表条目是可以有复选框进行勾选的,调用 QListWidgetItem::​setCheckState() 函数可以让列表条目的复选框显示出来。
如果要设置三态复选框,可以使用下面的代码:
    item->setFlags( (item->flags()) | Qt::ItemIsTristate ); //开启三态复选
    item->setCheckState( Qt::Unchecked );  //显示复选框
如果希望用户在双击条目时,自动开启条目文本的编辑器,可以用下面代码:
    item->setFlags( (item->flags()) | Qt::ItemIsEditable ); //双击条目会自动 开启文本编辑器
因为可以设置条目的 Qt::ItemIsEditable 标志位,所以列表控件 QListWidget 的一对持续编辑器函数  openPersistentEditor() 和 closePersistentEditor() 一般不需要手动调用,直接设置条目自身的可编辑标志位就行了。
对于启用可编辑标志位的条目,如果希望通过代码开启条目的编辑,可以调用列表控件的 ​editItem() 函数来实现,而不需要使用一对开关持续编辑器函数那么麻烦:
void QListWidget::​editItem(QListWidgetItem * item)

这里说明一下:列表条目本身是可以存储复选框的勾选状态数值,而条目显示的复选框是由列表控 件绘制并提供,列表条目自身不是控件,也不包含子控件。类似地,列表控件根据 Qt::ItemIsEditable 标志位检查是否需要为条目提供文本编辑器

在 QtCreator 设计模式和 Qt 设计师界面,都可以直接编辑列表控件、表格控件、树形控件的条目,右击这些控件,在右键菜单选择“编辑项目 ...” 菜单项即可,上面提到的条目数据,不管是通用的还是非通用的,只要可以修改,几乎都能在图形设计界面可视化编辑,等会例子中再示范。后面 8.2 节表格控件和 8.3 节的树形控件的条目有很多与列表控件类似的数据格式、接口函数、特性标志等,本节的表格关于函数和常量的枚举表格在后面两节都有用到,很多内容是一样的。关于列表控件和条 目的内容介绍到这,下面看看例子。

8.1.3 游戏装备列表示例

在 7.1.5 文件系统浏览示例,我们就已经使用到列表控件了,并且条目既有图标也有文本显示,读者可以复习一下前面的示例。本小节示例是一个可编辑的游戏装备列表,学习 QtCreator 设计模式和 Qt 设计师里面列表控件的可视化编辑,而程序运行时也可以自己选择装备图标、设置游戏装备名称,并支持条目数据的保存和加载。
在开始例子之前,先下载一个游戏装备图标压缩包:
https://lug.ustc.edu.cn/sites/qtguide/QtProjects/ch08/dotaitems/dotasytb.zip
压缩包解压之后,有个 DOTA-ITEM 文件夹,里面是游戏装备图标,我们抽取蝴蝶(fgfh.jpg)、金箍棒(odef.jpg)、大炮(rman.jpg) 三个图标备用,例子内嵌资源使用这三个图标,然后我们开始这个例子的学习。
打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 dotaitems,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。

在开始图形界面编辑之前,我们先添加图标和资源文件:
在源码文件夹 D:\QtProjects\ch08\dotaitems 里新建一个叫 icons 的子文件夹,然后把三个图标文件 fgfh.jpg、odef.jpg、rman.jpg 放到 icons 子文件夹。
然后在 QtCreator 左边项目管理面板,右击 dotaitems 项目名称,右键菜单选中 "添加新文件" ,弹出新建文件对话框,
rc1
选择新建 Qt Resource File,点击 "Choose..." 按钮,进入如下界面,
rc2
名称填写 icons,路径用当前项目文件夹,点击 "下一步",
rc3
项目管理界面不用修改,点击完成。进入 QtCreator 资源文件编辑界面,我们在左边项目管理面板右击 icons.qrc 文件名,
rc4
右键菜单选择 "添加现有文件",在弹出的文件对话框,选择 icons 子文件夹里三个图标文件添加,添加好之后如下图所示:
rc5
这样图标和资源文件就编辑好了,保存该资源文件,关闭该文件。接下来我们编辑 ui 界面文件。

我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
界面分为左边和右边两个大部分:
左边上方是列表控件,对象名为 listWidget;左下是单行编辑器,对象名为 lineEditToolTip,左边两个控件按照垂直布局排布。
右边是六个按钮,第一个按钮文本是 "添加",对象名 pushButtonAdd;第二个按钮文本是 "删除",对象名 pushButtonDel;第三个按钮文本是 "切换显示模式",对象名 pushButtonViewMode;第四个按钮文本为 "加载",对象名 pushButtonLoad;第五个按钮文本为 "保存",对象名为 pushButtonSave;在 "保存" 按钮下面是一个垂直的空白条,用默认的对象名即可;第六个按钮文本为 "编辑工具提示",对象名为 pushButtonEditToolTip。右边的控件也是按照垂直布局器排布,主界面是按照水平布局器排布。

这里解释一下这些控件的用途:
列表控件显示游戏装备的图标和名称列表,初始时显示资源文件内嵌的三个游戏装备;
"添加" 按钮可以从磁盘添加新的游戏装备图标和装备名;"删除" 按钮是从列表删除一个条目;
"切换显示模式" 按钮用于控制列表控件的显示方式,是小图标文字模式,还是大图标模式;
"保存" 按钮是将当前列表控件条目保存为自定义的 *.item 数据流文件,"加载" 按钮是加载之前存的文件;
单行编辑器和 "编辑工具提示" 按钮,是设置列表当前选中条目的工具提示信息,相当于游戏装备描述信息。

我们现在来学一下让列表控件初始时显示三个装备条目,在 QtCreator 和 Qt 设计师里面,可以直接为列表控件、表格控件、树形控件(都是 Item-Based)添加条目,这些条目可以自动在程序启动时显示。我们右击列表控件,在右键菜单看到 "编辑项目..." 菜单项:
item1
"编辑项目..." 这个菜单项的用途就是编辑条目的意思,点击之后看到如下条目编辑对话框:
item2
绿色加号图标按钮就是添加新的条目,我们添加三个条目,条目文本为 "蝴蝶"、"金箍棒"、"大炮" :
item3
条目不仅有文字,还可以编辑条目的 "属性",就是上面小节介绍的 QListWidgetItem 通用数据和非通用数据,点击 "属性" 按钮,可以看到条目的详细编辑内容:
item4
对于第一个条目 "蝴蝶" ,我们点击属性 icon,再点击 icon 右边一栏的小按钮,在菜单里点击 "选择资源" :
item5
然后我们选择蝴蝶的图标给该条目:
item6
添加之后,可以看到第一个条目有蝴蝶图标了:
item7
然后我们编辑蝴蝶条目的 toolTip ,可以自己在网上找点蝴蝶的描述信息填到该工具提示里面:
item8
上图设置的就是HTML丰富文本的工具提示信息,原本是从网页复制到 "多文本" 编辑模式里的,如果点击 "源" 编辑模式,可以看到 HTML 的源码:
<html><head/><body><p><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#0090ff; background-color:#121212;">+30点攻击力<br/>+30%攻速<br/>+30点敏 捷<br/></span><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#66ffff; background-color:#121212;">+35%的物理攻击闪避</span></p>< /body></html>
编辑后点击确定,回到条目编辑界面,关于蝴蝶条目,还有其他的数据信息可以编辑,读者可以自己试试看,上文提到的条目数据几乎都可以在右边的属性编辑栏里面找到。

然后我们如法炮制,为列表控件第二个、第三个条目添加图标和工具提示:
item9

金箍棒的描述信息也是丰富文本,HTML源码如下:
<html><head/><body><p><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#0090ff; background-color:#121212;">+88点攻击力<br/>+15%攻速<br/>< /span><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#66ffff; background-color:#121212;">被动:攻击不会落空(与某些法球冲突,可关闭该效果),攻击中有35%的机率造成0.01秒的晕眩并 且+100点附加伤害</span></p></body></html>
大炮的描述信息 HTML 源码如下:
<html><head/><body><p><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#0090ff; background-color:#121212;">+81点攻击力<br/>25%机率造成2.4倍伤害<br /></span><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#ffff00; background-color:#121212;">通俗称呼:大炮</span></p></body>< /html>
这些描述信息是从如下网站复制的,可以直接粘贴到丰富文本编辑对话框里面(在多文本模式粘贴):
http://db.pcgames.com.cn/dota/item_864.html

由于支持丰富文本的工具提示,Qt 的其他控件或控件条目可以展示非常棒的工具提示效果,不仅仅是丰富文本,我们还可以为工具提示添加资源里的图片,以第三个大炮的工 具提示编辑框为例:
item10
我们在多文本编辑模式,先在文字前按回车键,在文字之前新加空白行,然后点击上面 "插入图像" 按钮,看到选择资源图片的对话框:
item11
我们选中第三个大炮的图片添加到工具提示里面,可以看到如下效果:
item12
这是大炮的工具提示信息预览,等程序编译运行后我们再看看实际效果。
在工具提示添加大炮图片之后,可以看看工具提示 HTML 源码的变化:
<html><head/><body><p><img src=":/icons/rman.jpg"/></p><p><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#0090ff; background-color:#121212;">+81点攻击力<br/>25%机率造成2.4倍伤害<br/>< /span><span style=" font-family:'宋体,arial,sans-serif'; font-size:12px; color:#ffff00; background-color:#121212;">通俗称呼:大炮</span></p></body>< /html>
Qt编程中,一般支持丰富文本的控件或条目,都可以显示内嵌资源文件里的图片,内嵌资源里的图片供 HTML 引用的方式如下:
<img src=":/icons/rman.jpg"/>
除了以冒号打头的图片路径,其他与标准的 HTML 图片引用没区别。

关于条目属性编辑的内容还有很多:
item13
除了条目的文本、图标、工具提示,还可以设置状态栏信息、帮助信息、字体、文本对齐、背景色、前景色、条目特性标志以及复选框勾选状态等等。无论是通过 QtCreator 设计模式或 Qt 设计师的可视化编辑设置条目,还是通过编写代码调整条目的数据,都是可行的,等会我们再学习一些关于设置条目数据的代码。
关于列表控件条目的可视化编辑就介绍这些,读者可以自己试试,尤其是条目的特性标志,可以把条目设置为可编辑的 Editable 标志,等会我们通过代码来修改。

编辑好列表控件的条目之后,我们回到原来的界面编辑,为各个控件添加槽函数。
右击列表控件,在右键菜单选择 "转到槽..." ,添加 currentItemChanged(QListWidgetItem *, QListWidgetItem *) 信号对应的槽函数:
slot1
然后再类似操作一遍,还是为列表控件添加槽函数,再添加 itemChanged(QListWidgetItem * ) 信号对应的槽函数。列表控件对应的两个槽函数功能等会再详解。

主界面右边还有六个按钮控件,为每个按钮都添加 clicked() 信号对应的槽函数:
slot3
槽函数添加完成后,保存界面文件,关闭界面文件,回到 QtCreator 代码编辑模式,开始编写例子的代码。

我们首先编辑头文件 widget.h,主要添加两句头文件包含,其他的不修改:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QListWidget>  //列表控件
#include <QListWidgetItem> //列表控件条目

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_listWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);

    void on_listWidget_itemChanged(QListWidgetItem *item);

    void on_pushButtonAdd_clicked();

    void on_pushButtonDel_clicked();

    void on_pushButtonViewMode_clicked();

    void on_pushButtonLoad_clicked();

    void on_pushButtonSave_clicked();

    void on_pushButtonEditToolTip_clicked();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
列表控件对应的槽函数在头文件声明中用到了列表条目,所以提前包含了 <QListWidget> 和 <QListWidgetItem> 两个类头文件,其他的代码都是自动生成的,不用修改。
下面编辑源文件 widget.cpp ,添加实际的功能代码,这个文件代码比较多,我们逐个函数来看。
首先是头文件包含和构造函数代码:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QIcon>        //图标类
#include <QFileDialog>  //程序运行时可打开新图标文件
#include <QMessageBox>  //消息框
#include <QFile>        //文件类
#include <QDataStream>  //数据流,加载和保存条目数据

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //先把所有条目设置为可编辑特性,并且显示条目的复选框状态
    int nCount = ui->listWidget->count();
    for(int i=0; i<nCount; i++)
    {
        QListWidgetItem *item = ui->listWidget->item(i);
        //启用可编辑特性标志位
        item->setFlags( (item->flags()) | Qt::ItemIsEditable );
        //显示复选框
        item->setCheckState(Qt::Unchecked);
        //DOTA最多携带六个装备,有复选框之后,勾选表示携带,不勾选是不携带
        //后面代码检查只能带六个的限制条件
    }
    //程序运行时新增的装备条目也要开启编辑标志位、复选框,等会实现
    //装备名称可以直接双击修改
}

Widget::~Widget()
{
    delete ui;
}
头文件包含的图标类 <QIcon>,用于打开新增装备的图标文件。<QFileDialog> 是打开和保存文件时用的文件对话框。<QMessageBox> 用于显示消息框。<QFile> 和 <QDataStream> 用于保存装备条目文件和加载装备条目文件。
在构造函数里面新增了一段 for 循环代码,先获取列表控件条目的计数,然修改每个条目的特性标志位,开启可编辑特性,然后把复选状态设置为非选中状态,这样会显示出复选框。本例子列表条目大部分的数据都 是条目的通用数据,代码中可编辑特性标志位是属于非通用数据。
开启条目的可编辑特性,是方便用户双击条目修改装备名称,比如大炮是暴雪弩炮的俗称。
显示的复选框,主要是用于显示装备的携带状态,勾选状态是携带的,非勾选状态是不携带的,DOTA 最多可以带 6 个装备,我们在后面代码会检查携带 6 个的数目限制。
我们之前在列表控件预置了 3 个装备,程序运行时还可以选择装备图标文件,添加新的装备,这样列表里的装备可以有很多。

接着我们来编写条目选中序号变化时调用发的槽函数:
//当前高亮选中的条目序号变化
void Widget::on_listWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
{
    if( NULL == current )   //空指针不处理
    {
        return;
    }
    //获取新选中条目的工具提示信息显示到单行编辑器
    QString strToolTip = current->toolTip();
    ui->lineEditToolTip->setText( strToolTip );
    //在 "编辑工具提示" 按钮对应的槽函数实现修改条目的工具提示
}
当前选中条目的序号变化时,该槽函数第一个参数是现在新选中的条目指针,第二个参数是旧的选中条目指针。注意指针有可能是 NULL,如果要用到哪个指针,一定要判断指针是否为空值。这个槽函数主要是根据高亮选中条目的变化,在单行编辑器里同步显示当前条目的工具提示信息。
与这个条目选中变化槽函数紧密相关的是 "编辑工具提示" 按钮对应的槽函数,我们把这个按钮槽函数提到前面来讲解:
//修改条目的工具提示信息,相当于修改装备描述
void Widget::on_pushButtonEditToolTip_clicked()
{
    //新的工具提示信息
    QString strNew = ui->lineEditToolTip->text();
    //获取当前选中条目
    QListWidgetItem *curItem = ui->listWidget->currentItem();
    if( curItem != NULL )   //检查是否为空指针
    {
        //设置新的工具提示信息
        curItem->setToolTip(strNew);
    }
}
当用户点击 "编辑工具提示" 按钮时,这个槽函数会先获取单行编辑器的文本,然后获取当前高亮选中的条目 curItem ,判断 curItem 指针非空之后,将新的工具提示信息设置给该条目。凡是涉及到指针的,都要注意判断指针是否为 NULL。
涉及到工具提示信息的编辑的,就是上面两个槽函数。

然后我们来看看装备条目复选框的勾选检查,我们要保证勾选的装备不超过 6 个。列表控件的条目内部数据发生变化时,无论是文本、图标、工具提示、复选状态等等,都只触发列表控件的 itemChanged(QListWidgetItem *) 信号,所以我们要自己筛选需要的复选框状态信息:
//限定 DOTA 装备只能携带 6
void Widget::on_listWidget_itemChanged(QListWidgetItem *item)
{
    //空指针不处理
    if( NULL == item)
    {
        return;
    }
    //实际存在的条目变化
    if(item->checkState() != Qt::Checked)
    {
        //复选框没选中,没携带的不检查
        return;
    }
    //如果复选框处于选中状态,检查装备携带是否超过 6 
    int nCount = ui->listWidget->count();
    int nUsingItemsCount = 0;
    for(int i=0; i<nCount; i++)
    {
        QListWidgetItem* theItem = ui->listWidget->item(i);
        if( theItem->checkState() == Qt::Checked )
        {
            //携带数加一
            nUsingItemsCount++;
        }
    }
    //判断是否超过 6 
    if( nUsingItemsCount > 6 )
    {
        QMessageBox::warning(this, tr("携带数目检查"), tr("DOTA 装备最多带 6 个!"));
        //把当前变化的 item 设置为非选中状态
        item->setCheckState( Qt::Unchecked );
    }
}
该槽函数先判断参数里的 item 是否为空指针,如果为空指针就不处理。
接着判断 item 条目里的复选框状态,如果处于非选中状态,说明没携带该装备,就不用后续的判断,直接返回。
如果携带了该装备,那我们要枚举所有的列表条目,如果列表条目复选框处于勾选状态,那就增加携带计数 nUsingItemsCount。
统计好 nUsingItemsCount 之后,判断携带数量是否大于 6,大于 6 就把 item 条目的勾选状态取消,回到不携带的状态。

这个函数工作原理比较简单:如果用户取消勾选装备,那么不用判断,如果用户选中携带装备,那么每次携带都会让上面槽函数执行一次检查,直到超过 6 个就不会再携带超额的装备了。

因为我们例子初始时只有 3 个装备,当然不够,我们下面来看看添加新装备的槽函数:
//用户选择装备图标文件,添加新装备条目
void Widget::on_pushButtonAdd_clicked()
{
    //获取新装备图标的文件路径
    QString strItemFileName = QFileDialog::getOpenFileName(
                this,
                tr("选择装备图标文件"),
                tr("."),
                tr("Image files(*.jpg *.png *.bmp);;All files(*)") );
    if(strItemFileName.isEmpty())
    {
        //没有文件名,返回
        return;
    }
    //打开装备图标文件
    QIcon iconNew(strItemFileName);
    //新建条目,文本初始是默认的,自动添加到列表控件
    QListWidgetItem *itemNew = new QListWidgetItem(
                iconNew,
                tr("新装备名称"),
                ui->listWidget
                );
    //开启条目编辑特性
    itemNew->setFlags( (itemNew->flags()) | Qt::ItemIsEditable );
    //显示复选框
    itemNew->setCheckState( Qt::Unchecked );
    //设置新的当前条目
    ui->listWidget->setCurrentItem( itemNew );
    //新增条目自动启用编辑功能,让用户可以立即修改新装备名称
    ui->listWidget->editItem( itemNew );
}
该槽函数先获取要打开的图标文件名称 strItemFileName,并判断是否非空。
然后根据文件名定义图标对象 iconNew,并新建一个列表条目,新增条目初始时文本都是 "新装备名称",条目图标就是刚定义的图标对象,而且条目会自动添加到列表控件;
接着开启该条目的可编辑标志位,并设置条目的复选状态,显示条目配套的复选框;
最后将列表当前高亮选中的条目设置为刚刚新增的条目,并且调用 editItem() 函数,自动进入该条目文本的编辑框,方便用户立即修改这个新装备名称。

与增加装备相反的是删除条目的槽函数:
//删除当前选中的装备条目
void Widget::on_pushButtonDel_clicked()
{
    int nCurRow = ui->listWidget->currentRow();
    if( nCurRow < 0)
    {
        //没有选中的条目
        return;
    }
    //从列表控制移除选中的条目
    QListWidgetItem *itemDel = ui->listWidget->takeItem( nCurRow );
    //彻底删除移除的条目
    delete itemDel; itemDel = NULL;
}
删除条目的槽函数先获取当前选中条目的行号,判断行号是否合法;对于合法的行号,使用 takeItem() 函数从列表控件移除该条目,移除条目并不是从内存彻底删除,要彻底删除条目还得手动 delete 已经移除的条目。
注意列表控件的 takeItem() 函数只是根据行号解除该行条目的隶属关系,而不会从内存消掉该条目内容,因此需要手动 delete。

然后我们来看看列表控件切换显示模式的槽函数:
//切换小图标文本模式和大图标模式
void Widget::on_pushButtonViewMode_clicked()
{
    QListView::ViewMode vm = ui->listWidget->viewMode();
    if( QListView::ListMode == vm )
    {
        //切换到大图标显示,大图标可以拖动,但是不会改变内部真实的条目序号
        ui->listWidget->setViewMode( QListView::IconMode );
    }
    else
    {
        //切换到小图标文字显示,条目不可拖动
        ui->listWidget->setViewMode( QListView::ListMode );
    }
}
之前没介绍列表空间的显示模式属性 viewMode,这个属性是从基类 QListView 继承来的,显示模式目前只有两种:
QListView::ListMode,默认值是这个小图标文本列表模式,重点是显示条目的文本,所有条目是静态的,不可拖动;
QListView::IconMode,大图标显示模式,重点显示大图标,大图标可以拖动位置,但拖动位置不改变条目实际的排序。
条目是否可以拖动的特性是可以通过修改 movement 属性实现,这些等到讲解基类 QListView 再详细说,这里只是点一下。

最后我们来看看 "保存" 和 "加载" 两个按钮的槽函数,这两个槽函数稍微有点复杂,先看 "保存" 按钮的槽函数:
//保存所有条目到文件
void Widget::on_pushButtonSave_clicked()
{
    //获取保存文件名
    QString strSaveName = QFileDialog::getSaveFileName(
                this,
                tr("保存items文件"),
                tr("."),
                tr("Items files(*.items)"));
    //判断文件名
    if( strSaveName.isEmpty() )
    {
        return;
    }
    //打开要写入的文件
    QFile fileSave(strSaveName);
    if( ! fileSave.open( QIODevice::WriteOnly ))
    {
        //无法打开要写入的文件
        QMessageBox::warning(this, tr("打开写入文件"),
                             tr("打开要写入的文件失败,请检查文件名和是否具有写入权限!"));
        return;
    }
    //创建数据流
    QDataStream dsOut(&fileSave);
    //先写入列表条目计数
    qint32 nCount = ui->listWidget->count();
    dsOut<<nCount;
    //逐个写入条目
    for(qint32 i=0; i<nCount; i++)
    {
        QListWidgetItem *theItem = ui->listWidget->item(i);
        dsOut<< *theItem;   //把条目对象写入数据流,不是写指针数值
        //数据流仅写入条目通用数据,条目的非通用数据不写入,比如条目的标志位不写
    }
    //写入完毕
}
我们保存的文件是自定义的数据流文件,先写入条目计数,然后依次写入各个条目,文件扩展名为 *.items。
该槽函数先获取要保存的文件名,并判断文件名非空。
然后根据文件名定义文件对象 fileSave,以只写模式打开文件,如果打开出错就提示并返回。
如果文件打开成功,就根据文件对象定义数据流对象 dsOut;
获取列表控件的条目计数 nCount,写入到数据流;
然后枚举各个条目,将所有条目对象都写入数据流。

注意是将条目对象 *theItem 写入数据流,theItem 是指针,不能直接把指针值写入数据流,指针值本身是没用的,要存储条目对象的本体。数据流会自动把条目的通用数据(包括文本、图标、工具提示、复选框状态等等一大堆)存入 文件,但是非通用的数据,比如可编辑特性标志,是不写文件的。 QListWidgetItem::​flags() 的标志位都不会写入文件,等会加载 *.items 文件的时候,我们需要自己动手修改标志位。

再来看看 "加载" 按钮对应的槽函数:
//加载之前保存的文件
void Widget::on_pushButtonLoad_clicked()
{
    //获取要加载的文件名
    QString strOpenName = QFileDialog::getOpenFileName(
                this,
                tr("打开items文件"),
                tr("."),
                tr("Items files(*.items)"));
    //判断文件名
    if(strOpenName.isEmpty())
    {
        return;
    }
    //打开文件
    QFile fileOpen(strOpenName);
    if( ! fileOpen.open(QIODevice::ReadOnly))
    {
        //打开出错
        QMessageBox::warning(this, tr("打开文件"),
                             tr("打开指定文件失败,请检查文件是否存在和读取权限!"));
        return;
    }
    //创建读取数据流
    QDataStream dsIn(&fileOpen);
    //读取条目计数
    qint32 nCount;
    dsIn>>nCount;
    //判断计数数值
    if(nCount <= 0)
    {
        QMessageBox::warning(this, tr("文件加载"),
                             tr("文件中无条目数据可以加载!"));
        return;
    }
    //新建各个条目,并加载文件中的条目数据
    for(qint32 i=0; i<nCount; i++)
    {
        //新建条目,条目自动添加到列表控件
        QListWidgetItem *theItem = new QListWidgetItem(ui->listWidget);
        //加载条目通用数据,文字、图标、工具提示、复选状态等是通用数据,在数据流里面有保存
        dsIn>> *theItem;

        //条目的非通用数据是不保存到数据流的
        //这里需要手动把条目设置为可编辑的特性标志位
        theItem->setFlags( (theItem->flags()) | Qt::ItemIsEditable );
    }
    //加载完毕
}
这个槽函数先获取要加载的文件名,并检查文件名是否非空。
然后根据文件名定义文件对象 fileOpen,并用只读模式打开,如果打开失败就报错并返回。
如果文件打开正确,就定义数据流对象 dsIn;
先从数据流读取条目计数 nCount,判断 nCount 数值,如果不是正数,就提示没有条目数据,直接返回。
如果有正确的条目计数,那就用 for 循环逐个新建列表条目对象,新建的对象指针为 theItem ;
把数据流中的条目数据读取到对象本体 *theItem;
最后要注意的就是条目的非通用数据是不保存的,标志位需要自己手动设置,我们加了一句条目的 setFlags() 函数调用,为条目设置为可编辑的特性标志位。

这个游戏装备列表示例的代码就是上面那些,我们现在生成运行例子看看:
run1
当鼠标悬停在第三个大炮(暴雪弩炮)的图标上时,可以看到工具提示信息,有图有真相,就是装备的描述信息。
我们可以再添加多个装备条目,并测试 6 个装备限制条件:
run2
当勾选第 7 个装备的时候,弹出消息框提示只能带 6 个,点击消息框 OK 按钮之后,第 7 个装备会自动取消勾选,回到只勾选 6 个的状态。示例程序其他功能这里就不截图了,这个例子的代码较多,涉及的知识点也比较多,读者最好把每个槽函数的代码都吃透,并测试每个槽函数代码在程序运行时的实际效 果。

8.1.4 歌曲列表示例

上一小节涉及列表控件大部分的功能,我们在本小节再学习一个歌曲列表示例,歌曲列表可以一次添加多个音乐文件,支持同时选中多个音乐条目,也能同时删除多个音乐条 目。另外还能根据模板字符串查找匹配的条目进行高亮显示,歌曲列表还可以导出为 *.m3u 播放列表文件格式。 *.m3u 本质就是文本文件,最简单的 *.m3u 播放列表第一行是文本 #EXTM3U ,后面每行放一个音乐文件名,例如:
#EXTM3U
D:/我的文档/My Music/仙剑/蝶恋.mp3
D:/我的文档/My Music/仙剑/六月的雨.mp3
D:/我的文档/My Music/仙剑/沙破狼.mp3
D:/我的文档/My Music/仙剑/逍遥叹.mp3
D:/我的文档/My Music/仙剑/一直很安静.mp3
下面开始这个例子的学习,打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 musiclist,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
界面分成左右两个大部分:
左边有列表控件和单行编辑器,列表控件对象名 listWidget;单行编辑控件对象名 lineEditTemplate;两个空间按照垂直布局器排布。
右边是四个按钮和两个复选框,第一个按钮文本为 "添加",对象名 pushButtonAdd;第二个按钮文本为 "删除",对象名 pushButtonDel;第三个按钮文本为 "导出m3u",对象名 pushButtonExportM3U;第一个复选框文本为 "自动排序",对象名 checkBoxAutoSort;第二个复选框文本为 "逆序排列",对象名 checkBoxReverse;第二个复选框与最后一个按钮之间有垂直空白条;最后一个按钮文本为 "查找",对象名 pushButtonFind;右边的控件也按照垂直布局排布。
界面的主布局是按照水平布局器排布。

这里介绍一下按钮和复选框的用途,"添加" 按钮支持把多个音乐文件名添加到列表控件,"删除"按钮支持从列表控件删除多个条目;"导出m3u" 按钮是把当前列表控件的条目导出为播放列表文件 *.m3u ;勾选 "自动排序" 复选框之后,列表控件自动对新增的音乐文件名进行排序,音乐文件名的正序或逆序排列由 "逆序排列" 复选框的状态指定;"查找" 按钮是根据单行编辑器的模板字符串,查找哪些条目包含该模板字符串,查找到的条目都会高亮显示。

我们现在为界面的四个按钮和两个复选框添加槽函数,首先为四个按钮添加 clicked() 信号对应的槽函数:
slot1-4
然后为两个复选框都添加 clicked(bool) 信号对应的槽函数:
slot5-6
添加好共 6 个槽函数之后,保存界面文件,关闭界面文件,回到 QtCreator 代码编辑模式,开始代码的编写。

先来看看头文件 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_pushButtonAdd_clicked();

    void on_pushButtonDel_clicked();

    void on_pushButtonExportM3U_clicked();

    void on_pushButtonFind_clicked();

    void on_checkBoxAutoSort_clicked(bool checked);

    void on_checkBoxReverse_clicked(bool checked);

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H

我们现在打开源代码文件 widget.cpp ,添加实际的功能代码,首先是头文件包含和构造函数:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>  //消息框
#include <QListWidget>  //列表控件
#include <QListWidgetItem>  //列表控件条目
#include <QFileDialog>  //文件对话框
#include <QFileInfo>    //查询文件信息

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //开启列表控件多选模式
    ui->listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
    //开启自动排序
    ui->checkBoxAutoSort->setCheckState(Qt::Checked);
}

Widget::~Widget()
{
    delete ui;
}
包含的头文件在上一个例子基本都见过了,不重复讲了,最后一个头文件 <QFileInfo> 用于根据路径文件名提取文件的基本名,显示到列表条 目。
在构造函数里,主要添加了两句代码:
新增的第一句调用列表控件的 setSelectionMode(QAbstractItemView::ExtendedSelection) 函数,设置多选模式,用户可以像文件系统选中多个文件一样选中多个条目,Ctrl + 鼠标点击是离散多选,Shift + 鼠标点击是连续选中。
新增的第二句代码是将 "自动排序" 复选框勾选,默认情况下列表控件的条目就是自动排序的。

然后来看看 "添加" 按钮对应的槽函数代码:
//支持一次添加多个音乐文件
void Widget::on_pushButtonAdd_clicked()
{
    QStringList slist = QFileDialog::getOpenFileNames(
                this,
                tr("添加多个音乐文件"),
                tr("."),
                tr("Music files(*.mp3 *.wma *.wav);;All files(*)")
                );
    //判断文件名个数
    int nCount = slist.count();
    if(nCount < 1)
    {
        return;
    }
    //有选中的文件,逐个添加
    for(int i=0; i<nCount; i++)
    {
        //新建条目,并自动加到列表控件
        QListWidgetItem *theItem = new QListWidgetItem(ui->listWidget);
        //取出文件基本的短名设置为条目字符串
        QFileInfo fi(slist[i]);
        theItem->setText( fi.completeBaseName() );
        //设置全路径为工具提示信息
        theItem->setToolTip( fi.absoluteFilePath() );
    }
    //添加完毕
}
该槽函数里,先调用 QFileDialog::getOpenFileNames() 函数,弹出对话框可以选中多个音乐文件,这些文件名以 QStringList 列表对象返回。
然后我们判断字符串列表对象 slist 是否包含有字符串,如果没有说明用户没选中文件,就不处理;
如果用户选中某些文件,就利用 for 循环对这些文件逐个处理:
    新建一个属于列表控件 ui->listWidget 的条目;
    根据文件名字符串定义文件信息对象 fi;
    提取文件基本名设置为新建条目的文本;
    提取文件的绝对路径名设置为新建条目的工具提示信息。
所有文件名字符串都处理完,该函数就结束了。
注意这个例子只是处理文件名的列表,没有播放功能的。

接着来看看 "删除" 按钮对应的槽函数代码:
//支持一次删除多个条目
void Widget::on_pushButtonDel_clicked()
{
    //获取已选中的列表条目
    QList<QListWidgetItem *> itemList = ui->listWidget->selectedItems();
    //判断数目
    int nCount = itemList.count();
    if( nCount < 1 )
    {
        return;
    }
    //有选中的条目需要删除
    //一般删除多个条目时,从尾部开始删除,这样删除过程中前面的条目行号不会变
    for(int i=nCount-1; i>=0; i--)
    {
        //根据条目求出行号
        int theRow = ui->listWidget->row( itemList[i] );
        //移除条目
        ui->listWidget->takeItem( theRow );
        //彻底从内存删除
        delete itemList[i];     itemList[i] = NULL;
    }
    //清空临时条目列表
    itemList.clear();
}
因为用户可能选中多个条目,所以不能用 ​currentItem() 函数,currentItem() 只能获取用户最后点击的一个高亮选中的条目。获取当前 选中的多个条目需要用 selectedItems() 函数,这个函数返回 QList<QListWidgetItem *> 列表。
对于获取的选中条目列表 itemList,先判断个数,如果没有选中的就不处理;
如果有选中的,就利用 for 循环挨个处理,注意删除操作一般从最后一个开始删除,这样前面的条目行号不会变,尽量少对前面条目造成影响,for 循环内部:
    先根据条目获取条目在列表控件的序号 theRow ;
    然后移除列表控件该行的条目,这只是解除隶属关系;
    再手动调用 delete 函数删除条目内存,彻底删除掉。
选中的条目都删除之后,把 itemList 也清空,这样就处理完毕了。

以后读者如果遇到类似删除多个条目情况,建议是从末尾的条目开始删除,这样对前面的条目影响小,序号不容易出错,内存存储位置也较少腾挪,执行效率比较高。

下面我们来看看两个关于排序的复选框对应的槽函数代码,两个复选框功能关系非常密切,"自动排序" 复选框的代码如下:
//是否自动排序
void Widget::on_checkBoxAutoSort_clicked(bool checked)
{
    if(checked)
    {
        //开启自动排序
        ui->listWidget->setSortingEnabled(true);
        //把逆序排列的复选框启用
        ui->checkBoxReverse->setEnabled(true);
        //判断是正序还是逆序
        if( ui->checkBoxReverse->checkState() != Qt::Checked )
        {
            //正序
            ui->listWidget->sortItems(Qt::AscendingOrder);
        }
        else
        {
            //逆序
            ui->listWidget->sortItems(Qt::DescendingOrder);
        }
    }
    else
    {
        //不自动排序
        ui->listWidget->setSortingEnabled(false);
        //禁用逆序复选框
        ui->checkBoxReverse->setEnabled(false);
    }
}
如果用户勾选了 "自动排序" 复选框:
    调用列表控件的函数 setSortingEnabled(true),开启自动排序;
    因为启动了自动排序,这样 "逆序排列" 复选框就需要用到了,启用 "逆序排列" 复选框;
    根据 "逆序排列" 复选框的状态,判断是应该自动按照正序排列,还是自动按照逆序排列。
如果用户取消 "自动排序" 复选的勾选状态:
    调用列表空的函数 setSortingEnabled(false) 关闭自动排序;
    因为自动排序关了,那么也就没有正序逆序之分,就把 "逆序排列" 复选框设置为禁用状态。

上面代码保证了只有在开启自动排序的情况下,才让 "逆序排列" 复选框处于可用状态,让用户选择是逆序还是正序排列。然后来看看 "逆序排列" 复选框对应槽函数的代码:
//是否逆序排列
void Widget::on_checkBoxReverse_clicked(bool checked)
{
    if( ! checked)
    {
        //正序
        ui->listWidget->sortItems(Qt::AscendingOrder);
    }
    else
    {
        //逆序
        ui->listWidget->sortItems(Qt::DescendingOrder);
    }
}
这个槽函数代码比较简单,如果没选中该复选框,就是正序排列;
如果选中了该复选框,就是按照逆序排列。
 "逆序排列" 复选框主要是在开启自动排序的情况下起作用,其他时候是用不着的。

接下来我们看看 "导出m3u" 按钮的功能代码,导出时,列表控件里的条目按什么样的顺序排列,导出的 m3u 播放列表文件内部就是什么样的顺序排列:
// 这里导出为最简单的 m3u 格式列表
void Widget::on_pushButtonExportM3U_clicked()
{
    //判断列表条目总数
    int nCount = ui->listWidget->count();
    if(nCount < 1)
    {
        return;
    }
    //获取要保存的文件名
    QString strName = QFileDialog::getSaveFileName(
                this,
                tr("保存为 M3U 文件"),
                tr("."),
                tr("M3U files(*.m3u)")
                );
    //判断文件名
    if(strName.isEmpty())
    {
        return;
    }
    //打开文件
    QFile fileOut(strName);
    // QIODevice::Text 自动转换换行符为本地系统风格
    if( ! fileOut.open( QIODevice::WriteOnly | QIODevice::Text) )
    {
        QMessageBox::warning(this, tr("打开文件"),
                             tr("无法打开指定文件,请检查是否有写入权限!"));
        return;
    }
    //正确打开之后,定义文本流
    //文本流自动转换文本编码为本地系统格式
    //但文本流不处理换行符,换行符本地化通过文件打开时的 QIODevice::Text 指定
    QTextStream tsOut(&fileOut);
    //先写入文件头
    tsOut<<tr("#EXTM3U")<<endl;
    //逐个文件名条目写入
    for(int i=0; i<nCount; i++)
    {
        QString strCurName = ui->listWidget->item(i)->toolTip();
        tsOut<<strCurName<<endl;
    }
    //完成
}
该槽函数开始时先判断列表控件内的条目总数,如果没条目就不导出,如果有条目才进行后续操作。
然后获取要保存的文件名 strName ,判断文件名非空再进行后续处理。
根据非空文件名定义文件对象 fileOut,以只读和文本模式 QIODevice::Text 打开该文件,文本模式 QIODevice::Text 可以在文件写入时把换行符("\n"、endl)自动转为本地系统风格写入文件中,Windows换行风格是 "\r\n" ,其他系统一般只是 "\n" 换行。
文件正确打开之后,定义文本流 tsOut 用于写入内容。
文本流先写入文件头一行 "#EXTM3U" ;
然后用 for 循环将每个列表条目工具提示里存的音乐文件绝对路径,写入到文本流。
这样就完成了 *.m3u 文件的导出,这种播放列表文件可以用千千静听等播放器打开。

最后一个按钮是 "查找" 按钮,对应的功能代码如下:
//条目文本查找,包含模板字符串的都高亮显示
void Widget::on_pushButtonFind_clicked()
{
    //获取模板字符串
    QString strTemplate = ui->lineEditTemplate->text();
    //判断字符串是否为空
    if(strTemplate.isEmpty())
    {
        return;
    }
    //先把旧的高亮显示条目都清空
    ui->listWidget->setCurrentItem(NULL, QItemSelectionModel::Clear);

    //查找匹配的条目文本,第二个参数控制匹配行为
    QList<QListWidgetItem *> list =
            ui->listWidget->findItems(strTemplate, Qt::MatchContains);
    //判断是否有匹配的
    int nCount = list.count();
    if(nCount < 1)
    {
        QMessageBox::information(this, tr("查找条目"),
                                 tr("没有找到匹配的条目文本。"));
        return;
    }
    //把第一个匹配的条目设置为当前条目
    ui->listWidget->setCurrentItem( list[0] );
    //自动滚动到第一条匹配选中的条目,这个条目显示在可视区域顶部
    ui->listWidget->scrollToItem( list[0], QAbstractItemView::PositionAtTop);

    //然后再把所有匹配的条目设置为高亮选中
    for(int i=0; i<nCount; i++)
    {
        list[i]->setSelected(true); //条目自己可以设置高亮选中
    }

    //将显示焦点切换到列表控件
    ui->listWidget->setFocus();
}
这个槽函数功能就是根据单行编辑器里的文本,查找包含该文本的条目。
槽函数先获取单行编辑器里的模板字符串 strTemplate,字符串非空才进行后续操作。
因为我们要把所有匹配的条目都高亮显示,所以调用列表控件的 setCurrentItem(NULL, QItemSelectionModel::Clear) 函数,把之前选中的状态都清了。
然后调用列表控件的 findItems(strTemplate, Qt::MatchContains) 查找包含模板字符串的条目,这个函数第二个参数可以控制匹配行为,Qt::MatchContains 是包含模板字符串,而其他的,比如 Qt::MatchWildcard 是根据通配符(* ?)进行匹配。查找的结果以 QList<QListWidgetItem *> 类型的对象返回。
得到查找结果 list 之后,先判断个数,如果没有就提示没有找到匹配的条目;
如果找到了匹配的条目,那么:
把第一个匹配的条目设置为列表控件当前高亮选中的条目;
并滚动到第一个匹配的条目的位置,这个条目会尽量滚动到列表控件可视区域的顶部(QAbstractItemView::PositionAtTop );
现在只处理了第一个匹配的,然后我们再把匹配条目列表 list 里的所有条目用 for 循环处理一遍,把每个匹配的条目都设置为高亮选中状态;
最后再把显示焦点转移到列表控件,因为用户点击 "查找" 按钮时,焦点在该按钮上,焦点不在列表控件时,列表控件选中的条目是灰色的,转移焦点到列表控件后,列表控件内选中的所有条目都是高亮蓝色的,这样看起来更清晰。

例子的代码就讲到这,下面运行例子看看效果,我们添加很多的音乐文件条目之后,随便查找一个:
run
匹配的第一个条目会自动滚到显示区域顶部位置,匹配的条目都会高亮显示。还有其他按钮的功能,读者可以都测试一下,导出的 m3u 播放列表可以用千千静听等打开试试。
本节的主要内容就讲解到这,后面留几个小练习,建议读者都动手实践一下,因为靠瞎蒙是学不到东西的。



tip 练习
① 游戏装备列表示例中,加载文件的函数 on_pushButtonLoad_clicked() 最后的 for 循环内部的代码,如果注释掉末尾的  theItem->setFlags(***) 这行代码,会有什么影响?

② 还是游戏装备列表示例中,加载文件的函数 on_pushButtonLoad_clicked(),这个函数加载文件中新条目之前,没有调用 clear() 函数清除旧的列表。如果我们勾选 6 个装备,保存为文件,勾选状态会存到文件里,我们再立即加载该文件,会出现什么现象?会不会同时携带 12 个装备?

③ 歌曲列表示例中,条目都没有图标,请去网上下载 mp3、wma、wav 三种文件格式的图标,添加为示例程序内部资源,然后在 "添加" 按钮的槽函数代码里根据每个音乐文件的扩展名,为条目添加相对应的图标。



prev
contents
next