如何在Qt中使用Python并打包发布


在Qt中使用Python并打包发布

一、简介

Qt是一个很好用的C++可视化软件,但是往往在我们处理一些数据的时候,采用Qt的类过于麻烦,甚至一些基础库还不支持。最近在做项目的过程中就遇到这样的问题,需要使用Qt和Python进行混合编程。基于对现存网上叙述杂乱不完整的情况,我将在这篇文章中进行归纳总结。

值得注意的是:

  • 该文章面对有一些基础的朋友更为适合,例如Qt新建工程等操作将不再叙述,新手朋友建议自行搜索
  • 该文章中工程是在 Windows平台 下运行的,其它操作系统可做参考

通过这篇文章你将学到:

  • 如何在Qt中使用Python(其中包含Python类中方法的调用以及普通函数的调用,以及引入第三方库的情况下进行调用
  • 如何将写好的,包含Python程序的工程进行打包发布
  • 以及一些常见问题的解决方法

二、使用详解

2.1 准备工作

  1. Qt方面

    我在Qt新建了一个工程文件 pythonQt 用作示范,如下所示:

    在这里插入图片描述

  2. Python方面

    首先最为重要的是,在本机安装python环境,需要该目录便于查找!!!

    接下来,我用Python写了一个示例程序,其中包含类以及普通函数,如下所示:

    import numpy as np
    
    def printFunc():
        '''
        打印信息
        :return: None
        '''
        print("Hello World!")
    
    def printInfo(a, b, str):
        '''
        打印内容
        :param a: 整型
        :param b: 整型
        :param str: 字符串
        :return: None
        '''
        print("a + b = {} -- text: {}".format(a + b, str))
    
    def addFunc(a, b):
        """
        实现加法并返回得到的数
        :param a: 整型
        :param b: 整型
        :return: a + b 的值,整型
        """
        return a + b
    
    class myTest:
        def __init__(self, a=1, b=9, text="And"):
            """
            初始化
            """
            print("myTest 构造成功!")
    
            self.__a = a
            self.__b = b
            self.__text = text
            if a != 1 and b != 9 and text != "And":
                print("a = {}, b = {}, str = {}".format(self.__a, self.__b, self.__text))
    
        def getRand(self):
            """
            获取随机数
            :return: None
            """
            print(np.random.randint(0, self.__b, 1)[0])
    
        def retRand(self):
            """
            获取随机数,并返回该随机数
            :return: 生成的随机数,整型
            """
            return np.random.randint(0, self.__b, 1)[0]
    
        def myRand(self, num):
            """
            根据输入的数值,生成 0-num 之间的一个随机数
            :param num: 上限范围
            :return: 生成的随机数,整型
            """
            return np.random.randint(0, num, 1)[0]
    
        def __del__(self):
            """
            析构函数
            :return: None
            """
            print("myTest 析构完成!")
    

2.2 在Qt中使用

2.2.1 配置Qt中的python环境

首先我们需要找到之前安装的Python环境,我这里安装的是 Python3.11.6,如下所示:

在这里插入图片描述

这里推荐在别处新建一个文件夹,例如我这里在桌面新建文件夹 pythonEnv,并且将其中的 DLLsincludeLiblibspython3.dllpython311.dllvcruntime140.dll以及vcruntime140_1.dll文件放入其中,如下所示:

在这里插入图片描述

然后将这个Python环境放入Qt的编译目录中,例如,我使用 MinGW 64bit,采取 release 方式进行编译,那么我需要将其放在 build-pythonQt-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release 目录下。

如果是采用 Debug 方式进行编译,则目录为:

  • build-pythonQt-Desktop_Qt_5_12_3_MinGW_64_bit-Debug/debug

我采用release方式是为了以后能够直接打包,不需要再次部署环境

如下所示:

在这里插入图片描述

另外顺便把我们的python程序放入该目录中,如下所示:

在这里插入图片描述

然后在Qt项目文件的.pro中加入该路径,如下所示:

INCLUDEPATH += -I ../build-pythonQt-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release/pythonEnv/include        # python.h
LIBS += -L../build-pythonQt-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release/pythonEnv/libs -lpython311       # python311.lib

不想写这么麻烦也可以使用桌面上我们建立的 pythonEnv 路径,如下所示:

INCLUDEPATH += -I C:\Users\intern03\Desktop\pythonEnv\include   # python.h
LIBS += -LC:\Users\intern03\Desktop\pythonEnv\libs -lpython311  # python311.lib

其实这两种方式没有本质的区别,前者我们将环境放入编译目录,所属为该项目的环境,只是为了更好理解。

图片如下所示:

在这里插入图片描述

然后我们在 widget.c 中添加python库的头文件,如下所示:

在这里插入图片描述

这里是需要对 slots 重定义,防止冲突。是因为Python将slots作为变量,而Qt将slots作为关键字。

之后进行编译,一般无错误,如有错误请参考 第四章

值得注意的是,如果在程序中未引用第三方包(即需要通过pip下载的包),至此工作就可以结束了,而我为了让大家能够学会这种方式,在python文件中引入了 numpy 包。这时我们需要通过pip下载相应包。

在window下,使用 cmd 面板输入以下命令:

pip install numpy --target=C:\Users\intern03\Qt_WorkFile\testWorkFile\01.PythonQt\build-pythonQt-Desktop_Qt_5_12_3_MinGW_64_bit-Release\release

运行成功后,如下所示:

在这里插入图片描述

下载路径根据自己在.pro文件中引用的路径为主,例如我这里在.pro中使用的是:../build-pythonQt-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release/pythonEnv/ 的方式,那么我就需要在该目录下添加包。

  • 注意该路径可以直接打开资源管理器从里面去复制
  • 本人更推荐我所描述的这种形式,因为在后续打包过程中,我们仍然需要该环境。总之你在哪个环境中安装的包,后续就使用该环境进行打包操作。

至此,完成Qt中Python环境的部署工作!!!

2.2.2 调用Python

在进行这一步的时候,为了方便操作,我在UI界面中创建了一个按钮,和一个文本编辑框,通过按下按钮调用Python,并且将打印信息显示到文本框中。而网上一般都是在 widget 构造的时候进行Python调用,我这样做是为了后续打包后能够灵活调用。

按钮如下所示,对其绑定松手槽:

在这里插入图片描述

后续我们将在这个槽函数中进行python的调用。这里我写了一个简单的例子(widget.cpp 文件):

#include "widget.h"
#include "ui_widget.h"

#undef slots
#include <Python.h>
#define slots Q_SLOTS

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
}

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

/*!
 *  @File        : widget.cpp
 *  @Brief       : 按键释放回调函数
 *  @Details     : 相当于按键松手检测
 *  @Param       : void
 *  @Return      : void
 *  @Author      : Liu Jiahao
 *  @Date        : 2024-02-22 16:24:14
 *  @Version     : v1.1
 *  @Copyright   : Copyright By Liu Jiahao, All Rights Reserved
 *
 */
void Widget::on_PushButton_released()
{
    /* 初始化 Python 解释器 */
    Py_Initialize();

    // 判断是否初始化成功
    if (!Py_IsInitialized()) {
        ui->textEdit->insertPlainText("[db:] Py_Initialize fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] Py_Initialize success\n");

    /* 导入 sys 模块设置模块地址,以及Python脚本路径 */
    PyRun_SimpleString("import sys");
    // 该路径是相对 buil... 目录而言的
    PyRun_SimpleString("sys.path.append('./')");

    // 加载 python 脚本
    PyObject* pModule = PyImport_ImportModule("testQt");  // 脚本名称
    if (!pModule) {
        ui->textEdit->insertPlainText("[db:] pModule fail\n");
        PyErr_Print();
        PyErr_Clear();
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pModule success\n");

    /*************************************** 函数调用 *******************************************/
    PyObject* pFuncPrintFunc = PyObject_GetAttrString(pModule, "printFunc");    // 获取函数 printFunc - 无参、无返回值
    if (!pFuncPrintFunc) {
        ui->textEdit->insertPlainText("[db:] pFuncPrintFunc fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pFuncPrintFunc success\n");
    PyObject_CallFunction(pFuncPrintFunc, nullptr);                             // 调用函数 pFuncPrintFunc
    Py_DECREF(pFuncPrintFunc);                                                  // 释放指针

    PyObject* pFuncPrintInfo = PyObject_GetAttrString(pModule, "printInfo");    // 获取函数 printInfo - 有参、无返回值
    if (!pFuncPrintInfo) {
        ui->textEdit->insertPlainText("[db:] pFuncPrintInfo fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pFuncPrintInfo success\n");
    /* 构建参数并调用 方法一 */
    PyObject* args = Py_BuildValue("iis", 2, 3, "test");                        // 构建参数
    PyObject_CallObject(pFuncPrintInfo, args);                                  // 调用函数 printInfo 并传入参数
    /* 构建参数并调用 方法二 */
//    PyObject* args = PyTuple_New(3);                                            // 定义长度为 3 的变量元组
//    PyTuple_SetItem(args, 0, PyLong_FromLong(2));                               // 通过 PyLong_FromLong 函数构造参数
//    PyTuple_SetItem(args, 1, Py_BuildValue("i", 3));                            // 通过 Py_BuildValue 函数构造参数
//    PyTuple_SetItem(args, 2, PyUnicode_FromString("test"));                     // 通过 PyUnicode_FromString 函数构造参数,当然也可通过 Py_BuildValue 构造
//    PyObject_CallObject(pFuncPrintInfo, args);                                  // 调用函数 printInfo 并传入参数
    /* 构建参数并调用 方法三 */
//    PyObject_CallFunction(pFuncPrintInfo, "iis", 2, 3, "test");               // 调用函数 printInfo 并传入参数
    Py_DECREF(args);                                                            // 如果采用方法三则需要注释掉
    Py_DECREF(pFuncPrintInfo);                                                  // 释放指针

    PyObject* pFuncAddFunc = PyObject_GetAttrString(pModule, "addFunc");        // 获取函数 addFunc - 有参、有返回值
    if (!pFuncAddFunc) {
        ui->textEdit->insertPlainText("[db:] pFuncAddFunc fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pFuncAddFunc success\n");
    PyObject* pReturn = PyObject_CallFunction(pFuncAddFunc, "ii", 2, 3);         // 调用 addFunc 函数 并传入参数获取返回值
    int nResult;                                                                 // 声明接收返回值的变量
    PyArg_Parse(pReturn, "i", &nResult);                                         // 写入返回值
    ui->textEdit->insertPlainText("addFunc's return reslut is " + QString::number(nResult) + "\n");
    Py_DECREF(pReturn);
    Py_DECREF(pFuncAddFunc);

    /*************************************** 类方法调用 *******************************************/
    /* 从 pModule 中获取模块属性字典 pDict */
    PyObject* pDict = PyModule_GetDict(pModule);
    if (!pDict) {
        ui->textEdit->insertPlainText("[db:] pDict fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pDict success\n");

    /* 从属性字典 pDict 中根据类名获取类 pClassCalc */
    PyObject* pClassCalc = PyDict_GetItemString(pDict, "myTest");
    if (!pClassCalc) {
        ui->textEdit->insertPlainText("[db:] pClassCalc fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pClassCalc success\n");

    /* 通过类实例化对象 pInstance,无参情况 */
    PyObject* pNoArgInstance = PyObject_CallObject(pClassCalc, nullptr);
    if (!pNoArgInstance) {
        ui->textEdit->insertPlainText("[db:] pNoArgInstance fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pNoArgInstance success\n");

    /* 通过类实例化对象 pInstance,有参情况 */
    PyObject* argsClass = Py_BuildValue("iis", 2, 10, "class1");
    PyObject* pArgInstance = PyObject_CallObject(pClassCalc, argsClass);
    if (!pArgInstance) {
        ui->textEdit->insertPlainText("[db:] pInstance fail\n");
        return;
    }
    else
        ui->textEdit->insertPlainText("[db:] pInstance success\n");

    PyObject_CallMethod(pArgInstance, "getRand", NULL);                            // 调用 getRand 函数 - 无参、无返回值

    PyObject* pClassReturn = PyObject_CallMethod(pArgInstance, "retRand", nullptr);// 调用 retRand 函数 - 无参、有返回值
    int classResult;                                                               // 准备一个变量用于接收返回值
    PyArg_Parse(pClassReturn, "i", &classResult);                                   // 写入返回值
    ui->textEdit->insertPlainText("retRand's return reslut is " + QString::number(classResult) + "\n");
    Py_DECREF(pClassReturn);                                                       // 释放返回值指针

    PyObject* pFuncReturn = PyObject_CallMethod(pArgInstance, "myRand", "i", 15);  // 调用 myRand 函数 - 有参、有返回值
    int FuncResult;
    PyArg_Parse(pFuncReturn, "i", &FuncResult);                                     // 写入返回值
    ui->textEdit->insertPlainText("myRand's return reslut is " + QString::number(FuncResult) + "\n");
    Py_DECREF(pFuncReturn);                                                       // 释放返回值指针

    Py_DECREF(pNoArgInstance);                                                     // 释放 无参构造的类
    Py_DECREF(pArgInstance);                                                       // 释放 有参构造的类
    Py_DECREF(pClassCalc);
    Py_DECREF(argsClass);
    Py_DECREF(pDict);
    Py_DECREF(pModule);

    ui->textEdit->insertPlainText("\n");

    // 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。
    Py_Finalize();

    ui->PushButton->setEnabled(false);
}

运行后结果如下所示:

在这里插入图片描述

所有信息均成功输出。

值得注意的是,我在代码最后加上了 ui->PushButton->setEnabled(false); 这一行代码,禁用了按钮,之所以这么做,我将会在 第四章 中的 4.4 小节 叙述。

**至此,我们使用Qt调用Python大功告成!!**后续将进行打包工作。

另外,打包完成后,Python中print的内容我们是看不到的,有两种解决办法:

  • 更改Python函数,将print的内容返回出来,通过Qt的Ui控件显示
  • 捕获Qt的Debug事件,重写该方法,获取输出信息并显示到控件上

三、打包程序

3.1 修改自定义解释器

在进行打包之前,我们需要修改Python解释器运行路径,如下所示:

QString path = QCoreApplication::applicationDirPath() + "/pythonEnv/";
Py_SetPythonHome((wchar_t *)(reinterpret_cast<const wchar_t *>(path.utf16())));

具体位置如下所示:

在这里插入图片描述

修改完成后需要再次编译运行!!!

3.2 使用 qt 打包程序进行打包

对于有基础的朋友可以跳到本小节的第5步,不会的朋友可以跟着学习一下

  1. 新建一个空白文件夹用于打包(路径以及文件名最好不要有中文字符)。

  2. build-........-Release\release 目录下生成的 .exe 文件复制到我们第一步新建的文件夹中。如下所示:

    在这里插入图片描述

  3. 由于我们之前编译过程中,使用的是 MinGW 64bit release 方式进行编译,故我们找到Qt中对应的控制命令程序,如下所示(图中所示为Win10情况下,其他window版本如果找不到,可以自行通过名字进行搜索):

    在这里插入图片描述

  4. 打开命令窗口后,输入以下命令并按下回车运行:

    windeloyqt C:\Users\...\pthhonQt_App\pythonQt.exe
    

    **注意:**省略号处为你所在的实际目录,我这里只进行举例说明。

    至于文件就是刚才我们复制过去的 .exe 文件,不明白的朋友可以看看我上面复制文件的那个图

    运行成功后,会有以下显示:

    在这里插入图片描述

    这一步是将一些qt的库添加到我们新建的文件夹中。

  5. 然后我们回到 Qt 中,需要清理一下项目,步骤如下图所示:

    在这里插入图片描述

  6. 此时我们在 build-........-Release\release 目录下复制除 .exe以及__pycache__ 文件以外的文件,将其复制到我们刚才建立的目录中:

    在这里插入图片描述

  7. 然后我们从我们之前构建的 python 环境中复制以下文件到根目录下(即从 pythonEnv 文件夹中复制):

    在这里插入图片描述

  8. 之后双击我们的 .exe 文件即可实现运行。

    这里只做示例,故没有设置图标等操作。

    另外,大家还可以通过 Enigma Virtual Box 将该文件打包为单独的 .exe 文件,具体操作这里不再赘述,感兴趣的朋友可以自行百度。

四、出现问题及解决方案

一般来讲按照我所给出的流程能够正确运行。但仍然有可能存在以下问题。

4.1 编译运行后出现 “程序异常结束”

现象如下所示:

在这里插入图片描述

造成这种情况的原因有以下几种可能:

  1. 包含的库路径不对,无法正确读取库。

    需要我们手动检查库路径是否正确

  2. 对Python变量的操作方式不对

    例如,网上有以下写法:

    Py_CLEAR(pModule);
    Py_DECREF(pModule);
    

    这段代码中,将 pModule 变量释放了两次,故出现异常。但该异常没有任何报错信息,所以仍然需要我们对自己所创建的 PyObject 指针进行检查。

4.2 打包完成运行时出现缺少 .dll 文件

打包完成后,在我们运行 .exe 文件时,有时候会出现以下提示:

在这里插入图片描述

一般均为Python文件缺失,我们可以从我们复制过去的Python环境文件(在本文中为 pythonEnv 文件)中查找缺失的 .dll 文件,将其复制到根目录下,如下所示:

在这里插入图片描述

4.3 出现 pModule fail 问题

该问题是在引入第三方资源包的时候,没有找到相应包所造成的,如下所示:

在这里插入图片描述

解决方法是,找到我们使用的 python 文件,查看其 import 的包资源是哪个,通过 pip 重新安装到当前目录或复制电脑中的相关包到当前目录下。

这里我更推荐直接使用 pip 进行安装。安装方法在 2.2.1 配置Qt中的Python环境 中有所叙述,这里我再次进行描述:

  • 使用命令:

    pip install [包名称] --target=[目标路径]
    

对包进行重新安装后即可成功运行。

4.4 上述代码中运行完成禁用按钮的原因

在官方文档 概述 — Python 3.11.8 文档 中有以下描述:

在这里插入图片描述

其解释了,如果我们之前已经初始化过 python 环境后,再次进行初始化,其 扩展模块所分配的内存 是不会被释放的。

故,想通过按钮实现,点击一次调用一次python函数的操作,需要将 Py_Initialize() 以及 Py_Finalize() 写在外面,以及其模块的调用同样需要这样的操作方式。 但这样做并不能从根本上解决问题,从 Py_Finalize() function, why it does not finalise Python?? · Issue #98524 · python/cpython (github.com) 中以及网上大多数文章中都能看到,该问题一直都没有解决,后续如果有新的办法,我会及时分享。

其本质就是内存泄漏出现的问题,但官方一致没有给出好的解决方案,目前有几种解决方案,等本人尝试后会给予分享。

修改日志: 2024.03.25

  • 已经增加了解决方法,文章链接为:如何在Qt中多次调用python-CSDN博客
  • 如无法点击跳转,则可自行复制以下链接:https://blog.csdn.net/liujiahao_/article/details/137013648?spm=1001.2014.3001.5501

五、写在最后

本文介绍了 如何在Qt中使用Python并对程序进行打包

欢迎广大读者提出问题以及修改意见,本人看到后会给予回应,欢迎留言,后续会逐步进行开源!!!
另外,由于文章是作者手打的文字,有些地方可能文字会出错,望谅解,也可私信联系我,我对其进行更改。

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
Qt是一个跨平台的应用程序开发框架,常用于C++开发Python是一种动态类型的脚本语言。混合编程指的是在一个项目同时使用Qt/C++和Python进行开发。 混合编程的好处是可以充分发挥QtPython各自的优势。Qt/C++可以提供高性能和可靠性,适用于底层开发和系统级编程。Python则提供了简洁易懂、高效编程以及大量的第三方库,适用于快速开发和原型设计。 混合编程的打包过程可以分为以下几个步骤: 首先,要安装相应的编译工具和开发环境,如Qt、C++编译器和Python解释器。确定使用Qt版本与Python版本兼容。 其次,要为C++部分编写Qt代码,并将其编译成dll或so动态链接库,以供Python调用。这使用Qt提供的相关工具和库进行编译和链接。 然后,使用Python的相关库(如PyQt或PySide)来调用C++部分的Qt代码,并将其与Python代码结合起来。这样,就可以实现Qt界面与Python逻辑的交互。 最后,将项目打包成可执行文件、二进制文件或安装包。这可以使用Qt提供的打包工具,如Qt Installer Framework,或者使用第三方工具和脚本来完成。 要注意的是,在混合编程和打包过程要仔细处理Qt的信号与槽机制与Python的回调机制之间的交互,以确保二者能够正常工作。 总之,Qt C++和Python的混合编程可以充分利用两者的优势,打包要注意兼容性和交互的处理。这种方式可以更灵活地开发应用程序,并能够适应不同的求和平台。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值