用惯了C#和JAVA,再过来用QT发现很多不便。不能说谁优谁略,起码有些地方可以相互借鉴和互补的,例如线程的使用。
在C#上有Thread,有Task可以很方便创建后台任务线程,在JAVA上有Thread和Runnable,但在QT上相对麻烦一些,或者说至少代码逻辑没有那么的流畅。但也或许是个人习惯或功夫不到家造成的不违和感吧。不管怎么样,下面尝试在QT上写出类似C#的写法。
线程的使用多用于后台任务和UI同步,要在QT上写出类似C#的方式先要搞清楚C#是怎么实现多线程和UI同步的。
1. C#中的线程和UI同步
在C#中可以通过new Thread(Action)创建一个线程,然后调用Start方法启动线程。创建线程使用的Action是一个委托,在c++中可以认为是一个函数指针,并且是一个没有带参数和返回结果的函数指针。
线程创建之后可以通过UI对象的Invoke(Action),在Action中对UI进行更新。
而在C#中Action可以采用lambda,所以在代码编写多数采用这种写法。具体看下面代码。
//创建线程
var thread = new Thread(() => {
//线程同步
this.Invoke(new Action(() => {
this.Text = "在子线程中采用Invoke更新UI窗口";
}));
});
//开始线程
thread.Start();
C#中的UI同步机制是消息,包括象c++的MFC,C#的winform,WPF等都是采用这种消息机制,再到Android中的Looper也是类似这种方式。
这种机制简单的理解就是UI线程是一个循环体,不断从一个消息队列里取消息,并执行这条消息。而消息的表现方式是多样化,规则化,系统化的。比如鼠标点击,键盘的按下等都可以用消息表达。
随手牵来一张图片说明消息机制
2. QT中是怎么实现多线程和UI同步的
QT中常用有两种多线程实现的写法。一种是定义一个QThread的子类,在子类中实现任务处理逻辑。一种是定义一个QObject子类,在子类中实现任务处理逻辑,同时在使用的时候创建一个QThread,并将子类moveToThread到QThread中执行。
具体可以参考https://subingwen.cn/qt/thread/
QT中的UI同步则采用信号和槽的机制,跟C#的winform,vc++的mfc是类似的。具体就不细说了。
3. 如何进行简化
搞清楚C#和QT的线程实现方式之后能发现一个明显的区别就是采用了lambda,lambda在是c11才列入标准,而QT在c11之前很早就诞生了,个人理解是QT一直沿用原来的模式。因此我想着采用lambda进行简化。
具体看下面代码,为了方便演示,将函数体也在头文件中,
(这里分享个QTCreator的一个小技巧 右键函数声明或类声明 菜单 Refactor->Move Definition …可以对单个函数体或所有类的函数体在.h和.cpp中自动切换)
自定义函数
//定义函数结构,这里类似C#的Action.
typedef std::function<void ()> FUNC;
//定义自定义线程类,从QThread派生
class MyThread:public QThread{
Q_OBJECT
private:
FUNC m_func=0;//记录线程执行的函数
public:
//构造函数,func参数为线程执行的函数
explicit MyThread(FUNC func=NULL, QObject *parent = Q_NULLPTR):QThread(parent),
m_func(func)
{
//链接信号和槽,在里是子线程向UI同步的关键。
connect(this,&MyThread::invokeSignal,this,&MyThread::invokeSlot);
}
~MyThread()
{
//断开信号槽
disconnect(this,&MyThread::invokeSignal,this,&MyThread::invokeSlot);
}
//设置线程执行的函数,虽然构造函数里面已经有func,但在有些场景在lambda中需要访问MyThread,所以需要该函数
//该函数需要在start()调用
void setFunc(FUNC func){
this->m_func=func;
}
//同步到UI线程调用函数
void invoke(FUNC func){
emit invokeSignal(func);
}
protected:
//重写父函数,单线程开始执行会调用该函数,可以简单的认为是线程的入口。
void run() override{
//调用函数体,执行任务
m_func();
}
signals:
//UI同步的信号
void invokeSignal(FUNC fun);
public slots:
//UI同步的槽函数,该函数一般为同步到UI线程来调用
void invokeSlot(FUNC fun){
//调用UI同步函数。
fun();
}
};
测试代码
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建一按钮用于测试
QPushButton * btn=new QPushButton(this);
btn->setFixedSize(100,30);
btn->move(100,100);
btn->show();
//注册匿名函数类型
qRegisterMetaType<FUNC>("FUNC");
//输出UI线程ID
qDebug()<<"UIThread ID:"<<QThread::currentThreadId();
//新建一个线程
MyThread * th0=new MyThread();
//子线程任务逻辑,打印数字,更新UI
auto func=[=](){
int i=0;
while(i++<20){
qDebug()<<"sub thread id:"<< QThread::currentThreadId() <<" i:"<<i<<" tid:"<<QThread::currentThreadId();
QThread::msleep(500);
//同步到UI线程更新UI,因为这里用到th0,所以不能在MyThread的构造函数中定义
th0->invoke([=](){
btn->setText(QString::number(i));
btn->move(i, 100);
});
}
};
//设置任务函数
th0->setFunc(func);
//开始线程执行
th0->start();
//在窗体结束的时候停止线程
connect(this,&MainWindow::destroyed,[=](){
//结束线程,这里有时候不会生效,下次再对这个问题进行解释。
th0->exit(0);
th0->wait(3000);
});
}
4. 后续完善
以上实现会有几个问题,例如窗口释放了但线程还在运行,还需要UI同步就会异常、在某些时候如窗口关闭的时候需要停止线程无法停止等。因此需要有状态更新和通知的机制。今天就先写到这,下回有时间再补充。