完成 QML 应用程序开发技术总结.

Signed-off-by: Rick.Chan <cy@haoan119.com>
This commit is contained in:
Rick.Chan 2022-12-13 16:41:41 +08:00
parent b12ebf2549
commit 7b32475d7b
1 changed files with 489 additions and 43 deletions

View File

@ -1,12 +1,42 @@
# QML 应用程序开发技术总结 # QML 应用程序开发技术总结
- [QML 应用程序开发技术总结](#qml-应用程序开发技术总结)
- [1. 方法/属性名称大小写](#1-方法属性名称大小写)
- [2. 全局属性](#2-全局属性)
- [3. 信号与槽](#3-信号与槽)
- [3.1. 信号与信号处理器](#31-信号与信号处理器)
- [3.2. 属性变化信号与属性变化信号处理器](#32-属性变化信号与属性变化信号处理器)
- [3.3. 附加属性与附加信号处理器](#33-附加属性与附加信号处理器)
- [3.4. Connections 建立信号与槽的连接](#34-connections-建立信号与槽的连接)
- [3.5. connect()方法](#35-connect方法)
- [3.6. 自定义信号](#36-自定义信号)
- [4. 界面加载完成信号](#4-界面加载完成信号)
- [5. 包别名](#5-包别名)
- [6. 添加图标](#6-添加图标)
- [6.1. 制作图标](#61-制作图标)
- [6.2. 添加图标到应用](#62-添加图标到应用)
- [7. 绘制圆形](#7-绘制圆形)
- [8. Dialog 对象](#8-dialog-对象)
- [8.1. FileDialog](#81-filedialog)
- [8.2. MessageDialog](#82-messagedialog)
- [9. ComboBox](#9-combobox)
- [10. ScrollView](#10-scrollview)
- [11. GridView](#11-gridview)
- [12. QML 与 C++ 交互](#12-qml-与-c-交互)
- [12.1. QML 访问 C++ 中声明的类型](#121-qml-访问-c-中声明的类型)
- [12.2. C++ 访问 QML 对象](#122-c-访问-qml-对象)
- [12.3. 通过信号槽传递自建类型](#123-通过信号槽传递自建类型)
- [12.4. QML 与 C++ 交互综合示例](#124-qml-与-c-交互综合示例)
- [13. Windows 下 QML 程序的打包发布](#13-windows-下-qml-程序的打包发布)
- [14. 外部参考资料](#14-外部参考资料)
## 1. 方法/属性名称大小写 ## 1. 方法/属性名称大小写
QML 语法与 JS 相同,具体请参考相关教程,此处只对一些需要特殊注意的地方进行说明。 QML 语法与 JS 相同,具体请参考相关教程,此处只对一些需要特殊注意的地方进行说明。
QML 的属性、变量、函数等一般为小写字母开头,大写字母开头通常具有特殊意义,比如使用 QML 连接名为 demo() 的信号时,系统将自动连接 onDemo() 作为其槽函数。以下以连接 Button 的 clicked 信号为例进行演示: QML 的属性、变量、函数等一般为小写字母开头,大写字母开头通常具有特殊意义,比如使用 QML 连接名为 demo() 的信号时,系统将自动连接 onDemo() 作为其槽函数。以下以连接 Button 的 clicked 信号为例进行演示:
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -26,7 +56,7 @@ Window {
## 2. 全局属性 ## 2. 全局属性
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -41,54 +71,128 @@ Window {
} }
``` ```
## 3. 连接信号槽 ## 3. 信号
TODO: 细化 在 QML 中连接信号槽主要有以下 5 种方法。
信号与信号处理器 ### 3.1. 信号与信号处理器
```qml ```js
signal() signal()
on<Signal>: {} on<Signal>: {}
``` ```
属性变化信号与属性变化信号处理器 这是最常见的方法,比如 onClicked: {} 槽。
```qml ### 3.2. 属性变化信号与属性变化信号处理器
```js
property property
on<Property>Changed: {} on<Property>Changed: {}
``` ```
附加属性与附加信号处理器 QML 规范,当属性变化后会发送一个 \<Property\>Changed 信号。从 C 环境创建属性时需要注册 NOTIFY 信号,其命名格式也需要遵循该规范。
```qml ### 3.3. 附加属性与附加信号处理器
```js
Attached Signal(附加属性) Attached Signal(附加属性)
XX元素.on<附加属性>: {} XX元素.on<附加属性>: {}
``` ```
Connections 建立信号与槽的连接 最常见的是 Component.onCompleted{} 槽。
```qml ### 3.4. Connections 建立信号与槽的连接
```js
Connections { Connections {
target: 发送者 target: 发送者
发送者信号处理器:{} 发送者信号处理器:{}
} }
``` ```
connect()方法 信号连接信号 前面的信号连接都嵌入在某个对象中,当信号与槽的连接不依赖具体对象时,需要将其嵌入到 Connections 对象中。Connections 的 target 指向信号的发送者,一个 Connections 可以绑定同一个 target 的多个信号处理机。
```qml ```js
元素对象.信号.connect(信号) Connections {
target: whomSentTheSignal
function onSignal0(arg) {
console.log(arg)
}
function onSignal1(arg) {
console.log(arg)
}
// ...
}
```
### 3.5. connect()方法
使用 connect() 方法,可以为信号指定非 on\<Signal\> 格式命名的响应方法,也可以指令信号触发下一个信号形成信号的连锁反应。
```js
[元素对象.]信号.connect(信号/方法)
```
示例如下:
```js
Rectangle {
id: root
signal mySignal()
// 信号响应处理
onMySignal: console.log("clicked connect mySignal")
// 普通方法
function slt_clicked() {
console.log("clicked connect slt_clicked");
}
Component.onCompleted: {
mousearea.clicked.connect(slt_clicked);
mousearea.clicked.connect(mySignal);
}
MouseArea {
id: mousearea
anchors.fill: parent
}
}
```
### 3.6. 自定义信号
用以下形式声明自定义的信号:
```js
signal <name>[([<type> <parameter name>[, ...]])]
```
示例如下:
```js
Rectangle {
id: root
signal mysignal(int x, int y)
MouseArea {
anchors.fill: parent
onPressed: root.mysignal(mouse.x, mouse.y)
}
}
``` ```
## 4. 界面加载完成信号 ## 4. 界面加载完成信号
许多时候需要在 QML 程序界面完成加载后做一些事情,比如设置一些属性的初始值。这时候就需要用到 Component 的 completed() 信号: 许多时候需要在 QML 程序界面完成加载后做一些事情,比如设置一些属性的初始值。这时候就需要用到 Component 的 completed() 信号:
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -113,7 +217,7 @@ Window {
编写 QML 应用时需要像 include 那样引用一些软件包,有的时候软件包中的对象会有重名的情况,比如 QtQuick.Dialogs 1.3 和 Qt.labs.qmlmodels 1.0 中均包含了 FileDialog 对象,但他们的功能并不相同。此时可以使用 import as 来实现类似别名或者命名空间的功能。 编写 QML 应用时需要像 include 那样引用一些软件包,有的时候软件包中的对象会有重名的情况,比如 QtQuick.Dialogs 1.3 和 Qt.labs.qmlmodels 1.0 中均包含了 FileDialog 对象,但他们的功能并不相同。此时可以使用 import as 来实现类似别名或者命名空间的功能。
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Dialogs 1.3 import QtQuick.Dialogs 1.3
@ -147,19 +251,19 @@ magick.exe convert icon-16.png icon-32.png icon-256.png myappico.ico
在 Qt .pro 文件中添加以下内容,以便将图标编译到 Qt 程序中: 在 Qt .pro 文件中添加以下内容,以便将图标编译到 Qt 程序中:
```qt ```js
RC_ICONS = myappico.ico RC_ICONS = myappico.ico
``` ```
However, if you already have an .rc file, for example, with the name myapp.rc, which you want to reuse, the following two steps will be required. First, put a single line of text to the myapp.rc file: However, if you already have an .rc file, for example, with the name myapp.rc, which you want to reuse, the following two steps will be required. First, put a single line of text to the myapp.rc file:
```qt ```js
IDI_ICON1 ICON "myappico.ico" IDI_ICON1 ICON "myappico.ico"
``` ```
Then, add this line to your myapp.pro file: Then, add this line to your myapp.pro file:
```qt ```js
RC_FILE = myapp.rc RC_FILE = myapp.rc
``` ```
@ -196,7 +300,7 @@ Dialog 对象默认不显示,当调用 Dialog 的 open() 方法后弹出窗口
FileDialog 为标准文件对话框。 FileDialog 为标准文件对话框。
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -235,7 +339,7 @@ Window {
MessageDialog 为标准消息对话框。 MessageDialog 为标准消息对话框。
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -275,7 +379,7 @@ Window {
ComboBox 为标准组合框 ComboBox 为标准组合框
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -307,7 +411,7 @@ ScrollView 会为其所容纳的对象创建滚动条。
GridView 可以以网格的形式显示模型内容。可以使用 ListModel 或 XmlListModel 作为模型。 GridView 可以以网格的形式显示模型内容。可以使用 ListModel 或 XmlListModel 作为模型。
```qml ```js
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
@ -431,9 +535,13 @@ QML 使用 C++ 中声明的类型可以为类、结构体或枚举等。若需
qmlRegisterType<Type>("package.Type", <version>, <sub-version>,"Type"); qmlRegisterType<Type>("package.Type", <version>, <sub-version>,"Type");
``` ```
之后在 QML 中使用 C++ 类创建对象即可。 之后在 QML 中使用 C++ 类创建对象即可。若需要在 QML 中使用类中定义的枚举,格式如下:
若 QML 需要使用 C++ 中声明的枚举类型,则该枚举需要使用 Q_ENUM() 进行修饰;若访问属性成员则需要使用 Q_PROPERTY() 进行修饰;若访问方法则该方法需要使用 Q_INVOKABLE() 来修饰。C++ 中的信号不需要特殊处理QML 可直接访问。 ```js
<类名>.<枚举值>
```
若 QML 需要使用 C++ 中声明的枚举类型,则该枚举需要使用 Q_ENUM() 进行修饰(必须放在声明之后);若访问属性成员则需要使用 Q_PROPERTY() 进行修饰;若访问方法则该方法需要使用 Q_INVOKABLE() 来修饰。C++ 中的信号不需要特殊处理QML 可直接访问。
若对象在 C++ 上下文中创建,并需要在 QML 中使用该对象,可以使用: 若对象在 C++ 上下文中创建,并需要在 QML 中使用该对象,可以使用:
@ -457,9 +565,9 @@ auto qmlObj = root.first()->findChild<QObject*>("object name");
大部分情况下在 QML 中访问 C++ 即可实现较完善的功能QML 传递信息给 C++ 完全可以通过信号槽机制实现。除非需要在 C++ 中动态创建对象并连接到 QML 中的信号槽,否则没必要这样设计。 大部分情况下在 QML 中访问 C++ 即可实现较完善的功能QML 传递信息给 C++ 完全可以通过信号槽机制实现。除非需要在 C++ 中动态创建对象并连接到 QML 中的信号槽,否则没必要这样设计。
## 12.3. qRegisterMetaType ### 12.3. 通过信号槽传递自建类型
当使用信号槽机制,需要注意一点:如果需要通过信号槽传递自建类型数据,需要使用 qRegisterMetaType() 方法进行注册。 当使用信号槽机制,需要注意一点:如果需要通过信号槽传递自建类型数据,需要使用 qRegisterMetaType() 方法进行注册。
```cpp ```cpp
qRegisterMetaType<MyClass>("Myclass"); qRegisterMetaType<MyClass>("Myclass");
@ -467,16 +575,353 @@ qRegisterMetaType<MyClass>("Myclass");
### 12.4. QML 与 C++ 交互综合示例 ### 12.4. QML 与 C++ 交互综合示例
```cpp 该示例包含以下文件:
TODO: 添加示例
- QmlDemo.por
- qml.qrc
- DemoDirect.h
- DemoDirect.cpp
- DemoJoint.h
- DemoJoint.cpp
- DemoIndirect.h
- DemoIndirect.cpp
- main.qml
- main.cpp
QmlDemo.por 内容如下:
```js
QT += quick widgets
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
DemoDirect.cpp \
DemoIndirect.cpp \
DemoJoint.cpp \
main.cpp
RESOURCES += qml.qrc
# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =
# Additional import path used to resolve QML modules just for Qt Quick Designer
QML_DESIGNER_IMPORT_PATH =
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
HEADERS += \
DemoDirect.h \
DemoIndirect.h \
DemoJoint.h
``` ```
```cpp qml.qrc 内容如下:
TODO: 添加示例
```xml
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>
``` ```
```qml DemoDirect.h 内容如下
TODO: 添加示例
```cpp
#ifndef DEMODIRECT_H
#define DEMODIRECT_H
#include <QObject>
class DemoDirect : public QObject
{
Q_OBJECT
// 声明 cppStr 为只读属性. NOTIFY 信号是必须的.
Q_PROPERTY(QString cppStr READ readCppStr NOTIFY cppStrChanged)
public:
explicit DemoDirect(QObject *parent = nullptr);
// 使用 Q_INVOKABLE 向 QML 声明函数方法.
Q_INVOKABLE void writeCppStr(QString str);
Q_INVOKABLE QString readCppStr();
signals:
void cppStrChanged(QString str);
protected:
QString cppStr;
};
#endif // DEMODIRECT_H
```
DemoDirect.cpp 内容如下
```cpp
#include "DemoDirect.h"
#include <QDebug>
DemoDirect::DemoDirect(QObject *parent)
: QObject{parent}
{
cppStr = "Hello";
}
void DemoDirect::writeCppStr(QString str)
{
cppStr = str;
qDebug()<<"[CPP:DemoDirect:writeCppStr]srt="<<cppStr;
emit cppStrChanged(cppStr);
}
QString DemoDirect::readCppStr()
{
return cppStr;
}
```
DemoJoint.h 内容如下
```cpp
#ifndef DEMOJOINT_H
#define DEMOJOINT_H
#include <QThread>
#include <QSemaphore>
class DemoJoint : public QThread
{
Q_OBJECT
public:
enum DemoEnum {
DE_OPEN,
DE_READ,
DE_WRITE,
DE_CLOSE,
DE_OPEN_DONE,
DE_READ_DONE,
DE_WRITE_DONE,
DE_CLOSE_DONE
};
// 向 QML 声明 DemoEnum 类型.
Q_ENUM(DemoEnum);
explicit DemoJoint(QObject *parent = nullptr);
~DemoJoint();
signals:
// 从 QML 调用的信号, 信号不必使用 Q_INVOKABLE 进行显式声明.
void qml2Cpp(DemoJoint::DemoEnum op);
// 从 DemoIndirect 中发射该信号, QML 中响应该信号.
void cpp2Qml(DemoJoint::DemoEnum op);
protected:
class DemoIndirect* demoIndirect;
void run() override;
private:
QSemaphore initLock;
};
#endif // DEMOJOINT_H
```
DemoJoint.cpp 内容如下
```cpp
#include "DemoJoint.h"
#include "DemoIndirect.h"
DemoJoint::DemoJoint(QObject *parent)
: QThread{parent}
{
this->start();
initLock.acquire(1);
}
DemoJoint::~DemoJoint()
{
this->wait();
}
void DemoJoint::run()
{
this->setPriority(QThread::NormalPriority);
demoIndirect = new DemoIndirect(this, nullptr);
connect(this, &DemoJoint::qml2Cpp, demoIndirect, &DemoIndirect::onQml2Cpp, Qt::QueuedConnection);
initLock.release(1);
this->exec();
demoIndirect->deleteLater();
}
```
DemoIndirect.h 内容如下
```cpp
#ifndef DEMOINDIRECT_H
#define DEMOINDIRECT_H
#include <QObject>
#include "DemoJoint.h"
class DemoIndirect : public QObject
{
Q_OBJECT
public:
explicit DemoIndirect(DemoJoint* j, QObject *parent = nullptr);
public slots:
// 在 DemoJoint run() 方法中连接了该槽.
void onQml2Cpp(DemoJoint::DemoEnum op);
signals:
private:
DemoJoint* joint;
};
#endif // DEMOINDIRECT_H
```
DemoIndirect.cpp 内容如下
```cpp
#include "DemoIndirect.h"
#include <QDebug>
DemoIndirect::DemoIndirect(class DemoJoint* j, QObject *parent)
: QObject{parent}
{
joint = j;
}
void DemoIndirect::onQml2Cpp(DemoJoint::DemoEnum op)
{
qDebug()<<"[CPP:DemoIndirect:onQml2Cpp]op="<<op;
switch (op) {
case DemoJoint::DE_OPEN:
emit joint->cpp2Qml(DemoJoint::DE_OPEN_DONE);
break;
default:
emit joint->cpp2Qml(DemoJoint::DE_CLOSE_DONE);
break;
}
}
```
main.cpp 内容如下:
```cpp
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "DemoJoint.h"
#include "DemoDirect.h"
int main(int argc, char *argv[])
{
// 注册 DemoDirect 类型, 否则无法在 QML 中使用 DemoDirect 创建对象.
qmlRegisterType<DemoDirect>("project.DemoDirect", 1, 0, "DemoDirect");
// 注册 DemoJoint 类型, 否则无法在 QML 中使用 DemoJoint.DemoEnum 类型.
qmlRegisterType<DemoDirect>("project.DemoJoint", 1, 0, "DemoJoint");
// 注册用于在信号槽中使用的类型.
qRegisterMetaType<DemoJoint::DemoEnum>("DemoJoint::DemoEnum");
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QApplication app(argc, argv);
// 在 C 上下文创建 demoJoint 对象.
DemoJoint* demoJoint = new DemoJoint();
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
// 将 C 环境下的 demoJoint 注册到 QML 上下文中.
engine.rootContext()->setContextProperty("demoJoint", demoJoint);
engine.load(url);
return app.exec();
}
```
main.qml 内容如下:
```js
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
// 导入自定义包, 包名与版本号要与 C 环境下注册的一致.
import project.DemoDirect 1.0
import project.DemoJoint 1.0
Window {
width: 640
height: 480
visible: true
id: window
// 直接从 QML 环境下实例化 C 对象.
DemoDirect {
id: demoDirect
}
Text {
id: demoText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 8
anchors.leftMargin: 8
anchors.topMargin: 8
// 显示 demoDirect 的 cppStr 属性值, 注意这是一个只读属性.
text: demoDirect.cppStr
}
Button {
id: demoBut
width: 64
height: 24
text: qsTr("信号测试")
anchors.left: parent.left
anchors.top: demoText.bottom
anchors.leftMargin: 8
anchors.topMargin: 8
onClicked: {
// 从 QML 环境中向 C 环境发送信号.
demoJoint.qml2Cpp(DemoJoint.DE_OPEN)
// 直接调用 C 方法.
demoDirect.writeCppStr("Hello")
}
}
// 使用 Connections 与 C 信号建立连接, 并处理 cpp2Qml() 信号.
Connections {
target: demoJoint
function onCpp2Qml(op) {
console.log("[QML:onCpp2Qml]op=", op)
}
}
}
``` ```
## 13. Windows 下 QML 程序的打包发布 ## 13. Windows 下 QML 程序的打包发布
@ -494,12 +939,13 @@ Qt 自带的打包程序会添加额外的库,如果想进一步减小体积
1. [深入了解JS中的整数](https://www.jianshu.com/p/1ba45c3894ab) 1. [深入了解JS中的整数](https://www.jianshu.com/p/1ba45c3894ab)
2. [QML 中的信号与槽](https://blog.csdn.net/Love_XiaoQinEr/article/details/123746983) 2. [QML 中的信号与槽](https://blog.csdn.net/Love_XiaoQinEr/article/details/123746983)
3. [QML 控件类型ScrollBar、ScrollIndicator](https://blog.csdn.net/kenfan1647/article/details/122522063) 3. [QML 信号与响应方法的总结](https://zhuanlan.zhihu.com/p/576316521)
4. [QML类型GridView](https://blog.csdn.net/kenfan1647/article/details/120761466) 4. [QML 控件类型ScrollBar、ScrollIndicator](https://blog.csdn.net/kenfan1647/article/details/122522063)
5. [Qt Quick 常用元素ComboBox(下拉列表) 与 ProgressBar(进度条)](https://www.cnblogs.com/linuxAndMcu/p/11949814.html) 5. [QML 类型GridView](https://blog.csdn.net/kenfan1647/article/details/120761466)
6. [【QML Model-View】ListView-增删改查(二)](https://www.cnblogs.com/linuxAndMcu/p/13597106.html) 6. [Qt Quick 常用元素ComboBox(下拉列表) 与 ProgressBar(进度条)](https://www.cnblogs.com/linuxAndMcu/p/11949814.html)
7. [关于 Q_ENUMS 和 Q_ENUM 的区别和用法](https://blog.csdn.net/lsylovezsl/article/details/108766437) 7. [【QML Model-View】ListView-增删改查(二)](https://www.cnblogs.com/linuxAndMcu/p/13597106.html)
8. [C++ 共享枚举类型给 QML](https://www.cnblogs.com/linkyip/p/14462288.html) 8. [关于 Q_ENUMS 和 Q_ENUM 的区别和用法](https://blog.csdn.net/lsylovezsl/article/details/108766437)
9. [QML Connections: Implicitly defined onFoo properties in Connections are deprecated.](https://blog.csdn.net/weixin_43720622/article/details/112346039) 9. [C++ 共享枚举类型给 QML](https://www.cnblogs.com/linkyip/p/14462288.html)
10. [QML 调用 C++ 方法](https://blog.csdn.net/woshouji1/article/details/121348179) 10. [QML Connections: Implicitly defined onFoo properties in Connections are deprecated.](https://blog.csdn.net/weixin_43720622/article/details/112346039)
11. [Qml 与 C++ 交互3Qml 的信号与 C++ 的槽函数连接](https://blog.csdn.net/tanxuan231/article/details/124990296) 11. [QML 调用 C++ 方法](https://blog.csdn.net/woshouji1/article/details/121348179)
12. [Qml 与 C++ 交互3Qml 的信号与 C++ 的槽函数连接](https://blog.csdn.net/tanxuan231/article/details/124990296)