Qt/C++ 中调用 Python,并将软件打包发布、Python 含第三方依赖

背景:

工作中遇到 QT/C++ 调用我的 Python 代码,并且想要一键打包,这里我根据参考的以及个人实践的结果来简单实现一下。

开发环境: Win10, QT Creator 13.0.1, Python 3.9.10 (非 Anaconda 虚拟环境)

一、简单 QT 调用 Python 程序

1、创建 Qt 工程 with QMake

Create Qt Project

pro 文件

首先提示各位从 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\pypython39.dll 文件复制到 QT 项目编译出的 Exe 文件同级目录下

解决缺少 dll 文件的问题后直接运行可以发现终端中有打印出 hello world

image

二、调用 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


具体修改内容是:

  1. 创建对有参函数的调用,和一个定长元组,用来存放传入参数,中间还有个随机生成器
// 创建函数指针,有参调用
PyObject* pFunc= PyObject_GetAttrString(pModule, "process_data");  // 有参调用的
// 定义一个随机器
QRandomGenerator generator;
// 创建一个定长元组,用来存放传入参数
PyObject* pyArgs = PyTuple_New(20);
  1. 填充元组数据

    每个元组类似结构体、包含字符串,整型和浮点类型数据

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);  // 将结构体填充到列表中
 }
  1. main.cpp 头文件补充 #include <QRandomGenerator>
#include <QGuiApplication>
#include <QQmlApplicationEngine>

#include <QThread>
#include <QDebug>
#include <QRandomGenerator>
#include "Python.h"
  1. 调用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;	// 打印
    }
}
  1. 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

posted @ 2024-07-05 15:01  RioTian  阅读(2095)  评论(0编辑  收藏  举报