当前位置: 首页 > news >正文

《使用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 <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)

#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世界最关键的一步。

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

import QtQuick
import QtQuick.Controls
import QtQuick.LayoutsWindow {// ... (属性保持不变) ...ColumnLayout {// ... (布局保持不变) ...// --- 1. 结果展示区 (修改) ---// 我们用一个Label来显示状态信息Frame {id: resultFrameLayout.fillWidth: trueLayout.preferredHeight: 150background: Rectangle { color: "#2c3e50" }Label { // 使用Label代替Text,样式更统一id: statusLabeltext: "准备就绪"color: "white"font.pixelSize: 18anchors.centerIn: parent}}// --- 2. 控制区 (修改) ---RowLayout {// ... (布局保持不变) ...Button {id: startButtontext: "开始检测"Layout.preferredWidth: 120Layout.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 QtQuick
import QtQuick.Controls
import 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界面上。

http://www.lryc.cn/news/598630.html

相关文章:

  • LAYOUT 什么时候需要等长布线?
  • 【牛客刷题】和零在一起
  • 【Linux】Linux了解与基本指令(1)
  • 数据库期中复习
  • SkyWalking异步采集spring gateway日志
  • postgresql执行创建和删除时遇到的问题
  • AWS云S3+Glue+EMRonEC2+ReadShift
  • ThinkPHP8集成RabbitMQ的完整案例实现
  • 使用 Strands Agents 开发并部署生产级架构通用型个人助手
  • 【论文阅读】基于EEG的冥想状态数据挖掘研究(2025)
  • 深入探索Amazon SQS:构建弹性微服务与无服务器应用的秘密武器
  • Nodejs:从“模块未找到”到“大师之路”, 项目初始化与依赖安全完全指南
  • JimuReport 积木报表 v2.1.1 版本发布,免费开源的报表和大屏
  • 主流摄像头协议及其开源情况,GB/T 28181协议介绍
  • Word2Vec模型训练全流程解析:从数据预处理到实体识别应用
  • PyTorch常用Tensor形状变换函数详解
  • 如何恢复mysql,避免被研发删库跑路
  • 多模态数据处理系统:用AI读PDF的智能助手系统分析
  • 六、Element-快速入门
  • K8s WebUI 选型:国外 Rancher vs 国内 KubeSphere vs 原生 Dashboard,从部署到使用心得谁更适合企业级场景?
  • 从零用java实现 小红书 springboot vue uniapp(14) 集成阿里云短信验证码
  • Android安全存储:加密文件与SharedPreferences最佳实践
  • 【C++】使用箱线图算法剔除数据样本中的异常值
  • 进程通信----匿名管道
  • 【redis其它面试问题】
  • PHP 与 Vue.js 结合的前后端分离架构
  • 工具分享02 | Python批量文件重命名工具
  • 电商接口什么意思?
  • 数据所有权与用益权分离:数字经济时代的权利博弈与“商业机遇”
  • Claude Code是如何做上下文工程的?