Qt开发实战2-GUI开发
UI文件设计与运行机制
ui文件是可视化窗体的定义文件,查看其源代码可发现实际上是通过xml格式描述的控件和布局。当我们在Qt creator中双击ui文件将会自动启动Qt Designer进行ui设计。以下图为例介绍一下Qt Designer。
我将Qt Designer分为了6个部分:
- 组件面板区域:在这里可以通过选择适合的控件,拖拽到界面中完成简单的界面设计。最上面的输入框提供了筛选器的功能。另外此区域还隐藏了一个临时保存组合控件的功能,我们可以从界面上选择几个组合控件拖拽到此处以进行临时保存以便后续用到相同的控件。
- 设计区域:ui界面设计的主要工作区,进行常用控件的选择、摆放、叠放次序控制等等,我们进行ui设计后的显示区域。
- Action编辑区域:进行Action(动作)的可视化编辑,Action可以提供如图标、菜单、快捷键、状态栏、浮动帮助等信息设置。
- 信号槽编辑区域:将信号与槽进行可视化连接设计。
- 对象浏览区域:可以快速浏览当前设计区域的ui界面中所有元素的层级关系,帮助我们更方便的选择指定的控件,查看其属性;反过来从对象浏览区域选择控件后也会在设计区域选中显示。
- 属性显示与编辑区域:选择了指定的控件后,可以在此区域进行该控件的属性查看及编辑。
介绍完Qt Designer后,以HelloWorld工程说明ui文件的运行机制,HelloWorld是GUI程序,主要有以下工程文件:
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
我们首先进行该工程的QMake、然后进行构建。
编译HelloWorld工程成功后,我们发现输出中有着这样一句话“C:\Qt\Qt5.13.2\5.13.2\msvc2015_64\bin\uic.exe …\HelloWorld\mainwindow.ui -o ui_mainwindow.h”,那么这是干什么用的呢?要解释这个首先需要介绍下uic这个exe是做什么用的。uic全称User Interface Compiler即用户界面编译,uic.exe即qt内置的用于进行用户界面编译的命令行工具,通过这个工具我们可以将xml语言描述的ui文件转换为c++看得懂的.h文件。uic的语法如下:
uic [options] <uifile>
可选参数options有如下几种:
其中的[options]中最常用的参数就是我们上述编译过程中用到的"-o"了,即通过uic工具自动读取项目中的ui文件从而生成c++输出文件。
我们再来看mainwindow.h中的内容:
namespace Ui { class MainWindow; }
这句话声明了一个名叫Ui的命名空间,包含一个类MainWindow。这里这个MainWindow来源就是刚刚介绍的uic生成的ui_mainwindow.h文件中定义的。引入该类的目的是便于该窗口的.h、.cpp访问界面组件文件。该声明可以理解为外部类型声明。
具体访问界面组件时是通过Ui::MainWIndow类的指针ui进行的,该指针指向的即是该窗口的可视化界面,可通过该指针管理界面及其组件。
Ui::MainWindow *ui;
我们再看看.cpp文件里面有什么:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
这里执行父类QMainWindow的构造函数,创建一个Ui::MainWindow类的对象ui。设个ui就是MainWindow的私有部分定义的指针ui。
接下来构造函数里执行了一句“ui->setupUi(this)”,让我们跟着函数的源码看看里面究竟做了什么?
通过阅读其源码,我们发现setupUi内部原来设置了控件的一些基本属性,并且在最后调用了retranslateUi(MainWindow)和QMetaObject::connectSlotsByName(MainWindow),前者是进行一些多语言翻译的相关工作,在此不便再赘述,感兴趣的可以跟着源码再做进一步的研究;后者则是进行了信号槽的连接。
MainWindow::~MainWindow()
{
delete ui;
}
最后在类的析构函数中调用了"delete ui;"完成了ui指针的回收。
可以看到Qt的设计思想是很巧妙很严谨的,我们在学习过程中一定要多多发现,深入挖掘,研究其背后的机理。
样式表
作为一个跨平台的UI开发框架,Qt提供了强大而灵活的界面外观设计机制。 Qt样式表是一个可以自定义部件外观的十分强大的机制。Qt样式表的概念、术语和语法都受到了HTML的层叠样式表(Cascading StyleSheets,CSS)的启发,在此基础上我们来了解下Qt中的样式表(QSS)的一些特点。
首先我要说的一点是,学习QSS最好的方法是通过Qt中内置的Qt助手搜索Qt Style Sheets Reference关键字,便会列出很多相关的知识,这也是我们掌握QSS最有效的方法。
为窗体或控件设计样式表主要有如下两种方式:
- 在设计模式下直接编辑样式表(静态)
- 在代码中为相应的组件添加样式表(动态)
下面分别说下两种方式的不同和优劣:
1、在设计模式下直接编辑样式表,主要特点是方便、快捷且编辑后可以直接在设计模式下看到添加样式表后的效果,容易上手。
不过,也正是因为这一特性,使其应用范围较窄。由于是在设计模式中进行了静态QSS样式表设计,所以在代码运行时不能对其进行直接控制,也无法自由定制其应用时机(这里大家可以结合我前面的介绍想一想qss样式表的应用时机是在何时,感兴趣的也可以自己查阅下相关资料),导致其不具备更多可操作的空间,不过如果只是简单的应用且在运行中无需改变则用这种方式还是比较适合的。
2、在代码中为相应的组件添加样式表,这种方式是我们实际开发中常用的方式。
在代码中设置分为为界面中所有同一类的控件进行设置,如:
setStyleSheet("QPushButton { color: blue }");
就会给该窗体中所有QPushButton设置为蓝色。
为指定控件进行样式表设置,如:
ui->pushButton->setStyleSheet("color: blue");
这种就只会给界面中的pushButton设置为蓝色。
而代码中进行样式表方式最大的特点当然就是灵活了,可以进行更多复杂的定制化编码,这种方式要求程序开发人员对QSS的掌握更深入。
常用控件介绍
本章节我按照控件类型分为了Button(按钮类)、Container(容器类)、Widgets(组件类)、Layout(布局)
Buttons:
常用的Button有:
- PushButton:普通带文字点击按钮,也是最常用的按钮控件。
- ToolButton:可以将文字隐藏,添加纯图片类型的点击按钮控件。
- RadioButon:可带分组类型的按钮,每个分组内只可以进行单选的按钮。
- CheckBox:可带分组类型的按钮,每个分组内可进行勾选或不勾选(即复选)的按钮。
常用的Container有:
- GroupBox:分组容器,将置于该容器内的几个控件划分为同一分组,常用于RadioButton等。
- ScrollArea:滚动区域,放置该容器后可以实现自动滚动以显示同一屏内无法完全显示下的内容。
- TabWidget:类似TabView,具有Tab标签页的可切换视图容器。
- StackedWidget:堆叠容器,和TabWidget类似,但不具备Tab标签,且默认是通过左右箭头来进行切换的,该切换箭头可隐藏,对用户来说其中视图的数量不是已知的。
常用的Widget有: - LineEdit:简单单行文本输入框
- TextEdit:可支持多行的文本输入框
- TimeEdit:时间编辑控件
- DateEdit:日期编辑控件
- Label:文本显示控件
- ProgressBar:进度条控件
Layout(布局):实际不是控件,这里只做简单介绍 - Vertical Layout:垂直布局,将父容器按照垂直方向进行默认平均分割。
- Horizontal Layout:水平布局,将父容器按照水平方向进行默认平均分割。
至于另外两种,本质上是以上两种的组合拓展再此不在详细介绍。
信号槽
信号和槽机制是 QT 的核心机制,要精通 QT 编程就必须对信号和槽有所了解。信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方。信号和槽是 QT 自行定义的一种通信机制,它独立于标准的 C/C++ 语言,因此要正确的处理信号和槽,必须借助一个称为 moc(Meta Object Compiler)的 QT 工具,该工具是一个 C++ 预处理程序,它为高层次的事件处理自动生成所需要的附加代码。信号槽从本质上来说是观察者模式的设计思想。
在我们所熟知的很多 GUI 工具包中,窗口小部件 (widget) 都有一个回调函数用于响应它们能触发的每个动作,这个回调函数通常是一个指向某个函数的指针。但是,在 QT 中信号和槽取代了这些凌乱的函数指针,使得我们编写这些通信程序更为简洁明了。 信号和槽能携带任意数量和任意类型的参数,他们是类型完全安全的,不会像回调函数那样产生 core dumps。
所有从 QObject 或其子类 ( 例如 Qwidget) 派生的类都能够包含信号和槽。当对象改变其状态时,信号就由该对象发射 (emit) 出去,这就是对象所要做的全部事情,它不知道另一端是谁在接收这个信号。这就是真正的信息封装,它确保对象被当作一个真正的软件组件来使用。槽用于接收信号,但它们是普通的对象成员函数。一个槽并不知道是否有任何信号与自己相连接。而且,对象并不了解具体的通信机制。
信号与槽之间是多对多的关系,即你可以将很多信号与单个的槽进行连接,也可以将单个的信号与很多的槽进行连接,甚至于将一个信号与另外一个信号相连接也是可能的。
信号
信号的意思就是,当发生某个动作或事件时,发射一个相关联的信号,通知关注该信号的某个或多个所有者。当信号对其客户或所有者发生的内部状态发生改变,信号被一个对象发射。只有定义过这个信号的类及其派生类能够发射这个信号。当一个信号被发射时,与其相关联的槽将被立刻执行,就象一个正常的函数调用一样。信号 - 槽机制完全独立于任何 GUI 事件循环。只有当所有的槽返回以后发射函数(emit)才返回。 如果存在多个槽与某个信号相关联,那么,当这个信号被发射时,这些槽将会一个接一个地执行,但是它们执行的顺序将会是随机的、不确定的,我们不能人为地指定哪个先执行、哪 个后执行。
信号的声明是在头文件中进行的,QT 的 signals 关键字指出进入了信号声明区,随后即可声明自己的信号。如下:
Signals:
void Signal1();
void Signal2(int x);
在上面的定义中,signals 是 QT 的关键字,而非 C/C++ 的。从形式上讲信号的声明与普通的C++ 函数是一样的,但是信号却没有函数体定义,另外,信号的返回类型都是 void,不要指望能从信号返回什么有用信息。
信号由 moc 自动产生,它们不应该在 .cpp 文件中实现。
槽
槽是普通的 C++ 成员函数,可以被正常调用,它们唯一的特殊性就是很多信号可以与其相关联。当与其关联的信号被发射时,这个槽就会被调用。槽可以有参数,但槽的参数不能有缺省值。
既然槽是普通的成员函数,因此与其它的函数一样,它们也有存取权限。槽的存取权限决定了谁能够与其相关联。同普通的 C++ 成员函数一样,槽函数也分为三种类型,即 public slots、private slots 和 protected slots。
槽的声明也是在头文件中进行的,如:
public slots:
void Slot1();
void Slot2(int x);
信号与槽的连接
通过调用 QObject 对象的 connect 函数来将某个对象的信号与另外一个对象的槽函数相关联,即信号槽的连接函数。当发射者发射信号时,接收者的槽函数将被调用。连接函数的签名如下:
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
这个函数的作用就是将发射者 sender 对象中的信号 signal 与接收者 receiver 中的 member 槽函数联系起来。当指定信号 signal 时必须使用 QT 的宏 SIGNAL(),当指定槽函数时必须使用宏 SLOT()。如果发射者与接收者属于同一个对象的话,那么在 connect 调用中接收者参数可以省略。
这里要讲一下最后一个参数,这个参数平时我们不容易注意到,因为它是一个带有默认缺省参数设置的参数,我们平时进行信号槽连接的时候如果不设置就会使用该缺省参数即Qt::AutoConnection。除了AutoConnection还有四种,下面依次介绍下:
Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。
一般我们在程序开发中会使用默认的连接方式,但当默认的连接方式不能满足我们的需求时我们要记得信号槽还具备第五个参数,换一种连接方式也许就能满足我们的需求。
当信号与槽没有必要继续保持关联时,我们可以使用 disconnect 函数来断开连接。其函数签名如下:
static bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *member);
应用disconnect的场景主要有如下三种:
- disconnect() 等同于disconnect(this, 0, 0, 0), 删除作为信号的this与任何槽的关联。当我们在某个对象中定义了一个或者多个信号,这些信号与另外若干个对象中的槽相关联,如果我们要切断这些关联的话,就可以利用这个方法,非常之简洁。
- disconnect(receiver) 等同于disconnect(this, 0, receiver, 0), 删除this与作为槽的receiver的关联,删除指定的某两个对象之间的连接关系。
- disconnect(SIGNAL(Signal1()))等同于disconnect(this, SIGNAL(Signal1()), 0, 0),删除this的Signal1()信号与其他对象间的连接,使用后this发出的Signal1()信号没有对应的槽函数进行响应。
元对象编译器(moc)
前面我们介绍了Qt的信号槽是通过moc从而得到正确处理的,与之对应的是Qt中moc.exe即元对象编译工具。在这里我们简单介绍下moc。
元对象编译器 moc(meta object compiler)对 C++ 文件中的类声明进行分析并产生用于初始化元对象的 C++ 代码,元对象包含全部信号和槽的名字以及指向这些函数的指针。
moc 读 C++ 源文件,如果发现有 Q_OBJECT 宏声明的类,它就会生成另外一个 C++ 源文件,这个新生成的文件中包含有该类的元对象代码。例如,假设我们有一个头文件 mysignal.h,在这个文件中包含有信号或槽的声明,那么在编译之前 moc 工具就会根据该文件自动生成一个名为 mysignal.moc.h 的 C++ 源文件并将其提交给编译器;类似地,对应于 mysignal.cpp 文件 moc 工具将自动生成一个名为 mysignal.moc.cpp 文件提交给编译器。
元对象代码是 signal/slot 机制所必须的。用 moc 产生的 C++ 源文件必须与类实现一起进行编译和连接,或者用 #include 语句将其包含到类的源文件中。moc 并不扩展 #include 或者 #define 宏定义 , 它只是简单的跳过所遇到的任何预处理指令。
样例:
class TsignalApp:public QMainWindow
{
Q_OBJECT
...
// 信号声明区
signals:
// 声明信号 Signal1()
void Signal1();
// 声明信号 Signal2(int)
void Signal2(int x);
// 槽声明区
public slots:
// 声明槽函数 Slot1()
void Slot1();
// 声明槽函数 Slot2(int)
void Slot2(int x);
}
...
//tsignal.cpp
...
TsignalApp::TsignalApp()
{
...
// 将信号 Signal1() 与槽 Slot1() 相关联
connect(this,SIGNAL(Signal1()),SLOT(Slot1()));
// 将信号 Signal2(int) 与槽 Slot2(int) 相关联
connect(this,SIGNAL(Signal2(int)),SLOT(Slot2(int)));
}
信号和槽函数的声明一般位于头文件中,同时在类声明的开始位置必须加上 Q_OBJECT 语句,这条语句是不可缺少的,它将告诉编译器在编译之前必须先应用 moc 工具进行扩展。关键字 signals 指出随后开始信号的声明,这里 signals 用的是复数形式而非单数,siganls 没有 public、private、protected 等属性,这点不同于 slots。另外,signals、slots 关键字是 QT 自己定义的,不是 C++ 中的关键字。
信号的声明类似于函数的声明而非变量的声明,左边要有类型,右边要有括号,如果要向槽中传递参数的话,在括号中指定每个形式参数的类型,当然,形式参数的个数可以多于一个。
关键字 slots 指出随后开始槽的声明,这里 slots 用的也是复数形式。
槽的声明与普通函数的声明一样,可以携带零或多个形式参数。既然信号的声明类似于普通 C++ 函数的声明,那么,信号也可采用 C++ 中虚函数的形式进行声明,即同名但参数不同。例如,第一次定义的 void mySignal() 没有带参数,而第二次定义的却带有参数,从这里我们也可以看到 QT 的信号机制是非常灵活的。
其他注意事项
- 信号与槽的效率是非常高的,从机制上来说类似于回调函数。但是同真正的回调函数比较起来,由于增加了灵活性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的。如果在一些实时系统中就要尽可能的少用这种机制。
- 信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能产生死循环。
- 如果一个信号与多个槽相联系的话,那么,当这个信号被发射时,与之相关的槽被激活的顺序将是随机的。
- 宏定义不能用在 signal 和 slot 的参数中。
- 构造函数不能用在 signals 或者 slots 声明区域内。如下:
class SomeClass : public QObject
{
Q_OBJECT
public slots:
SomeClass( QObject *parent, const char *name )
: QObject( parent, name ) {} // 在槽声明区内声明构造函数不合语法
[...]
};
- 函数指针不能作为信号或槽的参数,如下:
class someClass : public QObject
{
Q_OBJECT
[...]
public slots:
void apply(void (*applyFunction)(QList*, void*), char*); // 不合语法
};
但是可以通过以下的方式绕过限制:
typedef void (*applyFunctionType)(QList*, void*);
class someClass : public QObject
{
Q_OBJECT
[...]
public slots:
void apply(applyFunctionType, char *);
};
- 信号与槽不能有缺省参数,如下:
class SomeClass : public QObject
{
Q_OBJECT
public slots:
void someSlot(int x=100); // 将 x 的缺省值定义成 100,在槽函数声明中使用是错误的
};
- 信号与槽也不能携带模板类参数。
如果将信号、槽声明为模板类参数的话,即使 moc 工具不报告错误,也不可能得到预期的结果。 例如,下面的例子中当信号发射时,槽函数不会被正确调用:
public slots:
void MyWidget::setLocation (pair<int,int> location);
[...]
public signals:
void MyObject::moved (pair<int,int> location);
但是,你可以使用 typedef 语句来绕过这个限制。如下所示:
typedef pair<int,int> IntPair;
[...]
public slots:
void MyWidget::setLocation (IntPair location);
[...]
public signals:
void MyObject::moved (IntPair location);
- 嵌套的类不能位于信号或槽区域内,也不能有信号或者槽。例如,下面的例子中,在 class B 中声明槽 b() 是不合语法的,在信号区内声明槽 b() 也是不合语法的。
class A
{
Q_OBJECT
public:
class B
{
public slots: // 在嵌套类中声明槽不合语法
void b();
[....]
};
signals:
class B
{
// 在信号区内声明嵌套类不合语法
void b();
[....]
}:
};
- 友元声明不能位于信号或者槽声明区内。
相反,它们应该在普通 C++ 的 private、protected 或者 public 区内进行声明。下面的例子是不合语法规范的:
class someClass : public QObject
{
Q_OBJECT
[...]
signals: // 信号定义区
friend class ClassTemplate; // 此处定义不合语法
};