一文看懂Qt creator的ui文件设计及PIMPL原理

2023-11-23 11:59

本文主要是介绍一文看懂Qt creator的ui文件设计及PIMPL原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在Qt creator中,可以使用Qt Designer(Qt设计师)来快速设计界面,只需拖放就可以设计并快速浏览样式,并且可以生成代码,替代了用代码设计界面的工作。主要是生成了ui文件代替了用代码生成界面。那么这个过程是如何实现的呢?

以下是个简单的例子。新建了一个项目名,类名叫HelloDialog,派生自QDialog。在对话框上添加了一个按钮和一个文本标签。如下所示:
在这里插入图片描述
点击构建按钮会生成ui文件,各个文件内容如下:

1.解析hellodialog.h文件

#ifndef HELLODIALOG_H
#define HELLODIALOG_H#include <QDialog>namespace Ui {
class HelloDialog;
}class HelloDialog : public QDialog
{Q_OBJECTpublic:explicit HelloDialog(QWidget *parent = nullptr);~HelloDialog();private:Ui::HelloDialog *ui;
};#endif // HELLODIALOG_H

在hellodialog.h头文件里定义了一个类HelloDialog,继承自QDialog。这个类中有一个指向Ui::HelloDialog类型的私有变量。Ui::HelloDialog是什么东西呢?看HelloDialog类前边,告诉了它是命名空间Ui中定义的一个类,这叫做前置声明。Ui::HelloDialog这个类是干嘛的呢,就是设计界面上各种部件的类,位于命名空间Ui中,他和我们定义的类名字都叫做HelloDialog,但不是同一个东西

1.1 为什么要用Ui::HelloDialog *ui指针?

用Ui::HelloDialog ui,并且加上include <ui_hellodialog.h>,即包含其所对应的头文件,是否可以?

不幸的是,这样便在我们编写的类和Ui::HelloDialog类形成了编译依存关系。如果界面有改动(即ui_hellodialog.h)任何改变,那么包含ui_hellodialog.h的文件都得重新编译。任何用到我们定义的HelloDialog类对象的文件也都要重新编译。造成一连串的重新编译。

也许你会很奇怪,为何非要重新编译?==因为C++要在编译时就要确定内存的大小。==如果在运行时才确定内存就会影响效率。如果修改了类的头文件变了(例如增加了成员),那么会导致该类占用的内存变化,那么用到该类对象的文件占用内存大小也要变化,所以编译器只好把这些文件全部编译一遍,从而重新确定所需内存大小。

1.2 为什么要用前置声明?

如果没有前置声明,编译器编译到Ui::HelloDialog *ui这里时,不知道Ui::HelloDialog是什么东西,就会报错。加上前置声明之后,所声明的HelloDialog(namespace Ui中的那个类)叫做不完全类型。就是说看到这个地方,我们只知道HelloDialog他是一个类,但不清楚他包含哪些成员。声明前置类型是为了避免编译器遇到Ui::HelloDialog *ui时报错。

==可以定义指向不完全类型的指针或者引用。==很容易想明白,因为虽然不知道不完全类型是干嘛的、占用多大内存不知道,但是一个指针或引用占用的大小是确定的,所以即使指向的类发生变化,但不会影响到本文件所占用的内存大小。所以编译时只需编译指向的类的文件。本类和用到本类对象的文件都不会重新编译,避免了引用头文件时出现的连锁编译问题。

这种设计模式叫做PIMPL(pointer to implement),即一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏。作用1、降低编译依赖,提高编译速度。2、接口与实现分离,隐藏实现细节,降低模块耦合。在本文最后会对PIMPL再进行举例说明。

关于PIMPL以及编译依存性,可参考《effective C++》的条款31:“将文件间的编译依存关系降至最低”。

2. 解析hellodialog.cpp文件

#include "hellodialog.h"
#include "ui_hellodialog.h"HelloDialog::HelloDialog(QWidget *parent) :QDialog(parent),ui(new Ui::HelloDialog)
{ui->setupUi(this);
}HelloDialog::~HelloDialog()
{delete ui;
}

在构造函数中的初始化列表中,HelloDialog是继承自基类QDialog,因此用QDialog(parent)意思是对基类进行初始化,ui(new Ui::HelloDialog)意思是ui指向一个new出来的Ui::HelloDialog对象。

Ui::HelloDialog是一个部件布局对象,他是用来控制界面布局和组件设置的,但它本身并不是,也不包含任何窗体实体(包括在Ui上面布局的控件实体),他只是控制窗体上的部件的行为。类似于通过基类QDialog构造出了一块布,而Ui::HelloDialog控制在这块布上绣什么花花草草,红的还是绿的。

控制窗体上的部件行为样式是通过ui->setupUi(this)这一行实现,ui对象所属的类中定义了setupUi函数,用于控制部件行为样式,将this对象,也就是我们定义的类的实例对象作为参数传入到setupUi参数中。意思就是说在我的这个窗体上生成部件并设置样式。

3. 解析hellodialog.ui文件

#define UI_HELLODIALOG_H#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QDialog>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>QT_BEGIN_NAMESPACEclass Ui_HelloDialog
{
public:QPushButton *pushButton;QLabel *label;void setupUi(QDialog *HelloDialog){if (HelloDialog->objectName().isEmpty())HelloDialog->setObjectName(QString::fromUtf8("HelloDialog"));HelloDialog->resize(400, 300);pushButton = new QPushButton(HelloDialog);pushButton->setObjectName(QString::fromUtf8("pushButton"));pushButton->setGeometry(QRect(100, 100, 93, 28));label = new QLabel(HelloDialog);label->setObjectName(QString::fromUtf8("label"));label->setGeometry(QRect(240, 110, 101, 16));retranslateUi(HelloDialog);QMetaObject::connectSlotsByName(HelloDialog);} // setupUivoid retranslateUi(QDialog *HelloDialog){HelloDialog->setWindowTitle(QApplication::translate("HelloDialog", "HelloDialog", nullptr));pushButton->setText(QApplication::translate("HelloDialog", "PushButton", nullptr));label->setText(QApplication::translate("HelloDialog", "hello world!", nullptr));} // retranslateUi};namespace Ui {class HelloDialog: public Ui_HelloDialog {};
} // namespace UiQT_END_NAMESPACE#endif // UI_HELLODIALOG_H```

在ui文件中,定义了一个Ui_HelloDialog类,这个类就是控制窗体上部件的行为样式。首先界面有两个指向QPushButton和QLabel的指针,表明窗体上有这两个部件。

void setupUi(QDialog *HelloDialog)函数里边,写了这两个部件具体的样式、行为。注意参数是QDialog 类型的指针,表明这些部件的父对象。当我们在自己定义的类构造函数里边使用ui->setupUi(this)时,是把定义的HelloDailog类的实例对象作为参数传进去,所以setupUi中的部件就创建到了我们定义的窗体HelloDailog实例上边,并设定了显示样式。

#include "hellodialog.h"
#include <QApplication>int main(int argc, char *argv[])
{QApplication a(argc, argv);HelloDialog w;w.show();return a.exec();
}

main.cpp文件中HelloDialog w创建了w对话框对象,相当于把&w作为参数传入到了setupUi中(QDiailog指针可以指向派生的w对象)。从而在w这个窗体上生成部件并按照设定样式进行显示。

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

经过上述一番解释,是否对Qt creator的ui文件原理了然于胸了呢。总结Qt通过PIMPL的设计如下:

1、分离实现细节。这样的话,所有关于窗体的元素、配置、布局,便从窗体中抽离出来,任何窗体对象想使用这样的Ui,用一个指向Ui类对象的指针,然后用该指针setupUi一下,把这个窗体对象传进去就好了。
2、减少重新编译。当修改ui文件中部件的设计时,只需编译该文件即可。不会导致由于很多文件编译依存关系导致的重新编译。

4. PIMPL原理

原文链接:https://blog.csdn.net/armman/article/details/1737719

4.1 城门失火殃及池鱼

pImpl惯用手法的运用方式大家都很清楚,其主要作用是解开类的使用接口和实现的耦合。如果不使用pImpl惯用手法,代码会像这样:

//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时间产生相当大的影响。

pImpl惯用手法可以将这种耦合消除,使用pImpl惯用手法的代码像这样:

//c.hppclass 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对象。

4.2 防火墙

“修改X的定义会导致所有使用C的源文件重新编译”这种事就好比“城门失火,殃及池鱼”,其原因是“护城河”离“城门”太近了(耦合)。

pImpl惯用手法又被成为“编译期防火墙”,什么是“防火墙”,指针?不是。C++的编译模式为“分离式编译”,即不同的源文件是分开编译的。也就是说,不同的源文件之间有一道天然的防火墙,一个源文件“失火”并不会影响到另一个源文件。

但是,这里我们考虑的是头文件,如果头文件“失火”又当如何呢?头文件是不能直接编译的,它包含于源文件中,并作为源文件的一部分被一起编译。

这也就是说,如果源文件S.cpp使用了C.hpp,那么class C的(接口部分的)变动将无可避免的导致S.CPP的重新编译。但是作为class C的实现部分的class X却完全不应该导致S.cpp的重新编译。

因此,我们需要把class X隔绝在C.hpp之外。这样,每个使用class C的源文件都与class X隔离开来(与class X不在同一个编译单元)。但是,既然class C使用了class X的对象来作为它的实现部分,就无可避免的要“依赖”于class X。只不过,这个“依赖”应该被描述为:“class C的实现部分依赖于class X”,而不应该是“class C的用户使用接口部分依赖于class X”。

如果我们直接将X的对象写在class C的数据成员里面,则显而易见,使用class C的用户“看到”了不该“看到”的东西——class X——它们之间产生了耦合。然而,如果使用一个指向class X的指针,就可以将X的二进制信息“推”到class C的实现文件中去,在那里,我们#include”x.hpp”,定义所有的成员函数,并依赖于X的实现,这都无所谓,因为C的实现本来就依赖于X,重要的是:此时class X的改动只会导致class C的实现文件重新编译,而用户使用class C的源文件则安然无恙!

指针在这里充当了一座桥。将依赖信息“推”到了另一个编译单元,与用户隔绝开来。而防火墙是C++编译器的固有属性。

4.3 穿越C++编译期防火墙

是什么穿越了C++编译期防火墙?是指针!使用指针的源文件“知道”指针所指的是什么对象,但是不必直接“看到”那个对象——它可能在另一个编译单元,是指针穿越了编译期防火墙,连接到了那个对象。

从某种意义上说,只要是代表地址的符号都能够穿越C++编译期防火墙,而代表结构(constructs)的符号则不能。

例如函数名,它指的是函数代码的始地址,所以,函数能够声明在一个编译单元,但定义在另一个编译单元,编译器会负责将它们连接起来。用户只要得到函数的声明就可以使用它。而类则不同,类名代表的是一个语言结构,使用类,必须知道类的定义,否则无法生成二进制代码。变量的符号实质上也是地址,但是使用变量一般需要变量的定义,而使用extern修饰符则可以将变量的定义置于另一个编译单元中。

这篇关于一文看懂Qt creator的ui文件设计及PIMPL原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/417795

相关文章

基于Qt开发一个简单的OFD阅读器

《基于Qt开发一个简单的OFD阅读器》这篇文章主要为大家详细介绍了如何使用Qt框架开发一个功能强大且性能优异的OFD阅读器,文中的示例代码讲解详细,有需要的小伙伴可以参考一下... 目录摘要引言一、OFD文件格式解析二、文档结构解析三、页面渲染四、用户交互五、性能优化六、示例代码七、未来发展方向八、结论摘要

element-ui下拉输入框+resetFields无法回显的问题解决

《element-ui下拉输入框+resetFields无法回显的问题解决》本文主要介绍了在使用ElementUI的下拉输入框时,点击重置按钮后输入框无法回显数据的问题,具有一定的参考价值,感兴趣的... 目录描述原因问题重现解决方案方法一方法二总结描述第一次进入页面,不做任何操作,点击重置按钮,再进行下

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

一文带你搞懂Nginx中的配置文件

《一文带你搞懂Nginx中的配置文件》Nginx(发音为“engine-x”)是一款高性能的Web服务器、反向代理服务器和负载均衡器,广泛应用于全球各类网站和应用中,下面就跟随小编一起来了解下如何... 目录摘要一、Nginx 配置文件结构概述二、全局配置(Global Configuration)1. w

python与QT联合的详细步骤记录

《python与QT联合的详细步骤记录》:本文主要介绍python与QT联合的详细步骤,文章还展示了如何在Python中调用QT的.ui文件来实现GUI界面,并介绍了多窗口的应用,文中通过代码介绍... 目录一、文章简介二、安装pyqt5三、GUI页面设计四、python的使用python文件创建pytho

QT实现TCP客户端自动连接

《QT实现TCP客户端自动连接》这篇文章主要为大家详细介绍了QT中一个TCP客户端自动连接的测试模型,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录版本 1:没有取消按钮 测试效果测试代码版本 2:有取消按钮测试效果测试代码版本 1:没有取消按钮 测试效果缺陷:无法手动停

基于Qt实现系统主题感知功能

《基于Qt实现系统主题感知功能》在现代桌面应用程序开发中,系统主题感知是一项重要的功能,它使得应用程序能够根据用户的系统主题设置(如深色模式或浅色模式)自动调整其外观,Qt作为一个跨平台的C++图形用... 目录【正文开始】一、使用效果二、系统主题感知助手类(SystemThemeHelper)三、实现细节

Qt实现文件的压缩和解压缩操作

《Qt实现文件的压缩和解压缩操作》这篇文章主要为大家详细介绍了如何使用Qt库中的QZipReader和QZipWriter实现文件的压缩和解压缩功能,文中的示例代码简洁易懂,需要的可以参考一下... 目录一、实现方式二、具体步骤1、在.pro文件中添加模块gui-private2、通过QObject方式创建

Qt QWidget实现图片旋转动画

《QtQWidget实现图片旋转动画》这篇文章主要为大家详细介绍了如何使用了Qt和QWidget实现图片旋转动画效果,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 一、效果展示二、源码分享本例程通过QGraphicsView实现svg格式图片旋转。.hpjavascript

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert