QT 关于namespace Ui
QT 关于namespace Ui
1 ui文件原理
在 Qt 中,可以使用Qt Designer 来快速设计界面,只需拖放就可以设计并快速浏览样式,并且可以生成代码,替代了用代码设计界面的工作。其主要原理是通过uic 工具将 ui 文件转换为了 ui_xxx.h代码文件。下面通过简单的例子记录 QT 关于 ui 文件的代码原理。
在 VS2019 + QT5.15 的环境下简单新建一个 widget,并在窗口中添加一个 label 和一个 pushbutton。
首先来查看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 | //MyWidget.h |
下面就是 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 | /******************************************************************************** |
在MyWidget.cpp文件中,主要就是语句ui.setupUi(this);,这里的 ui 对象就是指向类 Ui::MyWidgetClass。这里语句的作用和上面描述的一致,把我们定义的窗体对象传进去,setupUi 中的部件就创建到了我们定义的窗体实例上边,并设定了显示样式。
1 | //MyWidget.cpp |
2 PIMPL设计
PIMPL设计模式简单来讲就是把类的成员变量设置为指针,主要作用是减少重新编译时间以及解开类的使用接口和实现的耦合。
1 | //c.hpp |
像上面这样的代码,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 | //c.hpp |
在一个既定平台上,任何指针的大小都是相同的。之所以分为X*,Y*这些各种各样的指针,主要是提供一个高层的抽象语义,即该指针到底指向的是那个类的对象,并且,也给编译器一个指示,从而能够正确的对用户进行的操作(如调用X的成员函数)决议并检查。但是,如果从运行期的角度来说,每种指针都只不过是个32位的长整型(如果在64位机器上则是64位,根据当前硬件而定)。
正由于 pImpl 是个指针,所以这里 X 的二进制信息(sizeof?等)不会被耦合到 C 的使用接口上去,也就是说,当用户 “new C” 或 “C c1” 的时候,编译器生成的代码中不会掺杂 X 的任何信息,并且当用户使用 C 的时候,使用的是 C 的接口,也与 X 无关,从而 X 被这个指针彻底的与用户隔绝开来。只有 C 知道并能够操作 pImpl 成员指向的 X 对象。





