可视化UI设计
本节介绍创建一个稍微复杂一点的示例项目samp2_2,示例运行时界面如图 2-7 所示。这个 示例的设计过程包含GUI应用程序设计的完整过程,其中涉及一些关键技术。
- UI 可视化设计的布局管理问题。使用布局管理可以使界面上的各个组件合理地分布,并且可随窗口大小变化而自动调整大小和位置。
- 在可视化设计UI时,设置组件的内建信号与窗体上其他组件的公有槽函数关联。
- 在可视化设计UI时,为组件的内建信号创建槽函数,并且使 其与组件的信号自动关联。
- 创建资源文件,将图片文件导入资源文件,为界面上的组件 设置图标。
窗口界面可视化设计¶
1.创建项目和资源文件¶
通过New File or Project 对话框创建一个GUI项目,选择qmake构建系统, 窗口基类选择QDialog,勾选Generate form复选框。
几个按钮具有对应的图表,在Qt项目中,图表存储在资源文件里。我们先在项目的根目录下创建一个文件夹images,将需要用到的图标文件复制到此文件夹里,然后创建一个资源文件。
打开New File or Project 对话框,选择 Qt 分组里的Qt Resource File,然后按照向导的指引设 置资源文件的文件名,并将其添加到当前项目里。
本项目创建的资源文件名为 res.qrc,项目管理目录树里会自动创建一个Resources文件组。在资源文件名节点的快捷菜单中选择Open in Editor 菜单项可以打开资源文件编辑器。
资源文件的主要功能是存储图标和图片文件,以便在程序里使用。在资源文件里首先需要建一个前缀(prefix),例如 icons,前缀表示资源的分组。窗口右侧下方的功能区点击 Add Prefix 按钮就可以创建一个前缀,然后点击Add Files按钮添加图标文件。如果所选的图标文件不在本项目的子目录下,会提示需复制文件到项目的子目录下。所以,最好提前将图标文件放在项目的子目录下。
2. 窗口界面设计¶
在如上图所示的项目管理目录树上,双击文件dialog.ui就可以打开Qt Designer对窗口界面进行可视化设计。
在UI可视化设计时,对于需要在程序中访问的界面组件,例如各个按钮、需要读取输入的编 辑框、需要显示结果的标签等,应该修改其对象名称,以便在程序里区分。对于不需要在程序里 访问的界面组件,例如用于组件分组的分组框、布局等,无须修改其对象名称,由Qt Designer自动命名即可。
下图是已经设计好的窗体,我们在界面设计中使用了布局管理功能。窗体中间的文本框是一个QPlainTextEdit组件,在组件面板的Input Widgets分组里有这种组件。
对象名称 | 类名称 | 属性设置 | 说明 |
---|---|---|---|
plainTextEdit | QPlainTextEdit | text="Qt 6 C++开发指南" font.PointSize=20 | 简单的多行文本编辑器。设计时双击组件可设置其显示的文字 |
chkBoxUnder | QCheckBox | text="Underline" | 设置文字带有下划线 |
chkBoxItalic | QCheckBox | text="Italic" | 设置文字为斜体 |
chkBoxBold | QCheckBox | text="Bold" | 设置文字为粗体 |
radioBlack | QRadioButton | text="Black" checked=true | 设置文字颜色为黑色 |
radioRed | QRadioButton | text="Red" | 设置文字颜色为红色 |
radioBlue | QRadioButton | text="Blue" | 设置文字颜色为蓝色 |
btnClear | QPushButton | text="清空" | 清除编辑器中的文字 |
btnOK | QPushButton | text="确定" | 返回确定,并关闭窗口 |
btnExit | QPushButton | text="退出" | 退出程序 |
Dialog | Dialog | windowTitle="信号与槽的使用" | 窗体,其对象与窗口类同名 |
要为一个QPushButton按钮设置图标,只需在属性编辑器里修改其icon属性。在icon属性值输入框的右端下拉列表框里选择Choose Resource,就可以从项目的资源文件中选择图标。
对于界面组件的属性设置,需要注意:
- 对象名称是窗口上的组件的实例名称,界面上的每个组件需要有一个唯一的对象名称,程序里访问界面组件时都是通过其对象名称进行的,自动生成的槽函数名称里也有对象名称。所以,组件的对象名称设置好之后一般不要改动。若需要修改对象名称,涉及的代码需要相应改动。
- 窗体的对象名称会影响窗口UI类的名称。
dialog.ui
被UIC编译后生成文件ui_dialog.h
,窗体的对象名称与文件ui_dialog.h
中定义的窗口UI类有关。一般不在Qt Designer里修改 窗体的对象名称,除非是要重命名一个窗口,那么需要对窗口相关的4个文件都重命名。 - 设置窗体的font属性后,界面上其他组件的默认字体就是窗体的字体,无须再单独设置, 除非要为某个组件设置单独的字体,例如组件plainTextEdit的字体大小被单独设置为20。
- 组件的属性都有默认值,一个组件的某个属性被修改后,属性编辑器里的属性名称会以粗体显示。如果要恢复属性的默认值,点击属性值右端的还原按钮即可。
界面组件布局管理¶
1.界面组件的层次关系¶
为了将界面上的各个组件的分布设计得更加美观,我们经常使用一些容器类组件。例如,将3 个复选框(QCheckBox类)放置在一个分组框(QGroupBox类)里,这个分组框就是这3个复选框的容器,移动这个分组框就会同时移动其中的3个复选框。
2. 布局管理¶
组件面板里有Layouts和Spacers 两个分组,在上方的工具栏里有布局管理的按钮。
布局组件 | 说明 |
---|---|
![]() |
垂直方向布局,组件自动在垂直方向上分布 |
![]() |
水平方向布局,组件自动在水平方向上分布 |
![]() |
网格布局,网格布局大小改变时,每个网格的大小都改变 |
![]() |
表单布局,与网格布局类似,但是只有最右侧的一列网格会改变大小,适用于只有两列组件时的布局 |
![]() |
用于水平间隔的非可视组件 |
![]() |
用于垂直间隔的非可视组件 |
Qt Designer 有一个工具栏,用于使界面进入不同的设计状态,或进行布局设计,工具栏上各按钮的说明如下
按钮及快捷键 | 说明 |
---|---|
![]() |
界面设计进入编辑状态,就是正常的设计状态 |
![]() |
进入信号与槽的可视化设计状态 |
![]() |
进入伙伴关系编辑状态,可以设置一个标签与一个组件构成伙伴关系 |
![]() |
进入 Tab 顺序编辑状态,Tab 顺序是在键盘上按 Tab 键时,输入焦点在界面各组件之间移动的顺序 |
![]() |
将窗体上所选组件水平布局 |
![]() |
将窗体上所选组件垂直布局 |
![]() |
将窗体上所选组件用一个分割条 (QSplitter 类) 进行水平分割布局 |
![]() |
将窗体上所选组件用一个分割条进行垂直分割布局 |
![]() |
将窗体上所选组件按表单方式布局 |
![]() |
将窗体上所选组件按网格方式布局 |
![]() |
解除窗体上所选组件的布局,也就是打散现有的布局 |
![]() |
自动调整所选组件的大小 |
使用工具栏上的布局按钮时,只需在窗体上选中需要设计布局的组件,然后点击某个布局按钮。在窗体上选择组件的同时按住Ctrl键可以实现组件多选,选择某个容器组件相当于选择了其内部的所有组件。 |
在Qt Designer 里可视化设计布局时,要善于利用水平/垂直分隔器(spacer),善于设置组件的最大、最小宽度/高度属性,善于设置布局的 layoutStretch、layoutSpacing 等属性来达到较好的布局效果。
Tips
UI可视化设计和布局就是放置组件并合理地布局,设计过程如同拼图,设计经验多了自然就熟悉了。
3.伙伴关系与Tab顺序¶
伙伴关系是指界面上一个标签和一个具有输入焦点的组件相关联。
点击Qt Designer 工具栏上的Edit Buddies 按钮可以进入伙伴关系编辑状 态,例如设计一个窗体时,进入伙伴关系编辑状态之后界面如下图做所示。选中一个标签,按住鼠标左键,然后将其拖向一个编辑框(QLineEdit类),就可建立标签和编辑框的伙伴关系。
利用伙伴关系,可以在程序运行时通过快捷键将输入焦点切换到某个组件上。例如,在图左所示的界面上,设置“姓 名”标签的text属性为“姓 名(&N)”,其中符号“&”用来指定快捷字符,界面上并不显示“&”。这里指定快捷字符为N,那么在程序运行时,用户按下Alt+N快捷键,输入焦点就会快速切换到“姓 名”标签关联的编辑框内。
点击Qt Designer 工具栏上的Edit Tab Order 按钮进入Tab 顺序编辑状态,如图右所示。Tab 顺序是指在程序运行时,按下键盘上的Tab键时输入焦点的移动顺序。一个好的UI,在按Tab键时焦点应该以合理的顺序在界面组件上移动。
进入Tab 顺序编辑状态后,界面上会显示具有Tab顺序的组件的Tab顺序编号,依次按希望的顺序点击组件,就可以重排Tab顺序。没有输入焦点的组件是没有Tab 顺序的,例如标签。
信号与槽简介与使用¶
信号(signal)是在特定情况下被发射的通知,例如QPushButton较常见的信号就是点击鼠标时发射的 clicked()信号。GUI 程序设计的主要工作就是对界面上各组件的信号进行响应,只需要知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。
槽(slot)是对信号进行响应的函数。槽就是函数,所以也称为槽函数。槽函数与一般的C++函数一样,可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与信号关联,当信号被发射时,关联的槽函数被自动运行。
信号与槽关联是用函数QObject::connect()
实现的,使用函数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()里要指明各参数的类型,但不用指明参数名称。
- 多个信号可以连接同一个槽函数。
-
一个信号可以连接另一个信号
connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL(refreshInfo(int)));
这样,当发射一个信号时,也会发射另一个信号,以实现某些特殊的功能。
-
严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参 数。如果参数不匹配,会出现编译错误或运行错误。
- 在使用信号与槽的类中,必须在类的定义中插入宏
Q_OBJECT
- 当一个信号被发射时,与其关联的槽函数通常被立即运行,就像正常调用函数一样。只 有当信号关联的所有槽函数运行完毕后,才运行发射信号处后面的代码。
函数connect()
有多种参数形式,有一种常用的形式是不使用SIGNAL
和SLOT
宏,而是使用函数指针。例如:
QObject::connect(btnClose, &QPushButton::clicked, Widget, qOverload<>(&QWidget::close));
1. 信号与槽编辑器的使用¶
Qt 的界面组件都是从QWidget
继承而来的,都支持信号与槽的功能。每个类都有一些内建的信号和槽函数,例如QPushButton
类的常用信号是clicked()
,在按钮被点击时此信号被发射。QDialog
是对话框类,它有以下几个公有的槽函数:
accept()
,功能是关闭对话框,表示肯定的选择,如对话框上的“确定”按钮。reject()
,功能是关闭对话框,表示否定的选择,如对话框上的“取消”按钮。close()
,功能是关闭对话框。
虽然这些槽函数都可以关闭对话框,但是表示的对话框的返回值不同。我们可以在Action编辑器里设置组件的内建信号与其他组件的公有槽函数关联。
2. 为组件的信号生成槽函数原型和框架¶
下面介绍为窗体上设置字体的3个复选框的信号编写槽函数。选 中窗体上的复选框chkBoxUnder,在右键快捷菜单中点击菜单项Go to slot,会出现图所示的对话框。
这个对话框显示了QCheckBox类 的所有可用信号。
在Go to slot 对话框中选择clicked(bool)
信号,然后点击OK按钮,在Dialog
类的private slots
部分会自动增加槽函数声明,函数名是根据发射信号的对象名和信号名称自动命名的。
void on_chkBoxUnder_clicked(bool checked);
同时,在文件dialog.cpp 中会自动生成这个函数的代码框架, 在此函数中添加如下的代码,实现对文本框文字下划线的控制。
void Dialog::on_checkBox_clicked(bool checked)
{//Underline 复选框
QFont font=ui->plainTextEdit->font();
font.setUnderline(checked);
ui->plainTextEdit->setFont(font);
}
以同样的方法为Italic 和 Bold 两个复选框设计槽函数,为“清空”按钮的clicked()信号生成槽函数:
void Dialog::on_checkBox_2_clicked(bool checked)
{//Italic 复选框
QFont font=ui->plainTextEdit->font();
font.setItalic(checked);
ui->plainTextEdit->setFont(font);
}
void Dialog::on_checkBox_3_clicked(bool checked)
{//Bold 复选框
QFont font=ui->plainTextEdit->font();
font.setBold(checked);
ui->plainTextEdit->setFont(font);
}
void Dialog::on_Button_Delete_clicked()
{//“清空”按钮
ui->plainTextEdit->clear();
}
但是,查看Dialog类的构造函数,它这里没有发现设置组件信号与槽函数连接的connect()函数的语句,那么这些关联是如何实现的呢?
打开编译生成的文件ui_dialog.h
,查看函数setupUi()
的代码,它的最后有如下几行语句:
Object::connect(btnOK, &QPushButton::clicked, Dialog, qOverload<>(&QDialog::accept));
QObject::connect(btnExit, &QPushButton::clicked, Dialog, qOverload<>(&QDialog::reject));
QMetaObject::connectSlotsByName(Dialog);
前两行connect()
函数的语句就是"1. 信号与槽编辑器的使用"中两个信号与槽连接的实现代码。最后一行语句的功能是搜索Dialog界面上的所有组件,将名称匹配的信号和槽关联起来,假设槽函数的名称是:
void on_<object name>_<signal name>(<signal parameters>);
那么,函数connectSlotsByName()就会将此信号和槽函数关联起来,例如,对于void on_chkBoxUnder_clicked(bool checked);
相当于运行了下面的一条语句:
connect(chkBoxUnder, SIGNAL(clicked(bool)), this, SLOT(on_chkBoxUnder_clicked(bool)));
3. 使用自定义槽函数¶
设置文字颜色的3个单选按钮是互斥选择的,即一次只有一个单选按钮被选中,虽然也可以采用Go to slot 对话框为它们的clicked()信号生成槽函数,但是这样就需要生成3个槽函数。我们换一种方式,即设计一个自定义槽函数,将3 个单选按钮的clicked()信号都与这个自定义槽函数关联。为此,在Dialog类的private slots 部分增加如下的槽函数定义。
void do_setFontColor(); //设置文字颜色的自定义槽函数
将鼠标光标移动到这个函数名上面,点击鼠标右键,在弹出的快捷菜单中选择Refactor→Add Definition in dialog.cpp,就可以在文件 dialog.cpp 中自动生成该函数的代码框架。为该函数编写代 码,具体如下。
void Dialog::do_setFontColor()
{//自定义槽函数,设置文字颜色
QPalette plet=ui->plainTextEdit->palette();
if(ui->radioBlue->isChecked())
plet.setColor(QPalette::Text, Qt::blue);
else if (ui->radioRed->isChecked())
plet.setColor(QPalette::Text,Qt::red);
else if (ui->radioBlack->isChecked())
plet.setColor(QPalette::Text,Qt::black);
else
plet.setColor(QPalette::Text,Qt::black);
ui->plainTextEdit->setPalette(plet);
}
Tips
我们把所有不是通过Go to slot对话框生成的槽函数都称为自定义槽函数。作为习惯,自定义槽函数的函数名都以“do_”作为前缀。
由于这个槽函数是自定义的,因此它不会与界面上3个单选按钮的clicked()信号自动关联。 我们需要在Dialog类的构造函数中手动进行关联,代码如下:
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::Dialog)
{
ui->setupUi(this);
connect(ui->radioBlack,SIGNAL(clicked()),this,SLOT(do_setFontColor()));
connect(ui->radioRed, SIGNAL(clicked()),this,SLOT(do_setFontColor()));
connect(ui->radioBlue, SIGNAL(clicked()),this,SLOT(do_setFontColor()));
}
为应用程序设置图标¶
(1)将一个后缀为“.ico”的图标文件复制到项目根目录下,假设图标文件名是editor.ico。
(2)在项目配置文件(.pro文件)里用RC_ICONS设置图标文件名,即添加如下一行语句:
RC_ICONS = editor.ico
重新构建项目,生成的可执行文件以及窗口的图标就会换成设置的图标。
路径名问题
父级文件夹最好是英文,如果是中文,对于上述代码,可能会出现以下乱码,从而出现编译错误:
windres: can't open icon file `D:\Tech\椤圭洰姹囨€籠qt\demo2\icon.ico': Invalid argument
error: [Makefile.Debug:77: debug/demo2_resource_res.o] Error 1
Qt 项目构建过程基本原理¶
一个使用qmake构建系统的Qt项目,除了有项目配置文件,还有用Qt C++编写的头文件和源程序文件,以及窗口UI文件和资源文件。在构建项目的过程中,这3类文件会被分别编译为标准C++语言的程序文件,然后被标准C++编译器(如GNU C++编译器或MSVC编译器)编译成可执行文件或库。
总结一下,Qt项目的构建过程可以下述基本过程来描述。
graph LR
A[Qt C++头文件] -->|MOC编译| B[标准C++程序]
C[窗口UI文件] -->|UIC编译| B
D[资源文件] -->|RCC编译| B
B -->|标准C++编译器| E[可执行文件]
1. 元对象系统和MOC¶
Qt对标准C++语言进行了扩展,引入了元对象系统(meta-object system,MOS),所有从QObject 继承的类都可以利用元对象系统提供的功能。元对象系统支持属性、信号与槽、动态类型转换等特性。
我们在Qt Creator 中编写程序时使用的C++语言,实际上是经过Qt扩展的C++语言。例如, 在Dialog类的定义中插入一个宏Q_OBJECT,这是使用信号与槽机制的类必须插入的一个宏。Dialog类中的private slots 部分用于定义私有槽,这是标准C++语言中没有的特性。
Qt 提供了元对象编译器(MOC)。在构建项目时,项目中的头文件会先被MOC预编译。例如, 以Release 模式构建项目后,文件夹release里的文件如图所示。
其中,moc_dialog.cpp 是MOC读取文件dialog.h的内容后生成的一个元对象代码文件,文件moc_predefs.h里是一些宏定 义。结合moc_dialog.cpp 和moc_predefs.h,dialog.cpp 和 ui_dialog.h 就可以被标准 C++编译器编译。
2. UI文件和UIC¶
在构建项目时,可视化设计的窗口UI文件会被用户界面编译器(UIC)转换为一个C++源程序文件。例如文件widget.ui会被转换为文件ui_widget.h。
UIC 编译生成的文件与UI文件在同一个文件夹里,例如ui_widget.h在项目的根目 录下。文件ui_widget.h 之后会被标准C++编译器编译。
3. 资源文件和RCC¶
Qt 项目中的资源文件(.qrc文件)会被资源编译器(RCC)转换为C++程序文件。例如项目中的资源文件是images.qrc,经过RCC编译后生成的文件是qrc_images.cpp(见上图)。 文件qrc_images.cpp 之后会被标准C++编译器编译。
4. 标准C++编译器¶
标准C++编译器就是开发套件中的编译器,例如其在MinGW套件中是GNU C++编译器,在 MSVC套件中是Microsoft Visual C++编译器。
使用MOC、UIC和RCC编译各原始文件的过程称为预编译过程,预编译之后生成的是标准 C++语言的程序文件,它们被标准C++编译器编译和连接,最终生成可执行文件。