QT 关于namespace Ui

1 ui文件原理

在 Qt 中,可以使用Qt Designer 来快速设计界面,只需拖放就可以设计并快速浏览样式,并且可以生成代码,替代了用代码设计界面的工作。其主要原理是通过uic 工具ui 文件转换为了 ui_xxx.h代码文件。下面通过简单的例子记录 QT 关于 ui 文件的代码原理。

在 VS2019 + QT5.15 的环境下简单新建一个 widget,并在窗口中添加一个 label 和一个 pushbutton。

ui

首先来查看MyWidget.h文件。首先是宏Q_OBJECT,它的作用是提供信号槽机制等 QT 操作,凡是 QObject 类都需要在第一行代码写上 Q_OBJECT。然后是类的私有成员Ui::MyWidgetClass ui;,这里的 MyWidgetClass 和我们自己定义的 MyWidgetClass 类并不是同一个,它是 Ui 命名空间下的,具体是在 “ui_MyWidget.h” 文件。

在 VS 中 ui 直接是类对象,而在 Qt Creator 中一般是指针,按照我目前的理解,定义为指针是利用 PIMPL设计模式,减少了重新编译时间,即 VS 中定义为对象没有定义为指针好。

Qt 的 moc(元对象编辑器)工具会在预处理之前,找出所有的带有Q_OBJECT宏的类,生成 moc_xxx.cpp,这个过程就是在添加 Qt 的一些机制,然后才是正常的C++编译流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//MyWidget.h
#include <QtWidgets/QWidget>
#include "ui_MyWidget.h"

class MyWidget : public QWidget
{
Q_OBJECT

public:
MyWidget(QWidget *parent = nullptr);
~MyWidget();

private:
Ui::MyWidgetClass ui;
};

下面就是 uic 工具根据我们绘制的 ui 文件生成的对应代码文件Ui_mywidget.h。ui 文件中上面定义了一个类 Ui_MyWidgetClass,这个类就是控制窗体上部件的行为样式,比如它的两个成员 pushButton 和 label 就表明窗体上有这两个部件,而setupUi函数里边就是写了这两个部件具体的样式、行为。当我们在自己定义的类构造函数里边使用 ui->setupUi(this) 时,是把定义的 MyWidget 类的实例对象作为参数传进去,所以 setupUi 中的部件就创建到了我们定义的窗体 MyWidget 实例上边,并设定了显示样式。这样做的好处是分离实现细节。这样的话,所有关于窗体的元素、配置、布局,便从窗体中抽离出来,任何窗体对象想使用这样的 Ui,用一个指向 Ui 类对象的指针,然后用该指针 setupUi 一下,把这个窗体对象传进去就好了。

在 ui 文件最后,定义了 Ui命名空间,这个命名空间里边有一个类 class MyWidgetClass,公有派生自Ui_MyWidgetClass,就是说 Ui::MyWidgetClass 保留继承过来的公有属性,具有 Ui_MyWidgetClass 的行为和特点。这样绕一圈是为了避免用户定义类名与 Qt 自动生成的 ui 文件中的类名冲突,因此放在了 Ui 命名空间中。Ui::MyWidgetClass 就相当于 Ui_MyWidgetClass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/********************************************************************************
** Form generated from reading UI file 'MyWidget.ui'
**
** Created by: Qt User Interface Compiler version 5.15.2
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/

#ifndef UI_MYWIDGET_H
#define UI_MYWIDGET_H

#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>

QT_BEGIN_NAMESPACE

class Ui_MyWidgetClass
{
public:
QPushButton *pushButton;
QLabel *label;

void setupUi(QWidget *MyWidgetClass)
{
if (MyWidgetClass->objectName().isEmpty())
MyWidgetClass->setObjectName(QString::fromUtf8("MyWidgetClass"));
MyWidgetClass->resize(600, 400);
pushButton = new QPushButton(MyWidgetClass);
pushButton->setObjectName(QString::fromUtf8("pushButton"));
pushButton->setGeometry(QRect(220, 210, 131, 51));
QFont font;
font.setPointSize(10);
pushButton->setFont(font);
label = new QLabel(MyWidgetClass);
label->setObjectName(QString::fromUtf8("label"));
label->setGeometry(QRect(200, 120, 171, 61));
QFont font1;
font1.setPointSize(12);
label->setFont(font1);

retranslateUi(MyWidgetClass);

QMetaObject::connectSlotsByName(MyWidgetClass);
} // setupUi

void retranslateUi(QWidget *MyWidgetClass)
{
MyWidgetClass->setWindowTitle(QCoreApplication::translate("MyWidgetClass", "MyWidget", nullptr));
pushButton->setText(QCoreApplication::translate("MyWidgetClass", "\345\274\200\345\247\213\350\256\260\345\275\225", nullptr));
label->setText(QCoreApplication::translate("MyWidgetClass", "UI\346\226\207\344\273\266\345\216\237\347\220\206\350\256\260\345\275\225", nullptr));
} // retranslateUi

};

namespace Ui {
class MyWidgetClass: public Ui_MyWidgetClass {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_MYWIDGET_H

MyWidget.cpp文件中,主要就是语句ui.setupUi(this);,这里的 ui 对象就是指向类 Ui::MyWidgetClass。这里语句的作用和上面描述的一致,把我们定义的窗体对象传进去,setupUi 中的部件就创建到了我们定义的窗体实例上边,并设定了显示样式。

1
2
3
4
5
6
7
8
9
10
11
//MyWidget.cpp
#include "MyWidget.h"

MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
}

MyWidget::~MyWidget()
{}

2 PIMPL设计

PIMPL设计模式简单来讲就是把类的成员变量设置为指针,主要作用是减少重新编译时间以及解开类的使用接口和实现的耦合。

1
2
3
4
5
6
7
8
9
//c.hpp
#include<x.hpp>
class C
{
public:
void f1();
private:
X x; //与X的强耦合
};

像上面这样的代码,C 与它的实现就是强耦合的,从语义上说,x 成员数据是属于 C 的实现部分,不应该暴露给用户。从语言的本质上来说,在用户的代码中,每一次使用“new C”和“C c1”这样的语句,都会将X的大小硬编码到编译后的二进制代码段中(如果X有虚函数,则还不止这些)————这是因为,对“new C”这样的语句,其实相当于operator new(sizeof? )后面再跟上C的构造函数,而“C c1”则是在当前栈上腾出sizeof?大小的空间,然后调用 C 的构造函数。因此,**每次X类作了改动,使用c.hpp的源文件都必须重新编译一次,因为X的大小可能改变了。**在一个大型的项目中,这种耦合可能会对build时间产生相当大的影响。

1
2
3
4
5
6
7
8
9
//c.hpp

class X; //用前导声明取代include
class C
{
...
private:
X* pImpl; //声明一个X*的时候,class X不用完全定义
};

在一个既定平台上,任何指针的大小都是相同的。之所以分为X*,Y*这些各种各样的指针,主要是提供一个高层的抽象语义,即该指针到底指向的是那个类的对象,并且,也给编译器一个指示,从而能够正确的对用户进行的操作(如调用X的成员函数)决议并检查。但是,如果从运行期的角度来说,每种指针都只不过是个32位的长整型(如果在64位机器上则是64位,根据当前硬件而定)。

正由于 pImpl 是个指针,所以这里 X 的二进制信息(sizeof?等)不会被耦合到 C 的使用接口上去,也就是说,当用户 “new C” 或 “C c1” 的时候,编译器生成的代码中不会掺杂 X 的任何信息,并且当用户使用 C 的时候,使用的是 C 的接口,也与 X 无关,从而 X 被这个指针彻底的与用户隔绝开来。只有 C 知道并能够操作 pImpl 成员指向的 X 对象。

3 主要参考

深入浅出解析Qt creator的ui文件原理及PIMPL设计

[QT | C++] 关于 namespace Ui 的理解