Skip to content

可视化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按钮添加图标文件。如果所选的图标文件不在本项目的子目录下,会提示需复制文件到项目的子目录下。所以,最好提前将图标文件放在项目的子目录下。

image-20250203185648755

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="信号与槽的使用" 窗体,其对象与窗口类同名

image-20250203193857342

要为一个QPushButton按钮设置图标,只需在属性编辑器里修改其icon属性。在icon属性值输入框的右端下拉列表框里选择Choose Resource,就可以从项目的资源文件中选择图标。

对于界面组件的属性设置,需要注意:

  1. 对象名称是窗口上的组件的实例名称,界面上的每个组件需要有一个唯一的对象名称,程序里访问界面组件时都是通过其对象名称进行的,自动生成的槽函数名称里也有对象名称。所以,组件的对象名称设置好之后一般不要改动。若需要修改对象名称,涉及的代码需要相应改动。
  2. 窗体的对象名称会影响窗口UI类的名称。dialog.ui被UIC编译后生成文件ui_dialog.h,窗体的对象名称与文件ui_dialog.h中定义的窗口UI类有关。一般不在Qt Designer里修改 窗体的对象名称,除非是要重命名一个窗口,那么需要对窗口相关的4个文件都重命名。
  3. 设置窗体的font属性后,界面上其他组件的默认字体就是窗体的字体,无须再单独设置, 除非要为某个组件设置单独的字体,例如组件plainTextEdit的字体大小被单独设置为20。
  4. 组件的属性都有默认值,一个组件的某个属性被修改后,属性编辑器里的属性名称会以粗体显示。如果要恢复属性的默认值,点击属性值右端的还原按钮即可。

界面组件布局管理

image-20250203193734246

1.界面组件的层次关系

为了将界面上的各个组件的分布设计得更加美观,我们经常使用一些容器类组件。例如,将3 个复选框(QCheckBox类)放置在一个分组框(QGroupBox类)里,这个分组框就是这3个复选框的容器,移动这个分组框就会同时移动其中的3个复选框。

2. 布局管理

组件面板里有Layouts和Spacers 两个分组,在上方的工具栏里有布局管理的按钮。

布局组件 说明
image-20250203192556327Vertical Layout 垂直方向布局,组件自动在垂直方向上分布
image-20250203192648455Horizontal Layout 水平方向布局,组件自动在水平方向上分布
image-20250203192703511Grid Layout 网格布局,网格布局大小改变时,每个网格的大小都改变
image-20250203192749652Form Layout 表单布局,与网格布局类似,但是只有最右侧的一列网格会改变大小,适用于只有两列组件时的布局
image-20250203192811718Horizontal Spacer 用于水平间隔的非可视组件
image-20250203192842084Vertical Spacer 用于垂直间隔的非可视组件

Qt Designer 有一个工具栏,用于使界面进入不同的设计状态,或进行布局设计,工具栏上各按钮的说明如下

按钮及快捷键 说明
image-20250203192908222Edit Widget (F3) 界面设计进入编辑状态,就是正常的设计状态
image-20250203192921101Edit Signals/Slots (F4) 进入信号与槽的可视化设计状态
image-20250203193028645Edit Buddies 进入伙伴关系编辑状态,可以设置一个标签与一个组件构成伙伴关系
image-20250203193111021Edit Tab Order 进入 Tab 顺序编辑状态,Tab 顺序是在键盘上按 Tab 键时,输入焦点在界面各组件之间移动的顺序
image-20250203192648455Lay Out Horizontally (Ctrl+H) 将窗体上所选组件水平布局
image-20250203192556327Lay Out Vertically (Ctrl+L) 将窗体上所选组件垂直布局
image-20250203193225837Lay Out Horizontally in Splitter 将窗体上所选组件用一个分割条 (QSplitter 类) 进行水平分割布局
image-20250203193245248Lay Out Vertically in Splitter 将窗体上所选组件用一个分割条进行垂直分割布局
image-20250203192749652Lay Out in a Form Layout 将窗体上所选组件按表单方式布局
image-20250203192703511Lay Out in a Grid 将窗体上所选组件按网格方式布局
image-20250203193307144Break Layout 解除窗体上所选组件的布局,也就是打散现有的布局
image-20250203193337108Adjust Size (Ctrl+J) 自动调整所选组件的大小
使用工具栏上的布局按钮时,只需在窗体上选中需要设计布局的组件,然后点击某个布局按钮。在窗体上选择组件的同时按住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 顺序的,例如标签。

image-20250203194329965

信号与槽简介与使用

信号(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()是槽函数,需要带有括号,有参数时还需要指明各参数类型。

SIGNALSLOT是Qt的宏,分别用于指明信号和槽函数,并将它们的参数转换为相应的字符串。关于信号与槽的使用,有以下一些规则需要注意:

  • 一个信号可以连接多个槽函数,当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次运行。 当信号和槽函数带有参数时,在函数connect()里要指明各参数的类型,但不用指明参数名称。
  • 多个信号可以连接同一个槽函数。
  • 一个信号可以连接另一个信号

    connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL(refreshInfo(int)));
    

    这样,当发射一个信号时,也会发射另一个信号,以实现某些特殊的功能。

  • 严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参 数。如果参数不匹配,会出现编译错误或运行错误。

  • 在使用信号与槽的类中,必须在类的定义中插入宏Q_OBJECT
  • 当一个信号被发射时,与其关联的槽函数通常被立即运行,就像正常调用函数一样。只 有当信号关联的所有槽函数运行完毕后,才运行发射信号处后面的代码。

函数connect()有多种参数形式,有一种常用的形式是不使用SIGNALSLOT宏,而是使用函数指针。例如:

QObject::connect(btnClose, &QPushButton::clicked, Widget, qOverload<>(&QWidget::close));

1. 信号与槽编辑器的使用

Qt 的界面组件都是从QWidget继承而来的,都支持信号与槽的功能。每个类都有一些内建的信号和槽函数,例如QPushButton类的常用信号是clicked(),在按钮被点击时此信号被发射。QDialog是对话框类,它有以下几个公有的槽函数:

  • accept(),功能是关闭对话框,表示肯定的选择,如对话框上的“确定”按钮。
  • reject(),功能是关闭对话框,表示否定的选择,如对话框上的“取消”按钮。
  • close(),功能是关闭对话框。

虽然这些槽函数都可以关闭对话框,但是表示的对话框的返回值不同。我们可以在Action编辑器里设置组件的内建信号与其他组件的公有槽函数关联。

image-20250203200852547

2. 为组件的信号生成槽函数原型和框架

下面介绍为窗体上设置字体的3个复选框的信号编写槽函数。选 中窗体上的复选框chkBoxUnder,在右键快捷菜单中点击菜单项Go to slot,会出现图所示的对话框。

这个对话框显示了QCheckBox类 的所有可用信号。

image-20250203201513067

在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++编译器编译。

image-20250203214644955

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++编译器编译和连接,最终生成可执行文件。