——Qt开发无痛上路指南<第一期>
无论你是做UI开发、嵌入式开发还是纯C++开发,你或许都听说过大名鼎鼎的Qt框架。那么Qt到底是什么、包含什么、能够实现什么样的功能、有什么样的限制呢?要了解这些事情,就必须先从Qt的基础概念、体系结构开始逐步递进学习。当然,安装Qt对于某些开发者而言已经十分劝退了,这就是Qt教给我们的第一课——要么耐心要么弃坑。
A. Qt与其生态简介
Qt是一个跨平台的C++应用程序框架,广泛用于开发图形用户界面(GUI)程序以及非GUI程序。它提供了丰富的功能库,支持各种操作系统,如Windows、macOS、Linux等。Qt的主要特点包括高性能、易用性和灵活性,适用于桌面、移动、嵌入式等多种应用开发。通过Qt,开发者可以使用同一套代码生成适配不同平台的应用,从而大大提高开发效率和维护性。
1. Qt全组态结构
认识一下Qt的官网,访问速度慢的同学需要科学上网:

上图展示了Qt Group也就是整个Qt生态之中笔者认为按照常用程度与掌握优先级排列的几个比较有用的部分,这些部分基本上能够表征Qt的特征和功能,它们是:
Qt Framework,这个属于最核心的组件,我们常常说的Qt库就是这个东西。目前Qt开发框架已经支持了C++/Python/QML/Javascript这些语言的单独开发或者多语言联合开发。老一点的也是比较经典的开发框架是QWidget开发,而随着Qt6的发展和近年来声明式编程和响应式界面的大火,Qt也提供了较新的开发框架也就是以Qml为主的Quick框架。
Qt的所谓开发工具也就是Qt官方制作的用于开发Qt程序的IDE——QCreator,曾几何时在Qt的可移植性和通用性还不是很好的情况下,特别是非要使用
QMake的强制要求下,QCreator是Qt必不可少的一部分,甚至说不少人将QCreator当做了Qt本体。然而在Qt适配CMake的今天,甚至VsCode都已经涌现出大量面向Qt的扩展的情况下,UI老旧设计理念过时的QCreator其实就已经站在了退出历史舞台的边缘。本文在D部分之中就讲介绍使用CLion来开发Qt的方法。Qt的设计工具常用的主要有两个:
QLiguist Tool:Qt是支持多语言灵活翻译的框架(当然这一点在UI设计之中十分重要),故而提供了这样一个工具可以使开发者便捷的为工程之中的所用字符串翻译。
QDesign Studio:Qt的UI设计工具,这是笔者认为Qt官方做的最为优秀的设计工具,甚至说超过官方明面宣称的QCreator。这个工具作为一个桥梁将UI设计在图形化的高效省力设计方法与代码化精确入微的设计方法之间将二者统一起来,用户进行的UI设计将会直接被转换为xml代码,反之合格的自定义UI组件代码也可以在设计工具内预览。
Qt在代码测试和质量检测等方面也有涉猎,但是这一部分一般的开源开发者(买不起)和Qt初学者(不会用)其实都不会涉及。但是我们需要注意在软件的构建(build)过程之中不是只有编译和部署才是有效的范围,测试和质检有的时候更加重要。
2. Qt开发框架结构与组件
上文提到的Qt的各个组件之中,称得上是“Qt本体”的就是Qt开发框架(Qt Framework),这个框架之中包含我们在软件构建过程之中用到的各个代码库、支持包、运行时环境等等。Qt框架之中所有的内容都以模块(Module)的形式给出,后文会提到这里的框架其实就是CMake之中的一个Component。对于Qt来说模块可以分为三大类,对于6.7版本具体而言有这些模块:
Qt Essentials(必要模块):这部分内容相当于Qt开放框架的“核心”,每个模块都提供了非常基础、非常常用、作为其他模块依赖的功能。
Core:核心模块,对外提供不包含GUI的核心功能例如QCoreApplication类的支持。这个模块包含一些相当基础的Qt机制例如MOC、信号与槽等等。
GUI:图形界面模块,在Core的基础上为一个Application附加提供图形化和显示支持例如QGuiApplication和QWindow。这个模块不仅仅提供2D显示,还提供RHI/3D/OpenGL与OpenGL ES的集成支持,在一些嵌入式平台移植时十分重要。
Widgets:组件库模块,这是Qt编写图形界面的传统方法即组件化编程,Qt预先封装了编写图形话界面用到的内容例如Widget(组件,包括下拉单选框QComboBox,标签QLabel,滑动条QScrollBar等),Style(图形界面显示风格例如Windows,Mac,Fusion),Layout(布局与容器,例如水平/垂直布局,网格布局等等)
Test:测试模块,这是Qt构建流程之中用于对Application或者Library进行单元测试的支持,在Qt的开发流程之中(无论是C++还是Python)的测试部分可以通过这个模块进行,模块提供了对于类对象、对象属性、信号与槽完整的测试机制。
Qml:模型语言模块,本模块提供QML编程语言支持。QML是一种多范式的脚本语言,用于创建高度动态的应用程序,结合Quick编程框架可以实现声明式UI编写和响应式UI动态效果的构建。Javascript是QML的一个子集,因此在模型描述后可以使用Javascript继续完成更多功能,并且QML可以与Qt主体程序进行交互。
Quick:敏捷开发库,这是Qt编写图形界面的新兴方法即声明式编程。QML提供了脚本语言框架和运行引擎,在此之上Quick模块提供必要的类型定义和运行范式支持以使QML能够转化成为可视化的图形界面。Quick模块是沟通QML和Qt主体程序的桥梁,本模块需要其他模块的支持,这些支持模块不是Quick的子模块,也属于Qt Essentials的一部分:
Quick Controls:轻量化的的QML+Quick控制图形界面的支持库
Quick Layouts:Quick开发框架下图形界面的显示尺寸与位置布局支持库
Quick Dialogs:Quick开发框架下创建系统对话框的支持库
Quick Test:相比于QTest而言提供了对QML和Javascript代码的测试工具
Network:网络模块,此模块提供了一套基于TCP/IP协议栈的网络编程API,能够使网络编程的难度大大降低的同时提升程序的可移植性。同时对于其他协议例如HTTP等也有种类繁多的的类对象的支持。如果我们使用Quick框架进行开发,这个模块常常需要引用。
D-Bus:事件总线模块,此模块本质上是一个IPC(Inter-Process Commnication 进程间通信)与RPC(Remote Procedure Calling 远程程序调用)即使的接口实现。此模块主要运作在Linux或者其他类Unix系统上以实现系统进程级别的跨应用通信从而构建分布式程序。
Qt Addons(附属模块):这部分内容属于Qt的“外围支持”功能,每个模块往往对应着不同的单元功能,这些功能相对独立而互相之间又没有隶属关系。
Active:对于使用ActiveX和组件对象模型(Component Object Model, COM)的支持
3D:提供一个近乎实时性的对于2D和3D图形进行渲染的模拟系统
Qt5 Core Compability APIs:向下兼容上一代Qt5程序的各类核心但Qt6移除的接口
Bluetooth:提供对于蓝牙硬件和BL/BLE协议的访问与控制接口
Charts:图表模块,提供了折线图、柱状图、饼状图等等各类常用的静/动态图表绘制接口
Concurrent:多线程与异步模块,提供了完善的多线程和异步编程支持而不依赖于系统底层
Data Visualization:提供了3D的数据可视化模型的创建和控制接口
Help:提供一系列的类将程序帮助文档集成到Application之中去
Image Formats:提供对于像GIF/WEBP/TIFF/MNG/TGA/WBMP这样的图像格式的支持
Lottie Animation:提供一个搭配QML的加载json格式动画效果的支持库
OpenGL:二次封装了OpenGL接口以便在Qt应用程序之中便捷而可移植的使用OpenGL
Multimedia:对于QML和C++均可使用的处理多媒体和富文本信息的支持库
Network Authorization:提供了机遇OAuth的网络签名认证机制和认证服务
NFC:对于NFC硬件设备和通讯协议提供支持和控制接口
PDF:对于在Qt应用程序内渲染PDF文档提供了接口和支持库
Positioning:对于卫星定位和通信基站定位提供了访问接口和运行时支持库
Print Support:对于应用程序之中使用到打印机的部分提供支持和可移植性
Quick 3D:对于Quick开发框架提供一个高层级的3D编程API和支持库
Quick Timeline:对于Quick开发框架提供关键帧动画和参数化开发的支持库
Quick Widgets:提供在Widgets开发框架下显示Quick组件和QML的机制
Remote Objects:提供了进程间共享Qt对象数据的易用开发接口
SCXML:提供了从SCXML文件创建状态机并且嵌入到应用程序内的接口
Sensors:主要在移动平台上提供对于传感器的支持,也可用于桌面端或自定义传感器
Serial Bus:提供对于两种串行数据协议即ModBus、CAN的协议编程接口
Serial Port:提供对于串口硬件或者虚拟串口的访问和使用接口支持
Shader Tools:对于跨平台渲染器流水线提供图形化计算支持,可用于Quick框架
SQL:在Qt应用程序之中集成使用SQL操作数据库系统的接口和运行时库
State Machine:提供对于创建和执行状态机/状态图机制的编程接口和运行时库
SVG:对于SVG格式的矢量图提供了渲染和操作接口
UI Tools:UI工具包,能够将Qt Designer之中生成的表单在运行时动态的展示到UI
Wayland Compositor:提供对于Wayland Compositor(类似 X11的显示服务器)的支持库
WebChannel:能够在服务器(QML/C++)和客户端(H5/JS+QML)之间建立P2P通道
WebEngine:嵌入了一个Chromium核心以显示各种基于浏览器的网络页面和数据
WebSockets:提供基于RFC6455定义的WebSocket协议的支持库和接口
WebView:在Quick开发框架下展示Web数据,无需完全引入浏览器技术栈
Virtual KeyBoard:在移动平台和嵌入式平台上提供对于例如触摸软键盘等虚拟键盘的支持
XML:提供对于符合SAX和DOM标准的XML文件分析与操作的支持
Additional Qt Libraries(附加支持库),这部分内容距离Qt的核心功能相去甚远,有些只在Qt官方的应用市场之中能够找到,甚至都不在安装包中,在6.7.0版本可用的主要有:
Quick TreeView:Quick框架下提供树状视图支持
Quick Calendar:Quick框架下提供日历功能的支持
Quick MultiEffect:对Quick框架下的组件提供更加快速的动画效果渲染
Digital Advertising:提供一个在Qt程序之中展示广告的轻量库
VNC Server:适配Simple-VNC标准创建一个VNC服务器
Insight Tracker:提供一种统计应用在客户之中的使用状况的方案
Application Manager:对于嵌入式Linux的复杂UI程序提供了一个进程守护管理器
Interface FrameWork:对于中间件(Middleware)提供接口构建服务
B. Qt之中的基本概念
了解了Qt基本的组成部分和生态之后,我们就来到一个非常务实的部分:如何使用Qt开发框架和Qt生态之中的各种开发工具呢?这个部分十分令人迷惑,网络讨论之中关于这个部分常见各种编译不通过和运行库缺失亦或者出现诡异的兼容性问题或者版本问题。因此,本文在这个部分之中将从几个方面来解析Qt基础开发流程和作用机理。
1.Qt应用开发全流程分析
在软件开发过程之中我们经常提到一个名词”构建“,一般而言英文中construct会被翻译成构造、构建,而build一词会被直接翻译为构建,不知道读者有没有想过所谓的”构建“过程主要包括哪些细分步骤而这些细分步骤又具体起到什么作用?思考清楚这个问题可以帮助我们迅速理解任何一款开发工具或者支持库作用的范围、大致作用机理,这是一个事半功倍的工作。

从上图可以看到,任何软件应用程序的构建大致都可以分为五个步骤,Qt在这其中的几乎每一个步骤都提供对应的工具并且拥有良好的兼容性:
代码组织:这也就是大多数人对于软件开发的印象,也就是如何写代码
IDE方面Qt官方推荐Qt Creator,但是本文主要介绍使用CLion+CMake+Qt的方案,在后文之中可以看到CLion对于Qt的代码补全和组件识别兼容性良好。
在代码静态检查方面集成到CLion之中的Clang-Tidy,CLazy都对Qt有非常良好的兼容性。
在分类设计工具方面,我们常用的设计工具主要是Qt Designer去完成对于复杂图形界面的UI设计工作,在进行多语言支持的时候也使用Qt Linguist完成目标语言的翻译工作。
而跨平台兼容性和可移植性算得上是Qt最值得称道的点之一了,无论是Windows,macOS还是Linux,无论是x86还是arm架构只要能够在目标平台上通过交叉编译或者直接编译的方式获得运行时支持库那么就能够实现“一次编写,多次使用”的目标。
编译:这是大多数人对于“软件构建”一次的理解,当然各种教程在教导初学者时将编译等同于IDE之中的Build的按钮可能是原因之一,事实上编译是软件构建过程之中最重要的部分但是却是最基础的第一步(编写代码算是第0步吧),Qt在这一部分主要提供:
在Windows上提供MinGW(Windows上的MSVC或者其他系统的GCC不需要Qt额外提供)作为编译用到的工具链,提供编译器可执行文件
对于各个工具链和其他系统(考虑交叉编译)提供完整的编译支持库和运行时支持库,包括动态链接库和静态链接库
对于Qt对象的信号和槽提供MOC(Meta-Object Compiler)机制
对于Qt应用程序用到的资源文件例如图片或者音频提供RCC(Resource Compiler)机制
对于Qt Designer设计的UI配置文件提供UIC(User Interface Compiler)机制
对于多语言支持提供Qt Translator和Qt Linguist作为多语言工具
CMake构建模式下,编译目标可以是可执行二进制文件或者链接库或者App Bundle(MacOS)
测试:这个步骤常常写Java开发的朋友可能比较熟悉,JUnit也是大名鼎鼎。但是初学者和C++开发者可能略有生疏。测试是软件开发过程中非常重要的一个环节,这对于开发上游而言能够使开发者团队及时识别到错误,对于下游可以保证交付给用户的产品质量过关。QTest模块为图中提到的几个常用的测试环节都提供了相当好的支持。
打包:在Qt的语境下打包和部署是不一样的,打包的目的主要是为了提供一种分发到用户并使用户开箱即用的机制。Qt之中的打包过程主要使用Qt Installer Framework,这个工具能够使用一套代码制作出在各个平台上符合标准的安装包二进制文件。并且这个工具允许用户自定义安装包的分页流程、程序授权证书、用户协议以及允许用户自定义执行前后的脚本,并且提供了应用程序的维护工具(Maintenance Tools)使得开发者可以将注意力更加集中在APP的主体部分。
部署:所谓的部署的核心就是解依赖关系。当我们的二进制程序编译时除了我们编写的核心功能代码之外,在链接的过程之中会用到Qt提供的链接库,这些链接库分为静态链接库和动态链接库,对于静态链接库而言在链接(Linking)步骤就会打包到二进制文件之中去;但是对于动态链接库(.dll/.dylib/.so)往往需要在对应运行程序的操作系统之中存在并且可以找到才行。Qt提供了这样的工具也就是:windeployqt.exe /macdeployqt /linuxdeployqt(Linux部署工具为第三方)
2.Qt开发框架几大重要机制解析
在上一小节之中提到,Qt在编译时有几个比较核心比较重要的工作机制,用于处理Qt的类对象、资源文件、多语言支持等。那么在本小节之中,我们将重点聚焦于这些机制是如何生效的,这些机制如何作用于最本质的内容:代码。以C++开发Qt为例,可能我们在工程之中会用到图像文件(.png)作为按钮的图标,会用到翻译文件(.ts)用于多语言支持,会用到UI设计文件(.ui)用于打造图形界面……但是我们要始终牢记一件事情:所有的文件和配置都会落实到C++代码,通过C++代码才能作用到最终编译出的链接库或者可执行文件之中,从无例外。
2.1 元对象编译器MOC(Meta-Object Compiler)
元对象编译器虽然号称是一个Compiler,但是其工作流程实际上处于预处理阶段,也就是说MOC实际上是Qt框架提供的一个预处理器。它会解析带有Qt元对象特征标记的C++代码并且生成C++源文件,这份被生成的代码包括了元对象系统所需要的信息和实现代码从而能够支持Qt的特性。假设C++代码之中有这样的一个类:
class MyObject {
public:
MyObject();
}那么我们只要插入一个通过任何Qt头文件都可以引用(因为在Qt的类多态继承树之中,所有Qt类的基类都可以追溯到QObject )的宏定义Q_OBJECT 即可将我们自己写的类转换为Qt之中的元对象:
#inlcude<QObject>
class MyObject : public QObject{
Q_OBJECT
public:
explicit MyObject(QObject* parent = nullptr);
}上述代码之中我们可以看到我们进行了两个改变,第一个就是将类MyObject 声明为类QObject 的子类,这是必须的:一个使用Q_OBJECT宏的类必须直接或者间接的继承QObject类才能够实现功能否则就会编译报错。那么这个宏定义具体包括些什么东西呢:
public:
_Pragma("clang diagnostic push")
_Pragma("clang diagnostic ignored \"-Winconsistent-missing-override\"")
_Pragma("clang diagnostic ignored \"-Wsuggest-override\"")
static const QMetaObject staticMetaObject;
virtual const QMetaObject* metaObject() const;
virtual void* qt_metacast(const char *);
virtual int qt_metacall(QMetaObject::Call, int, void **);
static inline QString tr(const char *s, const char *c = nullptr, int n = -1) {
return staticMetaObject.tr(s, c, n);
}
private:
__attribute__((visibility("hidden")))static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
_Pragma("clang diagnostic pop")
struct QPrivateSignal {
explicit QPrivateSignal() = default;
};首先我们看到有几句
_Pragma它们的作用是确保在编译这段代码的时候不会因为没有在重载函数时添加或者缺失override关键字而触发警告clang diagnostic push保存当前的代码诊断状态clang diagnostic ignored \"-Winconsistent-missing-override\"忽略未一致性使用的override关键字clang diagnostic ignored \"-Wsuggest-override"忽略对于使用关键字override的所有建议clang diagnostic pop回复之前的代码诊断状态
其次声明了一个静态成员
QMetaObject类型的常量:这个静态常量将会包含该类的所有元数据信息包括信号、槽、属性等等——MOC机制的核心就是这个静态常量。而后续定义的常量成员虚函数*metaObject()就是用于返回这个静态元数据常量。定义虚函数
qt_metacast这个函数部分的实现了Java之中的“反射机制”也就是可以通过输入的字符串参数将对象转换为与该字符串相关的特殊类型。定义虚函数
qt_metacall这个函数用于调用对象的元函数例如信号(signals)和槽(slots),这个函数主要在信号和槽的链接过程之中使用,是MOC的信号/槽部分的核心。tr函数将会在多语言支持章节之中详细提到,可以看到这里的函数其实就只对真正的tr 函数进行了二次封装,MyObject.tr本质上就是MyObject.staticMetaObject.trprivate功能区之中首先定义了函数qt_static_metacall这个函数和前面的qt_metacall函数比较相似吗用于处理静态的元函数调用,此函数通过MOC生成的代码调用,是执行信号和槽函数的实际步骤。而使用修饰符属性visibility("hidden")目的是另该函数在共享库之子洪不可见,从而避免多个这样的函数出现名称上的冲突。结构体
QPrivateSignal用于标识private signals这样就能够避免信号函数的权限混乱问题也就是防止从类对象的外部直接发出私有信号。
上述提到的虚函数和静态变量都会在MOC编译过程之中被处理为一个新的CPP文件并且参与最终的可执行文件的编译流程。假设工程之中存在MyObject.h 和MyObject.cpp 那么MOC编译时就会自动处理这两个文件,在编译目录下的${PROJECT_NAME}_autogen 目录下生成moc_MyObject.cpp 文件其中包含了所有的函数定义和静态变量定义。如果有多个这样的量就会生成多个moc自动cpp文件,最后所有的这些cpp文件在自动生成的moc_predefs.h 和moc_compilation.cpp 文件之中被包含引用和生效,从而完成MOC机制的作用。
2.1.1 MOC机制之信号与槽
在软件开发之中有一个普遍要求就是六字箴言:高内聚,低耦合。然而对于“组件之间相互通讯”来说降低耦合度就是一个非常困难的问题了:一般而言我们使用某个函数作为一个类的通信接口,在函数的参数部分传递我们想要沟通的信息——这样就导致了我们编写的软件在通信部分几乎不可解耦!有的朋友可能会说我们为何不采用函数指针(function pointer)的方法,将通信理解为回调(callback)函数呢?事实上这是一个非常好的思路:在C/C++之中我们拥有函数指针的概念,我们可以定义某种指针范式,例如我们需要传递一个std::string 类型的字符串信息,需要目标对象同样给出一个字符串类型的回执结果,那么我们不妨这样定义:
#include<iostream>
#include<string>
#include<functional>
using messageFunc = std::function<std::string(int,const std::string&)>;
class ObjectRx {
public:
std::string messageArrive(int source, const std::string& message){
std::string result = "Message received from: "
+ std::to_string(source)
+ " is " + message;
return result;
}
};
class ObjectTx {
public:
messageFunc messageTarget;
ObjectTx(messageFunc messageTarget):messageTarget(messageTarget){}
void messageTransmit(const std::string& message){
std::cout << this->messageTarget(1,message);
}
};
int main(){
ObjectRx rx;
ObjectTx tx(
std::bind( &ObjectRx::messageArrive, &rx, std::placeholders::_1, std::placeholders::_2)
);
tx.messageTransmit("test message");
return 0;
}可以看到我们定义了两个类ObjectTx 用于发送数据,ObjectRx 用于接收数据并且返回一个字符串作为一次通信,二者之间通过messageFunc 类型的函数指针进行交互,从某种程度上来说算是进行了通信解耦,MOC之中的信号和槽就将这一机制完善并且发扬光大。
信号与槽机制是一个强大的特性,允许对象之间进行松耦合的通信,这种通信本身可以是多对多的甚至是跨线程的。这一机制由 Qt 的元对象编译器 (MOC) 支持,并且是 Qt 框架最重要的部分之一,在这个框架之中几个基本概念是:
信号(Signals):信号是一种由对象在特定事件发生时发出的消息。信号通常在类定义中声明,但在代码中不需要实现也就是在我们自己写的功能代码之中可以看作是一个虚函数。信号可以携带参数,并且这些参数的类型必须是 Qt 元对象系统支持的类型。
槽(Slots):槽是一个普通的成员函数,但它可以与信号连接,以便在信号发出时自动调用。槽最好具有与信号相同类型的参数,如果因为缺省参数等原因槽的参数类型和信号的参数类型不同,信号会自动寻找能够匹配槽的参数传递方式,如果没有这种方式那么此部分将会在MOC编译时报出相关错误信息。
信号和槽的连接通常在对象创建之后进行,使用
QObject::connect函数进行连接。连接后的信号与槽就像是函数指针或者说回调函数一样,但是其执行方式多种多样,大概包含这几类:Qt::DirectConnection这种连接形式表示信号和槽函数在同一个线程之中运行,信号发出时槽函数会立即被调用。这种直接调用的形式是强制在同一线程之中执行的。Qt::QueuedConnection这种连接形式表示信号和槽函数在两个不同的线程之中执行,具体在哪个线程之中取决于信号和槽属于的类对象在哪个线程之中工作。这种在两个不同线程之中执行的形式是强制的。Qt::BlockingQueuedConnection这种连接形式与上一种形式类似,但是发出的信号会阻塞发出线程,直到槽函数在接受线程之中执行完毕。这种连接形式主要用于跨线程同步,但是需要注意避免线程死锁。Qt::AutoConnection这种调用方式是Qt默认的调用形式,它将会自动判断信号与槽是否在不同的线程之中进而选择是使用第一种还是第二种调用方式。Qt::UniqueConnection这种调用方式不单独使用,一般而言和其他的调用形式以或运算的方式组合使用,保证目标信号和槽之前只有这个唯一的连接。这种连接方式不会关闭之前存在的连接,而是确保不会创建新的重复的连接。
信号可以和槽相连,也可以和另一个信号相连,也可以不连接任何信号或者槽。也就是说当一个信号被触发时可能同样触发与它连接的槽作为响应,触发与它连接的另一个信号或者不触发任何东西。信号和槽的连接关系可以是多对多的:也就是一个信号可以连接到多个槽,一个槽也可以接受来自多个信号的连接。
信号函数和槽函数的参数个数和每个参数的类型一般而言可以自由选择,但是Qt仅仅支持了比较基础的一些参数类型。如果信号携带的参数是自定义的类对象、结构体、枚举体、联合体(也就是一切Qt不包含的类型),那么需要将这个自定义的参数类型注册到Qt的MOC系统。在Qt6之中这个注册需求有所放宽,在某些状况下会自动注册。
以下是一个使用MOC机制之中信号和槽的例子,我们用到三个文件。首先是MocExample.h 头文件:
#ifndef MOC_EXAMPLE_H
#define MOC_EXAMPLE_H
#include<QObject>
#include<QString>
#include<QCoreApplication>
//定义通讯结构体
struct MessageStruct{
//发信对象ID和消息
int txObjectID;
QString message;
//结构体构造器
MessageStruct(int id, const char* message_str):txObjectID(id),message(message_str){};
};
//声明自定义结构体到Qt元类型
Q_DECLARE_METATYPE(MessageStruct);
//接受端类对象
class ObjectRx : public QObject {
Q_OBJECT
public:
explicit ObjectRx(int id, QObject* parent = nullptr);
public slots:
//声明槽函数
QString messageReceive(const MessageStruct& payload);
private:
int id;
};
//发送端类对象
class ObjectTx : public QObject {
Q_OBJECT
public:
explicit ObjectTx(int id, QObject* parent = nullptr);
//声明使用信号的接口函数
void sendMessage(const char* message_str);
signals:
//声明信号,不需要实现
QString messageTransmit(const MessageStruct& payload);
private:
int id;
};
#endif //MOC_EXAMPLE_H其次是MocExample.cpp 去实现上述头文件之中声明的函数:
/*==========MocExample.cpp==========*/
#include "MocExample.h"
//实现接受端构造器
ObjectRx::ObjectRx(int id, QObject *parent):QObject(parent),id(id) {}
//实现接受槽函数
QString ObjectRx::messageReceive(const MessageStruct& payload) {
QString result = "Object " + QString::number(id)
+ " received message: " + payload.message
+ " from: " + QString::number(payload.txObjectID);
return result;
}
//实现发送端构造器
ObjectTx::ObjectTx(int id, QObject *parent):QObject(parent),id(id) {}
//实现发送信号封装接口函数
void ObjectTx::sendMessage(const char *message_str) {
//构造信息结构体
MessageStruct payload = {id, message_str};
//发送信号并等待槽函数返回
QString result = emit messageTransmit(payload);
//命令行输出
qDebug() << result;
}最后在main.cpp 之中使用定义的这两个类对象并且连接他们的槽和信号而且调用:
#include <QCoreApplication>
#include "MocExample.h"
int main(int argc, char *argv[]) {
//开启无GUI Qt应用程序
QCoreApplication a(argc, argv);
//注册通信结构体
qRegisterMetaType<MessageStruct>("MessageStruct");
//定义类对象
ObjectTx tx(1);
ObjectRx rx(2);
//连接信号,这里Qt::AutoConnection会自动选择Qt::DirectConnection
QObject::connect(&tx, &ObjectTx::messageTransmit, &rx, &ObjectRx::messageReceive);
//发送信息
tx.sendMessage("Hello, World!");
//执行应用程序主循环
return QCoreApplication::exec();
}
2.1.2 MOC机制之Qt属性
在Qt之中的每一个被Q_OBJECT 宏定义标记了元对象的类都可以定义他们的属性(Property),这也就是MOC机制之中第二重要的部分:Qt Properties。例如说我们正在开发一个EDA软件,定义一个类抽象化表示电阻,那么其中最重要的一项属性就是阻值,我们可以这样做:
#include<QObject>
class Resistor : public QObject {
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
explicit Resistor(QObject* parent = nullptr):QObject(parent),q_value(0){}
int value() const { return q_value; }
void setValue(int newValue){
if(q_value==newValue) return;
q_value = newValue;
emit valueChanged(q_value);
}
signals:
void valueChanged(int newValue);
private:
int m_value;
}在上述例子之中,我们看到使用Qt Properties功能的核心代码第一个是之前提到的Q_OBJECT 去触发MOC机制,使用Q_PROPERTY 开启Qt属性功能。这个宏定义用于在Qt元数据系统之中注册类对象的属性,这些属性可以通过元对象接口进行访问,这个宏定义之中可以使用这些关键字:
Q_PROPERTY(<type> <name>)之中type表示属性类型名,name表示属性对外名称READ <function_read>表示从外部读取该属性时调用的函数名为function_read,相当于一般类结构之中对应相关参数的getter方法WRITE <function_write>表示从外部写入该属性时调用的函数名为function_write,相当于一般类结构之中对应相关参数的setter方法NOTIFY <function_change>表示当属性值改变(无论是通过Qt的属性操作API改变还是通过类本身的API操作改变)时触发的信号为function_changeRESET <function_reset>此项目为可选项,表示如何将对应属性设置为初始值也就是重置属性值,设定为初始值使用的方法为function_resetSTORED true/false此项目为可选项,是一个布尔值。此选项表示是否将属性存储在对象之中也就是当对象序列化和反序列化存储/读取时是否需要计算这个属性。所谓序列化就是将运行时的数据做持久化存储,例如将一个对象的数据转换为JSON或者XML,所以开启了这个选项之后相关的属性值就可以持久化存储而不只是运行时存在。USER true/false此项目为可选项,是一个布尔值。此选项表示相关属性在分类上是否是一个“用户属性”,也就是在Qt Designer等工具之中是否会优先显示和编辑。CONSTANT该选项是单一关键字,如果该关键字存在,那么WRTIE和NOTIFY关键字段可以不定义。这个关键字表示属性是一个常量,当第一次被赋值(一般是构造对象时)后不可更改。FINAL该选项是单一关键字,如果该关键字存在,那么该类的任何子类无法覆盖此类之中对该属性的设置,能够保证这个属性在多态继承之中永远不变。
看到这里有些读者可能会感到疑惑:按照上述例子,我已经完全完成了这个参数在类之中的getter 方法和setter 方法,那么我为什么还需要使用Q_PROPERTY 功能呢?Qt属性的作用相比于手动构建的方法有以下这些优势:
C++/QML/Javascript/Python多语言代码(同一工程之中)都能够使用属性,这些语言通过Qt提供的属性操作API接触到我们编写的原生代码API,也就是提供一层操作接口。
序列化支持:Qt属性功能(使用了
STORED关键字)支持将对象存储为JSON或者是XML以摆脱运行时数据的暂时性,能够实现良好的数据持久化。支持数据绑定、自动更新和属性反射机制:
//数据绑定和自动更新可以通过槽和信号实现:
QProperty<int> source;
QProperty<int> target;
//定义一个Lambda函数当source改变时自动维持target=source+3
QObject::connect(&source, &Property<int>::valueChanged, [&](){target=source+3;});
//对一个类对象动态的加入属性值:
QObject* obj = new QObject();
obj->setProperty("value", 99);
int value = obj->property("value").toInt();2.1.3 MOC机制之类型信息
Qt的元对象系统允许在运行时查询对象的类型信息,每个QObject 的直接或者间接子类(所有的派生类)都有一个与之关联的QMetaObject 对象包括类的类型信息,例如:
class MyObject : public QObject{
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
explicit MyObject(QObject* parent = nullptr);
int value();
void setValue(int newValue);
Q_INVOKABLE void customMethod();
signals:
void valueChanged(int newValue);
private:
int q_value;
};
MyObject* obj = new MyObject();
//例如获得类型名称
obj->metaObject()->className();
//例如获得父类的QMetaObject对象
obj->metaObject()->superClass();
//例如获得所有已注册的Qt属性数量
obj->metaObject()->propertyCount();
//调用对象使用Q_INVOKABLE的方法
QMetaObject::invokeMethod(&obj, "customMethod");事实上,所谓的MOC类型信息机制就是Qt为缺失了完整的反射机制的C++代码外挂了一个在Qt框架下可用的反射机制。这样可以使得Qt的应用程序可以更加灵活地处理对象之间的交互、属性的查询和修改,以及方法的动态调用。这些动态特性极大地增强了Qt在开发复杂应用程序时的便利性和效率。
2.1.4 MOC机制之动态对象创建
Qt之中的MOC机制还支持动态对象创建机制。这一机制允许在运行时根据类型信息动态创建对象,而无需显式地在代码中实例化具体类型的对象。这种功能对实现工厂模式、插件系统和动态模块加载等场景非常有用。例如我们前面提到的注册自定义的结构体和类等(虽然在Qt6之中已经部分自动化):
class MyClass : public QObject {
Q_OBJECT
//more definition
}
Q_DECLARE_METATYPE(MyClass*)
qRegisterMetaType<MyClass>("MyClass");而一旦创建并且将自定义的类型信息注册到Qt的元对象系统之中后我们就可以在代码运行时动态的创建对象,而不仅仅是在编译时固定的使用某种数据结构,例如这个函数:
QVariant dynamicallyCreateObject(const QString& typeName){
//获取类型名对应的Object ID
int typeID = QMetaType::type(typeName.toUtf8().constData());
//如果该类型名并未注册,返回一个空的QVariant值
if(typeID == QMetaType::UnknownType) return QVariant();
//通过反射机制创建一个对象并且转换为QObject
QObject* rawObj = static_cast<QObject*>(QMetaType::create(typeID));
//作为QVariant返回该指针
return QVariant::fromValue(obj);
}
//使用案例:
MyClass* obj = nullptr;
QVariant objVariant = dyanmicallyCreateObject("MyClass");
if(objVariant.isValid())
obj = qvariant_cast<MyClass>(objVariant);动态对象创建机制事实上大大增加了Qt外挂的反射机制的用处,按照笔者的使用经验,在以下四个方面之中这种机制可以表现出相当大的灵活性和良好的开发体验:
工厂模式:可以使用动态对象创建机制实现通用的工厂类,根据传入的类型名称动态创建对象实例,而无需在代码中显式地调用特定类型的构造函数。
插件系统:在插件系统中,可以根据插件描述文件中的类型信息动态加载并实例化插件对象,实现插件的动态加载和运行。
持久化存储与跨应用传输:本质上是序列化和反序列化,在对象序列化和反序列化过程中,可以使用动态对象创建机制,根据存储的类型信息动态创建对象实例并恢复其状态。
动态界面生成:可以根据XML、JSON等配置文件描述的界面结构动态创建界面元素,实现灵活的界面生成。配合Qt的QML+Quick开发框架事半功倍,可以实现非常多酷炫的功能。
2.2 用户界面编译器UIC(User Interface Compiler)
Qt除了跨平台之外最广为人知的特性就是“这东西可以用来写界面”,而Qt在这方面最令人满足的点就是拥有一个非常用户友好和功能强大的设计工具:Qt Designer。这就带来一个问题:众所周知这个设计工具保存的设计文件是.ui 文件,格式是XML,这可怎么转换成编译器“能看懂”的C++代码呢?于是我们有了UIC机制来解决这个问题,首先查看一个例子的工程结构:
Tutorial
├── CMakeLists.txt
├── cmake-build-debug
├── src
│ └── main.cpp
└── ui
├── TestWindow.cpp
├── TestWindow.h
└── TestWindow.ui让我们首先记住这个工程结构,我们使用Qt Designer打开TestWindow.ui 完成这种设计:

我们不妨查看这个文件的XML文本内容,如下所示。笔者在其中做出了一些注释,事实上如果开发者对于Qt的UI框架非常熟悉也可以直接写XML代码,Qt Designer的只是提供一个可视化操作界面,背后仍然是XML代码在生效,只不过笔者做的注释在UIC和Qt Designer之中会自动被删除掉(可能是Qt不鼓励用户直接改动XML代码,他们还是对自己的设计工具更加喜爱,也确实好用)
<?xml version="1.0" encoding="UTF-8"?>
<!-- Qt Designer 版本号 -->
<ui version="4.0">
<!-- UI窗口类名,继承自QMainWindow,记住这个名字十分重要 -->
<class>TestWindow</class>
<widget class="QMainWindow" name="TestWindow">
<!-- 大小和位置:没有偏移(x=0,y=0) 宽度400px 高度300px -->
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<!-- 窗口显示名称 -->
<property name="windowTitle">
<string>TestWindow</string>
</property>
<!-- 本窗口删除了菜单栏menubar和状态栏statusbar,保留主体部分-->
<widget class="QWidget" name="centralwidget">
<!-- 定义下方按钮,对象名称changeButton -->
<widget class="QPushButton" name="changeButton">
<!-- 按钮位置和尺寸,偏移距离相对于父容器也就是窗口的主体部分 -->
<property name="geometry">
<rect>
<x>160</x>
<y>200</y>
<width>80</width>
<height>30</height>
</rect>
</property>
<!-- 固定尺寸大小 -->
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<!-- 按钮文本 -->
<property name="text">
<string>改变文本</string>
</property>
</widget>
<!-- 显示文本的标签对象 -->
<widget class="QLabel" name="label">
<!-- 标签的尺寸和位置 -->
<property name="geometry">
<rect>
<x>160</x>
<y>90</y>
<width>80</width>
<height>30</height>
</rect>
</property>
<!-- 按钮未点击是默认显示的文本 -->
<property name="text">
<string>文字未改变</string>
</property>
<!-- 设置文本居中对齐 -->
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</widget>
</widget>
</ui>我们接下来查看文件TestWindow.h
#ifndef TUTORIAL_TEST_WINDOW_H
#define TUTORIAL_TEST_WINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class TestWindow; }
QT_END_NAMESPACE
class TestWindow : public QMainWindow {
Q_OBJECT
public:
explicit TestWindow(QWidget *parent = nullptr);
~TestWindow() override;
private:
Ui::TestWindow *ui;
};
#endif //TUTORIAL_TEST_WINDOW_H以及窗口内在逻辑的具体实现文件TestWindow.cpp
#include "TestWindow.h"
#include "ui_TestWindow.h"
TestWindow::TestWindow(QWidget *parent) :QMainWindow(parent), ui(new Ui::TestWindow) {
ui->setupUi(this);
connect(ui->changeButton, &QPushButton::clicked, [this](){
ui->label->setText("文字已改变");
});
}
TestWindow::~TestWindow() {
delete ui;
}最后是运行Qt应用程序的主程序,也就是main.cpp
#include <QApplication>
#include "TestWindow.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
TestWindow window;
window.show();
return QApplication::exec();
}
我们可以看到主逻辑非常简单,也就是开启QApplication 之后新建一个TestWindow 的示例 并且将其显示出来,最后进入QApplication::exec 主循环。这个示例工程之中需要说明的部分:
TestWindow继承自QMainWindow也就是派生自QObject,那么它就必须有一个同样指向一个对象QObject的指针作为父对象。Qt程序在销毁一个QObject派生对象时会删除掉其所有直接或者间接的子对象以避免内存泄漏。而没有parent指针的对象是所谓的“顶层窗口/顶层对象”,当所有的顶层对象全部被销毁,Qt应用程序就会退出。在
TestWindow.cpp之中定义了一个Lambda函数作为下方按钮的点击槽函数,当按钮点击时将上方的标签文字转换为“文字已改变”从而完成一个简单的动态效果。
这时候,我们就会发现很多问题,主要集中在:
我们根本没有建立
ui_TestWindow.h这个头文件是从哪里包含进来的?为什么我的IDE静态分析时报错根本找不到这个文件?我们也根本没有定义命名空间
Ui那么类Ui::TestWindow从哪里定义?ui指针怎么就能够找到我们定义的按钮changeButton和标签label?
这时候我们就要将目光转移到UIC之中了:当我们看到工程的构建中间文件夹cmake-build-debug 时我们会发现这样一个文件:
Tutorial/cmake-build-debug/Tutorial_autogen/include/ui_TestWindow.h
哦!问题A解决了,我们找到了这个文件,那么我们接着查看这个文件的内容:
#ifndef UI_TEST_WINDOW_H
#define UI_TEST_WINDOW_H
#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>
QT_BEGIN_NAMESPACE
class Ui_TestWindow
{
public:
QWidget *centralwidget;
QPushButton *changeButton;
QLabel *label;
void setupUi(QMainWindow *TestWindow)
{
if (TestWindow->objectName().isEmpty())
TestWindow->setObjectName("TestWindow");
TestWindow->resize(400, 300);
centralwidget = new QWidget(TestWindow);
centralwidget->setObjectName("centralwidget");
changeButton = new QPushButton(centralwidget);
changeButton->setObjectName("changeButton");
changeButton->setGeometry(QRect(160, 200, 80, 30));
QSizePolicy sizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed);
sizePolicy.setHorizontalStretch(0);
sizePolicy.setVerticalStretch(0);
sizePolicy.setHeightForWidth(changeButton->sizePolicy().hasHeightForWidth());
changeButton->setSizePolicy(sizePolicy);
label = new QLabel(centralwidget);
label->setObjectName("label");
label->setGeometry(QRect(160, 90, 80, 30));
label->setAlignment(Qt::AlignmentFlag::AlignCenter);
TestWindow->setCentralWidget(centralwidget);
retranslateUi(TestWindow);
QMetaObject::connectSlotsByName(TestWindow);
} // setupUi
void retranslateUi(QMainWindow *TestWindow)
{
TestWindow->setWindowTitle(QCoreApplication::translate("TestWindow", "TestWindow", nullptr));
changeButton->setText(QCoreApplication::translate("TestWindow", "\346\224\271\345\217\230\346\226\207\346\234\254", nullptr));
label->setText(QCoreApplication::translate("TestWindow", "\346\226\207\345\255\227\346\234\252\346\224\271\345\217\230", nullptr));
} // retranslateUi
};
namespace Ui {
class TestWindow: public Ui_TestWindow {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_TESTWINDOW_H我们通过观察这个文件,可以做出以下理解:
ui_TestWindow.h之中定义了命名空间Ui和其中的类Ui::TestWindow并且使这个类继承之前定义的类Ui_TestWindow,也就是新建前者将会获得后者之中所有的成员和方法。在
TestWindow.h之中通过前置声明namespace Ui和类Ui::TestWindow让这个类的成员和方法通过指针TestWindow.ui在类TestWindow之中可用。方法
void retranslateUi(QMainWindow* TestWindow)是为了多语言兼容性和翻译器正常工作,这个我们不必在意,会在下下小节之中详述。方法
void setupUi(QMainWIndow* TestWindow)结合上述翻译方法将.ui文件之中的XML代码全部实现成为C++代码因此可以正常参与编译。类
Ui::TestWindow并不是一个QObject的派生对象,因而没有父指针也无法直接在Qt的垃圾回收析构链条之中自动回收,所以需要在TestWindow类的析构器之中显示的delete用于进行垃圾回收和防止内存泄漏。
综上所述,UIC的工作机理可以这样总结:核心UIC就是将XML代码转换为C++代码,而联系着两端代码的就是XML文件之中的<class>Name</class> 标签,UIC会在编译开始前的预处理阶段将代码完成转换并且生成文件ui_TestWindow.h 供我们使用。此时我们只要在我们的头文件之中前置声明这个类并且在源代码文件之中包含这个头文件即可使用,随后一起编译,如图所示:

2.3 资源编译器RCC(Resource Compiler)
在我们构建的各类应用程序之中,我们可能会用到很多“资源文件”,例如按钮图标的图标,窗口背景的图片,操作交互的动画和音频以及各种文档等等……而这样就会引起很多问题,例如如下这些令人感到烦躁的使用体验:
资源文件杂乱无章,当项目做大之后管理十分困难
如果你的应用程序需要分发给别人使用,甚至要跨平台,那么各个平台上的文件系统千奇百怪寻址方式令人摸不着头脑,考虑从文件系统之中读取数据的兼容性十分痛苦
一旦某些不可预知的错误发生在文件读取过程之中或者干脆说某些手贱的用户或者NT的优化软件删除掉了你所用的资源文件,你的应用程序可能直接趴窝
这个时候我们就需要RCC(Resource Compiler)机制的接续,也就是创建资源文件索引,那么我们在上一小节的目录结构之中新建一个res 文件夹,并且注入资源文件:
res
├── resource.qrc
├── doc
│ ├── about.md
│ ├── operate.sql
│ ├── protocol.xml
│ └── template.js
├── font
│ ├── adobe.ttf
│ ├── google-sans.ttf
│ ├── nano-sans.ttf
│ └── source-code-pro.ttf
├── icon
│ ├── action1.png
│ ├── action2.png
│ ├── action3.png
│ ├── app.png
│ ├── badge.png
│ ├── buttonA_state1.png
│ ├── buttonA_state2.png
│ ├── buttonB.png
│ ├── osx.icns
│ └── win.ico
└── style
├── button.qss
├── label.qss
├── scroll.qss
├── table.qss
└── toast.qss
在上述资源文件夹之中有几个字文件夹,icon文件夹里面存在的是按钮图标和桌面应用图标,font文件夹之中是我们自定义的不在系统字体之中的ttf字体文件,doc文件夹下存储的是我们用到的各类文档,style文件夹之中存放的是Qt的样式表qss文件。所有的资源文件通过.qrc 文件整合到Qt编译出的可执行二进制文件之中,我们这样写这个qrc文件:
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/style">
<file alias="button">./style/button.qss</file>
<file alias="label">./style/label.qss</file>
<file alias="scroll">./style/scroll.qss</file>
<file alias="table">./style/table.qss</file>
<file alias="toast">./style/toast.qss</file>
</qresource>
<qresource prefix="/icon">
<file alias="action1">./icon/action1.png</file>
<file alias="action1">./icon/action2.png</file>
<file alias="action1">./icon/action3.png</file>
<file alias="app">./icon/app.png</file>
<file alias="badge">./icon/badge.png</file>
<file alias="buttonA_state1">./icon/buttonA_state1.png</file>
<file alias="buttonA_state2">./icon/buttonA_state2.png</file>
<file alias="buttonB">./icon/buttonB.png</file>
</qresource>
<qresource prefix="/font">
<file alias="adobe">./font/adobe.ttf</file>
<file alias="google">./font/google-sans.ttf</file>
<file alias="nano">./font/nano-sans.ttf</file>
<file alias="source-code-pro">./font/source-code-pro.ttf</file>
</qresource>
<qresource prefix="/doc">
<file alias="template.js">./doc/template.js</file>
<file alias="operate.sql">./doc/operate.sql</file>
<file alias="protocol.xml">./doc/protocol.xml</file>
<file alias="about.md">./doc/about.md</file>
</qresource>
</RCC>随后我们在CMakeLists.txt 之中加入这样的语句即可将资源整合进编译出的可执行二进制文件:
#开启自动RCC
set(CMAKE_AUTORCC ON)
#设定资源文件
set(${QRC_FILE} "res/resource.qrc")
qt_add_resources(RESOURCES ${QRC_FILE})
#将资源文件整合到target之中
add_executable(Tutorial ${SOURCE} ${UI} ${RESOURCE} ${HEADER})我们可以看到.qrc 文件的一些编写规则:
声明文档类型为
<!DOCTYPE RCC>,所有的内容要用标签<RCC version="1.0"></RCC>包裹资源条目可以进行分组,分组内容用
<qresource></qresource>包括,在这个标签之中我们可以使用属性prefix设定资源引用时的前缀每个条目不管类型是什么都可以使用
<file></file>标签包裹,内容是资源文件的绝对路径或者相对于qrc文件的路径,其中的alias属性是该条目的引用别名
假设我们现在要在C++代码之中使用我们设置的res/icon/app.png 作为应用程序运行时显示在任务栏或者Dock的图标,那么我们可以这样写:
auto appIcon = new QIcon(":/icon/app");
QApplication::setWindowIcon(*appIcon);可以看到,我们通过RCC机制可以将文件整合为
:/prefix/alias进行引用,在早期的Qt版本之中这种写法只能使用qrc:/prefix/alias写,但是目前的Qt版本二者都兼容。标签
<qresource></qresource>可以嵌套使用,这个标签和<file>标签就像是qrc文件系统之中的目录和文件一样,当然qresource标签需要指定prefix才可以当作目录使用。我们可以不指定属性
alias或者指定之后也不使用,直接使用:/+file标签之中指定的资源文件路径去调用资源文件。qrc文件可以被RCC编译器编译为二进制文件rcc或者以文本格式书写的包含完整二进制数据的源代码文件resource.cpp例如:
#编译为RCC二进制文件
rcc -o resources.rcc resource.qrc
#编译为包含二进制数据但是序列化为文本的cpp代码
rcc -name my_resource -o resources.cpp resource.qrc
#-name可以缺省,这个参数将会指定代码之中使用资源数据的的namespaceQt应用程序在运行时可以动态的调用文件系统之中能够访问到的
rcc文件而不能直接使用未编译为二进制数据的资源索引qrc文件,例如:
//动态注册挂载RCC
QResource::registerResource("path/to/dynamic_resource.rcc");
//使用其中的图片
QPixmap pixmap(":/prefix/in/dynamic_resource/alias")
//动态注销卸载RCC
QResource::unregisterResource("path/to/dynamic_resource.rcc");
2.4 多语言编译器与翻译器(Qt Translator/Linguist)
Qt框架另一个令人十分心动的功能就是Qt开发的多语言支持——在一个有可能在多种语言场景下使用的APP之中,为每种语言适配语言支持包是万分痛苦的,而Qt帮助我们便捷的实现了这一功能。Qt的多语言环境的加载机制可以通过这几点来概括:
Qt官方提供的多语言支持和翻译工作台是Qt Linguist,翻译语言包文件是
.ts后缀的符合xml语法的文件,文件结构大概为:
<?xml version="1.0" encoding="utf-8"?>
<!-- 声明文档类型为TS, language为语言缩写,中文为zh_CN -->
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<!-- 每个类的可翻译字符串分一个组,每个组是一个context -->
<context>
<!--Context 名称-->
<name>ContextName</name>
<!-- 每个字符串都是一条message -->
<message>
<!-- location 条目可以有很多,代表着这字符串在代码的什么地方使用 -->
<location></location>
<!-- source 就是可翻译字符串的名字,可以直接使用字符串的英文表述作为source -->
<source>sample text</source>
<!-- translation 标签包裹的是译文 -->
<translation>示例文本</translation>
</message>
</context>
</TS>可翻译字符串不能够直接使用,需要这样在代码之中进行标记,才能够对应到翻译文件之中声明的可翻译字符串从而完成翻译,如果在翻译文件之中找不到字符串,将会直接使用代码之中写入的字面值而不进行任何替换,因此字符串名称常常直接使用英文原文:
//完整的根据类名和字符串名使用, Class Name只要和ts之中的context>name相同即可,不必真有这个类
QApplication::translate("Class Name", "String Name");
//在类内省略类名直接使用:
tr("String Name");被标记过后的源代码可以使用
lupdate指令生成翻译文件,例如我想要将翻译文件生成到这样的路径,包括两种语言,那么我应当在一个含有所有源代码的目录例如Tutorial之中执行这样的命令,这条命令的位置取决于Qt的安装位置:
lupdate ./ -ts ./res/trans/zh_CN.ts
lupdate ./ -ts ./res/trans/en.ts在上述命令生成了所有的
ts文件之后,会将上文提到的文件结构都补全,只剩下所有的翻译译文标签translate之中的内容没有完成——这也就是Qt Linguist之中需要完成的工作,当然用户也可以直接更改XML代码。Qt应用程序不能够直接使用这种文件——需要转换为二进制翻译文件也就是qm格式的文件,这也是需要通过命令完成:
#这两条命令将会生成二进制的翻译文件 zh_CN.qm 和 en.qm
lrelease ./res/trans/zh_CN.ts
lrelease ./res/trans/en.ts在代码通过
QApplication::translate或者tr使用翻译文件之前必须先加载Qt翻译器,每个翻译器只能加载一种语言,加载翻译器之后执行翻译语句才会替换字符串。同样的更改翻译器之后无法自动动态更改翻译语句,必须重新执行翻译语句才可以:
//定义翻译器
QTranslator* translator = new QTranslator(this);
//加载对应语言,如果要更换语言必须先卸载翻译器,重新加载之后再安装
if(translator->load("path/to/your/qm_file")){
//如果加载没成功那么返回false,如果加载成功那么安装翻译器
QApplication::installTranslator(translator);
}
//卸载翻译器
QApplication::removeTranslator(translator);在
CMakeLists.txt之中其实可以采用更加自动化的方法使所有的翻译文件自动整合到RCC体系之中那么就可以将翻译文件当作资源文件使用了,具体的使用方法在后文之中介绍。

C. Qt的安装与CMake构建
通过前文的叙述,我们了解到Qt开发之中丰富的接口和灵活的机制,但是本质上还是C++代码和二进制数据,那么我们从本质上来说需要“最小化支持”的部分就是编译和运行时支持,这就需要我们安装至少有构建工具、编译工具链以及Qt开发框架。在Qt免费的开源版本之中,我们可以采用两种方式安装Qt:直接下载打包好的Qt开发框架或者从开源Qt代码在目标运行环境之中编译整个Qt库。由于Qt官方对于常见的系统和运行平台都有支持,所以我们主要介绍前者。关于从开源Qt在目标环境之中编译的问题,我们将会在其他主要描述Qt的跨平台开发和交叉编译的文章之中介绍。
1.从Qt在线安装工具安装Qt
直接下载打包好的Qt开发框架的安装方式最方便的实现方法就是使用Qt在线安装程序,而要实现这一点我们需要首先申请一个Qt账号,首先打开


1.1 Windwos下Qt的安装和组件选择说明
我们首先要解决的是Qt安装过程之中下载缓慢的问题:Qt的安装器采用的安装策略是在线组件化安装也就是根据用户选择的组件从位于国外的Qt官网下载对应的数据——这就导致中国大陆访问资源速度缓慢,在数十GB的数据量下数百Kbps的速度实在是令人绝望。我们不能直接通过GUI点击的方式启动安装器,而是要在命令行之中指定安装器使用的镜像源,这里我们使用中科大镜像:
.\qt-unified-windows-x64-4.7.0-online.exe --mirror https://mirrors.ustc.edu.cn/qtproject安装过程截图如下所示,步骤主要包括:
启动Qt安装器后输入我们之前注册的账号密码等待验证通过
确认我们同意Qt的开源规范并且不属于任何商业公司,是个人开发者
等待Welcome界面连接到Qt仓库拉取版本和组件列表
我选择不向Qt官方共享我的数据和使用记录,读者可以自行选择
选择安装目录,我这里选择
C:\Qt并且选择Custom Installation自定义安装组件选择组件,图中选择完整的安装稳定的发行版
6.7.0版本,Qt Design Studio以及Developer and Design Tools,读者在这一步时可以先参照下文之中的Qt组件说明自行选择同意Qt安装组件之中所有的开源协议
设置Qt在开始菜单之中的文件夹,我这里选择默认的
Qt名称,在本文介绍的构建方法之中这个选项意义不大,只涉及维护工具或者打开Qt Creator的部分确认安装体积,点击
Install开始下载并且安装,等待安装完毕即可









下面来详细说明Window下安装Qt可选的所有组件的功能和必要性,这里以笔者本文之中的6.7.0 为主要示例,对于其他版本而言,Qt5.x 可能略有不同,但是基本上大同小异:
Preview目录下存放的是正处于开发之中的预览版Qt开发框架,想尝鲜的读者可以安装试试,但是对于要使用于生产环境或者作为学习起步用途的读者来说不建议尝试Qt Design Studio目录下存放的是Qt官方提供的UI设计工具,一般来说这个文件夹下会有这样三个选项,这里只是举例,具体版本以实际安装器显示为准:Qt Design Studio 4.6.0-SNAPSHOT用于尝鲜的快照版本,不建议使用Qt Design Studio 4.1.0-LTSLTS(Long Time Support)长期支持维护版本,对于出现的各种问题和疑难杂症官方和论坛都有良好的可用资料,虽然版本略微落后吃不到最新的特性,但是胜在LTS,这是笔者最推荐的版本Qt Desgin Studio 4.5.0当前最新的稳定版本,可用,但是资料上肯定不如LTS强
Qt目录下就是真正的Qt开发框架了,目前Qt的大版本已经来到了Qt6,对于想要安装之前版本的读者,应当在组件选择界面右侧勾选Archives之后点击筛选按钮重新加载已经归档的老旧版本即可获得完整列表。这里以Qt/6.7.0为例介绍各个组件:WebAssembly这个组件其实由单线程single-thread和多线程multi-thread两个部分共同构成但是我们可以只安装一个。此组件提供对于WEB资源的访问和管理功能,如果用户没有过多的Web需求其实完全可以不安装。SourcesQt开源版本的源代码,如果读者没有直接阅读源代码以深层利用Qt框架机制或者规避不良特性的需求就可以不安装AndroidQt用于Android移动端系统开发的组件,不建议使用,对比原生Android开发框架而言支持性和兼容性都不是十分良好下面要提到一个编译过程之中核心的概念:工具链。我们在前文中已经知道,Qt开发框架并不是让我们从源代码开始编译整个支持库,而是提供了静态链接库和动态链接库,那么静态链接库和动态链接库是否与我们编译出的程序兼容呢?如果他们使用不同的编译器呢?如果他们甚至是不同的架构和总线位宽呢?所以我们对于编译器,链接器等等工具有一套完整的要求,根据这些要求我们需要选择版本不同的支持库。
例如我们工程使用Visual Studio的MSVC工具链进行编译,那么显然不能使用MinGW的支持库;又比如我们工程使用高版本的对C20版本支持良好的MinGW那我们显然不能使用低版本的只支持到C20早期特性的MinGW的支持库;再比如我们的工程和支持库甚至用的都是同一个版本的MinGW但是前者使用SJLJ模式后者使用SEH模式……
从根本上解决上述问题的方案是:确定最终工程使用的工具链版本,使用该工具链从Qt源代码开始直接编译一套和自己的工程完全适配的支持库,但是那显然就不是本文重点探讨的内容了。这部分内容笔者准备放在前文提到的Qt交叉编译相关的文章之中一起说。MinGW 11.2.0 64-bit用于适配64位MinGW工具链的支持库,使用11.2.0版本编译而成,记住这个版本,下文涉及到工具链配置需要匹配版本MSVC 2019 64-bit用于适配64位MSVC工具链的支持库,虽然标记版本为2019但是实际上在当下正在发行的MSVC2022工具链也能匹配LLVM-MinGW 17.0.6 64-bit用于适配64位LLVM-MinGW工具链的支持库,LVM-MinGW 是一个跨平台的工具链,它结合了 LLVM 项目的编译器(如 Clang 和 LLD)与 MinGW(Minimalist GNU for Windows)项目的库和工具,用于编译 Windows 应用程序。这些工具可以在非 Windows 平台(如 Linux 和 macOS)上编译和链接 Windows 可执行文件和库,这个主要是为了跨平台开发。MSVC 2019 ARM64(TP)针对运行在64位ARM架构的平台上的MSVC工具链支持库,TP缩写表示的是Technical Preview属于技术预览版本,不太稳定,可能出现相当多的BUG。不过在ARM平台上运行MSVC??
Qt Debug Information Files是对于MinGW或者GCC编译出二进制程序提供调试信息的支持库,这个组件建议安装,在碰到稀奇古怪BUG的时候或许有奇效。Qt Quick 3D等零零散散的库和Additinal Libraries之中的组件在前文Qt生态和开发框架组态之中有详细介绍,此处不再赘述
Qt/Developer and Designer Tools目录下是与开发框架无关但是在设计和开发过程之中常常用到的工具包,其中大致包括这些内容:Qt Maintenance Tool维护工具,在第一次安装Qt之后如果我们想要增加或者删除组件就启动这个工具就可以了,这个工具是Qt强制安装的。Open SSL Toolkit如果有使用SSL相关内容的需求可以安装,不过一般来说大多数的场景实际上用不到这个工具包。Debugging Tools for Windows适配Windows系统的调试工具,这个组件强烈推荐安装否则在调试过程之中可能遭受一些相当痛苦的体验。Qt Creator这是Qt官方的IDE,笔者自己包括在本文下文之中主要介绍的是CLion开发Qt的环境搭建,可见笔者平常几乎不用这个IDE,不过还是推荐安装,它有几个附属组件,这些附属组件如果读者不是主力使用Qt Creator开发就不要安装了:CDB Debugger Support,控制台调试桥(Console Debug Bridge)支持包
Debug Symbols,提供调试符号文件
Pulgin Development,基于Qt Creator的插件开发支持包
Qt Installer Framework用于制作Qt程序安装包的打包器,笔者用起来使用体验不错,如果读者有使用安装包分发自己构建的软件的需求和打算可以安装,相当好用。CMake和Ninja构建工具,不推荐通过这种方式安装。第一是因为如果使用CLion开发那么CLion本身内置了这个工具;第二是对于这种通用的构建工具除非你只开发Qt程序,否则更好的方式是官方独立安装这两个工具然后注册到系统环境变量比较好。各种工具链,包括LLVM和MinGW,如果读者前面安装了LLVM和MinGW的支持库并且打算使用它们那么在这里安装对应版本的工具链就不需要单独安装工具链了——这和前面提到的CMake不同,LLVM和MinGW工具链版本繁多复杂,很容易和支持库产生不兼容问题,不妨直接安装对应版本的工具链一次到位。例如之前提到
Qt6.7.0的MinGW支持库的版本标注是11.2.0,那么这里就应当勾选MinGW 11.2.0 64-bit。至于MSVC工具链Qt并不提供,MSVC工具链应当通过Visual Studio的C++工作负载安装。
1.2 MacOS下Qt的安装和组件选择说明
在MacOS下使用在线安装器安装Qt除了组件选择部分之外和Windows下安装并无二致:同样是下载对应系统的在线安装器之后按照上文给出的步骤逐步安装即可,此处主要说明一下MacOS下Qt各个组件的选择和对应功能和DMG安装包如何换源启动的问题:
MacOS下我们下载的安装器可能是一个
.dmg镜像,那么更换镜像源的问题可能略有不同:
#假设我们下载的安装器路径是/Users/<username>/Downloads/qt-unified-macOS-x64-4.7.0-online.dmg
#首先先挂载镜像
hdiutil attach ~/Downloads/qt-unified-macOS-x64-4.7.0-online.dmg
#随后进入挂载目录
cd /Volumes/qt-unified-macOS-x64-4.7.0-online.dmg
#从App Bundle之中定位可执行入口并执行镜像换源启动安装器
./qt-unified-macOS-x64-4.7.0-online.app/Contents/MacOS/qt-unified-macOS-x64-4.7.0-online --mirror https://mirrors.ustc.edu.cn/qtprojectPreview目录下同样是预览版本的Qt开发框架Qt Design Studio目录下同样是各个版本的Qt设计工具Qt/6.7.0目录下就是我们要安装的Qt开发框架的主体部分的组件内容了:Desktop表示桌面应用开发框架也就是GUI支持WebAssembly有单线程和多线程两个部分,这是WEB支持总成,包括多个用于提供Web资源访问与管理的组件Android和IOS分别对应两个移动端系统下的Qt开发框架支持库,但是笔者并不建议安装这两个之中的任何一个——Android和IOS原生开发框架还是比Qt的移动端框架支持完整性和兼容性都要好太多了Qt Quick 3D在Quick开发框架下使用3D渲染和坐标转换的支持Qt Quick Timeline在Quick开发框架下使用时间序列关键帧渲染动效的支持Qt Shader ToolsQt之中的着色器和渲染器支持工具包Qt5 Compatibility Module用于向下兼容Qt5 API的模块,这个强烈建议安装SourceQt本版本源代码,没有阅读源码需求的可以不安装Additional Libraries这个目录下事Qt的扩展库支持,具体内容参照前文对于Qt生态和开发框架之中各个组件功能的描述,这里不做赘述
Qt/Developer and Designer Tools目录下依旧是开发者工具:Qt Maintenance Tool维护工具仍然必须安装MacOS下我们一般而言通过XCode继承GCC/G++工具也就是配置好了工具链,所以不同于Windows下的安装模式,MacOS下不需要安装任何工具链
MacOS下更好的包管理方式是通过HomeBrew等包管理工具进行管理,所以笔者非常不建议通过Qt安装器安装一个独属于Qt的
CMake和Ninja如果读者确认不使用
Qt Creator构建工程可以选择不安装,同样其他Qt Creator附属的工具例如Debug Symbols和Plugin Development如无必要不应当安装如果读者有进行安装包打包的需求可以安装
Qt Installer Frame Work
2.配置CLion采用CMake构建Qt项目
在开始配置CLion+CMake+Qt最终的舒适开发环境之前我们还是应当对于我们要配置的环境之中有哪些组件起到什么作用有一个清晰的了解和总结:

Qt开发工具之中我们安装了对应的工具链(如果是MacOS或者Linux那么使用XCode或者包管理器安装的GCC也就你默认工具链即可)提供软件构建过程之中核心的编译步骤支持。
Qt开发框架之中我们安装了对应的工具链兼容的支持库,其中的
Qt6Config.cmake将会将这些支持库与头文件通过CMake加载到CLion之中。Qt Design Studio提供设计工具辅助我们进行UI界面设计和翻译等工作。
CLion内置了
CMake和构建器例如Ninja,当然我们也可以独立安装这些工具,不过直接使用CLion的内置工具没有任何问题,使用体验非常良好。CLion加载支持库之后为我们的代码编制索引、执行静态分析、提供数据流分析和代码补全,让我们能够专注于代码编辑和逻辑实现。
上述几乎所有操作CLion都能提供流畅易用的操作界面给开发者,最终辅助我们得到构建完成的目标软件二进制程序,并且通过CLion集成的GDB进行调试。
2.1 CLion开发环境基础配置与工具链设置
首先我们到JetBrains官网安装CLion,下载后有试用期,如果您是开源软件的开发者或者从事教育行业可以向JetBrains申请获取免费的License。打开IDE之后新建一个Qt工程,我们以一个GUI工程为案例那么就是新建一个Qt微件可执行文件(Qt Widget Executable),新工程共有五项配置:

因为我们新建的是有图形界面(GUI)的工程,不管是QWidget还是Quick开发都要选择图中所示的工程类型。这里要说明的是,工程实际上都是一个个的文本文档组成的,选择工程类型的目的只是让CLion为我们生成和一个模版作为工程的初始化而已。
这个位置指的是我们要把工程文件存储在哪里,最后一级目录可以不存在CLion会自动创建但是该目录最好为空以免产生一些不可知的错误。
这个前缀目录指的就是我们Qt开发框架支持库的存放位置,具体来说是提供一个CMake可以找到Qt支持库模块(找到
QtConfig.cmake文件)的路径,这个路径将会在CMake配置之中转变为一个变量CMAKE_PREFIX_APATH我们可以这样配置:
#例如在MacOS下,你的Qt整体安装路径是/Library/Qt版本是6.7.0
/Library/Qt/6.7.0/macos/lib/cmake
#例如在Windows下使用工具链MinGW对应的支持库,Qt安装位置C:/Qt
C:/Qt/6.7.0/mingw64/lib/cmake
#如果采用MSVC进行编译,那么需要找到MSVC对应的支持库位置
C:/Qt/6.7.0/msvc2019_64/lib/cmake
#在以上目录下能够找到Qt6/Qt6Config.cmake文件,当然如果胜率/lib/cmake路径CMake实际也能递归找到文件配置C++语言执行标准,一般配置C17或者C20,如果你想用到一些高级语法和新特性最好选择C20标准,当前的Qt6.7.0也全面支持了C20标准。如果选择标准过低,那么很多Qt的库有可能无法正常使用和加载。
本文之中安装的Qt版本是6.7.0那么选择6即可,如果读者安装了其他版本例如
5.1.5.2这个位置就写Qt的第一位大版本也就是5即可。
新建好工程之后应当具有相当简单的目录结构,为了开发过程的流畅,我们需要首先配置用于编译的工具链,如果您使用MacOS,那么需要通过XCode安装您Mac上可用的GCC,安装后CLiion会自动识别系统之中的GCC工具链,不需要额外配置任何参数:
xcode-select --install
#安装完成后我们查看gcc的版本验证是否安装成功
gcc -version如果您使用WIndows构建Qt工程那么可能略微麻烦一些,需要分情况讨论:如果您决定采用LLVM或者是MinGW构建Qt工程那么您需要找到我们之前提到的Qt安装之中安装的工具链的bin 文件夹并且在CLion设置>构建,执行,部署>工具链 之中新建一个工具链,这里以MinGW 11.2.0为例子,您找到的路径应当是:
#假设您的Qt整体安装位置为C:/Qt
C:/Qt/Tools/mingw1120_64/bin
而如果您决定采用MSVC构建Qt工程您需要首先安装Visual Studio并且一定要安装C++工作负载,例如说您安装了Visual Sutdio 2022并且设置了C++工作负载,按照64位工具链配置您应当有以下路径并且在CLion之中可以如下图识别:
#假设您将VS2022安装到系统Program Files文件夹之中
C:\Program Files\Microsoft Visual Studio\2022\Community
完成工具链配置之后我们按照下图流程选择CLion之中CMake配置文件使用的工具链为我们刚刚配置的工具链以事我们的配置生效,这里以Qt MinGW工具链为例子:


等待CLion重新加载Cmake配置编制索引无错误发生后,我们点击运行项目的按钮即可以Debug 模式编译并运行Qt项目,main.cpp 之中有CLion自动生成的样例代码,会弹出一个文字为“Hello world”的QPushButton 按钮,至此Qt开发环境已经能够正常编译并且运行项目了。
2.2 Qt设计工具设置为外部工具并新建UI类
下面,我们来配置外部工具,当我们在CLion之中直接点击.ui 界面设计文件或.ts 翻译文件时往往会直接展示这些文件内部的XML代码而不是打开对应的Qt设计工具,这时候我们需要在CLion之中添加外部工具以打开它们。打开CLion设置>工具>外部工具 新建一个名为Qt 的工具组,在组内添加两个工具:Qt Designer 和Qt Linguist ,这两个工具应当和您常用的工具链一致,我们以MinGW举例,如图所示:



#如果是在MacOS下对应的两个可执行文件路径为:
/Users/fenice/Qt/6.7.0/macos/bin/Linguist.app/Contents/MacOS/Linguist
/Users/fenice/Qt/6.7.0/macos/bin/Designer.app/Contents/MacOS/Designer
#如果是在Windows下对应的两个可执行文件路径为:
C:/Qt/6.7.0/mingw_64/bin/designer.exe
C:/Qt/6.7.0/mingw_64/bin/linguist.exe随后我们尝试通过CLion创建一个继承了QMainWindow 的UI类作为主窗口,并且配置参数如下:


这里解释参数配置方案:
名称:这将是新创建的类的名称,我们之后就可以在代码之中使用一个名为
SampleWindow的类。文件名基:请读者回顾B部分之中提到的UIC机制,我们配置文件名基为SampleWindow那么CLion将会根据内置模板为我们创建三个文件:
SampleWindow.h新的UI窗口类的声明头文件SampleWindow.cpp新的UI窗口类的实现源代码文件SampleWindow.ui新的UI窗口类对接Qt Designer的界面设计文件
父类:可以选择
QWidget创建最基本的组件,或者QDialog创建一个对话框,或者QMainWindow创建一个主窗口,如果不选择这三类可以写入一个自定义的父类名称。命名空间:之前在UIC之中提到的用于桥接C++代码和UIC自动生成代码的
namespace的名称,默认Ui是否添加到目标:勾选后将会自动改动
CMakeLists.txt文件将上述三个文件加入到编译之中
我们对应的修改main.cpp 文件如下所示,显示我们创建的窗口:
#include <QApplication>
#include "SampleWindow.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
SampleWindow window;
window.show();
return QApplication::exec();
}接下来我们右键文件SampleWindow.ui 可以看到我们的Qt外部工具组,在其中选择使用Qt Designer打开,并且完成如下设计:
窗体大小400*300,删除默认菜单栏,删除默认状态栏
插入
QPushButton设置高度30宽度100,坐标(150,200),名称changeButton,默认文本"Re-Translate"插入
QLabel设置高度30宽度100,坐标(150,100),名称languageName,more文本"English",居中设置窗体标题"Sample Project Window"
保存UI文件之后关闭Qt Designer重新编译运行工程,可见该项机制测试成功:

2.3 测试QRC集成资源和动态切换翻译器
首先我们完成这样一个逻辑:点击下方按钮时改变窗口语言,在中文和英文之间切换,根据前文在UIC机制之中的分析我们不难发现在UIC自动生成的头文件:
cmake-build-debug/SampleWindow_autogen/include/ui_SampleWindow.h
我们就能够发现自动生成的类内包含了一个函数用于翻译被Qt Designer标记为可翻译字符串的文本:
void retranslateUi(QMainWindow *SampleWindow)
{
SampleWindow->setWindowTitle(QCoreApplication::translate("SampleWindow", "Sample Project Window", nullptr));
changeButton->setText(QCoreApplication::translate("SampleWindow", "Re-Translate", nullptr));
langaugeName->setText(QCoreApplication::translate("SampleWindow", "English", nullptr));
} // retranslateUi那么我们只要在点击按钮时切换翻译器并且执行这个函数就可以了——而我们本身文本未翻译时就是英文,所以我们只要制作中文语言包zh_CN.ts 和zh_CN.qm 即可完成翻译,而将他们通过QRC机制集成到资源文件系统之中就可以直接在代码之中使用了。首先我们假定存在一个翻译二进制文件:zh_CN.qm 我们新建一个QRC文件名为resources.qrc并且这样写:
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="trans">
<file alias="zh_CN">./zh_CN.qm</file>
</qresource>
</RCC>随后我们需要在CMakeLists.txt之中添加资源文件并且讲资源文件添加到我们的编译目标之中:
#新增语句:添加资源文件
qt_add_resources(RESOURCES resources.qrc)
#修改语句:将资源文件添加到编译目标
add_executable(Sample main.cpp
SampleWindow.cpp
SampleWindow.h
SampleWindow.ui
${RESOURCES})随后我们就可以在代码之中使用这个翻译文件,我们修改SampleWindow.cpp 添加按钮的回调函数:
#include <QTranslator>
#include <QTimer>
#include "SampleWindow.h"
#include "ui_SampleWindow.h"
SampleWindow::SampleWindow(QWidget *parent) :
QMainWindow(parent), ui(new Ui::SampleWindow) {
ui->setupUi(this);
//从QRC文件系统加载中文翻译器
auto translator = new QTranslator();
if(!translator->load(":/trans/zh_CN")){
//如果加载失败那么输出Debug信息后1000ms以-1错误代码退出App
qDebug() << "Translation Error!";
QTimer::singleShot(1000,[](){QApplication::exit(-1);});
}
connect(ui->changeButton,&QPushButton::clicked, [this,translator](){
static bool isChinese = false;
if(isChinese) QApplication::removeTranslator(translator);
else QApplication::installTranslator(translator);
isChinese = !isChinese;
ui->retranslateUi(this);
});
}
SampleWindow::~SampleWindow() {
delete ui;
}我们现在来使用命令lupdate 和lrelease 制作翻译包,这两个命令同样也存在于对应工具链支持库之下,我们在CLion的终端之中直接执行命令先创建翻译文本文件:
C:/Qt/6.7.0/mingw_64/bin/lupdate.exe ./ -ts zh_CN.ts这样就会创建翻译文件,由于文件扩展名相同的问题翻译文件会被识别为TypeScript 脚本进而涌现出若干对于此文件的TS语法分析和静态分析报错,我们只要右键该文件设置”重写文件类型“更改为XML 即可。随后我们右键该翻译文件使用外部工具之中我们之前设置的Qt Linguist 打开该文件完成翻译即可:


保存文件之后我们使用lupdate 指令编译翻译文件生成二进制翻译器:
C:/Qt/6.7.0/mingw_64/bin/lrelease.exe ./zh_CN.ts 随后重新编译执行可见完整效果:

到这里位置我们已经完成了CLion+CMake+Qt开发环境的全部搭建和基本功能测试流程,本文之中使用的样例工程的压缩包可以由此下载。笔者将设置CMAKE_PREFIX_PATH 的语句注释掉了,所以直接加载工程CMake会报错,使用者必须先更改CMakeLists.txt 文件设置该变量为自己的安装路径方可应用。
3.CMake+Qt工程结构样例
笔者在这里给出一个基于QWidget 构建工程的项目结构示例,这不代表每个Qt工程都可以这样配置,例如使用QML的工程和考虑QTest的工程或者编译后考虑部署的工程肯定都要对CMakeList 做出修改,这里仅仅只是给出一个例子,对于更具体的依赖问题或者部署问题会在其他文章中详细阐述。首先是目录结构:
Template
├─build #用于存放CMake构建文件的目录
├─CMakeLists.txt #CMake工程配置文件
├─res #资源文件夹
│ ├─doc #文档资源
│ ├─font #自定义字体,假设均为.ttf
│ ├─icon #图标资源,假设均为.png
│ ├─style #.qss样式表文件
│ └─trans #.ts翻译文件
├─core #存放核心逻辑类和代码的.cpp/.h
│ └─main.cpp
└─ui #存放GUI界面的类和代码的.cpp/.h其中的CMakeLists.txt我们这样设置:
#这个版本可以根据实际Cmake版本减小
cmake_minimum_required(VERSION 3.27)
#设置工程名
project(Tempalte)
#设置强制需求C20标准
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
#设置自动开启MOC|RCC|UIC三大Qt预处理编译器
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
#检查加载cmake项目时是否制定了CMAKE_PREFIX_PATH即Qt安装路径
if(NOT DEFINED CMAKE_PREFIX_PATH OR NOT EXISTS ${CMAKE_PREFIX_PATH})
message(FATAL_ERROR "Please set CMAKE_PREFIX_PATH to specify Qt install path, this path should contain config .cmake")
endif()
message(STATUS "Qt install path: ${CMAKE_PREFIX_PATH}")
#设置Qt需要用到的组件,根据工程特性可以自由添加,这里仅仅是最小化需求
set(QT_COMPONENTS
Core #Qt核心库
Gui #Qt图形界面库
Widgets #使用基于QWidget的方式进行界面构建
LinguistTools #自动化翻译器编译工具
)
#使CMake识别需要的组件
find_package(Qt6 COMPONENTS ${QT_COMPONENTS} REQUIRED)
#设置include目录为core和ui
include_directories("core" "ui")
#在源代码目录下提取所有的.cpp作为源文件所有的.h作为头文件
file(GLOB_RECURSE HEADER "core/*.h" "ui/*.h")
file(GLOB_RECURSE SOURCE "core/*.cpp" "ui/*.cpp")
#在ui文件夹之中提取所有的.ui界面配置文件供UIC使用
file(GLOB_RECURSE UI "ui/*.ui")
#加载所有然亦文件并且将其与源文件建立翻译搜索关系
file(GLOB_RECURSE TS "res/trans/*.ts")
qt_create_translation(QM ${SOURCE} ${TS})
#我们不手动写resources.qrc转而使用自动脚本生成qrc文件
set(QRC_CONTENT "<!DOCTYPE RCC>\n<RCC version=\"1.0\">\n")
#将自动生成的所有.qm文件全部整合进qrc,集体前缀trans并且alias为语言缩写
set(QRC_CONTENT "${QRC_CONTENT}\t<qresource prefix=\"/trans\">\n")
foreach (QM_FILE IN LISTS QM)
get_filename_component(QM_FILE_NAME ${QM_FILE} NAME_WE)
set(QRC_CONTENT "${QRC_CONTENT}\t\t<file alias=\"${QM_FILE_NAME}\">${QM_FILE}</file>\n")
endforeach ()
set(QRC_CONTENT "${QRC_CONTENT}\t</qresource>\n")
#将icon目录下所有的png图片全部整合进qrc,集体前缀icon并且alias为不带后缀的文件名
file(GLOB PNG "res/icon/*.png")
set(QRC_CONTENT "${QRC_CONTENT}\t<qresource prefix=\"/icon\">\n")
foreach (PNG_FILE IN LISTS PNG)
get_filename_component(PNG_FILE_NAME ${PNG_FILE} NAME_WE)
set(QRC_CONTENT "${QRC_CONTENT}\t\t<file alias=\"${PNG_FILE_NAME}\">${PNG_FILE}</file>\n")
endforeach ()
set(QRC_CONTENT "${QRC_CONTENT}\t</qresource>\n")
#将style目录下所有的qss样式表全部整合进qrc,集体前缀style并且alias为不带后缀的文件名
file(GLOB QSS "res/style/*.qss")
set(QRC_CONTENT "${QRC_CONTENT}\t<qresource prefix=\"/style\">\n")
foreach (QSS_FILE IN LISTS QSS)
get_filename_component(QSS_FILE_NAME ${QSS_FILE} NAME_WE)
set(QRC_CONTENT "${QRC_CONTENT}\t\t<file alias=\"${QSS_FILE_NAME}\">${QSS_FILE}</file>\n")
endforeach ()
set(QRC_CONTENT "${QRC_CONTENT}\t</qresource>\n")
#将font目录下所有的ttf字体文件全部整合进qrc,集体前缀font并且alias为不带后缀的文件名
set(QRC_CONTENT "${QRC_CONTENT}\t<qresource prefix=\"/font\">\n")
file(GLOB TTF "res/font/*.ttf")
foreach (TTF_FILE IN LISTS TTF)
get_filename_component(TTF_FILE_NAME ${TTF_FILE} NAME_WE)
set(QRC_CONTENT "${QRC_CONTENT}\t\t<file alias=\"${TTF_FILE_NAME}\">${TTF_FILE}</file>\n")
endforeach ()
set(QRC_CONTENT "${QRC_CONTENT}\t</qresource>\n")
#将doc目录下所有的文档全部整合进qrc,集体前缀doc并且alias为完整的文件名
set(QRC_CONTENT "${QRC_CONTENT}\t<qresource prefix=\"/doc\">\n")
file(GLOB DOC "res/doc/*.js")
foreach (DOC_FILE IN LISTS DOC)
get_filename_component(DOC_FILE_NAME ${DOC_FILE} NAME)
set(QRC_CONTENT "${QRC_CONTENT}\t\t<file alias=\"${DOC_FILE_NAME}\">${DOC_FILE}</file>\n")
endforeach ()
set(QRC_CONTENT "${QRC_CONTENT}\t</qresource>\n")
#结束QRC内容自动生成
set(QRC_CONTENT "${QRC_CONTENT}</RCC>\n")
#将脚本生成的QRC内容写入文件
set(QRC_FILE "${CMAKE_CURRENT_BINARY_DIR}/resources.qrc")
file(WRITE ${QRC_FILE} "${QRC_CONTENT}")
#将自动生成的resources.qrc文件作为资源索引写入
qt_add_resources(RES ${QRC_FILE})
#添加头文件|源文件|UI文件|资源文件到编译目标,编译目标名称即为工程名
add_executable(${PROJECT_NAME} ${SOURCE} ${UI} ${RES} ${HEADER})
#将我们用到的所有Qt组件除了LinguistTools不参与执行,全部连接到可执行目标
set(QT_LIBS)
foreach (QT_LIB IN LISTS QT_COMPONENTS)
if(LinguistTools STREQUAL ${QT_LIB})
continue() #跳过LinguistTools翻译工具
endif()
if(NOT TARGET Qt::${QT_LIB})
#如果找不到Qt链接库就报错
message(FATAL_ERROR "Library Qt::${QT_LIB} not found")
endif()
#如果找到链接库就附加到链接库列表中
list(APPEND QT_LIBS "Qt::${QT_LIB}")
endforeach ()
#将整合的链接库列表添加到链接器之中
target_link_libraries(${PROJECT_NAME} PRIVATE ${QT_LIBS})
#拷贝支持库dll,Windows没有RPATH机制,如果不指定工具链文件的话只会在系统路径搜索dll
#此时需要将我们用到的dll拷贝到编译结果exe同级文件夹下
if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(DEBUG_SUFFIX)
#MSVC工具链的Debug版本的dll是后缀d的
if (MSVC AND CMAKE_BUILD_TYPE MATCHES "Debug")
set(DEBUG_SUFFIX "d")
endif ()
#默认给定的CMAKE_PREFIX_PATH是xxx或者xxx/lib或者xxx/lib/cmake,而dll的位置是xxx/bin
#如果当前目录下找不到bin那么就向上寻找找两层应该能找到
set(QT_INSTALL_PATH "${CMAKE_PREFIX_PATH}")
if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
endif ()
endif ()
#拷贝windows平台支持动态链接库
if (EXISTS "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll")
#添加两个命令:创建.exe同级别目录下的pulgins/platforms,随后拷贝dll到该地址
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
endif ()
#拷贝其他的Qt组件支持库
foreach (QT_LIB IN LISTS QT_COMPONENTS)
if (LinguistTools STREQUAL ${QT_LIB})
continue() #依旧跳过LinguistTools,它不参与运行
endif()
#为每一个dll添加编译后指令:拷贝它们到.exe同级别目录解决依赖性问题
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${QT_INSTALL_PATH}/bin/Qt6${QT_LIB}${DEBUG_SUFFIX}.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>")
endforeach (QT_LIB)
endif ()
#Linux和MacOS下编译文件有RPath机制,这决定了这两类平台不需要复制支持库,本机编译出来随时运行