一文看懂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中QGroupBox控件的实现

《Qt中QGroupBox控件的实现》QGroupBox是Qt框架中一个非常有用的控件,它主要用于组织和管理一组相关的控件,本文主要介绍了Qt中QGroupBox控件的实现,具有一定的参考价值,感兴趣... 目录引言一、基本属性二、常用方法2.1 构造函数 2.2 设置标题2.3 设置复选框模式2.4 是否

QT进行CSV文件初始化与读写操作

《QT进行CSV文件初始化与读写操作》这篇文章主要为大家详细介绍了在QT环境中如何进行CSV文件的初始化、写入和读取操作,本文为大家整理了相关的操作的多种方法,希望对大家有所帮助... 目录前言一、CSV文件初始化二、CSV写入三、CSV读取四、QT 逐行读取csv文件五、Qt如何将数据保存成CSV文件前言

一文详解如何在Python中从字符串中提取部分内容

《一文详解如何在Python中从字符串中提取部分内容》:本文主要介绍如何在Python中从字符串中提取部分内容的相关资料,包括使用正则表达式、Pyparsing库、AST(抽象语法树)、字符串操作... 目录前言解决方案方法一:使用正则表达式方法二:使用 Pyparsing方法三:使用 AST方法四:使用字

Qt中QUndoView控件的具体使用

《Qt中QUndoView控件的具体使用》QUndoView是Qt框架中用于可视化显示QUndoStack内容的控件,本文主要介绍了Qt中QUndoView控件的具体使用,具有一定的参考价值,感兴趣的... 目录引言一、QUndoView 的用途二、工作原理三、 如何与 QUnDOStack 配合使用四、自

Spring Boot循环依赖原理、解决方案与最佳实践(全解析)

《SpringBoot循环依赖原理、解决方案与最佳实践(全解析)》循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,:本文主要介绍SpringBoot循环依赖原理、解决方案与最... 目录一、循环依赖的本质与危害1.1 什么是循环依赖?1.2 核心危害二、Spring的三级缓存机制2.1 三

C#中async await异步关键字用法和异步的底层原理全解析

《C#中asyncawait异步关键字用法和异步的底层原理全解析》:本文主要介绍C#中asyncawait异步关键字用法和异步的底层原理全解析,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录C#异步编程一、异步编程基础二、异步方法的工作原理三、代码示例四、编译后的底层实现五、总结C#异步编程

电脑死机无反应怎么强制重启? 一文读懂方法及注意事项

《电脑死机无反应怎么强制重启?一文读懂方法及注意事项》在日常使用电脑的过程中,我们难免会遇到电脑无法正常启动的情况,本文将详细介绍几种常见的电脑强制开机方法,并探讨在强制开机后应注意的事项,以及如何... 在日常生活和工作中,我们经常会遇到电脑突然无反应的情况,这时候强制重启就成了解决问题的“救命稻草”。那

Qt spdlog日志模块的使用详解

《Qtspdlog日志模块的使用详解》在Qt应用程序开发中,良好的日志系统至关重要,本文将介绍如何使用spdlog1.5.0创建满足以下要求的日志系统,感兴趣的朋友一起看看吧... 目录版本摘要例子logmanager.cpp文件main.cpp文件版本spdlog版本:1.5.0采用1.5.0版本主要

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

鸿蒙中@State的原理使用详解(HarmonyOS 5)

《鸿蒙中@State的原理使用详解(HarmonyOS5)》@State是HarmonyOSArkTS框架中用于管理组件状态的核心装饰器,其核心作用是实现数据驱动UI的响应式编程模式,本文给大家介绍... 目录一、@State在鸿蒙中是做什么的?二、@Spythontate的基本原理1. 依赖关系的收集2.