> 技术文档 > 《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——4. 前后端联动:打通QML与C++的任督二脉

《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——4. 前后端联动:打通QML与C++的任督二脉


目录

  • 一、概述
    • 1.1 背景介绍:UI与逻辑的“隔阂”
    • 1.2 学习目标
    • 1.3 MVVM架构简介
  • 二、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 学习目标

通过本篇的学习,读者将能够:

  1. 理解并实践前后端分离的MVVM(Model-View-ViewModel)架构思想。
  2. 创建一个C++后端类(Backend),作为连接前端与业务逻辑的桥梁。
  3. 掌握在QML中调用C++方法的关键技术(Q_INVOKABLE)。
  4. 掌握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)

#ifndef BACKEND_H#define BACKEND_H#include #include 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)

#include \"backend.h\"#include #include  // 用于模拟耗时操作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世界最关键的一步。

#include #include #include #include  // 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++对象了。

import QtQuickimport QtQuick.Controlsimport QtQuick.LayoutsWindow { // ... (属性保持不变) ... 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. 运行结果
运行程序,点击“开始检测”按钮。会看到应用程序输出窗口依次输出如下:

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块中。这使得代码更清晰。

import QtQuickimport QtQuick.Controlsimport QtQuick.LayoutsWindow { 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界面上。