Qt如何 发送带结构体数据的信号

前提

在Qt6里用Q_DECLEAR_METATYPE注册之后不需要QVariant中转信号槽传递步骤了。写const MyStruct& data也可以直接传递过去

问题场景

有时候我们想通过信号槽传递一个自定义的结构体,比如学生信息、设备参数、配置项等。但 Qt 的信号槽系统默认只支持它自己认识的那些类型(比如 intQStringQList 等)。如果你直接写 signal(const MyStruct &s),编译可能能过,但一运行就会报类似"无法在队列中传递自定义类型"的警告,甚至直接崩溃。

这是因为 Qt 的信号槽在跨线程时会需要拷贝构造你的参数,而编译器不知道如何为你的结构体做元类型注册。

解决思路

Qt 提供了一套机制来支持自定义类型:

  1. Q_DECLARE_METATYPE(T) 宏注册结构体,让它能被 QVariant 识别。

  2. 信号函数里的参数类型写成 QVariant,发送时用 setValue() 把结构体塞进去,接收时用 value<T>() 取出来。

  3. 如果信号和槽在不同线程 ,还要再调用 qRegisterMetaType<T>() 提前告知 Qt 如何构造这个类型。

下面用一个学生信息结构体作为例子。

第一步:定义结构体并注册

假设我们有一个简单的结构体:

cpp 复制代码
struct SRT_STUDENT
{
    int age;
    char name[20];
};

为了让 QVariant 能存它,需要在结构体定义后加上:

cpp 复制代码
Q_DECLARE_METATYPE(SRT_STUDENT)

这个宏展开后会给结构体生成一些必要的元信息(比如 typeId ),这样 QVariant 就能知道怎么复制、销毁这个结构体了。

第二步:信号和槽的参数用 QVariant

MainWindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QtGui/QMainWindow>
#include <QVariant>

struct SRT_STUDENT
{
    int age;
    char name[20];
};
Q_DECLARE_METATYPE(SRT_STUDENT)

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();
    void sendSig();

public slots:
    void receiveSig(QVariant varValue);

signals:
    void sig_StudentInfo(QVariant varValue);   // 信号参数是 QVariant,不是直接的结构体
};

#endif

MainWindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include <QDebug>
#include <cstring>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // 老式 connect 写法(也可以,但更推荐新式)
    connect(this, SIGNAL(sig_StudentInfo(QVariant)),
            this, SLOT(receiveSig(QVariant)));
}

MainWindow::~MainWindow()
{
}

void MainWindow::sendSig()
{
    SRT_STUDENT stu;
    memset(&stu, 0, sizeof(stu));
    stu.age = 20;
    strcpy(stu.name, "lili");

    QVariant varValue;
    varValue.setValue(stu);          // 结构体装进 QVariant

    // 模拟某个条件触发信号
    for (int i = 0; i < 5; i++) {
        if (i == 4) {
            emit sig_StudentInfo(varValue);
            break;
        }
    }
}

void MainWindow::receiveSig(QVariant varValue)
{
    // 从 QVariant 里把结构体解出来
    SRT_STUDENT stu1 = varValue.value<SRT_STUDENT>();
    qDebug() << "age=" << stu1.age << "name=" << stu1.name;  // age=20 name=lili
}

第三步:跨线程时的额外操作

上面的例子是在同一个线程内(主线程自己发自己收),所以不需要 qRegisterMetaType。但如果你的信号是从工作线程发到主线程(或者反过来),Qt 会把参数放到队列里 ,然后在目标线程里再构造出来。这时候 Qt 需要知道如何构造 SRT_STUDENT 对象,就必须提前注册元类型。

注册方法很简单,一般在 main() 函数里或者某个初始化函数里写一次:

cpp 复制代码
qRegisterMetaType<SRT_STUDENT>("SRT_STUDENT");

如果你同时还要用这个结构体做跨线程的信号参数(比如信号直接传 SRT_STUDENT 而不是 QVariant),那这个注册更是必不可少的。但注意,我们这里的方案用的是 QVariant 包装,其实 QVariant 本身已经是 Qt 能识别的类型了,那为什么还需要注册呢?

关键点QVariant 虽然能被 Qt 直接传递,但它内部存储的自定义类型(也就是你的结构体)在跨线程队列中依然需要被正确构造。qRegisterMetaType 就是告诉 Qt 如何构造和析构这个类型。如果漏掉这一步,运行时会出现类似:

cpp 复制代码
QObject::connect: Cannot queue arguments of type 'SRT_STUDENT'
(Make sure 'SRT_STUDENT' is registered using qRegisterMetaType().)

所以保险起见,只要你的自定义类型被放进 QVariant 并且要通过信号跨线程传递,就加上 qRegisterMetaType

示例:

cpp 复制代码
// 一般在 main.cpp 或程序启动时
qRegisterMetaType<SRT_STUDENT>("SRT_STUDENT");

之后再进行 connect 就没问题了。

一些补充和坑

1. 为什么不直接让信号参数是结构体?

理论上你可以让信号写成 void sig_StudentInfo(const SRT_STUDENT &stu),并且配合 qRegisterMetaType,也是能工作的。但这样要求信号和槽两端都包含结构体的定义,而且跨线程时 Qt 会多次拷贝结构体。而用 QVariant 包装则多了一层抽象,代码更统一(任何自定义类型都可以这样传),缺点是取出来时稍微麻烦一点。

个人习惯:如果只是偶尔传一两个自定义结构,用 QVariant 省事;如果大量使用,直接注册类型然后用结构体做参数更直观。

2. 结构体里最好用 Qt 的容器类型

比如 char name[20] 这种 C 风格数组虽然能用,但跨线程拷贝时容易出问题(浅拷贝)。建议用 QStringQByteArray,它们已经是 Qt 元类型系统的一部分,配合 QVariant 更安全。

cpp 复制代码
struct SRT_STUDENT
{
    int age;
    QString name;   // 更好
};
3. 别忘了在接收端 include 结构体定义的头文件

varValue.value<SRT_STUDENT>() 这行代码需要知道结构体的完整定义,否则编译会报"不完整类型"。所以接收槽所在的 .cpp 文件里要包含定义结构体的头文件。

4. 使用新式 connect 避免拼写错误

老式的 SIGNAL()SLOT() 宏不会在编译期检查信号槽是否存在,如果写错函数名或参数类型,编译能过,运行时不触发。推荐用新式语法:

cpp 复制代码
connect(this, &MainWindow::sig_StudentInfo,
        this, &MainWindow::receiveSig);

但注意新式 connect 对参数类型要求更严格,如果你的信号是 QVariant,槽参数也必须是 QVariant,否则编译失败(这其实是好事)。

总结

  • 自定义结构体要传递信号槽,先用 Q_DECLARE_METATYPE 注册。

  • 信号参数写成 QVariant,发送时 setValue(),接收时 value<T>()

  • 跨线程场景一定要调用 qRegisterMetaType<T>() 注册类型,否则运行时报错。

  • 结构体内部尽量用 Qt 类型(QStringQList 等),避免 C 风格数组带来的浅拷贝风险。

这种方式虽然多了一层 QVariant 包装,但胜在稳定、通用,不需要为每个结构体单独写一套信号槽重载。如果你觉得频繁的 setValue/value 比较麻烦,也可以直接让信号参数为结构体并配合 qRegisterMetaType,两种方法都可以,看个人喜好。

相关推荐
吃好睡好便好2 小时前
在Matlab中绘制非默认峰值图
开发语言·学习·算法·matlab
NagatoYukee2 小时前
Java 商品交易实验(第二版)
java·开发语言
阳光九叶草LXGZXJ2 小时前
自制数据库迁移工具-C版-07-HappySunshineV1.6-(支持PG、达梦、Gbase8a)
linux·c语言·开发语言·数据库·学习·postgresql
不吃土豆的马铃薯2 小时前
5.SGI STL 二级空间配置器 _S_chunk_alloc核心函数解析
开发语言·c++·vscode·c·内存池
学掌门2 小时前
JavaScript:为什么命名参数比位置参数更好
开发语言·javascript·ecmascript
码界筑梦坊2 小时前
124-基于Python的航空旅客满意度数据可视化分析系统
开发语言·python·信息可视化·数据分析·flask·毕业设计
XMYX-02 小时前
31 - Go url 解析:从字符串到结构化请求的完整路径
开发语言·golang
hhb_6182 小时前
PHP开发实战:高频难点解析与优化方案
开发语言·php
夕除2 小时前
spring boot 8
java·开发语言