Qt多线程以及调用Python
记录用QT实现多线程以及调用Python的学习笔记。
1 前言
任务描述:使用QT和python联合编程,实现Python对传感器数据的读取并显示在QT界面上。
碎碎念:其实用PyQt是能避免掉多语言联合编程的,可这不是之前已经有很多工程都已经在QT上实现了嘛…
开发平台:Windows10 + Qt5.12.12 + Anaconda + Python3.9
2 环境配置
2.1 Qt5.12.12 安装
官网下载见[link],点击windows版下载即可。
安装过程参考[link],不需要将所有的组件都安装上,不然太大了。
2.2 Python 安装
如果只是用Python做一些简单的工作,并不涉及多环境的复杂配置,可以直接从官网[link]下载并直接安装到系统中,安装过程可参考[link],最主要的是注意可以直接选中添加到系统变量中。
如果用Anaconda,可以从官方[link]下载,安装过程可以参考[link]也是要注意系统变量的设置。
2.3 环境设置
2.3.1 Python 相关的设置
笔者这里的安装路径是D:\18--Anaconda3
,直接用base环境下的python就行。因为如果创建一个新的环境,那后面需要在系统变量中添加 PYTHONHOME
、PYTHONPATH
的时候会要求设置到对应环境,但这会导致anaconda的终端中无法识别conda
。可以按照顺序依次执行:
除了在第2.2步设置环境变量后,还需要在用户变量中添加
PYTHONHOME
和PYTHONPATH
,不然调用python程序的时候会提示找不到这两个变量报错。添加如下:- 变量名:
PYTHONHOME
变量值:D:\18--Anaconda3
- 变量名:
PYTHONPATH
变量值:D:\18--Anaconda3\Lib;D:\18--Anaconda3\DLLs;D:\18--Anaconda3\site-packages;D:\18--Anaconda3\Scripts
- 变量名:
将
D:\18--Anaconda3\python39.dll
拷贝到D:\18--Anaconda3\libs
目录下,不然会导致运行的时候报 程序异常终止 的错误。- 将
D:\18--Anaconda3\include\object.h
文件中第206行中定义的 PyType_Spec 结构体中的PyType_Slot *slots;
修改为PyType_Slot *_slots;
,因为这个slots
名和QT冲突了。这样做的风险是以后调用到python这个结构体的时候需要注意,幸好大概率不会用到。这里不主动修改,运行程序的时候也会有报错提示。
更多报错可以参考[link]解决。
2.3.2 QT相关的设置
- 注意程序中对Python安装路径的确认
- 注意除了在工程文件中添加python文件外,还需要将相应的python文件复制到QT编译好的可执行程序文件的相同目录
3 程序实现
3.1 python 程序
直接在 QT 中 Add New -> Python fle 新建一个 test_py.py
文件,注意文件名不能是 test
,而且其中的函数名也不能是 fun
,会和系统中的一些名字冲突。这样做的好处是在QT的工程文件*.pro
中不需要再添加python文件了,它会自动添加在*.pro
的末尾。
点击查看`test_py.py`文件内容
1 | # This Python file uses the following encoding: utf-8 |
3.2 QT程序
QT多线程的实现有两种方式:一种是继承 QThread 类并直接重写 run 函数,另一种是使用 movetothread() 方法。多线程的实现可参考[link],原理介绍可参考[link1[link2][link3]。很多人说都应用第二种方式实现多线程,笔者这里的程序就放了第二种实现方式。
在工程文件 *.pro
的末尾添加python路径:1
2INCLUDEPATH += -I D:\18--Anaconda3\include # python
LIBS += -LD:\18--Anaconda3\libs -lpython39 # python
注意只要修改路径和python版本就行,不要删除掉
-L
,-l
这里记录一下整个工程的构建思路,整个工程有两个线程:
- 主线程:用来实现界面的刷新;子线程:用来实现对python文件的调用。
- Controller 类用来实现开启子线程,会传递一个初始参数到子线程
workThread::dowork()
中,在构造函数中最后一步emit
之后子线程 dowork() 开始运行;子线程 workThread 类初始化的时候会初始化python解释器,在构造函数中workThread::workThread()
中需要设置python的路径、要调用的python文件名等,在析构函数workThread::~workThread()
中关闭python解释器;通过data_type.h
中的全局变量实现对子线程的关闭以及python返回数据的赋值。 - 主线程(即界面)关闭的时候,会先等待 workThread 中子线程 dowork() 结束,再依次析构 workThread、controller、MainWindow 类。workThread 中 dowork() 结束时会 emit 一个最终结果至 controller 中。
- QT中加了一个显示框用来显示全局的传感器数据。其基本逻辑是,用定时器来定时刷新这个变量框。将刷新函数与槽连接起来。
点击查看`main.cpp`文件内容
1 |
|
点击查看`data_type.h`文件内容
1 |
|
点击查看`mainwindow.h`文件内容
1 |
|
点击查看`mainwindow.cpp`文件内容
1 |
|
点击查看`controller.h`文件内容
1 |
|
点击查看`controller.cpp`文件内容
1 |
|
点击查看`workThead.h`文件内容
1 |
|
点击查看`workThead.cpp`文件内容
1 |
|
4 connect() 函数介绍
参考[link1][link2],signal和slot可参考官方[link]。
connect()函数实现的是信号与槽的关联。需要注意只有 QObject 类及其派生类才能使用信号与槽的机制。
上一个定时器的小例子:1
2
3
4
5
QTimer* timer = new QTimer(this);
connect(timer,SIGNAL(timeout()),this,SLOT(timerUpdate()));
timer->start(50);// 50ms刷新一次
timer 超时后会发出 timeout() 信号(定时器自带),所以在创建好定时器对象后给其建立信号与槽。timerUpdate() 函数就是定时器超时后要执行的函数(自行编写),例如,该函数的内容可为将一个变量写入 LineEdit。
从上述例子可以看出,connect()
函数的基本用法是:
connect(信号发出者地址,发什么信号,在那个类触发,触发事件)
connect()函数的原型:1
2
3
4
5static QMetaObject::Connection connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection);
inline QMetaObject::Connection connect(const QObject *sender, const char *signal,const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
参数说明:
- sender: 信号发射源对象的地址
- signal: 所发射的信号。信号需要使用宏
SIGNAL()
,如上面的定时器示例SIGNAL(timeout())
- receiver: 信号接收的对象。使用
this
表示本部件。第3种写法直接省略掉了这个参数,默认为this
- member/method: 要执行的槽,也可以指定一个信号,实现信号与信号的关联
- Qt::ConnectionType type: 一般使用默认值,在满足某些特殊需求的时候可能需要手动设置。可参考[link1]中的说明。
示例程序如下,完整的代码可参考第3节中的controller.cpp
。1
2
3
4
5
6
7
8
9
10auto *worker = new workThread; // workThread 是另一个类
worker->moveToThread(&workerThread_c); // workerThread_c 是 controller 类中的成员变量
connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int)));
connect(&workerThread_c, &QThread::finished, worker, &QObject::deleteLater);
connect(worker, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
workerThread_c.start();
emit operate(10);
上述程序中第一个connect()就是前面介绍的标准写法。它将本类中的operate()
关联到 worker 中的 doWork(int) 函数,最后一行再 emit
。 emit 是个宏定义符号,其定义为#define emit
,是空白的,所以emit operate(10)
相当于执行operate(10)
这个函数。
第二个 connect() 应该是和第二种connect原型对应的。对应第二种原型,需要注意函数重载时的问题,具体内容可参考[link1]中的说明。
推荐按照常规的第一种写法或者用lamda函数省略掉槽函数的写法:1
2
3
4
5
6
7
8
9
10
11
12connect(m_pBtn, QOverload<bool>::of(&MyButton::sigClicked), this, [=](bool check){
qDebug() << "do something";
});
connect(m_pBtn, static_cast<void (MyButton::*)(bool)>(&MyButton::sigClicked), this, [=](bool check){
qDebug() << "do something";
});
connect(ui->lineEdit, &QLineEdit::textEdited, this, [=](QString s){
qDebug() << s;
});
关于signal和slot,如果自定义的话,在类中书写的时候要注意,signal必须有关键字signals
,且不能加 public, protected, private。slot可以不写关键字slots
,但是如果写了则必须加public, protected, private。信号与槽的参数不能是宏和函数指针。使用 signals/slots 必须要加入宏 QObject
。 信号一般与emit
配合使用,使用 emit
发射信号给关联的槽。简要示例如下:1
2
3
4
5
6
7
8
9class cc : public QObject {
QObject
public:
int var;
signals:
void signal(); // signals 下的函数必须是 void 类型,而且只需给出声明
public slots: // 必须有 public 或其他的属性,但不加 slots 的函数也是可以与信号关联的
void func();
}
完整示例可以参考第3节的 workThread 类和 Controller 类。