文章目录
在Qt中使用Python并打包发布
一、简介
Qt是一个很好用的C++可视化软件,但是往往在我们处理一些数据的时候,采用Qt的类过于麻烦,甚至一些基础库还不支持。最近在做项目的过程中就遇到这样的问题,需要使用Qt和Python进行混合编程。基于对现存网上叙述杂乱不完整的情况,我将在这篇文章中进行归纳总结。
值得注意的是:
- 该文章面对有一些基础的朋友更为适合,例如Qt新建工程等操作将不再叙述,新手朋友建议自行搜索
- 该文章中工程是在 Windows平台 下运行的,其它操作系统可做参考
通过这篇文章你将学到:
- 如何在Qt中使用Python(其中包含Python类中方法的调用以及普通函数的调用,以及引入第三方库的情况下进行调用)
- 如何将写好的,包含Python程序的工程进行打包发布
- 以及一些常见问题的解决方法
二、使用详解
2.1 准备工作
-
Qt方面
我在Qt新建了一个工程文件
pythonQt
用作示范,如下所示: -
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
,并且将其中的 DLLs
、include
、Lib
、libs
、python3.dll
、python311.dll
、vcruntime140.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步,不会的朋友可以跟着学习一下
-
新建一个空白文件夹用于打包(路径以及文件名最好不要有中文字符)。
-
将
build-........-Release\release
目录下生成的 .exe 文件复制到我们第一步新建的文件夹中。如下所示: -
由于我们之前编译过程中,使用的是 MinGW 64bit release 方式进行编译,故我们找到Qt中对应的控制命令程序,如下所示(图中所示为Win10情况下,其他window版本如果找不到,可以自行通过名字进行搜索):
-
打开命令窗口后,输入以下命令并按下回车运行:
windeloyqt C:\Users\...\pthhonQt_App\pythonQt.exe
**注意:**省略号处为你所在的实际目录,我这里只进行举例说明。
至于文件就是刚才我们复制过去的 .exe 文件,不明白的朋友可以看看我上面复制文件的那个图
运行成功后,会有以下显示:
这一步是将一些qt的库添加到我们新建的文件夹中。
-
然后我们回到 Qt 中,需要清理一下项目,步骤如下图所示:
-
此时我们在
build-........-Release\release
目录下复制除.exe
以及__pycache__
文件以外的文件,将其复制到我们刚才建立的目录中: -
然后我们从我们之前构建的 python 环境中复制以下文件到根目录下(即从
pythonEnv
文件夹中复制): -
之后双击我们的
.exe
文件即可实现运行。这里只做示例,故没有设置图标等操作。
另外,大家还可以通过
Enigma Virtual Box
将该文件打包为单独的.exe
文件,具体操作这里不再赘述,感兴趣的朋友可以自行百度。
四、出现问题及解决方案
一般来讲按照我所给出的流程能够正确运行。但仍然有可能存在以下问题。
4.1 编译运行后出现 “程序异常结束”
现象如下所示:
造成这种情况的原因有以下几种可能:
-
包含的库路径不对,无法正确读取库。
需要我们手动检查库路径是否正确
-
对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并对程序进行打包。
欢迎广大读者提出问题以及修改意见,本人看到后会给予回应,欢迎留言,后续会逐步进行开源!!!
另外,由于文章是作者手打的文字,有些地方可能文字会出错,望谅解,也可私信联系我,我对其进行更改。
-
个人CSDN账号:刘梓谦_-CSDN博客
-
GitHub:Jiahao-Liu29 (github.com)