目录
- 一、概述
-
- [1.1 背景介绍:UI与逻辑的"隔阂"](#1.1 背景介绍:UI与逻辑的“隔阂”)
- [1.2 学习目标](#1.2 学习目标)
- [1.3 MVVM架构简介](#1.3 MVVM架构简介)
- [二、C++后端 (ViewModel) 的创建](#二、C++后端 (ViewModel) 的创建)
- 三、建立连接:从QML调用C++
- 四、反向通信:从C++更新QML
- 五、总结与展望
一、概述
1.1 背景介绍:UI与逻辑的"隔阂"
在前面的文章中,我们已经分别构建了C++后端的逻辑基础(第2篇)和QML前端的UI骨架(第3篇)。目前,它们就像一座大桥的两端,虽然各自都很坚固,但中间却是断开的------QML界面上的按钮还无法触发C++中的任何操作,C++中的数据也无法呈现在界面上。
本篇文章的核心任务,就是架设这座桥梁,打通QML与C++之间的"任督二脉"。我们将学习如何将一个C++对象"注入"到QML环境中,从而实现双向通信:既能从QML调用C++的函数,也能让C++在后台任务完成后,通过信号主动更新QML界面。
1.2 学习目标
通过本篇的学习,读者将能够:
- 理解并实践前后端分离的MVVM(Model-View-ViewModel)架构思想。
- 创建一个C++后端类(
Backend
),作为连接前端与业务逻辑的桥梁。 - 掌握在QML中调用C++方法的关键技术(
Q_INVOKABLE
)。 - 掌握C++通过信号(
signals
)更新QML界面的核心机制。
1.3 MVVM架构简介
在开始编码前,有必要了解我们即将采用的软件架构------MVVM。
- Model(模型): 负责存储和管理应用程序的数据。在我们的项目中,可以是一个代表螺丝信息的C++类。
- View(视图) : 用户看到的界面。在我们的项目中,就是
Main.qml
以及其他QML文件。 - ViewModel(视图模型): 作为一个"中间人"或"桥梁",它连接着Model和View。它负责处理View的交互请求(如按钮点击),调用Model执行业务逻辑,并将Model中的数据显示到View上。
想象一下你在餐厅吃饭:
-
你 (View / 视图)
- 你就是顾客。
- 你只关心菜单(UI)长什么样,以及怎么点菜(操作)。
- 你不需要知道后厨是怎么运作的。
-
服务员 (ViewModel / 视图模型)
- 他是连接你和后厨的中间人。
- 他把你点的菜("宫保鸡丁")传递给后厨。
- 他把后厨做好的菜(一盘宫保鸡丁)端回给你。
-
后厨 (Model / 模型)
- 后厨拥有食材(数据)和厨艺(业务逻辑)。
- 他们只负责根据订单做菜。
- 他们不需要知道你是谁,坐在哪。
一句话总结:
服务员(ViewModel)让你(View)和后厨(Model)可以各干各的,互不干扰,这就是MVVM架构的核心思想------解耦。
在本章中,我们将创建的Backend
类,正是扮演着ViewModel这一至关重要的角色。
二、C++后端 (ViewModel) 的创建
我们将创建一个Backend
类,它将成为所有业务逻辑的入口。
【例4-1】 创建Backend类。
1. 创建项目与类文件
- 延续上一篇修改后的
ScrewDetector
项目。 - 在Qt Creator中,右键点击项目名称,选择
添加新文件...
->C++
->C++ Class
。- 类名 :
Backend
- 基类 : 选择
QObject
- 类名 :
2. 编写代码 (backend.h)
cpp
#ifndef BACKEND_H
#define BACKEND_H
#include <QObject>
#include <QString>
class Backend : public QObject
{
Q_OBJECT // 必须添加,以支持信号槽和QML交互
public:
explicit Backend(QObject *parent = nullptr);
// 使用 Q_INVOKABLE 宏,使这个普通的C++成员函数可以被QML调用
Q_INVOKABLE void startScan();
signals:
// 定义一个信号,用于从C++向QML传递状态更新信息
void statusMessageChanged(const QString &message);
};
#endif // BACKEND_H
3. 编写代码 (backend.cpp)
cpp
#include "backend.h"
#include <QDebug>
#include <QTimer> // 用于模拟耗时操作
Backend::Backend(QObject *parent) : QObject(parent)
{
}
void Backend::startScan()
{
qDebug() << "C++: startScan() method called from QML.";
emit statusMessageChanged("正在准备扫描设备...");
// 使用QTimer::singleShot模拟一个2秒后的异步操作
QTimer::singleShot(2000, this, [this]() {
qDebug() << "C++: Simulated scan finished.";
// 任务完成后,再次发射信号更新状态
emit statusMessageChanged("扫描完成!");
});
}
关键代码分析:
(1) Backend
类 : 它继承自QObject
并包含Q_OBJECT
宏,这是它能与QML进行深度交互的基础。
(2) Q_INVOKABLE
: 这是一个Qt宏,是打通"从QML到C++"方向通信的最简单方式。任何被标记为Q_INVOKABLE
的公有成员函数,都可以像JavaScript函数一样在QML代码中被直接调用。
(3) signals
: statusMessageChanged
信号是打通"从C++到QML"方向通信的关键。当后端发生某个事件(如此处的扫描状态改变),就发射这个信号,QML可以监听并做出响应。
三、建立连接:从QML调用C++
现在,我们需要将创建的Backend
对象实例"告知"QML引擎,让QML能够找到并调用它。
【核心概念:上下文属性(Context Property)】
QML引擎维护着一个根上下文(Root Context),可以把它理解为QML世界的"全局作用域"。通过将一个C++对象设置为根上下文的属性,这个对象就成了一个在所有QML文件中都可以直接访问的"全局变量"。
【例4-2】 注册Backend对象并从QML调用。
1. 编写代码 (main.cpp)
这是连接C++和QML世界最关键的一步。
cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QIcon>
#include <QQmlContext> // 1. 包含上下文头文件
#include "backend.h" // 2. 包含我们自己的Backend头文件
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
app.setWindowIcon(QIcon(":/icons/appicon.png"));
QQmlApplicationEngine engine;
// 3. 创建Backend的实例
Backend backend;
// 4. 将C++对象注册为QML的上下文属性
// 第一个参数是QML中使用的名字,我们将其命名为"backend"
// 第二个参数是C++对象的地址
engine.rootContext()->setContextProperty("backend", &backend);
// ... (后续代码保持不变) ...
return app.exec();
}
2. 编写代码 (Main.qml)
现在,在Main.qml
中,可以直接通过名字backend
来访问C++对象了。
cpp
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Window {
// ... (属性保持不变) ...
ColumnLayout {
// ... (布局保持不变) ...
// --- 1. 结果展示区 (修改) ---
// 我们用一个Label来显示状态信息
Frame {
id: resultFrame
Layout.fillWidth: true
Layout.preferredHeight: 150
background: Rectangle { color: "#2c3e50" }
Label { // 使用Label代替Text,样式更统一
id: statusLabel
text: "准备就绪"
color: "white"
font.pixelSize: 18
anchors.centerIn: parent
}
}
// --- 2. 控制区 (修改) ---
RowLayout {
// ... (布局保持不变) ...
Button {
id: startButton
text: "开始检测"
Layout.preferredWidth: 120
Layout.preferredHeight: 40
// 关键:按钮点击时,调用C++ backend对象的startScan方法
onClicked: {
backend.startScan();
}
}
// ... (stopButton保持不变) ...
}
}
}
3. 运行结果
运行程序,点击"开始检测"按钮。会看到应用程序输出窗口依次输出如下:
bash
C++: startScan() method called from QML.
C++: Simulated scan finished.
关键代码分析:
(1) setContextProperty("backend", &backend)
: 这行代码是整座"桥梁"的基石。它告诉QML引擎:"现在有一个全局对象,它的名字叫backend
,它对应的是C++中的这个backend
实例。"
(2) backend.startScan()
: 在QML中,调用一个C++的Q_INVOKABLE
方法,语法与调用JavaScript函数完全相同。
四、反向通信:从C++更新QML
上面的例子已经展示了QML操作C++,本节讲解如何在QML中监听C++发来的信号------Connections
组件。
【核心概念:结构化的信号监听】
Connections
是一个QML组件,专门用于监听指定目标(target
)的所有信号。
【例4-3】 使用Connections组件响应信号。
1. 编写代码 (Main.qml)
我们修改Main.qml
,将信号处理逻辑从startScan
的调用处,移到一个集中的Connections
块中。这使得代码更清晰。
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Window {
id: rootWindow
// ... (属性保持不变) ...
// --- 关键:添加Connections组件 ---
Connections {
target: backend // 监听我们在main.cpp中注册的backend对象
// 当C++的backend对象发射statusMessageChanged信号时,这个函数会被自动调用
// 函数名规则: on + 信号名(首字母大写)
// 信号的参数会按顺序成为JS函数的参数
function onStatusMessageChanged(message) {
statusLabel.text = message;
}
}
ColumnLayout {
// ... (所有布局和组件与上一个例子完全相同) ...
}
}
2. 运行结果
运行程序。单击"开始检测"按钮后,界面上文本框显示"正在准备扫描设备...",等待两秒后,界面上显示"扫描完成"。
关键代码分析:
(1) Connections
: 这是一个非可视化的组件,它的作用是"订阅"某个QObject
对象(通过target
属性指定)的所有信号。
(2) function onStatusMessageChanged(message)
: 这是在Connections
内部定义的信号处理器。当target
(即backend
)发射statusMessageChanged
信号时,这个JavaScript函数就会被执行。QML会自动将C++信号的参数(const QString &message
)映射为JavaScript函数的参数(message
)。这种写法让所有与backend
的通信逻辑都集中在一个地方,极大地提高了代码的可读性和可维护性。
五、总结与展望
在本篇文章中,我们成功地架设了连接QML前端与C++后端的桥梁。我们掌握了:
- 使用上下文属性将C++对象暴露给QML。
- 通过**
Q_INVOKABLE
**宏,实现了从QML对C++方法的直接调用。 - 通过信号与槽 以及**
Connections
组件**,实现了从C++对QML界面的异步、解耦更新。
至此,我们的应用程序已经拥有了一个完整的、双向通信的现代化架构。前后端各司其职,并通过清晰的接口进行交互。
现在,这座桥梁已经准备好运输真正的"货物"了。在下一篇文章【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》------5. 集成OpenCV:让程序拥有"视力"】中,我们将开始集成强大的OpenCV库,并通过这座桥梁,将处理后的图像数据显示在QML界面上。