Qt/C++ 中调用 Python,并将软件打包发布、Python 含第三方依赖
背景:
工作中遇到 QT/C++ 调用我的 Python 代码,并且想要一键打包,这里我根据参考的以及个人实践的结果来简单实现一下。
开发环境: Win10, QT Creator 13.0.1, Python 3.9.10 (非 Anaconda 虚拟环境)
一、简单 QT 调用 Python 程序
1、创建 Qt 工程 with QMake
首先提示各位从 Python 过来的同仁,QT中有时候对项目“重新构建”,项目并不真正的重新构建,如果这样的话,我们需要在工程文件夹下找到对应的构建后的项目,即比较长的这个(对应的是debug模式下的编译构建),删除掉,再点击重新构建。
PS: 一般直接右键项目,点击菜单里的“清除” 按钮
2、配置 Python 环境
使用QT 调用 Python 需要加载 Python.h
头文件,我们在 main.cpp
里面引入 Python.h
。但原始配置是找不到Python.h
的,所以首先我们需要将安装好的 python 路径配置到QT的配置文件(.pro)中。
- 右键项目打开菜单,选择 【添加库】- 选择 【外部库】 - 按如下图格式添加库文件,和包含路径,并依次勾选掉一些东西,再点击下一步 再点击完成
- 此时 pro 文件中会新增若干条语句,但其中有一条是需要注释的
# 每个人 Python 安装路径都不一定相同, 所以这一块仅供参考
win32: LIBS += -L$$PWD/../../../software/language/py/libs/ -lpython39
INCLUDEPATH += $$PWD/../../../software/language/py/include
DEPENDPATH += $$PWD/../../../software/language/py/include
win32:!win32-g++: PRE_TARGETDEPS += $$PWD/../../../software/language/py/libs/python39.lib
# 注释下行代码, 原因是无需使用动态库
# else:win32-g++: PRE_TARGETDEPS += $$PWD/../../../software/language/py/libs/libpython39.a
- 配置完 pro 文件后会到
main.cpp
文件中会发现Python.h
的导入已经不再报错
- 键盘按下
Ctrl
+R
(macOS 对应的是Commond
+R
) 运行程序,一般情况下会出现 error
解决办法:在 object.h 中把slots改成slots1。Python将slots作为变量,而Qt将slots作为关键字,所以冲突了,再次编译该问题就没有了
- 再次编译可以观察到程序正常运行,界面窗口正常打开 (Python 环境配置至此结束)
3、执行简单 Python 命令
为了方便调试我们的程序是否成功,我们在 main.cpp
中加入 QDebug、QThread 和 宏输出
#include <QThread>
#include <QDebug>
#define dout qDebug() << "[" << __FILE__ << " " << __FUNCTION__ << "() " << __LINE__ << " " << "Thread ID: " << QThread::currentThreadId() << "]"
然后再 main.cpp 中编写如下,运行。(参考C++调用python脚本 - 知乎)
void py_test() { // 主函数中调用一下
// 初始化python解释器.C/C++中调用Python之前必须先初始化解释器
Py_Initialize();
// 判断python解析器的是否已经初始化完成
if(!Py_IsInitialized())
dout<<"[db:] Py_Initialize fail";
else
dout<<"[db:] Py_Initialize success";
// 执行 python 语句
PyRun_SimpleString("print('hello world') ");
// 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。
Py_Finalize();
}
直接运行发现提示 【找不到 python39.dll,无法继续执行代码】
解决方法:在 python 安装目录中,比如
D:\software\language\py
将python39.dll
文件复制到 QT 项目编译出的 Exe 文件同级目录下
解决缺少 dll 文件的问题后直接运行可以发现终端中有打印出 hello world
二、调用 Python 脚本
1、无参调用
把 python 脚本嵌入进 c++语句中,肯定不是我们想要的,我们想要的是QT C++能够调用执行python脚本的。
我们写一个简单的 python 脚本 py_test.py ,为了证明调用成功,我们使用python写一个空文件,内容如下。
def write_file():
with open("a.txt", "w") as f:
f.write("test")
将其放到 py_scripts 文件夹下,py_scripts 与 Release/Debug 文件夹的相对位置如下所示,即同属于./simple_test/build 文件夹下
修改 main.cpp 内容,如下
void py_test() {
// 初始化python解释器.C/C++中调用Python之前必须先初始化解释器
Py_Initialize();
// 判断python解析器的是否已经初始化完成
if(!Py_IsInitialized())
dout << "[db:] Py_Initialize fail";
else
qDebug()<<"[db:] Py_Initialize success";
// 执行 python 语句
PyRun_SimpleString("print('hello world') ");
// 导入sys模块设置模块地址,以及python脚本路径
PyRun_SimpleString("import sys");
// 该相对路径是以build...为参考的
PyRun_SimpleString("sys.path.append('../py_scripts')");
// 加载 python 脚本
PyObject *pModule = PyImport_ImportModule("py_test"); // 脚本名称,不带.py
if(!pModule) // 脚本加载成功与否
dout << "[db:] pModule fail";
else
dout << "[db:] pModule success";
// 创建函数指针
PyObject* pFunc= PyObject_GetAttrString(pModule,"write_file"); // 方法名称
if(!pFunc || !PyCallable_Check(pFunc)) // 函数是否创建成功
dout << [db:] pFunc fail";
else
dout << "[db:] pFunc success";
// 调用函数
PyObject_CallObject(pFunc, NULL); // 无参调用
// 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。
Py_Finalize();
}
执行完成后,会在 Release/Debug (根据你执行的模式) 文件夹下生成一个a.txt文件。
2、有参调用
以上为对python的无参调用,这里我们使用对python的有参调用。
因为python 是没有显性定义的,而C++是有定义的,我们要简单了解下python与C++的数据的类型 。类型对应参考 Here,简单来说就是 s
对应字符串,i
对应整型,f
对应float。使用方法可以参考 Qt项目中C++调用Python函数传多参问题。
这里就复制粘贴使用方法参考的文档,稍作修改,连带返回值和列表的使用都有了。
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QThread>
#include <QDebug>
#include <QRandomGenerator>
#include "Python.h"
#define dout qDebug() << "[" << __FILE__ << " " << __FUNCTION__ << "() " << __LINE__ << " " << "Thread ID: " << QThread::currentThreadId() << "]"
void py_test() {
Py_Initialize();
// 判断python解析器的是否已经初始化完成
if(!Py_IsInitialized())
dout << "[db:] Py_Initialize fail";
else
dout << "[db:] Py_Initialize success";
// 执行 python 语句
PyRun_SimpleString("print('hello world') ");
// 导入sys模块设置模块地址,以及python脚本路径
PyRun_SimpleString("import sys");
// 该相对路径是以build...为参考的
PyRun_SimpleString("sys.path.append('../py_scripts')");
// 加载 python 脚本
PyObject *pModule = PyImport_ImportModule("py_test"); // 脚本名称,不带.py
if(!pModule) // 脚本加载成功与否
dout << "[db:] pModule fail";
else
dout << "[db:] pModule success";
// 创建函数指针,有参调用
PyObject* pFunc= PyObject_GetAttrString(pModule, "process_data"); // 有参调用的
// 定义一个随机器
QRandomGenerator generator;
// 创建一个定长元组,用来存放传入参数
PyObject* pyArgs = PyTuple_New(20);
// 每个元组类似于结构体,包含字符串,整型和浮点类型数据
// 填充元组
for (int i = 0; i < 20; ++i) {
PyObject* pyTuple = PyTuple_New(3); //元组由三部分组成
// 组合下字符串
QString qst = "test string " + QString::number(i);
QByteArray baq = qst.toLatin1();
PyTuple_SetItem(pyTuple, 0, Py_BuildValue("s", baq.data())); // 字符串
PyTuple_SetItem(pyTuple, 1, Py_BuildValue("i", generator.generate() % 100)); // 整型
PyTuple_SetItem(pyTuple, 2, Py_BuildValue("f", 3.14f)); // 浮点型
PyTuple_SetItem(pyArgs, i, pyTuple); // 将结构体填充到列表中
}
// 调用python函数
PyObject* pyResult = PyObject_CallObject(pFunc, pyArgs);
int list_len = PyObject_Size(pyResult);// 计算返回过来的列表长度
dout << list_len;
// 判单是否成功
if (pyResult == NULL) {
PyErr_Print();
}
else {
// 解析返回值
for (int i = 0; i < 20; ++i) { // 已知列表长度有20个,预先不知道的话就使用上面定义的list_len
PyObject* pyTuple = PyList_GetItem(pyResult, i);
QString strVal = QString::fromUtf8(PyUnicode_AsUTF8(PyList_GetItem(pyTuple, 0)));
int intVal = PyLong_AsLong(PyList_GetItem(pyTuple, 1));
double floatVal = PyFloat_AsDouble(PyList_GetItem(pyTuple, 2));
dout << strVal << intVal << floatVal; // 打印
}
}
// 清理Python变量
Py_DECREF(pyArgs);
Py_DECREF(pFunc);
Py_DECREF(pModule);
Py_DECREF(pyResult);
// 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。
Py_Finalize();
}
int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(
&engine,
&QQmlApplicationEngine::objectCreated,
&app,
[url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
},
Qt::QueuedConnection);
engine.load(url);
py_test();
return app.exec();
}
python 源码如下,文件名称仍然是 py_test.py
def process_data(*args):
result = []
f = open("b.txt", "w")
for arg in args: # 从元组中读取数据
strVal, intVal, floatVal = arg # 按顺序一一对应取数据
f.write(strVal + " " + str(intVal) + '\n') # 写文档
# process the data
processed_strVal = strVal.upper()
processed_intVal = intVal + 1
processed_floatVal = floatVal ** 2
sub_result = [processed_strVal, processed_intVal, processed_floatVal]
result.append(sub_result) # 按列表格式返回数据
f.close()
return result
具体修改内容是:
- 创建对有参函数的调用,和一个定长元组,用来存放传入参数,中间还有个随机生成器
// 创建函数指针,有参调用
PyObject* pFunc= PyObject_GetAttrString(pModule, "process_data"); // 有参调用的
// 定义一个随机器
QRandomGenerator generator;
// 创建一个定长元组,用来存放传入参数
PyObject* pyArgs = PyTuple_New(20);
-
填充元组数据
每个元组类似结构体、包含字符串,整型和浮点类型数据
for (int i = 0; i < 20; ++i) {
PyObject* pyTuple = PyTuple_New(3); //元组由三部分组成
// 组合下字符串
QString qst = "test string " + QString::number(i);
QByteArray baq = qst.toLatin1();
PyTuple_SetItem(pyTuple, 0, Py_BuildValue("s", baq.data())); // 字符串
PyTuple_SetItem(pyTuple, 1, Py_BuildValue("i", generator.generate() % 100)); // 整型
PyTuple_SetItem(pyTuple, 2, Py_BuildValue("f", 3.14f)); // 浮点型
PyTuple_SetItem(pyArgs, i, pyTuple); // 将结构体填充到列表中
}
- main.cpp 头文件补充
#include <QRandomGenerator>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QThread>
#include <QDebug>
#include <QRandomGenerator>
#include "Python.h"
-
调用python函数,并输出使用返回值
注意我们传参的时候是使用元组(tuple),返回的时候使用的列表(list),这个见python代码
// 调用python函数
PyObject* pyResult = PyObject_CallObject(pFunc, pyArgs);
int list_len = PyObject_Size(pyResult);// 计算返回过来的列表长度
dout << list_len;
// 判单是否成功
if (pyResult == NULL) {
PyErr_Print();
}
else {
// 解析返回值
for (int i = 0; i < 20; ++i) { // 已知列表长度有20个,预先不知道的话就使用上面定义的list_len
PyObject* pyTuple = PyList_GetItem(pyResult, i);
QString strVal = QString::fromUtf8(PyUnicode_AsUTF8(PyList_GetItem(pyTuple, 0)));
int intVal = PyLong_AsLong(PyList_GetItem(pyTuple, 1));
double floatVal = PyFloat_AsDouble(PyList_GetItem(pyTuple, 2));
dout << strVal << intVal << floatVal; // 打印
}
}
-
python 代码的修改
仍然使用py_test文件,在文件中定义process_data函数。读取tuple内容,将结构体用list包装,并使用list 返回内容如下:
def process_data(*args):
result = []
f = open("b.txt", "w")
for arg in args: # 从元组中读取数据
strVal, intVal, floatVal = arg # 按顺序一一对应取数据
f.write(strVal + " " + str(intVal) + '\n') # 写文档
# process the data
processed_strVal = strVal.upper()
processed_intVal = intVal + 1
processed_floatVal = floatVal ** 2
sub_result = [processed_strVal, processed_intVal, processed_floatVal]
result.append(sub_result) # 按列表格式返回数据
f.close()
return result
执行完成后,会在 Release/Debug (根据你执行的模式) 文件夹下生成一个b.txt文件。,并且qt端输出内容。
三、打包部署
以上我们已经实现QT C++调用python的程序,现在我们要将项目部署在一个没有python环境下的机器上,QT打包发布成exe执行的。对代码的改动不多,对文件夹的修改移动比较多,注意一点。
继续学习:Here