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主线程控件的问题 的相关文章

  • Java GUI,mybatis实现资产管理系统

    Java GUI 资产管理系统 前言 为了做java课设 学了一手Java GUI 感觉蛮有意思的 写写文章 做个视频记录一下 欢迎大家友善指出我的不足 资产管理系统录制视频 从头敲到尾 模块划分 资产信息管理 资产信息查询 各种条件查询
  • QT学习笔记(一)之本地播放器

    先从百度百科上摘取一段QT定义 Qt是 一个1991年由奇趣科技开发的跨平台 C 图形用户界面 应用程序开发框架 它既可以开发 GUI 程序 也可用于开发非GUI程序 比如控制台工具和服务器 Qt是面向对象的框架 使用特殊的代码生成扩展 称
  • 系统建模与仿真项目驱动设计报告-基于MATLAB的GUI界面设计

    摘 要 MATLAB语言是一种十分有效得工具 能够容易解决在系统仿真以及控制系统计算机辅助设计领域的解决问题 在本次的系统建模与仿真设计中 需要使用人机交互界面 MATLAB GUI功能设计一个系统仿真GUI界面 由于GUI本身提供了Win
  • 【Qt教程】1.7 - Qt5带参数的信号、信号重载、带参数的槽函数、槽函数重载

    原理 与C 语法一致 信号 槽函数都可以发生重载 使其在名称不变的情况下 传递过程可以携带参数 示例说明 我们从一个最普通的信号槽工程中 来修改 对信号 槽进行重载 使信号 槽携带参数 1 普通信号 工程源码 widget h ifndef
  • python实现天气数据爬取实现数据可视化和天气查询gui界面设计

    在学校大一实训中 我的实训项目就是使用python爬取天气数据并且实现gui图形界面的设计 实训结束了 希望自己的代码可以帮助大家的学习 代码是大一时候写的 比较青涩 大家多包涵 第一个部分是getdata的部分 就是爬取数据 并将数据存放
  • tkinter创建真正的嵌套子窗口

    tkinter创建真正的嵌套子窗口 引言 获取窗口句柄 方法一 方法二 创造嵌入窗口 嵌套在组件中 一些问题 一个简单的例子 2022 7 22问题解决 结语 引言 在我之前的这篇纯tkinter创建嵌套子窗口的文章中 只提及了3种方法 原
  • 『贪吃蛇』AI 算法简易实现(中秋特别版)

    前言 一年一度的中秋节就快到了 平台也有各种各样的中秋发文活动 正在翻阅时偶然间我看到了这篇文章 兔饼大作战 吃月饼 见月亮 还能咬自己 欢庆中秋特制版 掘金 juejin cn 大家肯定比较熟悉了 这个游戏的内核就是贪吃蛇 作者也是对玩法
  • python 实现GUI(图形用户界面)编程

    Python支持多种图形界面的第三方库 包括 wxWidgets Qt GTK Tkinter Tkinter 模块 Tk 接口 是 Python 的标准 Tk GUI 工具包的接口 Tk 和 Tkinter 可以在大多数的 Unix 平台
  • Qt D、Q 指针学习和二进制兼容

    文章目录 Qt 中 D Q 指针的实现 Qt 中 D Q 指针的实现 Qt 中 D Q 指针机制的实现是通过宏定义 实现代码在 qtbase gt src gt corelib gt qglobal h 和 qobject h qobjec
  • Java项目---开发一个学生成绩管理系统(使用mysql数据库)

    Java项目 开发一个学生成绩管理系统 使用mysql数据库 1 设计数据库 2 添加jdbc驱动包及数据库连接 3 修改代码之前 我们还需要将数据库中用到的列名进行定义 定义在一个新的Applicant类里面 并且再get set将其封装
  • QT5开发

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

    文章目录 以按钮为例 共有参数 动态属性 tkinter系列 GUI初步 布局 绑定变量 绑定事件 消息框 文件对话框 控件样式 扫雷小游戏 强行表白神器 以按钮为例 tkinter对控件的诸多属性提供了可定制的功能 下面以最常用的按钮作为
  • 【GUI】LVGL8内存泄漏分析

    LVGL版本 V8 0 2 平台 ESP32S3 在调试过程中 发现有两个界面 在重复退出再进入时内存会不断增加的吃内存现象 然后做了分析和研究 1 样式style吃内存 在主页面 进入simple页面 再退出到主页面 再次进入simple
  • python3 GUI- 登陆界面

    python3 GUI 登陆界面 from tkinter import root Tk def Show root1 Tk if En get user and En1 get 123 Label root1 text 登陆成功 bg G
  • 【深入QT】信号槽机制浅析

    一 信号槽的基本概念 关于QT信号槽的基本概念大家都懂 通过信号槽机制 QT使对象间的通信变得非常简单 A对象声明信号 signal B对象实现与之参数相匹配的槽 slot 通过调用connect进行连接 合适的时机A对象使用emit把信号
  • 【python办公自动化】PysimpleGUI中更新Listbox组件选定元素的格式

    pysimplegui中更新Listbox组件选定元素的格式 背景 问题解决 创建窗口布局 创建窗口 背景 在进行打分时候 由于打分的指标较多 因此为了辨别已经打完分数的指标 可以考虑对打过分的指标进行标记 故可以采用格式修改的方法调整 比
  • 通过PAGE生成python GUI界面(用PAGE拖出需要的GUI界面)

    注 当前我的使用环境为windows10 64bit python v3 6 PAGE v4 14 Tcl v8 6 7 0 当前我定义一个目标 最终需要生成一个登录界面的GUI代码 如下 安装好各软件后 就可以运行PAGE来像VB一样所见
  • 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 组件
  • 使用 Python、PyQt 和 SQLite 构建联系簿

    目录 演示 Python 联系手册 项目概况 先决条件 Step 1 Creating the Contact Book s Skeleton App With PyQt 构建通讯录项目 创建应用程序的主窗口 编码和运行应用程序 第 2 步

随机推荐