2. Qt基础回顾

在工业设备上位机开发中,Qt 之所以能长期占据主流位置,不仅因为它跨平台、界面美观,更因为它提供了一套稳定而优雅的底层机制。理解这些机制,是写出可维护、可扩展设备软件的前提。本章将围绕 元对象系统、信号与槽、事件循环 三大核心,结合常用类库,做一次系统性的回顾。

一、元对象系统:Qt 的“反射”基石

标准 C++ 本身没有运行时类型信息和反射能力,而 Qt 通过 元对象系统(Meta-Object System) 弥补了这一空白。它由三部分构成:QObject 基类、Q_OBJECT 宏,以及 moc(元对象编译器)。任何需要使用信号槽、属性系统、动态类型转换的类,都必须继承 QObject 并在类声明首行写上 Q_OBJECT。

moc 会在编译前扫描头文件,为带 Q_OBJECT 的类生成 moc_xxx.cpp,其中包含 metaObject()、qt_metacall() 等函数,从而让对象在运行时能够查询自己的类名、属性、信号与槽。这也是 qobject_cast、Q_PROPERTY、QMetaObject::invokeMethod 能工作的原因。

关键点:Q_OBJECT 必须写在类体第一行;若忘记添加,编译可能通过,但运行时会报 “undefined reference to vtable” 或信号槽静默失效。

二、信号与槽:对象间解耦的通信方式

信号与槽是 Qt 最具代表性的机制。信号在特定事件发生时被 emit,槽是接收响应的函数。二者通过 QObject::connect 建立连接,连接关系在运行时维护,因此发送者无需知道接收者的具体类型,实现了松耦合。

// 传统字符串连接(Qt4 风格,运行时检查)
connect(btnStart, SIGNAL(clicked()), this, SLOT(onStart()));

// 函数指针连接(Qt5+ 推荐,编译期检查)
connect(btnStart, &QPushButton::clicked, this, &MainWindow::onStart);

// Lambda 连接,适合临时逻辑
connect(btnStop, &QPushButton::clicked, this, [=](){
    plc->writeCoil(0, false);
});

连接类型决定了槽的调用时机,工业场景中尤其要注意跨线程通信:

连接类型行为典型场景
AutoConnection同线程直连,跨线程队列默认选择,最常用
DirectConnection立即在发送者线程执行同线程、需同步返回
QueuedConnection投递到接收者线程事件队列采集线程更新 UI
BlockingQueued队列连接并阻塞等待需等待结果再继续

三、事件循环:程序运转的心脏

Qt 程序由 QApplication::exec() 启动主事件循环,它不断从事件队列取出事件并分发。鼠标、键盘、定时器、网络、绘制请求,本质上都是事件。信号槽的队列连接、QTimer 超时、deleteLater() 的延迟删除,都依赖事件循环驱动。

工业上位机常见误区是:在槽函数中执行耗时操作(如等待 PLC 响应、读写大文件),会阻塞事件循环,导致界面“假死”。正确做法是把耗时任务放入 QThread 或 QtConcurrent,通过信号把结果回传主线程。

判断事件循环是否被阻塞,可观察界面是否还能响应重绘与点击。若 QTimer 不再触发,通常说明主线程被长时间占用。

四、常用类库速览

  • 容器类:QList、QVector、QMap、QHash,隐式共享,适合设备点位表、报警记录。
  • 字符串:QString 支持 Unicode,配合 QTextStream 处理中文日志。
  • IO 与网络:QFile、QSerialPort、QTcpSocket、QModbusTcpClient。
  • 模型视图:QAbstractTableModel + QTableView,适合实时数据表。
  • 定时器:QTimer 用于轮询采集,精度约毫秒级。

五、机制协同关系图

下图展示元对象系统、信号槽、事件循环与常用类库之间的协作关系:moc 为类生成元信息,信号经 connect 建立连接,队列连接把调用投递到事件循环,最终由事件循环分发给槽函数处理业务。

Qt 核心机制协同关系 QObject + Q_OBJECT 类声明与基类 moc 元对象编译器 生成 metaObject 信息 信号 emit 事件发生通知 connect 连接 直连 / 队列连接 事件循环 exec() 事件队列分发 槽函数执行 业务逻辑处理 常用类库:QString / QList / QTimer / QSerialPort / QModbusTcpClient 为信号槽与事件循环提供数据与 IO 支撑

六、案例与复盘:采集线程更新界面

假设设备以 100ms 周期采集温度,若直接在采集线程操作 QLabel,会触发 Qt 的跨线程警告甚至崩溃。正确做法是:采集线程 emit 信号,主线程槽函数更新界面,连接类型使用 Qt::QueuedConnection。

class Collector : public QObject {
    Q_OBJECT
signals:
    void temperatureReady(double value);
public slots:
    void poll() {
        double v = readSensor();
        emit temperatureReady(v);   // 跨线程安全
    }
};

// 主线程中
connect(collector, &Collector::temperatureReady,
        this, &MainWindow::updateTemp,
        Qt::QueuedConnection);
常见误区:
1. 在非 GUI 线程创建 QWidget 或调用其方法;
2. 忘记 Q_OBJECT 导致信号槽失效;
3. 在槽中执行阻塞操作,拖垮事件循环;
4. 使用字符串版 connect 时参数拼写错误,运行时才报错。