如何在Qt主线程中正确执行GUI操作?

15
我有一个简单的程序,由两个线程组成:
  1. 由Qt QApplication::exec操作的主GUI线程
  2. boost::asio::io_service操作的TCP网络线程

TCP事件(如连接或接收数据)会导致GUI中的更改。这些更改通常是在QLabel上使用setText和隐藏各种小部件。目前,我正在TCP客户端线程中执行这些操作,这似乎非常不安全。

如何正确地向Qt主线程发送事件? 我正在寻找Qt版本的boost::asio::io_service::strand::post,它将事件发布到boost::asio::io_service事件循环中。


3
请查看信号与槽 - Mohamad Elghawi
@MohamadElghawi 我知道信号和槽的使用,但我应该如何具体实现呢?我不想在我的TCP客户端类中包含Q_OBJECT,因此无法将其连接到应用程序。 - Tomáš Zato
3个回答

10

如果您不想将TCP类作为QObject,则另一个选择是使用QMetaObject::invokeMethod()函数。

要求是目标类必须是QObject,并且必须调用目标上定义的槽函数。

假设您的QObject定义如下:

class MyQObject : public QObject {
    Q_OBJECT
public: 
    MyObject() : QObject(nullptr) {}
public slots:
    void mySlotName(const QString& message) { ... }
};

然后你可以从你的TCP类中调用那个插槽。
#include <QMetaObject>

void TCPClass::onSomeEvent() {
    MyQObject *myQObject = m_object;
    myMessage = QString("TCP event received.");
    QMetaObject::invokeMethod(myQObject
                               , "mySlotName"
                               , Qt::AutoConnection // Can also use any other except DirectConnection
                               , Q_ARG(QString, myMessage)); // And some more args if needed
}

如果您在调用时使用Qt::DirectConnection,则槽将在TCP线程中执行,并且可能会导致崩溃。
编辑:由于invokeMethod函数是静态的,因此您可以从任何类中调用它,而该类不需要是QObject。

+1. 我只想说,目标方法不一定必须是一个槽。只要它通过元类型系统提供即可,这也可以通过标记方法 Q_INVOKABLE 来实现。 - Mohamad Elghawi
@TomášZato 我已经在帖子中添加了更多的代码,以使它更清晰明了。 正如MohamadElghawi所指出的那样,如果该方法被标记为 Q_INVOKABLE,那么它就不需要像上面的示例代码一样成为一个槽。 - CJCombrink
@TheBadger 非常感谢你,这帮助了我很多。 - Tomáš Zato
你的回答可能需要更详细的说明。 - mike510a
@MikeNickaloff 或许是这样,但你有什么建议吗?我应该讨论一下 invokeMethod 的作用,还是链接到 Qt 页面足以描述该函数?或者我应该给出一个更好的示例来更新 QLabel?答案应该给出足够的信息,让人们在问题和限制条件下开始工作。 - CJCombrink
显示剩余3条评论

8

如果您的对象继承自QObject,则只需发出一个信号并将其连接(使用标志Qt :: QueuedConnection)到主线程中的一个槽。信号和槽是线程安全的,最好使用。

如果不是QObject,则可以创建一个带有GUI代码的lambda函数,并使用单次定时器QTimer将其排队到主线程中,并在回调中执行它。这是我正在使用的代码:

#include <functional>

void dispatchToMainThread(std::function<void()> callback)
{
    // any thread
    QTimer* timer = new QTimer();
    timer->moveToThread(qApp->thread());
    timer->setSingleShot(true);
    QObject::connect(timer, &QTimer::timeout, [=]()
    {
        // main thread
        callback();
        timer->deleteLater();
    });
    QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0));
}

...
// in a thread...

dispatchToMainThread( [&, pos, rot]{
    setPos(pos);
    setRotation(rot);
});

感谢https://riptutorial.com/qt/example/21783/using-qtimer-to-run-code-on-main-thread的原创作者。

请小心操作,因为如果您删除了对象,您的应用程序可能会崩溃。有两个选项:

  • 在删除之前调用qApp->processEvents()以清空队列;
  • 也可以使用dispatchToMainThread将删除排队;

1
喜欢这个解决方案! - CybeX
1
这是一个非常好的解决方案,令人惊讶的是它不是Qt库的一部分。 - StainlessSteelRat

2

在@CJCombrink的优秀答案基础上进行扩展; 无需定义插槽,您也可以使用lambda:

QMetaObject::invokeMethod(
          myQObject, [=]() { /* ... whatever ... */ }, Qt::QueuedConnection);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接