洁洁!头像
Qt 信号槽机制在跨进程 UI 中的应用封面图

Qt 信号槽机制在跨进程 UI 中的应用

Qt 信号槽机制在跨进程 UI 中的应用——倒计时窗口的线程设计与信号路由

本文以工具的交易签名确认窗为例,讲清三件事:Qt 子线程如何安全地监听 FIFO、跨线程信号如何路由到 UI、QTimer 倒计时如何被外部事件重置。适合做 Qt 跨线程编程的入门案例。

一、场景

交易签名时弹出确认窗口:

  • 显示交易信息(金额、账号等)
  • 60 秒倒计时,归零自动关闭
  • 支持三种外部输入:Key 上的确认键(继续签名)、Key 上的取消键(中止)、Key 上的翻页键(重置倒计时)

挑战在于事件源全都不在 Qt 进程内:

Qt 进程需要在一个非主线程里持续 read() FIFO(阻塞调用不能放主线程,否则界面卡死),再把结果安全地通知主线程更新 UI。这正是 Qt 信号槽 Qt::QueuedConnection 的经典用武之地。

二、线程架构

分工:

线程职责禁止事项
主线程所有 UI 操作、QTimer 倒计时阻塞式 read
monitor 线程阻塞轮询 FIFO、发射信号直接操作 UI 控件

三、monitor 线程:FIFO 监听与信号发射

3.1 头文件(monitor.h)

#ifndef MONITOR_H
#define MONITOR_H

#include <QThread>

#define FIFONAME "/tmp/FIFO_BANKA"

class monitor : public QThread
{
    Q_OBJECT    // ← 有信号必须加这个宏

public:
    monitor();
    ~monitor();

    void run() override;    // QThread 的线程入口

signals:
    void closeSignal();         // 通知 UI 关闭(对应 "close")
    void notifyResetTimer();    // 通知 UI 重置倒计时(对应 "Refresh")

private:
    int m_fd;
};

#endif

3.2 实现(monitor.cpp)

void monitor::run()
{
    char str[20] = {0};

    // FIFO 由动态库创建;不存在说明 dylib 侧没准备好,直接放弃
    if (::access(FIFONAME, F_OK) == -1)
        return;

    m_fd = ::open(FIFONAME, O_RDONLY | O_NONBLOCK);
    if (m_fd < 0)
        return;

    while (true)
    {
        memset(str, 0, sizeof(str));
        int nread = ::read(m_fd, str, sizeof(str) - 1);

        if (nread > 0)
        {
            // 消息1:关闭窗口
            if (0 == strcmp(str, "close"))
            {
                emit closeSignal();
                break;      // 关闭后线程使命完成,退出
            }

            // 消息2:重置倒计时 {0x07,'R','e','f','r','e','s','h'}
            // 注意不能 strcmp:首字节 0x07 不是合法 C 字符串结尾前的内容,
            // 且消息是二进制语义,必须 memcmp 定长比较
            unsigned char szRefresh[] = {0x07,0x52,0x65,0x66,0x72,0x65,0x73,0x68};
            if (nread >= (int)sizeof(szRefresh) &&
                0 == memcmp(str, szRefresh, sizeof(szRefresh)))
            {
                emit notifyResetTimer();
                continue;   // ← 关键:不 break,继续监听后续信号
            }
        }
        msleep(100);        // 无数据时休眠,避免 CPU 空转
    }

    ::close(m_fd);
    m_fd = -1;
}

三个设计要点:

  1. strcmp vs memcmp:close 是纯文本消息,strcmp 足够;Refresh 消息首字节是二进制长度 0x07,strcmp 在遇到 0x07 后的行为依赖实现,必须用 memcmp 且先校验 nread 长度。
  2. close 后 break,Refresh 后 continue:语义完全不同——关闭意味着整个对话框要销毁;重置倒计时只是周期性事件,线程必须继续活着接收下一次重置。
  3. msleep(100) 轮询:FIFO 以 O_NONBLOCK 打开,read 无数据立即返回 0,不 sleep 会 100% CPU 空转。(更优方案是 select/poll 或去掉 O_NONBLOCK 让 read 阻塞,此处轮询胜在实现简单且延迟 100ms 可接受。)

四、倒计时窗口:QTimer 与槽函数

4.1 头文件(transconfirm.h)

class TransConfirm : public QDialog
{
    Q_OBJECT

public:
    explicit TransConfirm(QWidget *parent = nullptr);
    ~TransConfirm();

public slots:
    void slotTimeDecrease();    // QTimer timeout:每秒递减
    void slotResetTimer();      // monitor 信号:重置为 60

private:
    Ui::TransConfirm *ui;
    QTimer *m_timer;
    monitor *m_monitor;
    int m_timeCount;            // 剩余秒数
};

4.2 实现(transconfirm.cpp)

TransConfirm::TransConfirm(QWidget *parent)
    : QDialog(parent), ui(new Ui::TransConfirm)
{
    ui->setupUi(this);

    // 倒计时初始化
    m_timeCount = 60;
    ui->label_timeCount->setText(QString::number(m_timeCount));

    // 每秒滴答
    m_timer = new QTimer(this);
    connect(m_timer, &QTimer::timeout,
            this, &TransConfirm::slotTimeDecrease);
    m_timer->start(1000);

    // 启动 FIFO 监听线程
    m_monitor = new monitor();
    connect(m_monitor, &monitor::notifyResetTimer,
            this, &TransConfirm::slotResetTimer);
    connect(m_monitor, &monitor::closeSignal,
            this, &QDialog::reject);
    m_monitor->start();     // QThread::start 触发 run() 在新线程执行
}

void TransConfirm::slotTimeDecrease()
{
    m_timeCount--;
    if (m_timeCount <= 0)
    {
        m_timer->stop();
        reject();           // 超时,按取消处理
        return;
    }
    ui->label_timeCount->setText(QString::number(m_timeCount));
}

void TransConfirm::slotResetTimer()
{
    m_timeCount = 60;
    ui->label_timeCount->setText(QString::number(m_timeCount));
}

重置为什么这么简单? 因为倒计时的"状态"只有 m_timeCount 一个变量,QTimer 只负责固定周期递减它。重置 = 改回 60,下一个 tick 自然从 60 重新递减。不需要 restart QTimer,不需要任何同步操作。

五、核心机制:跨线程信号槽为什么安全

5.1 自动连接类型

connect(m_monitor, &monitor::notifyResetTimer,
        this, &TransConfirm::slotResetTimer);

connect 默认第五参数是 Qt::AutoConnection:

  • 发射者与接收者同线程 → 直接调用(DirectConnection)
  • 跨线程 → 事件投递(QueuedConnection):emit 把调用打包成事件放入接收者线程(主线程)的事件队列,主线程事件循环取出来执行槽函数

本例中 emit notifyResetTimer() 在 monitor 线程执行,slotResetTimer() 在主线程执行——这就是为什么子线程里只 emit 不碰 UI 是安全的,Qt 在底层帮你做了线程切换。

5.2 参数传递的坑

Queued 连接的信号参数会被拷贝进事件队列。自定义类型必须注册:

qRegisterMetaType<MyStruct>("MyStruct");

否则运行时报:

QObject::connect: Cannot queue arguments of type 'MyStruct'

本例的两个信号都无参数,天然规避了这个问题。

5.3 线程亲和性

一个常见误解是"QThread 对象在哪个线程,run 就在哪个线程"。正确理解:

  • monitor 对象本身(构造、信号发射器)活在其被创建的线程——本例是主线程
  • run() 函数体在 start() 后的新线程里执行
  • 所以 emit notifyResetTimer() 语句执行位置 = monitor 线程 = 与接收者(主线程)不同 → 自动走 Queued

如果把 emit 写在构造函数里(主线程执行),同样的 connect 就会走 Direct——同一条信号槽连接的行为随发射线程而变,这是初学者最容易混淆的点。

六、窗口销毁时的线程清理

对话框关闭(reject())时必须停掉 monitor 线程,否则线程还在 read FIFO、对象却析构了 → 崩溃:

TransConfirm::~TransConfirm()
{
    if (m_monitor && m_monitor->isRunning())
    {
        m_monitor->quit();     // 请求事件循环退出(对 run 轮询无效,见下)
        // 本例 run 是裸 while 循环没有事件循环,quit 无效,
        // 依赖 closeSignal 分支的 break 自然退出;
        // 若 FIFO 无后续消息,可用管道写端注入终止标记,或
        // m_monitor->wait(1000) 后 terminate()(最后手段)
    }
    delete m_monitor;
    delete ui;
}

实际上本例的退出闭环是:dylib 发 "close" → monitor 线程 break → 线程函数返回 → 线程自然结束。只要保证任何退出路径最终都有人往 FIFO 写一条 close(包括超时路径由 UI 自己 reject() 时注意线程回收顺序),就不会泄漏线程。

七、总结

这个案例麻雀虽小,覆盖了 Qt 多线程编程的核心知识点:

  1. QThread 的正确用法:继承 + 重写 run(),耗时 I/O 放子线程
  2. 子线程只 emit,主线程管 UI:跨线程信号自动走 Queued 连接
  3. 二进制协议用 memcmp,文本协议用 strcmp,混用必出诡异 bug
  4. 语义决定循环控制流:close 是终态(break),refresh 是瞬态(continue)
  5. 状态机式倒计时:单变量 + 固定周期递减,重置只需改状态,不动定时器
  6. 线程退出要有闭环:靠协议消息(close)驱动线程自然结束,优于暴力 terminate

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2202_75623950/article/details/164064145

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--