QT中工作线程调用GUI主线程控件的问题

2023-05-16

QT中线程调用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
http://www.qtcn.org/bbs/read.php?tid=18586&keyword=%CF%DF%B3%CC
下面截取里面比较有价值的回复:
线程里面直接操作界面元素是肯定不行的,即使不总是出现错误,偶尔也会出莫明其妙,在 文档里面已经有说了,你再找找看,
你需要使用比如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.h:
#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();
}
这个例子就演示了如何在非GUI线程里调GUI线程里的控件。

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

Qt信号槽的一些事 Qt::带返回值的信号发射方式 

一般来说,我们发出信号使用emit这个关键字来操作,但是会发现,emit并不算一个调用,所以它没有返回值。那么如果我们发出这个信号想获取一个返回值怎么办呢?

两个办法:1.通过出参形式返回,引用或者指针的方式带回;比如emit sig(int& i)或者emit sig(void* pointer),但是这个方法有一个弊端,稍后介绍第二种方式会提醒。

2.通过qt自带的invoke机制调用:参考文档对QMetaObject::invokeMethod的说明:Invokes the member (a signal or a slot name) on the object obj.也就是说回调是可以回调信号或者槽的。一般来说,我们使用invokeMethod是在子线程需要调度UI操作的时候(已经有很多文章详细说明了使用方式,不再赘述),因为UI操作只能在主线程中使用(否则会出现未定义错误),通过这种回调方式,让要操作的事件回到主线程时间片的时候再来执行。大部分情况下,我们把UI操作封装在一个槽里,用回调方式来调度。同样信号也可以用这种方式,但是有几点需要注意的是,1.调用回调的连接方式:如果信号和连接槽在一个线程内,那么必须用Qt::DirectConnection或者Qt::AutoConnection,这样的话,保证信号回调后,线程会等待信号连接槽执行完毕,才可能取到我们需要的返回值;如果使用了Qt::QueuedConnection,那么信号只是负责把事件交给事件队列,然后马上做出返回,这样,是否有返回值就无法确定了(这也就是第一个方法的弊端,因为信号发射是根据信号和槽各自的线程情况来选择的连接方式).如果信号和槽在两个线程中,那么首先肯定不能使用Qt::DirectConnection,除非你很清楚连接槽的动作是否保证了线程安全。但根据第一条的说明,也不能使用Qt::QueuedConnection。不过还好qt提供了一个额外的连接方式就是Qt::BlockingQueuedConnection,这个连接方式会阻塞住发射信号的线程一直等到队列连接槽返回后,才会恢复阻塞,这样就可以保证我们能拿到真正的返回值。(但是使用这种方式需要你清楚的知道,发射线程是否允许阻塞和连接槽是否对这个阻塞线程有什么特别的操作,一般来说,如果这个线程并不是由你自己控制的话,不要随便尝试去阻塞别人的线程,因为你并不清楚别人线程的执行逻辑)

调用方式大致代码如下bool bReturn; QMetaObject::invokeMethod(&object, "sig", Qt::DirectConnection/*Qt::QueuedConnection*/, Q_RETURN_ARG(bool, bReturn), Q_ARG(int, i));

https://github.com/KaiMingPrince

注:此文是站在Qt5的角度说的,对于Qt4部分是不适用的。

1.先说Qt信号槽的几种连接方式和执行方式。

1)Qt信号槽给出了五种连接方式:

Qt::AutoConnection0自动连接:默认的方式。信号发出的线程和糟的对象在一个线程的时候相当于:DirectConnection, 如果是在不同线程,则相当于QueuedConnection
Qt::DirectConnection1直接连接:相当于直接调用槽函数,但是当信号发出的线程和槽的对象不再一个线程的时候,则槽函数是在发出的信号中执行的。
Qt::QueuedConnection2队列连接:内部通过postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。如果信号参数是引用类型,则会另外复制一份的。线程安全的。
Qt::BlockingQueuedConnection3阻塞连接:此连接方式只能用于信号发出的线程(一般是先好对象的线程) 和 槽函数的对象不再一个线程中才能用。通过信号量+postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。但是发出信号后,当前线程会阻塞,等待槽函数执行完毕后才继续执行。
Qt::UniqueConnection0x80防止重复连接。如果当前信号和槽已经连接过了,就不再连接了。

2)信号槽的调用方式和线程:

UniqueConnection 模式:严格说不算连接方式,方式就是4中,此只是一个附加的参数。不讨论。

AutoConnection 模式:这个模式是默认的,但其可以看作是DirectConnection和QueuedConnection的自动选择,直接分析那两种也就行了。

发出信号,调用槽的方式也可以简单的分为两种:同步调用和异步调用

同步调用:发出信号后,当前线程等待槽函数执行完毕后才继续执行。

异步调用:发出信号后,立即执行剩下逻辑,不关心槽函数什么时候执行。

所以有下表:

线程/模式DirectConnectionQueuedConnectionBlockingQueuedConnection
相同线程直接调用,同步调用。通过事件进行队列调用。异步调用.不可用
不同线程直接调用。同步调用。槽函数在发出信号的线程执行。有线程安全隐患。通过事件进行队列调用。异步调用.槽函数在对象所在的线程执行。线程安全。通过事件进行阻塞调用。同步调用。槽函数在对象所在的线程执行。线程安全。
Qt事件循环依赖直接调用,不依赖Qt事件循环通过事件进行队列调用。依赖,槽函数所在对象的线程必须启用Qt事件循环通过事件进行队列调用,用信号量实现阻塞。依赖,槽函数所在对象的线程必须启用Qt事件循环

2.Qt信号连接多个槽,调用顺序。

先说基本原则:

槽函数开始调用的顺序和连接的顺序是一致的。

但是,上面也说了,有同步调用和异步调用。

对于同步调用,你观察的结果和基本原则一样。

但是对于异步调用,可能你最先连接的它,但是可能其他都执行完毕了,但是其还没执行。是因为对于异步调用:是开始调用的时候,生成一个需要调用这个函数的事件,然后放到事件队列里。然后立即返回,去执行调用其他槽函数或者槽函数都执行了,不关心槽函数的执行状态的。等到事件队列里任务轮到此事件再去调用。

3.信号的返回值。

大都说Qt信号槽不能使用返回值。其实不不准确的,Qt5中,信号槽是有返回值的。只是Qt的一个信号可以连接多个槽,还有同步调用和异步调用的问题,没发支持的很好,所以,返回值虽有,但只是鸡肋。

先说下返回值的规则把:

  • 同步调用才有返回值,异步调用的返回值永远为返回值类型默认构造函数出来的。
  • 连接的多个槽都返回值,那么结果是最后调用(连接)的那个。

也就是说对于QueuedConnection连接的信号槽,永远只是返回返回类型的默认构造函数的。对于AutoConnection连接的,如果发出信号的线程和槽函数线程不同亦然。

测试小例子地址:https://github.com/dushibaiyu/DsbyLiteExample/tree/master/QtSignalsSlotTest

4.信号参数的安全问题:

因为一个信号可以连接多个槽函数,如果参数是T * 或者是T &话会不会第一个槽函数改变参数的值,然后第二此调用的参数就已经不是信号发出的值?

1)对于T &: 在同步调用中则是变化的,不可用于异步,不可跨线程。所以BlockingQueuedConnection方式的同步也不行。(T& 不可用在队列调用(QueuedConnection)和阻塞调用(BlockingQueuedConnection)中。只能使用const T &。)

因为同步调用,你可以理解成直接调用,那么连接多个槽函数就相当于直接连续调用多个函数。类似于:

1

2

3

4

5

6

7

8

9

10

11

// 函数原型都是:void  (int &a )

int a;

fun1(a);

fun2(a)

·····

// 函数原型都是:void  (int * a )

int a;

pfun1(&a);

pfun2(&a)

·····

  

这样,当第一个函数执行改变参数值之后,其后的函数调用都要受影响。

2) 对于T *,最好不要同时连接多个槽。

对于同步调用:是一个接着一个调用的,执行顺序类似上面,所以值也是每次调用也会变化的。

对于异步调用:其内容确实不确定的,因为异步调用的时间是不可控的。如果还有跨线程相关,则还有线程安全问题。

5.信号槽性能损失:

注:仅仅代码层进行的理论分析,非实际测试,不严谨,不权威。

关于信号槽(很多吐槽Qt就是说的这个):

(1)Qt4语法的,都说是匹配字符串,其实只是链接信号槽的用的匹配字符串 的方法,通过字符串找到信号和槽在QMeatObject里存的索引位置int类型,还有槽函数的索引,然后调用的时候通过索引号用switch去区分的 发射的那个函数,然后取出对应的链接槽的list,循环检测槽函数的参数是否匹配,然后调用槽函数。。这个链接时会耗时查找,但是你能有多少信号?这个链 接也耗时不多,调用的时候耗时主要就是在参数匹配上了。

(2)Qt5 语法的,Qt5 的槽函数链接和执行是基于模板实现的,函数对象。信号和槽的参数问题是编译时检查的,执行效率更高,但是编译就慢点了。链接时也是通过信号的地址找到其的 信号索引,至于槽函数直接是生成一个函数对象的,然后调用的时候也是先switch找到发射的信号,取出list,然后逐个调用其储存的函数对象,所以对 于Qt5 语法的信号槽,调用性能损失几乎可以说无的。

(3)链接的信号槽的时候,Qt::UniqueConnection的链接方式会对已经链接过的此先好的槽函数进行遍历,会有链接时的损失。其他链接的损失就在上面说过了。
(3)在信号槽调用的时候,还有一些链接方式和线程的判断和为了安全问题的锁操作。关于这个就还涉及到调用槽函数的线程问题。

对于同线程直接调用,较函数对象直接调用的损失,就只有链接方式和线程的判断的几个if 分支和 锁的操作。
对于线程间通讯的调用,跨线程。信号槽内部也是通过Qt事件循环机制实现的,跨线程就不是时时调用了,主要是安全了,对于性能有没有损失没法评论的。对于跨线程阻塞的调用,这个也是事件实现,只是但发射信号的线程会阻塞,这个找不到对应的直接调用的比较,也不好说。
关于信号槽Qt是作何很多方便使用和安全调用,较之函数指针,性能会有损失,但是也没损失多少的。对于函数对象调用,Qt5语法的调用,几乎是不损失什么的。

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

Qt使用invokeMethod反射机制实现线程间的通信

   对于Qt来说,UI线程是主线程,对于同一UI线程中对象的通信可以通过connect进行信号与槽关联来实现,但是当UI中对象A中的子线程B需要和另外UI对象C进行通信的时候,如果这个时候使用connect来进行通信的话,需要B对象和A对象进行关联将信号发送到主线程中,然后A对象和C对象再建立联系,这样处理起来会比较繁琐。Qt提供了invokeMethod反射机制,就可以实现任何线程中的数据之间传输,使用invokeMethod的前提条件是1)对象继承QObject; 2)定义的类中使用Q_OBJECT(可以使用信号与槽),具体invokeMethod函数原型如下:

[static] bool QMetaObject::invokeMethod(Qobject *object, const char*member, Qt::Connection Type type, QGenericReturnArgument val = QGenericArgument(), .....);
其中object为C对象,member为C对象中的信号或者槽方法的名字(字符串),type为调用信号或者槽函数的同步异步方式,后面的为信号或者槽函数中的参数类型(元对象系统能够识别的参数类型)及需要传递的参数;具体调用方式如下所示:

class C: public Qobject
{
    Q_OBJECT
public:
    static C* getInstance() {
        static QScopedPoint(C) inst;
        if (Q_UNLIKELY(!inst)) {
            if (!inst) {
                inst.reset(new C);
            }
        }
        return inst.data();
    }  
    static void doAddNum(int a, int b) { 
     //通过单例来获取对象C,从而调用C对象的槽方法;
     //也可以通过封装doAddNum方法来实现一个类D,这样的话C中只需要创建一个对象D然后调用D中的方法
        QMetaObject::invokeMethod(getInstance(), "addNum", Q_ARG(int, a), Q_ARG(int, b);
    }
public slots:
    void addNum(int a, int b);
private:
    C(Qobject *parent = nullptr) {}
};

class A : public QObject 
{
    Q_OBJECT
public:
    A(QObject *object = nullptr) 
    {
        //一般来说实现B和C直接的通信,需要B把数据发送给A,然后A发送给C来实现;
        //A和B同属于主线程,C属于子线程
        B* thread = new B(); 
        thread->start();
    }
}
class B: public QThread
{
    Q_OBJect
public:
    B() {}
    void run() {
        C::doAddNum(1, 2);     //在子线程中完成对C对象中的槽方法的调用,属于跨线程调用
    } 
}
 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

QT中工作线程调用GUI主线程控件的问题 的相关文章

  • Python GUI案例之看图猜成语开发(第三篇)

    Python GUI案例之看图猜成语 第三篇 前言 看图猜成语小程序开发 第三篇 游戏闯关模式页面 Python GUI案例之看图猜成语开发 第一篇 Python GUI案例之看图猜成语开发 第二篇 Python GUI案例之看图猜成语开发
  • Java程序开发学习之组件及事件处理

    学习参考书 Java2实用教程第5版 一 GUI编程 容器类 Container 和组件类 Component javax swing包中JComponent类是java awt包中Container类的一个直接子类 java awt包中C
  • Java GUI,mybatis实现资产管理系统

    Java GUI 资产管理系统 前言 为了做java课设 学了一手Java GUI 感觉蛮有意思的 写写文章 做个视频记录一下 欢迎大家友善指出我的不足 资产管理系统录制视频 从头敲到尾 模块划分 资产信息管理 资产信息查询 各种条件查询
  • LVGL使用记 - 数据更新显示

    介绍一下界面数据显示方法 关键API Set a an event handler function for an object Used by the user to react on event which happens with t
  • 【Qt教程】1.7 - Qt5带参数的信号、信号重载、带参数的槽函数、槽函数重载

    原理 与C 语法一致 信号 槽函数都可以发生重载 使其在名称不变的情况下 传递过程可以携带参数 示例说明 我们从一个最普通的信号槽工程中 来修改 对信号 槽进行重载 使信号 槽携带参数 1 普通信号 工程源码 widget h ifndef
  • VS环境下Qt工程.UI文件不生成头文件的问题

    在VS环境下创建的Qt工程会出现 UI文件不生成头文件的问题 可以通过右击 ui文件 点击编译生成头文件 但是 我创建的工程的 ui文件不能编译 右键编译选项是灰的 这种情况下 我想到的办法是 重新添加一个带UI文件的GUI类 与工程同名
  • 发现一个xdotool,是个神器

    xdotool是linux下 类似 按键精灵 的工具 在一些自动测试时 经常用到 以上为xdotool正常使用 比如说 模拟击键a xdotool key a 模拟两个键alt tab xdotool key alt Tab 自动输入wor
  • 点击按钮,QMovie动画播放结束时关闭当前窗口

    点击QToolButtoon弹出一个widget widget上有一个按钮 点击按钮后开始播放动画 当动画播放结束的时候发送finish信号 通知窗口关闭函数去关闭当前窗口 点击按钮播放此动画 直接拖控件完成布局 上代码 main cpp
  • 用Qt写一个简单的音乐播放器(二):增加界面(开始和暂停音乐)

    一 前言 在用Qt写一个简单的音乐播放器 一 使用QMediaPlayer播放音乐中 我们已经知道如何去使用QMediaPlayer播放音乐 但是一个对于一个音乐播放器来说 这是远远不够的 至少我们需要有一个简单的用户操作界面吧 让用户开业
  • QT5开发

    摘要 Qt5主窗口是大部分Qt应用使用的基本界面 常见应用都会通过对主窗口进行界面布局来实现 一 QT5主窗口构成 1 基本元素 QMainWindow是一个为用户提供主窗口程序的类 包含一个菜单栏 menubar 多个工具栏 tool b
  • QT 信号发送多个参数

    你可以把多个参数包装为一个类发送 实测是可以的
  • 【Qt教程】1.9 - Qt5菜单栏、工具栏、状态栏、核心窗口、浮动窗口、QMainWindow

    1 窗口应用布局样式及组成 PC端软件 最基本的一个窗口应用布局样式 如下 大体可分为菜单栏 工具栏 状态栏 核心窗口 浮动窗口 使用时按需相互组合 2 Qt中调用这些控件 1 新建一个工程 使用QMainWindow类 2 例程源码 具体
  • 【深入QT】信号槽机制浅析

    一 信号槽的基本概念 关于QT信号槽的基本概念大家都懂 通过信号槽机制 QT使对象间的通信变得非常简单 A对象声明信号 signal B对象实现与之参数相匹配的槽 slot 通过调用connect进行连接 合适的时机A对象使用emit把信号
  • Python GUI 设计(一)———Tkinter窗口创建、组件布局

    本篇开始介绍用Python的Tkinter模块来设计图形化界面 尽量用简洁的语言和实例让读者能看明白 轻松入门 1 1 创建窗口 tkinter模块是Pyhton自带的标准GUI库 可以直接导入 导入tkinter模块后 可以使用下面的方法
  • Unity3D笔记第三天——GUI

    GUI GUI是Graphical User Interface的缩写 Unity的图形界面系统能容易和快速创建出各种交互界面 与传统的方法 创建GUI对象 标出位置 再写对应的事件函数不同 只需要用很少的代码 就可以把这些工作搞定 原理是
  • 修改lvgl Linux demo使用tslib获取触摸坐标

    1 为什么要修改 o 交叉编译官方lvgl linux demo发现触摸坐标不准确 官方demo获取坐标的做法是 evdev fd open EVDEV NAME O RDWR O NOCTTY O NDELAY if evdev fd 1
  • YOLOv8目标检测PySide6 GUI可视化界面

    课程链接 https edu csdn net course detail 38552 YOLOv8目标检测PySide6 GUI可视化界面效果图如下 YOLOv8目标检测PySide6 GUI可视化界面支持本地图片和视频推理 摄像头实时视
  • 在 esp32 上运行 lvgl + freetype

    前言 最近有个需求 如何在 esp32 上运行 lvgl freetype 这个想法的难点是 freetype 的环境搭建 我想将其做得非常简单 最好的办法是做成组件来使用 所以我将 freetype 的相关依赖做成了 esp idf 组件
  • python3GUI--抖音无水印视频下载工具(附源码)

    文章目录 一 准备工作 二 预览 0 复制抖音分享短链接 1 启动 2 运行 3 结果 三 设计流程 1 总体设计 2 详细设计 四 源代码 五 说明 总结 hello 大家好啊 失踪人口回归了 捂脸 本次使用tkinter撰写一篇 抖音无
  • 使用 Tkinter 进行 Python GUI 编程

    目录 Building Your First Python GUI Application With Tkinter 添加小部件 检查你的理解情况 Working With Widgets 使用标签小部件显示文本和图像 使用按钮小部件显示可

随机推荐