以下内容为本人的著作,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/dvamU6q5lZQb5hztfD2zNg
初识Qt Quick
很高兴可以来到这一章,终于可以开始讲讲最近几年Qt的热门技术Quick这一块了。
啥是Qt?
哦,这是一个宣称可以跨任意平台,开发各种场景应用软件的开发框架。从三个维度来讲,就是开发库framework,集成开发平台IDE,以及成熟的开发思维模式。
Qt Quick最早出现在Qt的4.7版本中,目标是在UI设计者与开发者之间搭建一个更高效合作平台,给开发者更好的UI开发体验。虽然几经易手,Qt在digia公司这些年的努力迭代更新下,Qt Quick终于迎来了成熟稳定的版本(这也是我愿意在最近的项目里转用它的原因)。
至于Qt Quick和老一套开发核心Qwidget的区别,其中最重点的就是提供了新的UI描述语言QML(Qt Meta-object Language,Qt元对象描述语言)。QML乍看起来有点像json,但是核心思想却是模仿web页面。没错,在QML文件中允许搭配Javascript代码,就可以辅助实现丰富的UI交互逻辑。
如果你以往习惯QWidget开发,那么Qt Quick真的非常值得上手试试。
好了,口水吐多了招人厌,下面直入庐山一窥真面目!
手剥一个简单的功能程序开发栗子
在Qt开发过程中,Qt官方IDE(Qt Creator)提供了好几种工程构建工具,比如简单易懂的qmake,火上天的cmake,还有貌似没人听说过的Qbs。而目前Qt主推的构建方式就是cmake,下面要讲的例子也是用cmake。
1.开发环境配置
Win10
Qt 6.2.4
Qt Creator 8.0.1
Mingw 11.2.0 64bit
Cmake 3.23.2
这里选用的Qt版本是写作时最新的LTS版本,LTS意思就是官方长期支持更新。比如说,一两年内还会发布一下补丁和安全更新,至于新功能特性就别想了。
2.创建Qt Quick工程
先用Qt Creator创建一个简单的quick工程,工程构建描述的内容就保存在工程根目录的配置文件CMakeLists.txt中,如下:
cmake_minimum_required(VERSION 3.16)
project(instance VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
#set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 6.2 COMPONENTS Quick REQUIRED)
file(GLOB_RECURSE SOURCE_FILES
./src/*.cpp
./src/*.h
)
qt_add_resources(SOURCE_FILES instance.qrc)
qt_add_executable(instance
${SOURCE_FILES}
)
set_target_properties(instance PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
target_link_libraries(instance
PRIVATE Qt6::Quick)
install(TARGETS instance
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
cmake_minimum_required
用于声明当前的配置文件适用于的cmake最低版本,同时为了防止使用过于低级的版本来构建当前工程,避免某些指令不支持或者不兼容。
project
用于声明当前工程名称,和开发语言。CXX代表了C++。
CMAKE_AUTOMOC
用于标记是否开启自动MOC。Qt不仅仅是开发库,它同时也对开发语言(比如C++)做了拓展,那么源码文件中就会多多少少会包含有通用编译器无法识别的部分内容。MOC就是用于对Qt的扩展内容进行转换的工具。
CMAKE_AUTORCC
用于标记是否开启自动RCC。在Qt工程中会包含有被最终输出的执行程序所需要的资源内容,比如音视频,图片等等。那么为了高效调用这些资源,势必需要对原本的资源文件进行处理再保存到额外的二进制文件中,甚至内嵌到执行文件中。RCC就是对这些资源文件进行处理和再输出工具。
由于我的例子工程中需要用到MOC和RCC,所以CMAKE_AUTOMOC
和CMAKE_AUTORCC
都打开。
CMAKE_AUTOUIC
用于标记是否开启自动UIC。如果开发界面用的技术栈是QWidget,那么在Qt工程中就需要创建.ui文件并保存设计内容到其中,编译的时候也需要用UIC把.ui文件转换成.h文件。不过,这里用Quick技术栈开发界面,因此无需打开CMAKE_AUTOUIC
,在最前面添加#
表示注释掉该行语句(该行语句会被解析器忽略)。
用find_package
导入Qt的Quick模块。
由于本工程需要用到多个C++源文件,所以这里采用了递归引用文件的方式把特定文件夹下面的所有.cpp、.h等文件都囊括进来,需要辅以通配符*。所有被囊括的文件路径被追加到动态数组SOURCE_FILES中,方便后边引用。语法格式如下:
file(GLOB_RECURSE <variable> [FOLLOW_SYMLINKS]
[LIST_DIRECTORIES true|false] [RELATIVE <path>]
[<globbing-expressions>...])
格式里的variable实际使用SOURCE_FILES代替。
qt_add_resources
的作用是调用RCC对资源文件(.qrc)编译成qrc_开头的源文件再输出,并且把输出的源码文件路径追加到动态数组SOURCE_FILES中。
当然,动态数组SOURCE_FILES这个名字可以按照需求自定义设定,这里取名为源文件。
qt_add_executable
指明构建的目标是二进制文件instance,引用的源文件来自于动态数组SOURCE_FILES。
target_link_libraries
用于指明构建时链接Qt6::Quick的相关库。
剩余的语句都是Qt Creator创建工程时自动添加的内容,这里略过。
然后看看我的工程目录结构在Qt Creator中的展示:
如果用VSCODE打开工程目录,可以看到:
3.使用元对象描述文件(QML)描述界面
使用Qt Creator自动创建的Quick应用,除了会自动生成配置文件CMakeLists.txt之外,还包含了main.cpp和main.qml文件,源码只实现了启动之后弹出一个窗口。
这里稍作修改,实现简单的文件选择,以及将选中的文件路径名显示出来。这个功能用QWidget技术栈来实现其实是很简单的啦,不过我们这里目的是演示Quick技术框架怎么用,所以下面来具体看看界面这块怎么玩:
1)主页面
// main.qml
import QtQuick
Window {
width: 640
height: 200
visible: true
title: qsTr("Tool V1.0.0")
Viewer {
anchors.fill: parent
}
}
main.qml 这个文件是首页元对象的描述文件,一般QML引擎加载的第一个元对象所在的文件就命名为main.qml。不过,命名为main.qml不是硬性规定。
首先,可以看到这里通过import导入了模块QtQuick。同一行,后边还可以加上版本号。
Window是一种模块里预定义的类型,用于窗体描述。当然,类型也可以自定义,下面会说到。这里通过对类型Window的实例化来描述一个窗体对象。
类型后边的{}内部包含了类型实例化后的成员属性、函数、信号、信号处理句柄等,比如width、height、visible、title等都是预定义的属性,这些属性如字面意思比较简单易懂就不一一展开了,大伙要是有兴趣可以反馈给我,我再看看意见给大家细聊。
Viewer其实是自定义的类型,这里通过对类型Viewer的实例化来补充添加新的界面元素。Viewer对象内部的属性anchors.fill: parent
描述的是对象在其父对象(Window)中把父对象填充满。QML一般通过anchors属性来锚定对象的位置。
上面这个对象里并没有定义或者引用到函数或者信号等。
2)自定义类型
下面来看看怎么自定义类型
// Viewer.qml
import QtQuick 2.15
import QtQuick.Controls 6.2
import Qt.labs.platform 1.1
Item {
function log(...msg) {
let msgs = "";
msg.forEach((item) => {
if (msgs.length != 0) {
msgs += " ";
}
msgs += item;
});
console.log(msgs);
}
FileDialog {
id: fileDialog
objectName: "fileDialog"
currentFile: selectedFileTextArea.text
onFileChanged: {
log(objectName + ".file =", file.toString().slice(8));
fileMgrInstance.run(file.toString().slice(8));
}
}
Label {
id: fileLable
x: 292
y: 26
text: qsTr("文件:")
verticalAlignment: Text.AlignVCenter
font.pointSize: 14
}
TextField {
id: selectedFileTextArea
x: 70
y: 70
width: 500
objectName: "selectedFileTextArea"
text: fileDialog.file.toString().slice(8)
font.pointSize: 12
placeholderText: qsTr("选择文件")
}
Button {
id: selectFileButton
x: 268
y: 124
width: 105
height: 54
text: qsTr("选择")
font.pointSize: 10
onClicked: {
log(text, "clicked");
fileDialog.open();
}
}
}
Item
是类库QtQuick的预定义组件类型,描述的是一个基础可视组件,quick中所有的可视组件都继承于它。
一般在QML中自定义类型都会使用基础的类型Item,然后在其基础上定制内部属性、函数、信号、信号处理句柄等。
Item的继承链是这样的:
Item -> QtObject -> QObject
看到这里,可以猜测一下,其实所有的Quick预定义组件都是继承于QObject,和QWidget里提供的类库太相似了。
function log(...msg)
定义了函数log,function是关键词,log是函数名,后边小括号里的...
表示参数不定,这样子在调用log时就可以不限制输入的参数个数了。
要注意的是,QML内部的函数使用的语法是ECMAScript,也就是我们常常听到的JavaScript。
FileDialog
是类库Qt.labs.platform的预定义组件类型,描述的是一个文件选择窗口。属性id,继承于QObject,用于标记唯一的对象,也就是说所有对象的id都不能重复,无论对象是否处于同一个QML文件。objectName描述的属性可用于对对象树中的对象进行查找。currentFile描述了当前选中的文件名(包括路径),在确定最后选中的文件之前,此属性也会跟随选择而改变。onFileChanged描述了当属性file值改变时,自动产生的信号的处理句柄(handle),用{}限定处理范围。log是上面定义的函数调用,输入两个参数。fileMgrInstance是C++源文件暴露给QML引擎的特定对象id,通过该id可以调用C++中的相应对象的方法属性(代码中调用了run方法,方法的详情定义看下文)。
C++和QML源文件之间的对象相互调用,会有后续的文章专门介绍,这里不再细聊,敬请关注。
Label
是类库QtQuick.Controls的预定义组件类型,描述的是一个文本标签。x、y描述的是坐标。text描述的是显示文本。qsTr()用于标识文本可被翻译,类似Qt C++里的tr()。verticalAlignment描述的是垂直排列方式,这里的属性值标识垂直中间排列。font.pointSize描述字体的大小。
TextField
是类库QtQuick.Controls的预定义组件类型,描述的是一个单行文本编辑窗。width是几何宽度。placeholderText描述的是占位符。
Button
是类库QtQuick.Controls的预定义组件类型,描述的是一个可点击的按键。height描述的是几何高度。onClicked描述了当信号clicked发生时,该信号的处理句柄(handle),用{}限定处理范围,这里调用了上面的文件选择窗的打开函数。
信号的处理句柄(handle)中,在on后边书写时信号的首字母需要大写。
3)预览界面
什么?代码没写完就可以预览界面了?
是的,QML文件支持用工具预览,非常方便于UI设计过程中的调试。
打开预览的方式是调用qmlscene或者用Qt Design Studio,如下图用的是qmlscene。
看看Viewer.qml页面预览的实际效果。
4.使用C++代码实现逻辑处理
Qt Quick使用QML的目的是为了简化界面的设计开发,而软件除了界面的互动之外还有大量的后台逻辑处理功能也需要实现,针对这块业务,Qt其实还是推荐使用C++,正所谓术业有专攻,毕竟C++对性能的利用还是有两把刷子的。废话不多说,马上看下文。。。
既然如此,那么就来看看负责逻辑处理功能的C++代码部分。不过,这里假设各位看官已经熟悉C++的各项业务技能,所以下面只针对和QML对象的互动来简单介绍一下。
后续也会有更加详细的专题文章介绍这部分,敬请留意哈!
1)QML对象的加载和C++对象传递
QML对象的创建和展现是通过QML引擎来加载的,一般每个程序会由单个引擎对象负责管理。不过,QML引擎对象不是直接管理QML对象,而是通过管理上下文(context)对象来分别管理QML对象。所以在C++里边如果需要往QML对象传递信息也是直接传给对应的上下文对象,然后再在QML对象中通过传入对象时指定的id名调用对应的方法属性。
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "./FileMgr/FileMgr.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(u"qrc:/src/QML/main.qml"_qs);
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
qInfo("%s start\n", QCoreApplication::applicationName().toLatin1().data());
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
FileMgr fileMgr;
engine.rootContext()->setContextProperty(QStringLiteral("fileMgrInstance"), &fileMgr);
engine.load(url);
return app.exec();
}
上面的main.cpp文件代码中,把对象fileMgr传入引擎的根上下文中,并设定id为fileMgrInstance。传入根上下文,意味着引擎加载的所有QML对象都可以通过id=fileMgrInstance访问fileMgr对象内容。这里要注意,fileMgr对象是在C++源码中实例化了的。
把C++对象暴露给QML对象的方法,除了上面这种通过上下文的方式外,还有一种是通过直接往元对象系统(Meta-Object System)注册类型的方式,这种方式也是最根本的方式,因为Qt Quick框架的底层实现原理就依赖于元对象系统。
使用的接口原型:
template <typename T> int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)
如果通过这种注册的方式实现,上面的栗子可以掰成这样:
qmlRegisterType<FileMgr>("com.englyf.qmlcomponents", 1, 0, "FileMgrItem");
根据上面的定义,com.englyf.qmlcomponents是命名空间,1.0是命名空间的版本号,FileMgrItem是QML中的类型名。
然后在QML文件内,导入并实例化这个类:
import com.englyf.qmlcomponents 1.0
FileMgrItem {
// ...
}
要强调的是,通过注册的方式,类型的实例化会放在QML里边做,而C++源码就不需要再对类FileMgr作实例化了
2)C++类型的定义
既然Qt Quick依赖于元对象系统,那么对C++类型的定义就有必然的要求了。
C++类型需要继承于QObject,并且类开头应该声明宏Q_OBJECT,这样才可以使用元对象系统提供的服务,包括信号槽机制等等。
需要被QML对象调用的方法应该添加修饰Q_INVOKABLE。这个修饰符表明该方法可被元对象系统调用。同时,该方法的参数类型和返回类型,都推荐使用类型QVariant。
如有开放给QML对象的可访问属性,那么也需要对属性声明为Q_PROPERTY。这里暂不举例,可关注后续的专题文章。
// FileMgr.h
#ifndef FILEMGR_H
#define FILEMGR_H
#include <QObject>
#include <QVariant>
class FileMgr : public QObject
{
Q_OBJECT
public:
explicit FileMgr(QObject *parent = nullptr);
Q_INVOKABLE QVariant run(QVariant file);
};
#endif // FILEMGR_H
// FileMgr.cpp
#include "FileMgr.h"
FileMgr::FileMgr(QObject *parent)
: QObject{parent}
{}
QVariant FileMgr::run(QVariant file)
{
QString fileStr = file.toString();
qDebug("C++ get file:%s selected", fileStr.toStdString().data());
return 0;
}
自动化部署
这部分讲点高级的内容,以往看到网上的教程都是教初学者部署的时候,进入exe生成的目录,然后手动调用windeployqt执行部署。这个程序是Qt自带的,会自动把所有依赖的动态库拷贝过来存放在指定目录下。
这里就介绍一下怎么在Qt Quick软件工程编译结束时自动部署所有依赖项。
首先,debug开发模式下是不需要部署软件的,那么我们就先切换到release模式下。
然后,在Build的步骤
下,Build步骤
之后新添加一个Custom Process Step的步骤。
我把配置都拷过来:
Command: windeployqt
Arguments: --qmldir %{ActiveProject:NativePath}\src\QML\ %{ActiveProject:RunConfig:Executable:NativeFilePath}
Working directory: %{Qt:QT_INSTALL_BINS}
由于Qt Quick工程涉及到QML文件,所以这里需要带上选项--qmldir
,这个选项后边紧跟着参数值是代码工程中存放自定义的QML文档的根目录。
%{ActiveProject:NativePath}
代表着当前工程的主目录的本地化路径。
%{ActiveProject:RunConfig:Executable:NativeFilePath}
代表着当前工程的exe文件输出目录的本地化路径。
Working directory项意思是Command命令的工作目录,这里填上%{Qt:QT_INSTALL_BINS}
,代表Qt安装目录下的bin目录。
按照上面的介绍过程配置完整,以后如果需要部署输出,只需要切换到release模式下,然后点击编译,等编译完成就会自动进入部署流程,整个过程就是这么舒心。
生活简单才是美好,部署也不例外!
到最后,一起来看看跑起来的程序: