概述
Qt 5.9 C++开发指南学习笔记
第2章 GUI应用程序设计基础
2.1 UI文件设计与运行机制
2.1.2 项目管理文件
后缀为“.pro”的文件是项目的管理文件,文件名就是项目的名称。
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES +=
main.cpp
widget.cpp
HEADERS +=
widget.h
FORMS +=
widget.ui
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
“QT += core gui”表示项目中加入core gui模块。core gui是Qt用于GUI设计的类库模块。
QT类库以模块的形式组织各种功能的类,根据项目涉及的功能需求,在项目中添加适当的类库模块支持。例如,如果项目中使用到了涉及数据库操作的类就需要用到sql模块,在pro文件中需要增加如下一行:
Qt += sql
“greaterThan(QT_MAJOR_VERSION, 4): QT += widgets”是个条件执行语句,表示当Qt主版本大于4时,才加入widgets模块。
后面的SOURSE、HEADERS、FORMS记录了项目中包含的源程序文件、头文件和窗体文件(.ui文件)的名称。
2.1.4 主函数文件
main.cpp是实现main()函数的文件。
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
QApplication是Qt的标准应用程序类,第一行代码定义了一个QApplication类的实例a,就是应用程序对象。
然后定义了一个Widget类的变量w,Widget是本实例设计的窗口的类名,定义此窗口后再用w.show()显示此窗口。
最后一行用a.exec()启动应用程序的执行,开始应用程序的消息循环和事件处理。
2.1.5 窗体相关的文件
对于一个窗体,有4个文件,各文件的功能说明见表2-2。
文件 | 功能 |
---|---|
widget.h | 定义窗体类的头文件,定义了类Widget |
widget.cpp | Widget类的功能实现源程序文件 |
widget.ui | 窗体界面文件,由UI设计器自动生成,存储了窗体上各个组件的属性设置和布局 |
ui_widget.h | 编译后,根据窗体上的组件及其属性、信号和槽的关联等自动生成的一个类的定义文件,类的名称是Ui_Widget |
1. widget.h文件
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.h文件有几个重要的部分。
-
namespace声明:代码中有一个namespace声明 namespace Ui { class Widget; }
这是声明了一个名称为Ui的命名空间(namespace),包含一个类Widget。但是这个类Widget并不是本文件里定义的类Widget,而是ui_widget.h文件里定义的类,用于描述界面组件的。这个声明相当于一个外部类型声明。
-
Widget类的定义。widget.h文件的主体部分是一个继承于QWidget的类Widget的定义,也就是本实例的窗体类。
在Widget类中使用宏Q_OBJECT,这是使用Qt的信号与槽(signal和slot)机制的类都必须加入的一个宏。
在public部分定义了Widget类的构造函数和析构函数。
在private部分又定义了一个指针。这个指针是用前面声明的namespace Ui里的Widget类定义的,所以指针ui是指向可视化设计的界面,后面会看到要访问界面上的组件,都需要通过这个指针ui。
2. widget.cpp文件
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
注意到,在这个文件的包含文件部分自动加入了如下一行内容:#include “ui_widget.h”
这个就是Qt编译生成的与UI文件widget.ui对应的类定义文件。
目前只有构造函数和析构函数。其中构造函数的头部是:Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
其意义是:执行父类QWidget的构造函数,创建一个Ui::Widget类的对象ui。这个ui就是Widget的private部分定义的指针变量ui。
构造函数里只有一行语句:ui->setupUi(this)
它是执行了Ui::Widget类的setupUi()函数,这个函数实现窗口的生成与各种属性的设置、信号与槽的关联。
析构函数只是简单地删除用new创建地指针ui。
所以,在ui_widget.h文件里有一个namespace名称为Ui,里面有一个类Widget是用于描述可视化设计地窗体,且与widget.h里定义地类同名。在Widget类里访问Ui::Widget类的成员变量或函数需通过Widget类里的ui指针,如同构造函数里执行ui->setupUi(this)函数那样。
4. ui_widget.h文件
/********************************************************************************
** Form generated from reading UI file 'widget.ui'
**
** Created by: Qt User Interface Compiler version 5.9.9
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef UI_WIDGET_H
#define UI_WIDGET_H
#include <QtCore/QVariant>
#include <QtWidgets/QAction>
#include <QtWidgets/QApplication>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>
QT_BEGIN_NAMESPACE
class Ui_Widget
{
public:
QPushButton *btnClose;
QLabel *LabDemo;
void setupUi(QWidget *Widget)
{
if (Widget->objectName().isEmpty())
Widget->setObjectName(QStringLiteral("Widget"));
Widget->resize(800, 600);
btnClose = new QPushButton(Widget);
btnClose->setObjectName(QStringLiteral("btnClose"));
btnClose->setGeometry(QRect(500, 430, 93, 28));
LabDemo = new QLabel(Widget);
LabDemo->setObjectName(QStringLiteral("LabDemo"));
LabDemo->setGeometry(QRect(200, 210, 231, 91));
QFont font;
font.setPointSize(20);
font.setBold(true);
font.setWeight(75);
LabDemo->setFont(font);
retranslateUi(Widget);
QObject::connect(btnClose, SIGNAL(clicked()), Widget, SLOT(close()));
QMetaObject::connectSlotsByName(Widget);
} // setupUi
void retranslateUi(QWidget *Widget)
{
Widget->setWindowTitle(QApplication::translate("Widget", "Widget", Q_NULLPTR));
btnClose->setText(QApplication::translate("Widget", "Close", Q_NULLPTR));
LabDemo->setText(QApplication::translate("Widget", "Hello, World", Q_NULLPTR));
} // retranslateUi
};
namespace Ui {
class Widget: public Ui_Widget {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_WIDGET_H
查看ui_widget.h文件的内容,发现它主要做了以下的一些工作。
-
定义了一个类Ui_Widget,用于封装可视化设计的界面。
-
自动生成了界面各个组件的类成员变量定义。在public部分为界面上每个组件定义了一个指针变量,变量的名称就是设置的objectName。
-
定义了setupUi()函数,这个函数用于创建各个界面组件,并设置其位置、大小、文字内容、字体等属性,设置信号与槽的关联。
setupUi()函数体的第一部分是根据可视化设计的界面内容,用C++代码创建界面上各组件,并设置其属性。
接下来,setupUi()调用了函数retranslateUi(Widget),用于设置界面各组件的文字内容属性,如标签的文字、按键的文字、窗体的标题等。将界面上的文字设置的内容独立出来作为一个函数restranslateUi(),在设计多语言界面时会用到这个函数。
setupUi()函数的第三部分是设置信号与槽的关联,本文件中有以下两行:
QObject::connect(btnClose, SIGNAL(clicked()), Widget, SLOT(close())); QMetaObject::connectSlotsByName(Widget);
第1行是调用connect()函数,将在UI设计器里设置的信号与槽的关联转换为语句。
第2行是设置槽函数的关联方式,用于将UI设计器自动生成的组件信号的槽函数与组件信号相关联。
所以,在Widget的构造函数里调用ui->setupUI(this),就实现了窗体上组件的创建、属性设置、信号与槽的关联。
-
定义了namespace Ui,并定义了一个从Ui_Widget继承的类Widget。
2.2 可视化UI设计
2.2.2 界面组件布局
2. 布局管理
Qt为界面设计提供了丰富的布局管理功能,功能见表2-4.
布局组件 | 功能 |
---|---|
Vertical Layout | 垂直方向布局,组件自动在垂直方向上分布 |
Horizontal Layout | 水平方向布局,组件自动在水平方向上分布 |
Grid Layout | 网格状布局,网状布局大小改变时,每个网格的大小都改变 |
Form Layout | 窗体布局,与网格状布局类似,但是只有最右侧的一列网格会改变大小 |
Horizontal Spacer | 一个用于水平分隔的空格 |
Vetical Spacer | 一个用于垂直分隔的空格 |
在设计窗体的上方有一个工具栏,用于调整设计器进入不同的状态,以及进行布局设计,工具栏上各按钮的功能见表2-5
按钮 | 功能 |
---|---|
Edit Widget | 界面设计进入编辑状态,就是正常的设计状态 |
Edit Signal/Slots | 进入信号与槽的可视化设计状态 |
Edit Buddies | 进入伙伴关系编辑状态,可以设置一个Label与一个组件称为伙伴关系 |
Edit Tab Order | 进入Tab顺序编辑状态,Tab顺序是在键盘上按Tab键时,输入焦点在界面各组件之间跳动的顺序 |
Lay Out Horizontally | 将窗体上所选组件水平布局 |
Lay Out Vetically | 将窗体上所选组件垂直布局 |
Lay Out Horizontally in Splitter | 将窗体上所选组件用一个分割条进行水平分割布局 |
Lay Out Vetically in Splitter | 将窗体上所选组件用一个分割条进行垂直分割布局 |
Lay Out in a Form Layout | 将窗体上所选组件按窗体布局 |
Lay Out in a Grid | 将窗体上所选组件网格布局 |
Break Layout | 解除窗体上所选组件的布局,也就是打散现有的布局 |
Adjust Size | 自动调整所选组件的大小 |
3. 伙伴关系与Tab顺序
伙伴关系(Buddy)是指界面上一个Label和一个组件相关联,单击一个Label,按住鼠标左键,然后拖向一个组件,就建立了Label和组件之间的伙伴关系。
伙伴关系是为了在程序运行时,在窗体上用快捷键快速将输入焦点切换到某个组件上。
Tab顺序是指在程序运行时,按下键盘上的Tab键时输入焦点的移动顺序。一个好的用户界面,在按Tab键时,焦点应该以合理的顺序在界面上移动,而不是随意地移动。
没有输入焦点的组件是没有Tab顺序的,如Label组件。
2.2.3 信号与槽
信号与槽(Signal & Slot)是Qt编程的基础,也是Qt的一大创新。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时变得更加直观和简单。
信号(Signal)就是在特定情况下被发射的事件。GUI程序设计的主要内容就是对界面上各组件的信号的响应,只需要知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。
槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public、private或protected),可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
信号与槽关联使用QObject::connect()函数实现的,其基本格式是:
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
connect()是QObject类的一个静态函数,而QObject是所有Qt类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
其中,sender是发射信号的对象的名称,signal()是信号名称。信号可以看做是特殊的函数,需要带括号,有参数还需要指明参数。receiver是接收信号的对象名称,slot()是槽函数的名称,需要带括号,有参数时还需指明参数。
SIGNAL和SLOT是Qt的宏,用于指明信号与槽,并将它们的参数转换为相应的字符串。
关于信号与槽的使用,有以下一些规则需要注意。
-
一个信号可以连接多个槽。
当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次执行。
当信号和槽函数带有参数时,在connect()函数里,要写明参数的类型,但可以不屑参数名称。
-
多个信号可以连接同一个槽。
-
一个信号可以连接另一个信号。
-
严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。
-
在使用信号与槽的类中,必须在类的定义中加入宏Q_OBJECT。
-
当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。
信号与槽机制是Qt GUI编程的基础,使用信号与槽机制可以比较容易地将信号与响应代码关联起来。
2.2.4 可视化生成槽函数原型和框架
1. 字体样式设置
窗体在设计模式下,选中chkBoxUnder组件,单击右键调出其快捷菜单。在快捷菜单中单击菜单项“Go to slot…”。
选择clicked(bool),然后单击“OK”按钮,在QWDialog地类定义中,会在private slots部分自动添加一个槽函数声明,函数名是根据发射对象及其信号名称自动命名的。
void on_chkBoxUnder_clicked(bool checked);
同时,在qwdialog.cpp文件中自动添加了函数on_chkBoxUnder_clicked(bool)的框架,在此函数中添加代码,实现功能。
查看编译生成的ui_qwdialog.h文件。构造函数里调用的setupUi()是在ui_qwdialog.h文件里实现的。在setupUI()里发现了如下的一条语句
QMetaObject::connectSlotByName(QWDialog);
connectSlotsByName(QWDialog)函数将搜索QWDialog界面上的所有组件,将信号与槽函数匹配的信号与槽关联起来,它假设槽函数的名称是
void on_<object name>_<signal name>(<signal parameters>);
这就是用UI设计器可视化设计某个组件的信号响应槽函数,而不用手工去将其关联起来的原因,都是在界面类的构造函数里调用setupUi()自动完成了关联。
2. 字体颜色设置
设计一个槽函数,将3个RadioButton的clicked()信号关联到这一个槽函数。
为此,在QWDialog类的private slots部分增加一个槽函数定义如下:
void setTextFontColor();
在qwdialog.cpp文件中,为setTextFontColor()编写实现代码如下:
void Dialog::setTextFontColor()
{
QPalette plet = ui->txtEdit->palette();
if (ui->rBtnRed->isChecked())
plet.setColor(QPalette::Text, Qt::red);
else if(ui->rBtnBlue->isChecked())
plet.setColor(QPalette::Text, Qt::blue);
else if(ui->rBtnBlack->isChecked())
plet.setColor(QPalette::Text, Qt::black);
else
plet.setColor(QPalette::Text, Qt::black);
ui->txtEdit->setPalette(plet);
}
由于这个槽函数是自定义的,所以不会自动与RadioButton的clicked()事件关联,此时编译后运行程序不会实现改变字体颜色的功能。需要在QWDialog的构造函数中手工进行关联,代码如下:
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::Dialog)
{
ui->setupUi(this);
connect(ui->rBtnRed, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
connect(ui->rBtnBlue, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
connect(ui->rBtnBlack, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
}
3. 三个按钮的功能设计
下面采用可视化的方式,将按钮的clicked()信号与这些槽函数关联起来。在UI设计器里,单击上方工具栏里的“Edit Signals/Slots”按钮,窗体进入信号与槽函数编辑状态,将鼠标移动到“确定”按钮上方,再按下鼠标左键,移动到窗体的空白区域释放左键,这时出现关联设置对话框。
2.3 代码化UI设计
2.3.2 界面创建
1. QWDlgManual类定义
完成功能后 的qwdlgmanual.h文件中QWDlgManual类的完整定义如下:
#ifndef QWDLGMANUAL_H
#define QWDLGMANUAL_H
#include <QWidget>
#include <QCheckBox>
#include <QRadioButton>
#include <QPlainTextEdit>
#include <QPushButton>
class QWDlgManual : public QWidget
{
Q_OBJECT
private:
QCheckBox *chkBoxUnder;
QCheckBox *chkBoxItatic;
QCheckBox *chkBoxBold;
QRadioButton *rBtnBlack;
QRadioButton *rBtnRed;
QRadioButton *rBtnBlue;
QPlainTextEdit *txtEdit;
QPushButton *btnOK;
QPushButton *btnCancel;
QPushButton *btnClose;
void iniUI(); // UI 创建与初始化
void iniSignalSlots(); // 初始化信号与槽的链接
private slots:
void on_chkBoxUnder(bool checked); // Underline的槽函数
void on_chkBoxItalic(bool checked); // Italic的槽函数
void on_chkBoxBold(bool checked); // Bold的槽函数
void setTextFontColor(); // 设置字体颜色
public:
QWDlgManual(QWidget *parent = nullptr);
~QWDlgManual();
};
#endif // QWDLGMANUAL_H
在QWDlgManual类的private部分,声明了界面上的各个组件的指针变量,这些界面组件都需要在QWDlgManual类的构造函数里创建并在窗体上布局。
在private部分自定义了两个函数,iniUI()用来创建所有界面组件,并完成布局和属性设置,iniSignalSlots()用来完成所有的信号与槽函数的关联。
在private slots部分声明了4个槽函数,分别是3个CheckBox的响应槽函数,以及3个颜色设置的RadioButton的共同响应槽函数。
这几个槽函数在访问界面组件时,无需使用ui指针,而是直接访问QWDlgManual类里定义的界面组件的成员变量即可。
2. 界面组件的创建与布局
iniUI()函数实现界面组件的创建与布局,以及属性设置。
void QWDlgManual::iniUI()
{ // 创建 Underline, Italic, Bold 3个CheckBox, 并水平布局
chkBoxBold = new QCheckBox(tr("Bold"));
chkBoxUnder = new QCheckBox(tr("Underline"));
chkBoxItatic = new QCheckBox(tr("Italic"));
QHBoxLayout *HLay1 = new QHBoxLayout;
HLay1->addWidget(chkBoxBold);
HLay1->addWidget(chkBoxUnder);
HLay1->addWidget(chkBoxItatic);
// 创建 Black, Red, Blue 3个RadioButton, 并水平布局
rBtnRed = new QRadioButton(tr("Red"));
rBtnBlue = new QRadioButton(tr("Blue"));
rBtnBlack = new QRadioButton(tr("Black"));
rBtnBlack->setChecked(true);
QHBoxLayout *HLay2 = new QHBoxLayout;
HLay2->addWidget(rBtnRed);
HLay2->addWidget(rBtnBlue);
HLay2->addWidget(rBtnBlack);
// 创建 确定,取消,退出3个PushButton, 并水平布局
btnOK = new QPushButton(tr("确定"));
btnCancel = new QPushButton(tr("取消"));
btnClose = new QPushButton(tr("退出"));
QHBoxLayout *HLay3 = new QHBoxLayout;
HLay3->addStretch();
HLay3->addWidget(btnOK);
HLay3->addWidget(btnCancel);
HLay3->addStretch();
HLay3->addWidget(btnClose);
// 创建 文本框,并设置初始字体
txtEdit = new QPlainTextEdit;
txtEdit->setPlainText("Hello WorldrnIt is my demo");
QFont font = txtEdit->font(); // 获取字体
font.setPointSize(20); // 修改字体大小
txtEdit->setFont(font); // 设置字体
// 创建 垂直布局,并设置为主布局
QVBoxLayout *VLay = new QVBoxLayout;
VLay->addLayout(HLay1); // 添加字体类型组
VLay->addLayout(HLay2); // 添加字体颜色组
VLay->addWidget(txtEdit); // 添加PlainTextEdit
VLay->addLayout(HLay3); // 添加按键组
setLayout(VLay); // 设置为窗体的主布局
}
2.3.3 信号与槽的关联
函数iniSignalSlots()初始化所有的信号与槽的关联。
void QWDlgManual::iniSignalSlots()
{
// 三个颜色QRadioButton的clicked()信号与setTextFontColor()槽函数关联
connect(rBtnRed, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
connect(rBtnBlue, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
connect(rBtnBlack, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
// 三个字体设置的QCheckBox的clicked(bool)信号与相应的槽函数关联
connect(chkBoxBold, SIGNAL(clicked(bool)), this, SLOT(on_chkBoxBold(bool)));
connect(chkBoxUnder, SIGNAL(clicked(bool)), this, SLOT(on_chkBoxUnder(bool)));
connect(chkBoxItatic, SIGNAL(clicked(bool)), this, SLOT(on_chkBoxItalic(bool)));
// 三个按钮的信号与窗体的槽函数关联
connect(btnOK, SIGNAL(clicked()), this, SLOT(accept()));
connect(btnCancel, SIGNAL(clicked()), this, SLOT(reject()));
connect(btnClose, SIGNAL(clicked()), this, SLOT(close()));
}
2.4 混合方式UI设计
2.4.2 创建项目并添加资源文件
在Qt项目中,图标可以存储在资源文件里,为此先创建一个资源文件。在Qt Creator里单击“File"->"New File or Project…"菜单项,在新建文件与项目对话框里选择”Qt Resource File“,然后按照向导的指引设置资源文件的文件名,并添加到当前项目里。
资源文件最主要的一个功能就是存储图标和图片文件,以便在程序里使用。在资源文件里首先建一个前缀(Prefix),前缀就类似于是资源的分组。
2.4.3 设计Action
QAction是一个非常有用的类,在界面设计时创建Action,并编写其trigger()信号的槽函数。使用设计的Action可以创建菜单项、工具栏按钮,还可以设置为QToolButton按钮的关联Action。点击这些由Action创建的菜单项、按钮就是执行Action的槽函数。
2.4.4 设计菜单和工具栏
工具栏上按钮的显示方式由多种,只需设置工具栏的toolButtonStyle属性,这是Qt::ToolButtonStyle枚举类型,缺省的是Qt::ToolButtonIconOnly,即只显示按钮的图标。还可以设置为:
- Qt::ToolButtonTextBesideIcon——文字显示在按钮旁边;
- Qt::ToolButtonTextOnly——只显示文字;
- Qt::ToolButtonTextUnderIcon——文字显示在按钮下方。
2.4.6 Action的功能实现
3. Action的enabled和checked属性的更新
QTextEdit的所有信号,有两个可以利用的信号。
- copyAvailable(bool)信号在有内容可以被复制时发射,并且传递了一个布尔参数,可以利用此信号来改变actCut,actCopy的enabled属性。
- selectionChanged()信号在选择的文字发生变化时发射,利用此信号,可以读取当前文字的格式,从而更新粗体、斜体和下划线3种字体设置Action的checked属性。
2.4.8 为应用程序设置图标
如果需要为应用设置一个自己的图标,其操作很简单,只需两步。
-
将一个图标文件(必须是”.ico“后缀的图标文件)复制到项目源程序目录下。
-
在项目配置文件里用RC_ICONS设置图标文件名,添加下面一行代码。
RC_ICONS = AppIcon.ico
其中,”AppIcon.ico“就是复制到项目源程序目录下的图标文件名称。
第3章 Qt类库概述
3.1 Qt核心特点
3.1.2 元对象系统
Qt的元对象系统(Meta-Object System)提供了对象之间通信的信号与槽机制、运行时类型信息和动态属性系统。
元对象系统由以下三个基础组成。
- QObject类是所有使用元对象系统的类的基类。
- 在一个类的private部分声明Q_OBJECT宏,使得类可以使用元对象的特性,如动态属性、信号与槽。
- MOC(元对象编译器)为每个QObject的子类提供必要的代码来实现元对象系统的特性。
构建项目时,MOC工具读取C++源文件,当它发现类的定义里有Q_OBJECT宏时,它就会为这个类生成另外一个包含有元对象支持代码的C++源文件,这个生成的源文件连同类的实现文件一起被编译和连接。
除了信号与槽机制外,元对象还提供如下一些功能。
- QObject::metaObject()函数返回类关联的元对象,元对象类QMetaObject包含了访问元对象的一些接口函数,例如QMetaObjct::className()函数可在运行时返回类的名称字符串。
- QMetaObjct::newInstance()函数创建类的一个新的实例。
- QObject::inherits(const char *className)函数判断一个对象实例是否是名称为className的类或QObject的子类的实例。
- QObject::tr()和QObject::trUtf8()函数可翻译字符串,用于多语言界面设计。
- QObject::setProperty()和QObject::property()函数用于通过属性名称动态设置和获取属性值。
对于QObject及其子类,还可以使用qobject_cast()函数进行动态投射(dynamic cast)。
使用动态投射,使得程序可以在运行时对不同的对象做不同的处理。
3.1.3 属性系统
1. 属性定义
Qt提供了一个Q_PROPERTY()宏可以定义属性,它也是基于元对象系统实现的。
在QObject的子类中,用宏Q_PROPERTY()定义属性,其使用格式如下:
Q_PROPERTY(type name
(READ getFunction [WRITE setFunction] |
MEMBER memberName [(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[CONSTANT]
[FINAL])
Q_PROPERTY宏定义一个返回值类型为type,名称为name的属性。属性的类型可以是QVariant支持的任何类型,也可以用户自定义类型。
Q_PROPERTY宏定义属性的一些主要关键字的意义如下。
- READ指定一个读取属性值的函数,没有MEMBER关键字时必须设置READ。
- WRITE指定一个设定属性值的函数,只读属性没有WRITE设置。
- MEMBER指定一个成员变量与属性关联,成为可读可写的属性,无需再设置READ和WRITE。
- RESET是可选的,用于指定一个设置属性缺省值的函数。
- NOTIFY是可选的,用于设置一个信号,当属性值变化时发射此信号。
- DESIGNABLE表示属性是否在Qt Designer里可见,缺省为true。
- CONSTANT表示属性值是一个常数,对于一个对象实例,READ指定的函数返回值是常数,但是每个实例的返回值可以不一样。具有CONSTANT关键字的属性不能有WRITE和NOTIFY关键字。
- FINAL表示所定义的属性不能被子类重载。
2. 属性的使用
不管是否用READ和WRITE定义了接口函数,只要知道属性名称,就可以通过QObject::property()读取属性值,并通过QObject::setProperty()设置属性值。
3. 动态属性
QObject::setProperty()函数可以在运行时为类定义一个新的属性,称之为动态属性。动态属性是针对类的实例定义的。
动态属性可以使用QObject::property()查询,就如在类定义里用Q_PROPERTY宏定义的属性一样。
4. 类的附加信息
属性系统还有一个宏Q_CLASSINFO(),可以为类的元对象定义“名称——值”信息。
用Q_CLSSINFO()宏定义附加类信息后,可以通过元对象的一些函数获取类的附加信息,如classInfo(int)获取某个附加信息,函数原型定义如下:
QMetaClassInfo QMetaObject::classInfo(int index) const
返回值是QMetaClassInfo类型,有name()和value()两个函数,可获得类附加信息的名称和值。
3.1.4 信号与槽
1. connect()函数的不同参数形式
QObject::connect()函数有多重参数形式,一种参数形式的函数原型是:
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
使用这种参数形式的connect()进行信号与槽函数的连接时,一般句法如下:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
另外一种参数形式的connect()函数的原型是:
QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
对于具有默认参数的信号与槽(即信号名称是唯一的,没有参数不同而同名的两个信号),可以使用这种函数指针形式进行关联,如:
connect(lineEdit, &QLineEdit::textChanged, this, &widget::on_textChanged);
QLineEdit只有一个信号textChanged(QString),在自定义窗体类Widget里定义一个槽函数on_textChanged(QString),就可以用上面的语句将此信号与槽关联起来,无需出现函数参数。这在信号的参数比较多时更简便一些。
而对于具有不同参数的同名信号就不能采用函数指针的方式进行信号与槽的关联。
不管是哪种参数形式的connect()函数,最后都有一个参数Qt::ConnectionType type,缺省值为Qt::AutoConnection。枚举类型Qt::ConnectionType表示了信号与槽之间的关联方式,有以下几种取值。
- Qt::AutoConnction(缺省值):如果信号的接收者与发射者在同一个线程,就使用Qt::DirectConnection方式;否则使用Qt::QueuedConnection方式,在信号发射时自动确定关联方式。
- Qt::DirectConnection:信号被发射时槽函数立即执行,槽函数与信号在同一个线程。
- Qt::QueuedConnection:在事件循环回到接收者线程后立即执行槽函数,槽函数与信号在不同的线程。
- QT::BlockingQueuedConnection:与Qt::QueuedConnection相似,只是信号线程会堵塞直到槽函数执行完毕。当信号与槽函数在同一个线程时绝对不能使用这种方式,否则会造成死锁。
2. 使用sender()获得信号发射者
在槽函数里,使用QObject::sender()可以获取信号发射者的指针。如果知道信号发射者的类型,可以将指针投射为确定的类型,然后使用这个确定类的接口函数。
3. 自定义信号及其使用
在自己设计的类里也可以自定义信号,信号就是在类定义里声明的一个函数,但是这个函数无需实现,只需发射(emit)。
例如,在下面的自定义类QPerson的signals部分定义一个信号ageChanged(int)。
class QPerson : public QObject
{
Q_OBJECT
private:
int m_age = 10;
public:
void incAge();
signals:
void ageChanged(int value);
};
信号函数必须是无返回值的函数,但是可以有输入参数。信号函数无需实现,只需在某些条件下发射信号。例如,在incAge()函数中发射信号,其代码如下。
void QPerson::incAge()
{
m_age++;
emit ageChanged(m_age); // 发送信号
}
在incAge()函数里,当私有变量m_age变化后,发射信号ageChanged(int)。至于是否有与此信号相关联的槽函数,信号发射者并不管。
3.1.5 元对象特性测试实例
1. QPerson类的定义
在项目创建后,创建一个类QPerson。qperson.h文件中的类定义如下所示:
#ifndef QPERSON_H
#define QPERSON_H
#include <QObject>
class QPerson : public QObject
{
Q_OBJECT
Q_CLASSINFO("author", "Wang")
Q_CLASSINFO("company", "UPC")
Q_CLASSINFO("version", "1.0.0")
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(int score MEMBER m_score)
private:
int m_age = 10;
QString m_name;
int m_score = 79;
public:
QPerson(QString fName, QObject *parent = nullptr);
int age();
void setAge(int value);
void incAge();
signals:
void ageChanged(int value);
public slots:
};
#endif // QPERSON_H
QPerson是QObject的子类,在类定义部分使用了宏Q_OBJECT,这样QPerson就获得了元对象系统支持,能使用信号与槽、属性等功能。
下面是QPerson类的实现代码:
#include "qperson.h"
QPerson::QPerson(QString fName, QObject *parent) : QObject(parent)
{ // 构造函数
m_name = fName;
}
int QPerson::age() // 返回age
{
return m_age;
}
void QPerson::setAge(int value) // 设置age
{
m_age = value;
emit ageChanged(m_age); // 发送信号
}
void QPerson::incAge()
{
m_age++;
emit ageChanged(m_age); // 发送信号
}
setAge(int)函数用于设置年龄,代码里设置年龄后发射信号ageChanged()。
incAge()是一个单独的接口函数,与属性无关,但是也发射信号ageChanged()。
2. 元对象特性的使用
主窗口是基于QWidget的可视化设计的类QmyWidget,其定义如下:
#ifndef QMYWIDGET_H
#define QMYWIDGET_H
#include <QWidget>
#include <qperson.h>
QT_BEGIN_NAMESPACE
namespace Ui { class QmyWidget; }
QT_END_NAMESPACE
class QmyWidget : public QWidget
{
Q_OBJECT
private:
QPerson *boy;
QPerson *girl;
public:
QmyWidget(QWidget *parent = nullptr);
~QmyWidget();
private:
Ui::QmyWidget *ui;
private slots:
// 自定义槽函数
void on_ageChanged(int value);
void on_spin_valueChanged(int arg1);
// 界面按钮的槽函数
void on_btnClear_clicked();
void on_btnClassInfo_clicked();
void on_btnBoyInc_clicked();
void on_btnGirlInc_clicked();
};
#endif // QMYWIDGET_H
QmyWidget类里定义了两个QPerson类型的指针变量,定义了两个自定义槽函数。构造函数的代码如下:
QmyWidget::QmyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::QmyWidget)
{
ui->setupUi(this);
boy = new QPerson("王小明");
boy->setProperty("score", 95);
boy->setProperty("age", 10);
boy->setProperty("sex", "Boy"); // 动态属性
connect(boy, &QPerson::ageChanged, this, &QmyWidget::on_ageChanged);
girl = new QPerson("张晓丽");
girl->setProperty("score", 81);
girl->setProperty("age", 20);
girl->setProperty("sex", "Girl"); // 动态属性
connect(girl, &QPerson::ageChanged, this, &QmyWidget::on_ageChanged);
ui->spinBoy->setProperty("isBoy", true); // 动态属性
ui->spinGirl->setProperty("isBoy", false);
connect(ui->spinBoy, SIGNAL(valueChanged(int)), this, SLOT(on_spin_valueChanged(int)));
connect(ui->spinGirl, SIGNAL(valueChanged(int)), this, SLOT(on_spin_valueChanged(int)));
}
创建QPerson类型对象boy后,使用setProperty()函数设置了score、age属性的值,这两个属性是QPerson类里定义的。还设置了一个属性sex的值。
boy->setProperty("sex", "Boy");
sex属性在QPerson类里没有定义,所以这个属性是个动态属性。
自定义槽函数on_ageChanged()用于响应QPerson的ageChanged()信号,其实现代码如下:
void QmyWidget::on_ageChanged(int value)
{
// 响应QPerson的ageChanged()信号
Q_UNUSED(value);
QPerson *aPerson = qobject_cast<QPerson *>(sender()); // 类型投射
QString hisName = aPerson->property("name").toString(); // 姓名
QString hisSex = aPerson->property("sex").toString(); // 动态属性
int hisAge = aPerson->age(); // 通过接口函数获取年龄
// int hisAge = aPerson->property("age").toInt(); // 通过属性获得年龄
ui->textEdit->appendPlainText(hisName + "," + hisSex + QString::asprintf(",年龄=%d",hisAge));
}
这里使用了QObject::sender()函数获取信号发射者。因为信号发射者是QPerson类型对象boy或girl,所以可以用qobject_cast()将发射者投射为具体的类型:
QPerson *aPerson = qobject_cast<QPerson *>(sender());
这样得到信号发射者QPerson类型的对象指针aPerson,它指向boy或girl。
使用aPerson指针,通过property()函数获取name属性的值,也可以获取动态属性sex的值。
spinBox的valueChanged(int)信号与槽函数on_spin_valueChanged(int)关联,槽函数代码如下:
void QmyWidget::on_spin_valueChanged(int arg1)
{ // 响应界面上spinBox的valueChanged(int)信号
Q_UNUSED(arg1);
QSpinBox *spinBox = qobject_cast<QSpinBox *>(sender());
if (spinBox->property("isBoy").toBool())
boy->setAge(spinBox->value());
else
girl->setAge(spinBox->value());
}
这里也使用了信号发射者的类型投射,投射为QSpinBox类型指针spinBox,然后根据spinBox的动态属性isBoy的值,确定调用boy或girl的setAge()函数。
界面上“类的元对象信息”按钮的响应代码如下:
void QmyWidget::on_btnClassInfo_clicked()
{ // "类的元对象信息"按钮
const QMetaObject *meta = boy->metaObject();
ui->textEdit->clear();
ui->textEdit->appendPlainText("==元对象信息==n");
ui->textEdit->appendPlainText(QString("类名称: %1n").arg(meta->className()));
ui->textEdit->appendPlainText("property");
for (int i = meta->propertyOffset(); i < meta->propertyCount(); i++)
{
QMetaProperty prop = meta->property(i);
const char* propName = prop.name();
QString propValue = boy->property(propName).toString();
ui->textEdit->appendPlainText(QString("属性名称=%1,属性值=%2").arg(propName).arg(propValue));
}
ui->textEdit->appendPlainText("");
ui->textEdit->appendPlainText("classInfo");
for (int i = meta->classInfoOffset(); i < meta->classInfoCount(); i++)
{
QMetaClassInfo classInfo = meta->classInfo(i);
ui->textEdit->appendPlainText(QString("Name=%1; Value=%2").arg(classInfo.name()).arg(classInfo.value()));
}
}
代码里通过boy->metaObject()获得boy的元对象。元对象类QMetaObject封装了访问类的元对象的各种接口函数,例如,QMetaObject::className()返回类的名称。
QMetaObject用于属性操作的函数有以下几种。
- propertyOffset():返回类的第一个属性的序号,第一个属性的序号不一定是0。
- propertyCount():返回类的属性个数
- QMetaProperty property(int index):返回序号为index的属性的对象,返回值是QMetaProperty类型,它封装了对属性的更多特征查询功能,以及属性值的读写功能。
QMetaClassInfo类封装了classInfo的访问接口函数,只有name()和value()两个接口函数。
3.2 Qt全局定义
头文件包含了Qt类库的一些全局定义,包括基本数据类型、函数和宏,一般的Qt类的头文件都会包含该文件,所以不用显示包含这个头文件也可以使用其中的定义。
3.2.1 数据类型定义
中定义的数据类型见表3-1。
Qt数据类型 | 等效定义 | 字节数 |
---|---|---|
qint8 | signed char | 1 |
qint16 | signed short | 2 |
qint32 | signed int | 4 |
qint64 | long long int | 8 |
qlonglong | long long int | 8 |
quint8 | unsigned char | 1 |
quint16 | unsigned short | 2 |
quint32 | unsigned int | 4 |
quint64 | unsigned long long int | 8 |
qulonglong | unsigned long long int | 8 |
uchar | unsigned char | 1 |
ushort | unsigned short | 2 |
uint | unsigned int | 4 |
ulong | unsigned long | 8 |
qreal | double | 8 |
qfloat16 | 2 | |
qfloat16是Qt5.9.0中新增的一个类,用于表示16位的浮点数,要使用qfloat16,需要包含头文件。
3.2.2 函数
表3-2是中常用的全局函数定义,列出了函数的输入和输出参数(若存在double和float两种参数版本,只列出double类型参数的版本)。
函数 | 功能 |
---|---|
T qAbs(const T &value) | 返回变量value的绝对值 |
const T&qBound(const T &min, const T &value, const T &max) | 返回value限定在min或max范围之内的数 |
bool qFuzzyCompare(double p1, double p2) | 若p1和p2近似相等,返回true |
bool qFuzzyIsNull(double d) | 如果参数d约等于0,返回true |
double qInf() | 返回无穷大的数 |
bool qIsFinite(double d) | 若d是一个有限的数,返回true |
bool qIsInf(double d) | 若d是一个无限大的数,返回true |
bool qIsNaN(double d) | 若d不是一个数,返回true |
const T &qMax(const T &value1, cont T &value2) | 返回value1和value2中较大的值 |
const T &qMin(const T &value1, const T &value2) | 返回value1和value2中较小的值 |
qint64 qRound64(double value) | 将value近似为最接近的qint64整数 |
int qRound(double value) | 将value近似为最接近的int整数 |
int qrand() | 标准C++中rand()函数的线程安全型版本,返回0至RAND_MAX之间的伪随机数 |
void qsrand(uint seed) | 标准C++中srand()函数的线程安全型版本,使用种子seed对伪随机数序列初始化 |
还有一些基础的数学运算函数在头文件中定义。
3.2.3 宏定义
头文件定义了很多宏,以下一些是比较常用的。
-
QT_VERSION
这个宏展开为数值形式0xMMNNPP(MM = major, NN = minor, PP = patch)表示Qt编译器版本。这个宏常用于条件编译设置,根据Qt版本不同,编译不同的代码段。
-
QT_VERSION_CHECK
这个宏展开为Qt版本号的一个整数表示。
-
QT_VERSION_STR
这个宏展开为Qt版本号的字符串。
-
Q_BYTE_ORDER、Q_BIG_ENDIAN和Q_LITTLE_ENDIAN
Q_BYTE_ORDER表示系统内存中数据的字节序,Q_BIG_ENDIAN表示大端字节序,Q_LITTLE_ENDIAN表示小端字节序。在需要判断系统字节序时会用到。
-
Q_DECL_IMPORT和Q_DECL_EXPORT
在使用或设计共享库时,用于导入或导出库的内容。
-
Q_DECL_OVERRIDE
在类定义中,用于重载一个虚函数。
使用Q_DECL_OVERRIDE宏后,如果重载的虚函数没有进行任何重载操作,编译器将会报错。
-
Q_DECL_FINAL
这个宏将一个虚函数定义为最终级别,不能再被重载,或定义一个类不能再被继承。
-
Q_UNUSED(name)
这个宏用于在函数中定义不在函数体里使用的参数。
-
foreach(variable, container)
foreach用于容器类的遍历。
-
forever
forever用于构造一个无限循环。
-
qDebug(const char *message, …)
在debugger窗体显示信息,如果编译器设置了Qt_NO_DEBUG_OUTPUT,则不作任何输出。
类似的宏还有qWarning、qCritical、qFatal、qInfo等,也是用于在debugger窗体显示信息。
3.3 容器类
3.3.1 容器类概述
Qt的容器类分为顺序容器 和关联容器。
容器迭代类用于遍历容器里的数据项,有Java类型的迭代类和STL类型的迭代类。Java类型的迭代类易于使用,提供高级功能,而STL类型的迭代类效率更高一些。
Qt还提供了foreach宏用于遍历容器内的所有数据项。
3.3.2 顺序容器类
Qt的顺序容器类有QList、QLinkedList、QVector、QStack和QQueue。
1. QList
QList是最常用的容器类,虽然它是以数组列表(array-list)的形式实现的,但是在其前或后添加数据非常快,QList以下标索引的方式对数据项进行访问。
QList用于添加、插入、替换、移动、删除数据项的函数有:insert()、replace()、removeAt()、move()、swap()、append()、prepend()、removeFirst()和removeLast()等。
QList提供下标索引方式访问数据项,如同数组一样,也提供at()函数,例如:
QList<QString> list;
list << "one" << "two" << "three";
QString str1 = list[1]; // str1 == "two"
QString str0 = list.at(0); // str0 = "one"
QList的isEmpty()函数在数据项为空时返回true,size()函数返回数据项的个数。
2. QLinkedList
QLinkedList是链式列表(linked-list),数据项不是用连续的内存存储的,它基于迭代器访问数据项,并且插入和删除数据项的操作时间相同。
除了不提供基于下标索引的数据项访问外,QLinkedList的其他接口函数与QList基本相同。
3. QVector
QVector提供动态数组的功能,以下标索引访问数据。
QVector的函数接口与QList几乎完全相同,QVector的性能比QList更高,因为QVector的数据项是连续存储的。
4. QStack
QStack是提供类似于堆栈的后入先出(LIFO)操作的容器类,push()和pop()是主要的接口函数。例如:
QStack<int> stack;
stack.push(10);
stack.push(20);
stack.push(30);
while(!stack.isEmpty())
cout << stack.pop() << endl;
5. QQueue
QQueue是提供类似于队列先入先出(FIFO)操作的容器类。enqueue()和dequeue()是主要的操作函数。例如:
QQueue<int> queue;
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
while (!queue.isEmpty())
cout << queue.dequeue() << endl;
3.3.3 关联容器类
Qt还提供关联容器类QMap、QMultiMap、QHash、QMultiHash和QSet。
QMultiMap和QMultiHash支持一个键关联多个值,QHash和QMultiHash类使用散列(Hash)函数进行查找,查找速度更快。
1. QSet
QSet是基于散列表的集合模板类,它存储数据的顺序是不定的,查找值的速度非常快。QSet内部就是用QHash实现的。
定义QSet容器和输入数据的实例代码如下:
QSet<QString> set;
set << "dog" << "cat" << "tiger"
测试一个值是否包含于这个集合,用contains()函数,示例如下:
if (!set.contains("cat"))
...
2. QMap
QMap<Key, T>提供一个字典(关联数组),一个键映射到一个值。QMap存储数据是按照键的顺序,如果不在乎存储顺序,使用QHash会更快。
定义QMap<QString, int>类型变量和赋值的示例代码如下:
QMap<QString, int> map;
map["one"] = 1;
map["two"] = 2;
map["three"] = 3;
也可以使用insert()函数赋值,或remove()移除一个键值对,示例如下:
map.insert("four", 4);
map.remove("two");
要查找一个值,使用运算符"[]"或value()函数,示例如下:
int num1 = map["one"];
int num2 = map.value("two");
如果在映射表中没有找到指定的键,会返回一个缺省构造值(default-constructed values),例如,如果值的类型是字符串,会返回一个空的字符串。
在使用value()函数查找键值时,还可以指定一个缺省的返回值,示例如下:
timeout = map.value("TIMEOUT", 30);
这表示如果在map里找到键“TIMEOUT”,就返回关联的值,否则返回值为30。
3. QMultiMap
QMultiMap是QMap的子类,是用于处理多值映射的便利类。
多值映射就是一个键可以对应多个值。QMap正常情况下不允许多值映射,除非使用QMap::insertMulti()添加键值对。
QMultiMap是QMap的子类,所以QMap的大多数函数在QMultiMap都是可用的,但是有几个特殊的,QMultiMap::insert()等效于QMap::insertMulti(),QMultiMap::replace()等效于QMap::insert()。
QMultiMap使用示例如下:
QMultiMap<QString, int> map1, map2, map3;
map1.insert("plenty", 100);
map1.insert("plenty", 2000); // map1.size() == 2
map2.insert("plenty", 5000); // map2.size() == 1
map3 = map1 + map2; // map3.size() == 3
QMultiMap不提供“[]”操作符,使用value()函数访问最新插入的键的单个值。如果要获取一个键对应的所有值,使用value()函数,返回值是QList类型。
QList<int> values = map.values("plenty");
for (int i = 0; i < values.size(); ++i)
cout << values.at(i) << endl;
4. QHash
QHash是基于散列表来实现字典功能的模板类,QHash<Key, T>存储的键值对具有非常快的查找速度。
QHash与QMap的功能和用法相似,区别在于以下几点:
- QHash比QMap的查找速度快;
- 在QMap上遍历时,数据项是按照键排序的,而QHash的数据项是任意顺序的;
- QMap的键必须提供“<“运算符,QHash的键必须提供”==“运算符和一个名称为qHash()的全局散列函数。
5. QMultiHash
QMultiHash是QHash的子类,是用于处理多值映射的便利类,其用法与QMultiMap类似。
3.4 容器类的迭代
3.4.1 Java类型迭代器
1. Java类型迭代器总表
对于每个容器类,有两个Java类型迭代器:一个用于只读操作,一个用于读写操作,各个Java类型的容器类见表3-3。
容器类 | 只读迭代器 | 读写迭代器 |
---|---|---|
QList, QQueue | QListIterator | QMutableListIterator |
QLinkedList | QLinkedListIterator | QMutableLinkedIterator |
QVector, QStack | QVectorIterator | QMutableVectorIterator |
QSet | QSetIterator | QMutableSetIterator |
QMap<Key, T>, QMultiMap<Key, T> | QMapIterator<Key, T> | QMutableMapIterator |
QHash<Key, T>, QMultiHash<Key, T> | QHashIterator<Key, T> | QMutableHashIterator |
QMap和QHash等关联容器类的迭代器用法相同,QList和QLinkedList、QSet等容器类的用法相同。
2. 顺序容器类的迭代器的使用
下面是遍历访问一个QList容器的所有数据项的典型代码。
QList<QString> list;
list << "A" << "B" << "C" << "D";
QListIterator<QString> i(list);
while (i.hasNext())
qDebug() << i.next()
QListIterator用于移动指针和读取数据的函数见表3-4。
函数名 | 功能 |
---|---|
void toFront() | 迭代器移动到列表的最前面(第一个数据之前) |
void toBack() | 迭代器移动到列表的最后面(最后一个数据项之后) |
bool hasNext() | 如果迭代器不是位于列表最后位置,返回true |
const T & next() | 返回下一个数据项,并且迭代器后移一个位置 |
const T & peekNext() | 返回下一个数据项,但是不移动迭代器位置 |
bool hasPrevious() | 如果迭代器不是位于列表的最前面,返回true |
const T & previous() | 返回前一个数据项,并且迭代器前移一个位置 |
const T & peekPrevious() | 返回前一个数据项,但是不移动迭代器指针 |
QListIterator是只读访问容器内数据项的迭代器,若要在遍历过程中对容器的数据进行修改,需要使用QMutableListIterator。例如下面的示例代码为删除容器中数据为奇数的项。
QList<int> list;
list << 1 << 2 << 3 << 4 << 5;
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() % 2 != 0)
i.remove();
}
remove()函数移除next()函数刚刚跳过的一个数据项,不会使迭代器失效。
setValue()函数可以修改刚刚跳过去的数据项的值。
3. 关联容器类的迭代器的使用
对于关联容器类QMap<Key, T>,使用QMapIterator和QMutableMapIterator迭代器类,它们具有表3-4所示的所有函数,主要是增加了key()和value()函数用于获取刚刚跳过的数据项的键和值。
例如,下面的代码将删除键(城市名称)里以”City“结尾的数据项。
QMap<QString, QString> map;
map.insert("Pairs", "France");
map.insert("New York", "USA");
map.insert("Mexico", "USA");
map.insert("Moscow", "Russia");
...
QMutableMapIterator<QString, QString> i(map);
while(i.hasNext()) {
if (i.next().key().endsWith("City"))
i.remove();
}
如果是在多值容器里遍历,可以用findNext()或findPrevious()查找下一个或上一个值,如下面的代码将删除上一示例代码中map里值为”USA“的所有数据项。
QMutableMapIterator<QString, QString> i(map);
while (i.findNext("USA"))
i.remove();
3.4.2 STL类型迭代器
1. STL类型迭代器总表
STL迭代器与Qt和STL的原生算法兼容,并且进行了速度优化。具体类型见表3-5。
对于每一个容器类,都有两个STL类型迭代器:一个用于只读访问,一个用于读写访问。
容器类 | 只读迭代器 | 读写迭代器 |
---|---|---|
QList, QQueue | QList::const_iterator | QList::iterator |
QLinkedList | QLinkedList::const_iterator | QLinkedList::iterator |
QVector, QStack | QVector::const_iterator | QVector::iterator |
QSet | QSet::const_iterator | QSet::iterator |
QMap<Key, T>, QMultiMap<Key, T> | QMap<Key, T>::const_iterator | QMap<Key, T>::iterator |
QHash<Key, T>, QMultiHash<Key, T> | QHash<Key, T>::const_iterator | QHash<Key, T>::iterator |
STL类型的迭代器是数组的指针,所以”++“运算符使迭代器指向下一个数据项,”*“运算符返回数据项类容。
begin()函数使迭代器指向容器的第一个数据项,end()函数使迭代器指向一个虚拟的表示结尾的数据项,end()表示的数据是无效的,一般用作循环结束条件。
2. 顺序容器类的迭代器的用法
下面的示例代码将QList list里的数据项逐项输出。
QList<QString> list;
list << "A" << "B" << "C" << "D";
QList<QString>::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i)
qDebug() << *i;
constBegin()和constEnd()是用于只读迭代器的,表示起始和结束位置。
若使用反向读写迭代器,并将上面示例代码中list的数据项都改为小写,代码如下:
QList<QString>::reverse_iterator i;
for (i = list.rbegin(); i != list.rend(); i++)
*i = i -> toLower();
3. 关联容器类的迭代器的用法
对于关联容器类QMap和QHash,迭代器的”*“操作符返回数据项的值。如果想返回键,使用key()函数。对应的,用value()函数返回一个项的值。
例如,下面的代码将QMap<int, int> map中所有项的键和值输出。
QMap<int, int> map;
...
QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i)
qDebug() << i.key() << ':' << i.value();
Qt API包含很多返回值为QList或QStringList的函数,要遍历这些返回的容器,必须先复制。由于Qt使用了隐式共享,这样的复制并无多大开销。例如下面的代码是正确的。
const QList<int> sizes = splitter -> sizes();
QList<int>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
...
提示:隐式共享(Implicit Sharing)是对象的管理方法。一个对象被隐式共享,只是传递该对象的一个指针给使用者,而不实际复制对象数据,只有在使用者修改数据时,才实质复制共享对象给使用者。
对于STL类型的迭代器,隐式共享还涉及另外一个问题,即当有一个迭代器在操作一个容器变量时,不要去复制这个容器变量。
3.4.3 foreach关键字
如果只是想遍历容器中所有的项,可以使用foreach关键字。foreach是头文件中定义的一个宏。使用foreach的句法是:
foreach (variable, container)
使用foreach的代码比使用迭代器更简洁。例如,使用foreach遍历一个QLinkedList的示例代码如下:
QLinkedList<QString> list;
...
QString str;
foreach (str, list)
qDebug << str;
用于迭代的变量也可以在foreach语句里定义,foreach语句也可以使用花括号,可以使用break退出迭代,示例代码如下:
QLinkedList<QString> list;
...
foreach (const QString &str, list) {
if (str.isEmpty())
break;
qDebug() << str;
}
对于QMap和QHash,foreach会自动访问“键——值”对里的值,所以无需调用value()。如果需要访问键则可以调用keys(),示例代码如下:
QMap<QString, int> map;
...
foreach (const QString &str, map.keys())
qDebug() << str << ":" << map.value(str);
对于多值映射,可以使用两重foreach语句,示例代码如下:
QMultiMap<QString, int> map;
...
foreach (const QString &str, map.uniqueKeys()) {
foreach (int i, map.values(str))
qDebug() << str << ":" << i;
}
注意:foreach关键字遍历一个容器变量是创建了容器的一个副本,所以不能修改原来容器变量的数据项。
3.5 Qt类库的模块
Qt类库里大量的类根据功能分为各种模块,这些模块又分为几大类。
- Qt基本模块(Qt Essentials):提供了Qt在所有平台上的基本功能。
- Qt附加模块(Qt Add-Ons):实现一些特定功能的提供附加价值的模块。
- 增值模块(Value-Add Modules):单独发布的提供额外价值的模块或工具。
- 技术预览模块(Technology Preview Modules):一些处于开发阶段,但是可以作为技术预览使用的模块。
- Qt工具(Qt Tools):帮助应用程序开发的一些工具。
3.5.1 Qt基本模块
Qt基本模块是Qt在所有平台上的基本功能。这些具体的基本模块见表3-6。
模块 | 描述 |
---|---|
Qt Core | 其他模块都用到的核心非图形类 |
Qt GUI | 设计GUI界面的基础类,包括OpenGL |
Qt Multimedia | 音频、视频、摄像头和广播功能的类 |
Qt Multimedia Widgets | 实现多媒体功能的界面组件类 |
Qt Network | 使网络编程更简单和轻便的类 |
Qt QML | 用于QML和JavaScript语言的类 |
Qt Quick | 用于构建具有定制用户界面的动态应用程序的声明框架 |
Qt Quick Controls | 创建桌面样式用户界面,基于Qt Quick的用户界面控件 |
Qt Quick Dialogs | 用于Qt Quick的系统对话框类型 |
Qt Quick Layouts | 用于Qt Quick 2界面元素的布局项 |
Qt SQL | 使用SQL用于数据库操作的类 |
Qt Test | 用于应用程序和库进行单元测试的类 |
Qt Widgets | 用于构建GUI界面的C++图形组件类 |
Qt Core模块是Qt类库的核心,所有其他模块都依赖于此模块,Qt Core模块是自动被加入项目的。
Qt GUI模块提供了用于开发GUI应用程序的必要的类,使用qmake构建应用程序时,Qt GUI模块是自动被加入项目的。如果项目中不使用GUI功能,则需要在项目配置文件中加入如下的一行:
QT -= gui
其他的模块一般不会被自动加入到项目,如果需要在项目中使用某个模块,则可以在项目配置中添加此模块。
需要在项目中使用Qt SQL模块,就在项目配置文件中加入如下的语句:
QT += sql
3.5.5 Qt工具
Qt工具在所有支持的平台上都可以使用,用于帮助应用程序的开发和设计。
工具 | 描述 |
---|---|
Qt Designer | 用于扩展Qt Designer的类 |
Qt Help | 在应用程序中集成在线文档的类,实现类似于Qt Assistant的功能 |
Qt UI Tools | 操作Qt Designer生成的窗体的类 |
第4章 常用界面设计组件
4.1 字符串与输入输出
4.1.1 字符串与数值之间的转换
QLabel用于显示字符串,QLineEdit用于显示和输入字符串。这两个类都有如下的两个函数用于读取和设置显示文字。
QString text() const
void setText(const QString &)
QString类是Qt程序里经常使用的类,用于处理字符串。
1. 普通数值与字符串之间的转换
QString类从字符串转换为整数的函数有:
int toInt(bool *ok = Q_NULLPTR, int base = 10) const
int toLong(bool *ok = Q_NULLPTR, int base = 10) const
int toShort(bool *ok = Q_NULLPTR, int base = 10) const
int toUInt(bool *ok = Q_NULLPTR, int base = 10) const
int toULong(bool *ok = Q_NULLPTR, int base = 10) const
这些函数如果不设置参数,缺省表示从十进制表示的字符串转换为整数;若指定整数基参数,还可以直接将二进制、十六进制字符串转换为整数。
QString将字符串转换为浮点数的函数有:
double toDouble(bool *ok = Q_NULLPTR) const
double toFloat(bool *ok = Q_NULLPTR) const
得到计算结果变量total,再将此数值显示在编辑框editTotal中。由于计算结果是浮点数,希望显示两位小数,下面4行语句都可以实现这个功能。
str = QString::number(total, 'f', 2);
str = QString::asprintf("%.2f", total);
str = str.setNum(total, 'f', 2);
str = str.sprintf("%.2f", total);
可以使用QString的静态函数number()和asprintf(),也可以使用其公共函数setNum()和sprintf()。
2. 进制转换
将一个整数转换为不同进制的字符串,可以使用QString的函数setNum()或静态函数number(),它们的函数原型是:
QString &setNum(int n, int base = 10);
QString number(int n, int base = 10);
其中n是待转换的整数,base是使用的进制,缺省为十进制,也可以指定为十六进制和二进制。
4.1.2 QString的常用功能
QString存储字符串采用的是Unicode码,每一个字符是一个16位的QChar。
QString常用函数:
-
append()和prepend()
append()在字符串的后面添加字符串,prepend()在字符串的前面添加字符串,如:
QString str = "卖", str2 = "拐"; QString str3 = str1; str1.append(str2); // str1 = "卖拐" str3.prepend(str2); // str3 = "拐卖"
-
toUpper()和toLower()
toUpper()将字符串内的字母全部转换为大写形式,toLower()将字母全部转换位小写形式,如:
QString str1 = "Hello, World", str2; str2 = str1.toUpper(); // str2 = "HELLO, WORLD" str2 = str1.toLower(); // str2 = "hello, world"
-
count()、size()和length()
count()、size()和length()都返回字符串的字符个数,这3个函数是相同的,但是要注意,字符串中如果有汉字,一个汉字算一个字符。
QString str1 = "NI 好"; N = str1.count(); // N = 3 N = str1.size(); // N = 3 N = str1.length(); // N = 3
-
trimmed()和simplified()
trimmed()去掉字符串首尾的空格,simplified()不仅去掉首尾的空格,中间连续的空格也用一个空格替换。
QString str1 = " Are you ok? ", str2; str2 = str1.trimmed(); // str1 = "Are you ok?" str2 = str1.simplified(); // str1 = "Are you ok?"
-
indexOf()和lastIndexOf()
indexOf()函数的原型为:
int indexOf(const QString &str, int from = 0, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
其功能是在自身字符串内查找参数字符串str出现的位置,参数from是开始查找的位置,Qt::CaseSensitivity cs参数指定是否区分大小写。
lastIndexOf()函数则是查找某个字符串最后出现的位置。
QString str1 = "G:Qt5BookQT5.9Studyqw.cpp"; N = str1.indexOf("5.9"); // N = 13 N = str1.lastIndexOf("\"); // N = 21
-
isNull()和isEmpty()
两个函数都判读字符串是否为空,但是稍有差别。如果一个空字符串,只有"