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

目录

一、概述

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)

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界面上。

相关推荐
Shingmc35 分钟前
【C++】二叉搜索数
开发语言·c++
洁可2 小时前
上位机程序开发基础介绍
c++·笔记
寒心雨梦3 小时前
本地preload hook案例
c++
滴水成川4 小时前
现代 C++ 开发工作流(VSCode / Cursor)
开发语言·c++·vscode·cursor
张同学的IT技术日记4 小时前
重构 MVC:让经典架构完美适配复杂智能系统的后端业务逻辑层(内附框架示例代码)
c++·后端·重构·架构·mvc·软件开发·工程应用
万能的小裴同学4 小时前
星痕共鸣数据分析2
c++·数据分析
刚入坑的新人编程5 小时前
暑期算法训练.8
数据结构·c++·算法·面试·哈希算法
TalkU浩克5 小时前
C++中使用Essentia实现STFT/ISTFT
开发语言·c++·音频·istft·stft·essentia
小比卡丘6 小时前
【C++进阶】第7课—红黑树
java·开发语言·c++
不断努力的根号七6 小时前
qt框架,使用webEngine如何调试前端
开发语言·前端·qt