记录用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就行。因为如果创建一个新的环境,那后面需要在系统变量中添加 PYTHONHOMEPYTHONPATH 的时候会要求设置到对应环境,但这会导致anaconda的终端中无法识别conda。可以按照顺序依次执行:

  1. 除了在第2.2步设置环境变量后,还需要在用户变量中添加PYTHONHOMEPYTHONPATH,不然调用python程序的时候会提示找不到这两个变量报错。添加如下:

    • 变量名:PYTHONHOME 变量值:D:\18--Anaconda3
    • 变量名:PYTHONPATH 变量值:D:\18--Anaconda3\Lib;D:\18--Anaconda3\DLLs;D:\18--Anaconda3\site-packages;D:\18--Anaconda3\Scripts
  2. D:\18--Anaconda3\python39.dll 拷贝到 D:\18--Anaconda3\libs 目录下,不然会导致运行的时候报 程序异常终止 的错误。

  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
# This Python file uses the following encoding: utf-8

def hello():
print("hello world!")

def test_add(a, b):
c = a + b
# print( a, "+", b," = ",c)
return c


if __name__ == "__main__":
hello()

3.2 QT程序

QT多线程的实现有两种方式:一种是继承 QThread 类并直接重写 run 函数,另一种是使用 movetothread() 方法。多线程的实现可参考[link],原理介绍可参考[link1[link2][link3]。很多人说都应用第二种方式实现多线程,笔者这里的程序就放了第二种实现方式。

在工程文件 *.pro 的末尾添加python路径:

1
2
INCLUDEPATH += -I D:\18--Anaconda3\include  # python
LIBS += -LD:\18--Anaconda3\libs -lpython39 # python

注意只要修改路径和python版本就行,不要删除掉-L-l

这里记录一下整个工程的构建思路,整个工程有两个线程:

  1. 主线程:用来实现界面的刷新;子线程:用来实现对python文件的调用。
  2. Controller 类用来实现开启子线程,会传递一个初始参数到子线程 workThread::dowork() 中,在构造函数中最后一步emit之后子线程 dowork() 开始运行;子线程 workThread 类初始化的时候会初始化python解释器,在构造函数中 workThread::workThread() 中需要设置python的路径、要调用的python文件名等,在析构函数 workThread::~workThread() 中关闭python解释器;通过 data_type.h 中的全局变量实现对子线程的关闭以及python返回数据的赋值。
  3. 主线程(即界面)关闭的时候,会先等待 workThread 中子线程 dowork() 结束,再依次析构 workThread、controller、MainWindow 类。workThread 中 dowork() 结束时会 emit 一个最终结果至 controller 中。
  4. QT中加了一个显示框用来显示全局的传感器数据。其基本逻辑是,用定时器来定时刷新这个变量框。将刷新函数与槽连接起来。
点击查看`main.cpp`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "controller.h"  // --------- user definition -----------
#include "mainwindow.h"
#include <QApplication>
#include <iostream>
#include "data_type.h" // --------- user definition -----------

using namespace std;
int main(int argc, char *argv[])
{
qDebug() << "I am main Thread, my ID: " << QThread::currentThreadId() << "\n" ;
QApplication a(argc, argv);
MainWindow w;
w.show();
Controller c ; // 自定义的类 --------- user definition -----------
cout << "Finish ..." << endl;
return a.exec();
}
点击查看`data_type.h`文件内容
1
2
3
4
5
#ifndef DATA_TYPE_H
#define DATA_TYPE_H
extern bool g_exit_flag;
extern double g_sensor_data;
#endif // DATA_TYPE_H
点击查看`mainwindow.h`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
void timerUpdate(void);

private:
Ui::MainWindow *ui;

};
#endif // MAINWINDOW_H
点击查看`mainwindow.cpp`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>
#include "data_type.h"

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QTimer* timer = new QTimer(this);

connect(timer,SIGNAL(timeout()),this,SLOT(timerUpdate()));

timer->start(50);// 50ms刷新一次
}

MainWindow::~MainWindow()
{
delete ui;
}

void MainWindow::timerUpdate(void)
{
ui->lineEdit->setText(QString::number(g_sensor_data));//根据你的代码改变
}
点击查看`controller.h`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef CONTROLLER_H
#define CONTROLLER_H

#include <QObject>
#include <QThread>
#include <QDebug>

#include "workthread.h"

class Controller : public QObject
{
Q_OBJECT
QThread workerThread_c;
public:
explicit Controller(QObject *parent = nullptr);
~Controller() override ;

public slots:
static void handleResults(int result); // 处理子线程执行的结果

signals:
void operate(const int); // 发送信号,触发线程

};

#endif // CONTROLLER_H
点击查看`controller.cpp`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "controller.h"
#include "data_type.h"
#include <iostream>

bool g_exit_flag = 0;
double g_sensor_data = 0.0;

Controller::Controller(QObject *parent) : QObject(parent)
{
auto *worker = new workThread ;

// 调用 moveToThread 将该任务交给 workThread
worker->moveToThread(&workerThread_c);

// operate 信号发射后启动线程工作
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();

// 发射信号,开始执行
qDebug() << "emit the signal to execute!" ;
qDebug() << "\tCurrent thread ID:" << QThread::currentThreadId() << '\n' ;

emit operate(10); // workthread中 dowork 函数传入的参数
}

Controller::~Controller(){
g_exit_flag = 1; // 全局变量退出线程
workerThread_c.quit();
workerThread_c.wait();
qDebug() << "controller delete...";
std::cout << "g_sensor_data = " << g_sensor_data << std::endl;
std::cout << "delete MainWindow" << std::endl;
}

void Controller::handleResults(const int result){
qDebug() << "receive the resultReady signal" ;
qDebug() << "\tCurrent thread ID: " << QThread::currentThreadId() << '\n' ;
qDebug() << "\tThe last result is: " << result ;
}
点击查看`workThead.h`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef WORKTHREAD_H
#define WORKTHREAD_H

#include <QObject>
#include <QDebug>
#include <QThread>
#include <Python.h>
class workThread : public QObject
{
Q_OBJECT
public:
explicit workThread(QObject *parent = nullptr);
~workThread() override ;
bool stop_flag = 0;
public slots:
void doWork(int parameter); // doWork 定义了线程要执行的操作
double call_py(double a);

signals:
void resultReady(const int result); // 线程完成工作时发送的信号

private:
PyObject* pModule;
};

#endif // WORKTHREAD_H
点击查看`workThead.cpp`文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include "workthread.h"
#include <unistd.h>
#include "data_type.h"
workThread::workThread(QObject *parent) : QObject(parent)
{
Py_SetPythonHome(L"D:\18--Anaconda3"); // 设置python路径,很重要
Py_Initialize();
if( !Py_IsInitialized() ){
return ;
}
//执行单句Python语句,用于给出调用模块的路径,否则将无法找到相应的调用模块
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");
//获取py_test.py模块的指针
pModule = PyImport_ImportModule("py_test");//**最重要**
if (! pModule){
printf("Can't open python file\n");
return ;
}
}

workThread::~workThread(){
//销毁Python解释器,这是调用的最后一步
Py_Finalize();
qDebug() << "workThread delete...";
}
void workThread::doWork(int parameter)
{
qDebug() << "receive the execute signal" ;
qDebug() << "\tCurrent thread ID: " << QThread::currentThreadId();

// 循环一百万次
// for (int i = 0; i != 10; ++i)
while(!g_exit_flag)
{
++parameter ;
sleep(1);
g_sensor_data = call_py(parameter); // 为了一直获取python端的数据
qDebug() << "parameter: " << parameter << "; a+2: " << g_sensor_data;
}

// 发送结束信号
qDebug() << "\tFinish the work and sent the result Ready signal\n" ;

emit resultReady(parameter);
}

double workThread::call_py(double a){
//获取hello函数的指针
PyObject* pHelloHandler = PyObject_GetAttrString(pModule,"hello");// 加载hello函数
PyObject* pFunHandler = PyObject_GetAttrString(pModule,"test_add");// 加载hello函数
if (!pFunHandler){
printf("Get function hello failed\n");
return 0.0;
}
//调用函数,传入参数为NULL
PyObject_CallFunction(pHelloHandler,NULL); // 调用hello函数
PyObject * args = PyTuple_New(2);

//【例 1】:输入参数:int 输出参数:int
// int iin1=a,iin2=2; //输入参数赋值
// PyTuple_SetItem(args,0,Py_BuildValue("i",iin1));
// PyTuple_SetItem(args,1,Py_BuildValue("i",iin2));
// PyObject* pReturn1=PyEval_CallObject(pFunHandler,args); //调用test_add函数
// if(PyLong_Check(pReturn1))
// {
// int iout =PyLong_AsLong(pReturn1);
// qDebug()<< a << "+ 2 = " << iout; //打印输出
// }
// else
// qDebug()<<"return type is not Long!";

// //【例 2】:输入参数:float 输出参数:float
float fin1=a,fin2=2.2; //输入参数赋值
PyTuple_SetItem(args,0,Py_BuildValue("f",fin1));
PyTuple_SetItem(args,1,Py_BuildValue("f",fin2));
PyObject* pReturn2=PyEval_CallObject(pFunHandler,args);
if(PyFloat_Check(pReturn2))
{
float fout=PyFloat_AsDouble(pReturn2);
// qDebug()<<fout; //打印输出
return fout;
}
else
qDebug()<<"return type is not Float!";

// //【例 3】:输入参数:QString 输出参数:QString
// QString strin1="Sun",strin2="tian"; //输入参数赋值
// PyTuple_SetItem(args,0,Py_BuildValue("s",strin1.toStdString().c_str()));
// PyTuple_SetItem(args,1,Py_BuildValue("s",strin2.toStdString().c_str()));
// PyObject* pReturn3=PyEval_CallObject(pFunHandler,args);
// if(PyUnicode_Check(pReturn3)){
// char* s=PyUnicode_AsUTF8(pReturn3);
// QString strout=QLatin1String(s);
// qDebug()<<strout; //打印输出
// }
// else
// qDebug()<<"return type is not Unicode!";
}

4 connect() 函数介绍

参考[link1][link2],signal和slot可参考官方[link]。
connect()函数实现的是信号与槽的关联。需要注意只有 QObject 类及其派生类才能使用信号与槽的机制
上一个定时器的小例子:

1
2
3
4
5
#include <QTimer>

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
5
static 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
10
auto *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
12
connect(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
9
class cc : public QObject {
QObject
public:
int var;
signals:
void signal(); // signals 下的函数必须是 void 类型,而且只需给出声明
public slots: // 必须有 public 或其他的属性,但不加 slots 的函数也是可以与信号关联的
void func();
}

完整示例可以参考第3节的 workThread 类和 Controller 类。