zl程序教程

您现在的位置是:首页 >  工具

当前栏目

qt 子线程(多线程)更新gui的几种方法

Qt多线程方法线程 更新 几种 GUI
2023-09-11 14:16:44 时间

直接进入主题,据我所知,方法有3种:

1、自定义signal、slot,这个就不多说了,标准方法

2、使用QmetaMethod::invoke,这是可以直接在子线程调用的,看起来比较另类

3、从5.4开始,有了另一种新方法:Qtimer::singleShot(0),这玩意儿可不是定时启动那么简单,你给他0的时间他就是GUI线程安全的,完整例子可以测试:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

#include <QCoreApplication>#include <QTimer>

#include <QDebug>

#include <thread>

#include <QThread>

#include <QEventLoop>

#include <QThread>

using std::thread;

class TestObj

        :public QObject

{

// Used new connect syntax no need for Q_OBJECT define

// you SHOULD use it. I used just to upload one file

//Q_OBJECT

public slots:

    void doWork()

    {

        qDebug() << "QThread id" << QThread::currentThreadId();

        QTimer::singleShot(0, qApp, []()

        {

            qDebug() << "singleShot from QThread" << QThread::currentThreadId();

        });

    }

};

int main(int argc, char *argv[])

{

    QCoreApplication a(argc, argv);

    qDebug() << "main thread id" << QThread::currentThreadId();

    thread testThread([]()

    {

        QEventLoop loop;

        Q_UNUSED(loop)

        qDebug() << "std::thread id" << QThread::currentThreadId();

        QTimer::singleShot(0, qApp, []()

        {

            qDebug() << "singleShot from std thread with qApp" << QThread::currentThreadId();

        });

        qDebug() << "std::thread finished";

    });

    testThread.detach();

    QThread testQThread;

    TestObj testObj;

    testObj.moveToThread(&testQThread);

    QObject::connect(&testQThread, &QThread::started, &testObj, &TestObj::doWork);

    testQThread.start();

    // 下列方法2选1,都可以退出app    

    //QMetaObject::invokeMethod(&a, "quit", Qt::QueuedConnection);

    QTimer::singleShot(0, &a, [&a]()

    {

        a.quit();

    });

    // 直接调用a.quit();是无法退出的,因为没有到eventloop

    // a.quit();

    a.exec();

    testQThread.quit();

    testQThread.wait();

    return 0;

}

输出:

1

2

3

4

5

6

main thread id 0x1bb4

std::thread id 0x294c

std::thread finished

QThread id 0x1bd4

singleShot from std thread with qApp 0x1bb4

singleShot from QThread 0x1bb4

可以看到,qtimer执行的一直是主线程,也就是GUI线程。

/******************************

之前有篇文章说,是传界面指针到线程中去,从而在线程中操作主界面中控件。
今天发现,这种方法是极其错误的,文章我已经删掉,希望没有误人子弟。
前面转的两篇文章中对于为什么不能在线程中操纵界面控件指针有了很好的解释。下面在做下解释:
尽管QObject是可重入的,但GUI类,特别是QWidget与它的所有子类都是不可重入的。它们仅用于主线程。正如前面提到过 的,QCoreApplication::exec() 也必须从那个线程中被调用。实践上,不会在别的线程中使用GUI类,它们工作在主线程上,把一些耗时的操作放入独立的工作线程中,当工作线程运行完成,把 结果在主线程所拥有的屏幕上显示。

 

下面的几个帖子是我在解决问题的过程中看到的几个很有帮助的帖子,一并贴下:
http://www.qtcn.org/bbs/read.php?tid=12121&keyword=%CF%DF%B3%CC
http://www.qtcn.org/bbs/read.php?tid=13508&keyword=%CF%DF%B3%CC
QT里面可以在非GUI线程里发个事件,让GUI线程执行完后再返回呢?老问题 - QTCN开发网 - Powered by phpwind


下面截取里面比较有价值的回复:


线程里面直接操作界面元素是肯定不行的,即使不总是出现错误,偶尔也会出莫明其妙,在 文档里面已经有说了,你再找找看,
你需要使用比如postEvent之类的异步处理方式.
至于进程中的那个错误,你设断点调试下吧.
不可以在非界面的进程中直接操作界面的
Qt的文档有这么说
据说4之前版本可以,没有试过


signal/slot目前有三种调用方式
1.DirectConnection
和以前一样,在emit处直接invoke你的slot 函数,一般情况是sender,receiver在同一线程


2.QueuedConnection
将 发送Event给你的receiver所在的线程
postEvent(QEvent::MetaCall,...)
slot 函数会在receiver所在的线程的event loop中进行处理,一般情况是sender,receiver不在同一线程


3.BlockingQueuedConnection
调 用sendEvent(QEvent::MetaCall,...),在receiver所在的线程处 理完成后才会返回;只能当sender,receiver不在同一线程时才可以


好了,上面的是说为什么不行的,那如果非要在非GUI线程里操作GUI线程里的控件,该怎么做呢?
答案是使用signal/slot。
在线程里的run()里定期emit signal,GUI线程里建立连接,写槽函数,注意connect的第五个参数应该使用Queued方式。

下面列个从QT论坛上找的例子,专门用来解释这个问题的


thread.h:


#ifndef THREAD_H
#define THREAD_H
#include <QThread>


class Thread : public QThread
{
Q_OBJECT
public:
Thread();
signals:
void sendString(QString);
protected:
void run();
};


#endif // THREAD_H


widget.h:


#ifndef WIDGET_H
#define WIDGET_H
#include <QtGui/QWidget>
#include <QtGui/QTextEdit>


class Thread;


class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
private:
QTextEdit *m_textEdit;
Thread *m_thread;
};
#endif // WIDGET_H


thread.cpp:


#include "thread.h"
#include <QtCore/QTime>
Thread::Thread()
{
}
void Thread::run()
{
while(1)
{
emit sendString(QTime::currentTime().toString("hh:mm:ss.zzz"));
msleep(200);
}
}


widget.cpp:


#include <QtGui/QHBoxLayout>
#include "widget.h"
#include "thread.h"


Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_textEdit = new QTextEdit(this);
QHBoxLayout * layout = new QHBoxLayout(this);
layout->addWidget(m_textEdit);
setLayout(layout);
m_thread = new Thread();
connect(m_thread, SIGNAL(sendString(QString)), m_textEdit, SLOT(append(QString)));
m_thread->start();
}
Widget::~Widget()
{
}


main.cpp:


#include <QtGui/QApplication>
#include "widget.h"


int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}

/************************************

线程里面直接操作界面元素是肯定不行的,即使不总是出现错误,偶尔也会出莫明其妙,在文档里面已经有说了,你再找找看,
你需要使用比如postEvent之类的异步处理方式.
至于进程中的那个错误,你设断点调试下吧.

/******************************************

1. 线程与界面组件需要注意的地方

  • 在QThread线程中不能直接创建QWidget之类的界面组件.
  • 因为在QT中,所有界面组件相关的操作都必须在主线程中(也就是GUI thread)
  • 所以, QThread线程不能直接操作界面组件.

2.QThread线程如何操作界面组件-方法1

  • 将多线程类对象封装为GUI界面类的类成员
  • 然后在子线程定义信号函数,通过信号槽机制,向界面组件emit发射信号,从而实现间接操作.

3.QThread线程如何操作界面组件-方法2

  • 使用QApplication::postEvent()实现向界面发送事件,从而能够封装一个自定义类

4.使用Invokes()函数来调用界面组件的信号槽-方法3

一般使用该函数(用来调用对方的私有信号或槽):

该函数的连接方式默认使用的是Qt::AutoConnection

  • 表示如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。

比如,当我们想调用一个obj下的compute(QString, int, double)槽函数时:

则只需要写入:

QMetaObject::invokeMethod(obj, "compute",
                            Q_ARG(QString, "sqrt"),                        
                            Q_ARG(int, 42),
                            Q_ARG(double, 9.7));

示例如下所示:

在Testtherd线程类里通过invokeMethod向父界面类的paintMsg槽函数发送信息

void Testtherd::run()
{
    int count=0;
    while(1)
    {
        QString str="请稍等,正在验证用户,登录中";
        for(int i =0;i<count;i++)
           str.append('.');                   //循环添加小数点
        count=(count+1)%7;
        QMetaObject::invokeMethod(this->parent(), "paintMsg",
                                            Q_ARG(QString, str));

        msleep(500);
    }
}

父界面类的paintMsg槽函数如下所示:

void loginwindow:: paintMsg(QString msg) {
this->LineHint->setText(msg);
}

运行效果如下: