文章目录
一个典型的线程与gui界面交互的例子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtoTMfIz-1616319064393)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616078038655.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L134Lhb1-1616319064396)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616078073236.png)]
线程类
#ifndef WORKTHREAD2_H
#define WORKTHREAD2_H
#include <QThread>
class WorkThread2 : public QThread
{
Q_OBJECT
public:
int m_stop;
WorkThread2()
{
m_stop=false;
moveToThread(this);
}
void work()
{
for(int i=0;i<11;i++)
{
emit signalProgessValue(i*10);
sleep(1);
}
}
signals:
void signalProgessValue(int value);
protected:
void run()
{
work();
exec();
}
};
#endif // WORKTHREAD2_H
界面
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <workthread2.h>
#include <QProgressBar>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
QProgressBar *m_progress;//进度条
WorkThread2 *m_thread;//工作线程
protected slots:
void onProgress(int value);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
m_progress = new QProgressBar(this);
m_progress->move(10, 10);
m_progress->setMinimum(0);
m_progress->setMaximum(100);
m_progress->setTextVisible(true);
m_progress->resize(100, 30);
m_thread = new WorkThread2();
m_thread->start();
//监听工作线程结束的信号
connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater()));
//连接工作线程的信号到界面的槽函数
//connect(m_thread, SIGNAL(signalProgressValue(int)), this, SLOT(onProgress(int)));
connect(m_thread,&WorkThread2::signalProgessValue,this,&Widget::onProgress);
}
Widget::~Widget()
{
delete ui;
}
void Widget::onProgress(int value)
{
m_progress->setValue(value);
}
描述
使用信号槽解决多线程与界面组件的通信的方案:
A、在子线程中定义界面组件的更新信号
B、在主窗口类中定义更新界面组件的槽函数
C、使用异步方式链接更新信号到槽函数
子线程通过发送信号的方式更新界面组件,所有的界面组件对象只能依附于GUI线程(主线程)。
子线程更新界面状态的本质是子线程发送信号通知主线程界面更新请求,主线程根据具体信号以及信号参数对界面组件进行修改。
使用信号槽在子线程中更新主界面中进度条的进度显示信息。
3、发送自定义事件方式
A、自定义事件用于描述界面更新细节
B、在主窗口类中重写事件处理函数event
C、使用postEvent函数(异步方式)发送自定义事件类对象
子线程指定接收消息的对象为主窗口对象,在event事件处理函数更新界面状态
事件对象在主线程中被处理,event函数在主线程中调用。
发送的事件对象必须在堆空间创建
子线程创建时必须附带目标对象的地址信息
自定义事件类:
#ifndef PROGRESSEVENT_H
#define PROGRESSEVENT_H
#include <QEvent>
class ProgressEvent : public QEvent
{
int m_progress;
public:
const static Type TYPE = static_cast<Type>(QEvent::User+0xFF);
ProgressEvent(int progress=0):QEvent(TYPE)
{
m_progress=progress;
}
int progress()const
{
return m_progress;
}
};
#endif // PROGRESSEVENT_H
线程
#ifndef WORKTHREAD3_H
#define WORKTHREAD3_H
#include <QThread>
#include <QApplication>
#include "progressevent.h"
class WorkThread3 : public QThread
{
Q_OBJECT
public:
WorkThread3()
{
m_stop=false;
}
void stop()
{
m_stop=true;
}
void work()
{
for(int i=0;i<11;i++)
{
QApplication::postEvent(parent(),new ProgressEvent(i*10));
sleep(1);
}
}
protected:
volatile bool m_stop;
void run()
{
work();
exec();
}
};
#endif // WORKTHREAD3_H
界面
处理事件
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QProgressBar>
#include "workthread3.h"
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
QProgressBar *m_progress;//进度条
WorkThread3 *m_thread;
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
protected:
bool event(QEvent *event);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
m_progress= new QProgressBar(this);
m_progress->move(10,10);
m_progress->setMinimum(0);
m_progress->setMinimum(100);
m_progress->setTextVisible(true);
m_progress->resize(100,30);
m_thread=new WorkThread3();
m_thread->setParent(this);
m_thread->start();
}
Widget::~Widget()
{
m_thread->quit();
delete ui;
}
bool Widget::event(QEvent *event)
{
bool ret=true;
if(event->type()==ProgressEvent::TYPE)
{
ProgressEvent *evt=dynamic_cast<ProgressEvent*>(event);
if(evt!=NULL)
{
//设置进度条的进度为事件参数的值
m_progress->setValue(evt->progress());
}
else {
ret=QWidget::event(event);
}
}
return ret;
}
拎一个引用安利
读文件线程
#ifndef READTHREAD_H
#define READTHREAD_H
#include <QFile>
#include <QThread>
#include <QTextStream>
class ReadThread : public QThread
{
Q_OBJECT
public:
ReadThread(QObject*obj);
signals:
void toLine(QString line);
protected:
void run();
private:
QFile *file;
QObject *m_obj;
};
#endif // READTHREAD_H
#include "readthread.h"
ReadThread::ReadThread(QObject*obj):m_obj(obj)
{
file=new QFile("F:\\qtproject\\reverseqt\\file\\widget.cpp");
}
void ReadThread::run()
{
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
while(1)
{
while (!stream->atEnd()) {
/*文件没到末尾就执行循环体*/
QString line=stream->readAll();
emit toLine(line);
QThread::msleep(15);
}
}
}
界面
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "readthread.h"
#include <QTextEdit>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
QTextEdit *edit;
ReadThread *thread;
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
protected slots:
void append(QString lineTemp);
void FinishThread();
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
edit = new QTextEdit(this);
edit->resize(200,300);
thread=new ReadThread(this);
thread->start();
connect(thread,&ReadThread::toLine,this,&Widget::append);
connect(thread,&ReadThread::finished,this,&Widget::FinishThread);
edit->append("开始准备");
}
Widget::~Widget()
{
delete ui;
}
void Widget::append(QString lineTemp)
{
edit->append(lineTemp);
}
void Widget::FinishThread()
{
thread->quit();
}
描述
其中appendText是MainWindow的槽函数,Q_ARG的两个形参分别为槽函数的形参类型和实参。
在使用invokeMethod方法后,使用了QThread的静态函数msleep,因为读取的文件太大,每读取一行就要更新GUI,太耗资源,会导致GUI忙不过来,读一行后稍微休息一下,否则也会阻塞GUI。
QThread的子类一般不定义槽函数,这是不安全的,可能造成主线程和子线程同时访问它,除非使用mutex保护。 但可以定义signal,而且可以在run函数中发射, 因为信号发射是线程安全的。
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
元对象系统实现线程间通信
将ReadThread类中的信号去掉,再把run()中的emit所在行换成:
QMetaObject::invokeMethod(m_obj,"appendText",Qt::AutoConnection,
Q_ARG(QString,line) );
在构造函数中直接起线程
invoke:援引
QMetaObject::invokeMethod()中的第三个参数是信号与槽的连接方式,如果目标进程与当前线程相同则用Qt::DirectConnection;不同则用Qt::QueuedConnection,想对象所在线程发送事件,进入目标线程的事件循环;如果是Qt::AutoConnection,则会根据线程的情况自动判断。 显然这里可以用后两者。
这一机制依赖Qt内省机制,所以只有信号、槽、带Q_INVOKABLE关键字的成员函数才能使用此方法,本例的appendText为槽函数。
本例的函数形参类型是QString,但如果所调函数的参数类型不是内建数据类型、不属于 QVariant,会报错,即该类型的参数无法进入信号队列。这时需要我们在类的声明之后调用宏
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
Q_DECLARE_METATYPE(MyClass),当然前提是该类提供了公有的构造函数、拷贝构造函数和析构函数,并且要在跨线程通信前使用qRegisterMetaType<MyClass>("MyClass")来注册整个类型,
我们知道QWidget及其子类都是不可重入的,也就是GUI类只能在GUI线程使用,本例中如果在子线程直接调用appendText,可能也能得到正确结果,但这种做法并不正确。
另外我们无法在类外调用private成员函数。本例中我们可以在子线程使用MainWindow的私有方法,只要把appendText改为带Q_INVOKABLE修饰的私有成员函数即可。
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
moveToThread分离线程与任务
一个QObject的线程依附性(thread affinity)是指该对象驻足(live in)在某个线程内。在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。
一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部)
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
将计算任务和线程管理分离,即在一个 QObject 中处理任务,并使用 QObject::moveToThread 改变QObject的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程(一般是GUI线程)中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。
QThread所依附的线程,就是执行QThread * t=new QThread() 的线程,一般就是GUI线程。QThread管理的线程,就是 run 启动的线程,也就是子线程。线程ID只能在run()函数中调用QThread::currentThreadId()查看。
此外,Qt要求一个QObject的孩子必须与它们的父对象驻足在同一个线程中。这意味着:不能使用QObject::moveToThread()作用于有父对象的对象; 千万不要在一个线程中创建对象的同时把QThread对象自己作为它的父对象。比如这种做法是错的
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
class MyThread : public QThread {
void run() {
QObject* obj = new QObject(this);
} };
然后在GUI线程的构造函数里创建MyThread对象,运行线程后会报错: QObject: Cannot create children for a parent that is in a different thread.
(Parent is MyThread(0x2d07e70), parent’s thread is QThread(0x2cea418)
也就是说MyThread对象在GUI线程,而obj在子线程。
moveToThread底层是依赖Qt事件循环实现的(QCoreApplication::postEvent),所以使用moveToThread必须是在开启Qt事件循环的程序中,就是main函数中调用QCoreApplication::exec的程序。
自定义QObject的子类MyObj,注意不能是QWidget的子类,因为它不可重入:
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
class MyObj : public QObject
{
Q_OBJECT
public:
MyObj();
signals:
void toLine(QString line);
private slots:
void doWork();
};
void MyObj::doWork()
{
QFile* file = new QFile("E:\qtgui.index");
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
qDebug()<<"do work's thread:"<<QThread::currentThread();
while(!stream->atEnd())
{
QString line = stream->readLine();
emit toLine(line);
QThread::msleep(15);
}
}
t = new QThread(); //QThread
obj = new MyObj();
obj->moveToThread(t);
qDebug()<<"main thread:"<<QThread::currentThread();
connect(t,SIGNAL(started()), obj, SLOT(doWork()));
connect(obj,SIGNAL(toLine(QString)),this,SLOT(appendText(QString) ) );
connect(t,SIGNAL(finished()), obj, SLOT(deleteLater()) );
//connect(this,SIGNAL(closeMe()), t, SLOT(terminate()) );
t->start();
第一个connect是启动线程t后,执行任务处理的槽函数;第二个connect是obj执行中发出信号后,文本框添加文本;第三个connect是等线程t结束时,删除obj指针;启动线程t后就可以读取文件并刷新GUI了。
停止子线程的方法最好是给while循环添加布尔量做控制,以及t->quit(); t->wait();。
注意: 发出信号toLine的obj和this不是同一个线程。
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
注意: 发出信号toLine的obj和this不是同一个线程。
代码中的默认connect类型是Qt::AutoConnection,如果在一个线程就是Qt::DirectConnection,不在一个线程就是Qt::QueuedConnection;
如果是Qt::DirectConnection,相当于直接调用槽函数,但是当信号发出的线程和槽的对象不在同一个线程的时候,槽函数是在发出的信号中执行的。所以appendText在子线程。
如果是Qt::QueuedConnection,线程安全,内部通过postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。所以appendText在GUI线程
————————————————
版权声明:本文为CSDN博主「SilentAssassin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yao5hed/article/details/81108507
查看qt线程的命令
netsh trace start xxxx
可以实现抓包,但是并不是实时,且不会显示结果
静态成员变量
对象的内存中包含了成员变量,不同的对象占用不同的内存(已在《C++对象的内存模型》中提到),这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。例如有两个相同类型的对象 a、b,它们都有一个成员变量 m_name,那么修改 a.m_name 的值不会影响 b.m_name 的值。
可是有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数,以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1。
在C++中,我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰,例如:
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为 m_total 分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了 m_total,也会影响到其他对象。
static 成员变量必须在类声明的外部初始化,具体形式为:type 是变量的类型,class 是类名,name 是变量名,value 是初始值。将上面的 m_total 初始化:静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
static 成员变量既可以通过对象来访问,也可以通过类来访问。请看下面的例子:
//通过类类访问 static 成员变量
Student::m_total = 10;
//通过对象来访问 static 成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通过对象指针来访问 static 成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
单例模式
懒汉式
懒汉式的特点是延迟加载,比如配置文件,采用懒汉式的方法,顾名思义,懒汉么,很懒的,配置文件的实例直到用到的时候才会加载。。。。。。
class CSingleton
{
public:
static CSingleton* GetInstance()
{
if ( m_pInstance == NULL )
m_pInstance = new CSingleton();
return m_pInstance;
}
private:
CSingleton(){};
static CSingleton * m_pInstance;
}
————————————————
版权声明:本文为CSDN博主「zhanghuaichao」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhanghuaichao/article/details/79459130
饿汉式
饿汉式的特点是一开始就加载了,如果说懒汉式是“时间换空间”,那么饿汉式就是“空间换时间”,因为一开始就创建了实例,所以每次用到的之后直接返回就好了。饿汉式有两种常见的写法,写法1和写法2
class CSingleton
{
private:
CSingleton()
{
}
public:
static CSingleton * GetInstance()
{
static CSingleton instance;
return &instance;
}
}
这种写法不是线程安全的,因为静态的局部变量是在调用的时候分配到静态存储区,所以在编译的时候没有分配,
静态局部对象:
在程序执行到该对象的定义处时,创建对象并调用相应的构造函数!
如果在定义对象时没有提供初始指,则会暗中调用默认构造函数,如果没有默认构造函数,则自动初始化为0。
如果在定义对象时提供了初始值,则会暗中调用类型匹配的带参的构造函数(包括拷贝构造函数),如果没有定义这样的构造函数,编译器可能报错!
直到main()结束后才会调用析构函数!
————————————————
正确的写法应该像下面这样。
class CMsBsGPSInfoStart
{
public:
static CMsBsGPSInfoStart& GetInstance();
protected:
CMsBsGPSInfoStart();
~CMsBsGPSInfoStart();
private:
static CMsBsGPSInfoStart _instance;
private:
//CLock m_lkMsBsGPSInfoStartFlag;
bool m_bMsBsGPSInfoStartFlag; //
public:
bool GetMsBsGPSInfoStart();
bool SetMsBsGPSInfoStart(bool bIsStart);
};
CMsBsGPSInfoStart CMsBsGPSInfoStart::_instance;
CMsBsGPSInfoStart::CMsBsGPSInfoStart() : m_bMsBsGPSInfoStartFlag(false)
{
std::cout << "enter CMsBsGPSInfoStart::CMsBsGPSInfoStart() " << endl;
}
CMsBsGPSInfoStart::~CMsBsGPSInfoStart()
{
std::cout << "enter CMsBsGPSInfoStart::~CMsBsGPSInfoStart() " << endl;
}
CMsBsGPSInfoStart& CMsBsGPSInfoStart::GetInstance()
{
std::cout << "CMsBsGPSInfoStart::GetInstance()" << endl;
return _instance;
}
bool CMsBsGPSInfoStart::SetMsBsGPSInfoStart(bool bIsStart)
{
m_bMsBsGPSInfoStartFlag = bIsStart;
return true;
}
————————————————
版权声明:本文为CSDN博主「zhanghuaichao」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhanghuaichao/article/details/79459130
多线程下的单例
在懒汉式的单例类中,其实有两个状态,单例未初始化和单例已经初始化。假设单例还未初始化,有两个线程同时调用GetInstance方法,这时执行 m_pInstance == NULL 肯定为真,然后两个线程都初始化一个单例,最后得到的指针并不是指向同一个地方,不满足单例类的定义了,所以懒汉式的写法会出现线程安全的问题!在多线程环境下,要对其进行修改。
————————————————
版权声明:本文为CSDN博主「zhanghuaichao」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhanghuaichao/article/details/79459130
class Singleton
{
private:
static Singleton* m_instance;
Singleton(){}
public:
static Singleton* getInstance();
};
Singleton* Singleton::getInstance()
{
if(NULL == m_instance)
{
Lock();//借用其它类来实现,如boost
if(NULL == m_instance)
{
m_instance = new Singleton;
}
UnLock();
}
return m_instance;
}
————————————————
版权声明:本文为CSDN博主「zhanghuaichao」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhanghuaichao/article/details/79459130
使用double-check来保证thread safety.但是如果处理大量数据时,该锁才成为严重的性能瓶颈
继承准则
但无论哪种继承方式,上面两点都没有改变:
1.private成员只能被本类成员(类内)和友元访问,不能被派生类访问;
2.protected成员可以被派生类访问。
典型QObject单例测试
模型以上一个引用为模型
增加单例发送信号
#ifndef TESTINSTANCE_H
#define TESTINSTANCE_H
#include <QObject>
class testInstance : public QObject
{
Q_OBJECT
public:
explicit testInstance(QObject *parent = nullptr);
static testInstance *ins;
static testInstance* getInstance();
void runtest();
signals:
void data_process();
public slots:
};
#endif // TESTINSTANCE_H
testInstance *testInstance::getInstance()
{
return ins;
}
void testInstance::runtest()
{
emit data_process();
}
widget.cpp更新为
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
edit = new QTextEdit(this);
edit->resize(200,300);
thread=new ReadThread(this);
thread->start();
//connect(thread,&ReadThread::toLine,this,&Widget::append);
connect(thread,&ReadThread::finished,this,&Widget::FinishThread);
edit->append("开始准备");
connect(testInstance::getInstance(),&testInstance::data_process,this,[](){
qDebug()<<"响应成功";
});
}
Widget::~Widget()
{
delete ui;
}
void Widget::append(QString lineTemp)
{
edit->append(lineTemp);
}
void Widget::FinishThread()
{
thread->quit();
delete testInstance::getInstance();
}
关于以上代码的解读
线程以行方式读取文件,每次读取一行,获取testInstance实例调用成员函数,发射信号,将其写在控件上,
nclude “ui_widget.h”
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
edit = new QTextEdit(this);
edit->resize(200,300);
thread=new ReadThread(this);
thread->start();
//connect(thread,&ReadThread::toLine,this,&Widget::append);
connect(thread,&ReadThread::finished,this,&Widget::FinishThread);
edit->append(“开始准备”);
connect(testInstance::getInstance(),&testInstance::data_process,this,{
qDebug()<<“响应成功”;
});
}
Widget::~Widget()
{
delete ui;
}
void Widget::append(QString lineTemp)
{
edit->append(lineTemp);
}
void Widget::FinishThread()
{
thread->quit();
delete testInstance::getInstance();
}
关于以上代码的解读
线程以行方式读取文件,每次读取一行,获取testInstance实例调用成员函数,发射信号,将其写在控件上,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPTOR1Mo-1616319064400)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1616318642791.png)]