工作中遇到qt c++調用我的python 代碼,并且想要一鍵打包,這里我根據參考的以及個人實踐的結果來簡單實現一下。
環境:windows系統,QT Creater 4.5, python 3.8(anaconda虛擬環境)
1. 簡單QT調用python程序
1.創建QT工程
中間省略3個步驟圖。創建完成后,如圖。
首先提示各位從python過來的同仁,QT中有時候對項目“重新構建”,項目并不真正的重新構建,如果這樣的話,我們需要在工程文件夾下找到對應的構建后的項目,即比較長的這個(對應的是debug模式下的編譯構建),刪除掉,再點擊重新構建。
2. 配置python 環境
使用QT 調用python需要加載Python.h頭文件,我們在Headers/mainwindow.h里面引入Python.h。但原始配置是找不到Python.h的,所以首先我們需要將安裝好的python路徑配置到QT的配置文件(.pro)中。?
打開(項目名.pro)文件,按照如下格式填寫。這里我將一個python 環境的DLLs,include,Lib,libs和python3.dll, python38.dll 以及vcruntime.dll 復制過來,為該項目單獨做個python環境。
(參考在QT C++中調用 Python并將軟件打包發布(裸機可運行)_互聯網集市)
我是創建一個python_38的python環境,拷貝了miniconda3/envs/cat虛擬環境中的DLLs,include,Lib,libs和python3.dll, python38.dll 以及vcruntime140.dll (這個python環境要能夠支撐后面的python代碼的運行,就是在原來的虛擬環境中,下面的python代碼也可以執行的)
INCLUDEPATH += -I D:\output\envs\python_38\include # python.h
LIBS += -LD:\output\envs\python_38\libs -lpython38 # python38.lib
其中?INCLUDEPATH 里面配置的是python.h的路徑,LIBS配置的是python38.lib的路徑。(參考Qt調用Python詳細圖文過程記錄_python_腳本之家)
問題1:出現C2059錯誤
解決辦法:在object.h中把slots改成slots1。Python將slots作為變量,而Qt將slots作為關鍵字,所以沖突了,再次編譯該問題就沒有了(參考Qt調用Python詳細圖文過程記錄_python_腳本之家)
問題2?
如果出現找不到python38_d.lib是因為系統默認我們采用的是debug模式編譯的(圖片左下角所示)
我們可以
1在D:\output\envs\python_38\libs 復制python38.lib,粘貼成python38_d.lib
2 將編譯模式修改成release模式。
以下操作在release模式進行
再次編譯,如果不報錯,則表示編譯成功,點擊運行,出現彈窗。
為了方便調試我們的程序是否成功,我們在mainwindow.h中加入QDebug
然后再mainwindow.cpp中編寫如下,運行。(參考C++調用python腳本 - 知乎)
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);// 初始化python解釋器.C/C++中調用Python之前必須先初始化解釋器Py_Initialize();// 判斷python解析器的是否已經初始化完成if(!Py_IsInitialized())qDebug()<<"[db:] Py_Initialize fail";elseqDebug()<<"[db:] Py_Initialize success";// 執行 python 語句PyRun_SimpleString("print('hello world') ");// 并銷毀自上次調用Py_Initialize()以來創建并為被銷毀的所有子解釋器。Py_Finalize();}MainWindow::~MainWindow()
{delete ui;
}
代碼能夠解釋執行python語句,并輸出hello world,表示我們配置運行成功。
3. 調用python腳本
把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與build-simple_test-Desktop_Qt_5_10_0_MSVC2015_64bit-Release文件夾的相對位置如下所示,即同屬于./qt文件夾下
修改mainwindow.cpp內容,如下
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);// 初始化python解釋器.C/C++中調用Python之前必須先初始化解釋器Py_Initialize();// 判斷python解析器的是否已經初始化完成if(!Py_IsInitialized())qDebug()<<"[db:] Py_Initialize fail";elseqDebug()<<"[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"); // 腳本名稱,不帶.pyif(!pModule) // 腳本加載成功與否qDebug()<<"[db:] pModule fail";elseqDebug()<<"[db:] pModule success";// 創建函數指針PyObject* pFunc= PyObject_GetAttrString(pModule,"write_file"); // 方法名稱if(!pFunc || !PyCallable_Check(pFunc)) // 函數是否創建成功qDebug()<<"[db:] pFunc fail";elseqDebug()<<"[db:] pFunc success";// 調用函數PyObject_CallObject(pFunc, NULL); // 無參調用// 并銷毀自上次調用Py_Initialize()以來創建并為被銷毀的所有子解釋器。Py_Finalize();}MainWindow::~MainWindow()
{delete ui;
}
執行完成后,會在build-simple_test-Desktop_Qt_5_10_0_MSVC2015_64bit-Release文件夾下生成一個a.txt文件。
2. 有參調用
以上為對python的無參調用,這里我們使用對python的有參調用。
因為python 是沒有顯性定義的,而C++是有定義的,我們要簡單了解下python與C++的數據的類型?。類型對應參考(如何在C++中使用一個Python類-[PyImport_ImportModule、PyModule_GetDict、PyDict_GetItemString、PyObject_CallFuncti]-CSDN博客),簡單來說就是s對應字符串,i對應整型,f對應float。使用方法可以參考(Qt項目中C++調用Python函數傳多參問題_qt調用python_平頭猿小哥的博客-CSDN博客)
這里就復制粘貼使用方法參考的文檔,稍作修改,連帶返回值和列表的使用都有了。
QT C++源碼如下
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);// 初始化python解釋器.C/C++中調用Python之前必須先初始化解釋器Py_Initialize();// 判斷python解析器的是否已經初始化完成if(!Py_IsInitialized())qDebug()<<"[db:] Py_Initialize fail";elseqDebug()<<"[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"); // 腳本名稱,不帶.pyif(!pModule) // 腳本加載成功與否qDebug()<<"[db:] pModule fail";elseqDebug()<<"[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);// 計算返回過來的列表長度qDebug() << list_len;// 判單是否成功if (pyResult == NULL) { PyErr_Print(); }else {// 解析返回值for (int i = 0; i < 20; ++i) { // 已知列表長度有20個,預先不知道的話就使用上面定義的list_lenPyObject* 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));qDebug() << strVal << intVal << floatVal; // 打印}}// 清理Python變量Py_DECREF(pyArgs);Py_DECREF(pFunc);Py_DECREF(pModule);Py_DECREF(pyResult);// 并銷毀自上次調用Py_Initialize()以來創建并為被銷毀的所有子解釋器。Py_Finalize();}MainWindow::~MainWindow()
{delete ui;
}
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 dataprocessed_strVal = strVal.upper()processed_intVal = intVal + 1processed_floatVal = floatVal ** 2sub_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);
2.填充元組數據
每個元組類似于結構體,包含字符串,整型和浮點類型數據
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); // 將結構體填充到列表中}
3.對mainwindow.h的修改
?因為用到了QRandomGenerator ,所以在mainwindow.h中引入#include <QRandomGenerator>頭文件
#include <QMainWindow>
#include "Python.h"
#include <QDebug>
#include <QRandomGenerator>
?4.調用python函數,并輸出使用返回值
注意我們傳參的時候是使用元組(tuple),返回的時候使用的列表(list),這個見python代碼
// 調用python函數PyObject* pyResult = PyObject_CallObject(pFunc, pyArgs);int list_len = PyObject_Size(pyResult);// 計算返回過來的列表長度qDebug() << list_len;// 判單是否成功if (pyResult == NULL) { PyErr_Print(); }else {// 解析返回值for (int i = 0; i < 20; ++i) { // 已知列表長度有20個,預先不知道的話就使用上面定義的list_lenPyObject* 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));qDebug() << strVal << intVal << floatVal; // 打印}}
?5.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 dataprocessed_strVal = strVal.upper()processed_intVal = intVal + 1processed_floatVal = floatVal ** 2sub_result = [processed_strVal, processed_intVal, processed_floatVal]result.append(sub_result) # 按列表格式返回數據f.close()return result
?執行結果是會在build-simple_test-Desktop_Qt_5_10_0_MSVC2015_64bit-Release文件夾下生成一個b.txt文件,并且qt端輸出內容。
3. 打包部署
以上我們已經實現QT C++調用python的程序,現在我們要將項目部署在一個沒有python環境下的機器上,QT打包發布成exe執行的。對代碼的改動不多,對文件夾的修改移動比較多,注意一點。
由于.pro只修改一行,這里只附上mainwindow.cpp的源碼
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);// 設置 python 路徑Py_SetPythonHome((wchar_t*)(L"./python_38")); // 相對位置以exe為參考// 初始化python解釋器.C/C++中調用Python之前必須先初始化解釋器Py_Initialize();// 判斷python解析器的是否已經初始化完成if(!Py_IsInitialized())qDebug()<<"[db:] Py_Initialize fail";elseqDebug()<<"[db:] Py_Initialize success";// 執行 python 語句PyRun_SimpleString("print('hello world') ");// 導入sys模塊設置模塊地址,以及python腳本路徑PyRun_SimpleString("import sys");// 該相對路徑是以build...為參考的PyRun_SimpleString("sys.path.append('./py_scripts')"); //以exe為參考位置// 加載 python 腳本PyObject *pModule = PyImport_ImportModule("py_test"); // 腳本名稱,不帶.pyif(!pModule) // 腳本加載成功與否qDebug()<<"[db:] pModule fail";elseqDebug()<<"[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);// 計算返回過來的列表長度qDebug() << list_len;// 判單是否成功if (pyResult == NULL) { PyErr_Print(); }else {// 解析返回值for (int i = 0; i < 20; ++i) { // 已知列表長度有20個,預先不知道的話就使用上面定義的list_lenPyObject* 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));qDebug() << strVal << intVal << floatVal; // 打印}}// 清理Python變量Py_DECREF(pyArgs);Py_DECREF(pFunc);Py_DECREF(pModule);Py_DECREF(pyResult);// 并銷毀自上次調用Py_Initialize()以來創建并為被銷毀的所有子解釋器。Py_Finalize();}MainWindow::~MainWindow()
{delete ui;
}
具體修改如下:
1. 修改編譯輸出目錄(生成exe的目錄),到 qt_output
在項目.pro中添加
DESTDIR = $$PWD/../qt_output
與python路徑合在一起展示如下。
FORMS += \mainwindow.uiDESTDIR = $$PWD/../qt_outputINCLUDEPATH += -I D:\output\envs\python_38\include # python.h
LIBS += -LD:\output\envs\python_38\libs -lpython38 # python38.lib
編譯后結果如下 ,qt_output中只有exe文件,可知這個PWD的路徑是以build-simple_test-Desktop_Qt_5_10_0_MSVC2015_64bit-Release為參考的
?2.將python 環境拷貝到qt_output目錄下
即將python_38文件夾復制到qt_output目錄下。
3.在QT C++ 中指定python 庫地址
在初始化之前,添加
Py_SetPythonHome((wchar_t*)(L"./python_38")); // 相對位置以exe為參考
ui->setupUi(this);// 設置 python 路徑Py_SetPythonHome((wchar_t*)(L"./python_38")); // 相對位置以exe為參考// 初始化python解釋器.C/C++中調用Python之前必須先初始化解釋器Py_Initialize();
4.將python腳本文件移入qt_output文件夾中,并修改相對路徑
// 導入sys模塊設置模塊地址,以及python腳本路徑PyRun_SimpleString("import sys");// 該相對路徑是以build...為參考的PyRun_SimpleString("sys.path.append('./py_scripts')"); //以exe為參考位置// 加載 python 腳本PyObject *pModule = PyImport_ImportModule("py_test"); // 腳本名稱,不帶.py
2,3,4,步執行完成后,文件夾中內容如下。
程序中運行,會在exe同文件夾下生成 b.txt
在文件夾中,點擊exe文件直接運行,會出現找不到Qt5Core.dll和Qt5Widgets.dll錯誤。
這就用到windeployqt命令了 ,
5.開始鍵輸入找到如圖的客戶端,打開后
輸入windeployqt D:\workspace\qt\qt_output\simple_test.exe 具體內容根據項目路徑來寫,運行完成后會在qt_output文件夾中生成程序運行所需要的依賴包(具體叫啥不知道,這個是qt的東西)。
qt_output文件夾內除了以前的這些文件外,又多了些文件夾和文件(依賴庫)?。
再點擊simple_test.exe,則出現QT的彈窗,并且生成新的b.txt文件。
6. 經驗證,還需要將python_38文件夾里面的python38.dll文件移到外面來,放在和simple.exe同一級別。
4. python 中帶有第三方包的部署(忘了參考哪個了,主要是找不到參考的那個網頁了)
我們首先修改下py_test.py的內容,引入numpy包,因為numpy是第三方的包。修改如下:
import numpy as npdef write_file():with open("a.txt", "w") as f:f.write("test")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 dataprocessed_strVal = strVal.upper()processed_intVal = intVal + 1processed_floatVal = floatVal ** 2sub_result = [processed_strVal, processed_intVal, processed_floatVal]result.append(sub_result) # 按列表格式返回數據f.close()arr = np.array(result)return result
?在返回之前,生成一個并不使用的變量arr = np.array(result),這個我們就是為了測試第三方包而做的,生成的arr沒有任何意義。
再次點擊simple_test.exe不會生成b.txt,表示python腳本程序運行錯誤,第三方包調用失敗。
解決方法
1.使用pyinstaller生成依賴文件
這個就需要我們python 的一個包了,需要pip(conda)安裝pyinstaller。因為我這里供QT C++ 使用的python環境(D:\workspace\qt\qt_output\python_38)是從D:\miniconda3\envs\cat虛擬環境中復制出來的部分,所以我使用的是激活cat的虛擬環境,并再這里面執行pyinstaller,生成依賴文件。
(cat) PS D:\tmp> conda activate cat
(cat) PS D:\tmp> cd d:/tmp
(cat) PS D:\tmp> pyinstaller D:\workspace\qt\qt_output\py_scripts\py_test.py
1.激活環境,2.生成的依賴在那個文件夾中(隨便寫的一個文件夾),3.對那個python文件生成依賴
執行完成之后,會在d:/tmp中生成兩個文件夾,dist 和 build?
2.我們將dist/py_test/_internal?中的所有文件(夾)全部復制到QT編譯生成的qt_output文件夾中
將得到依賴包的qt_output文件夾放在在新機器上部署執行帶有第三方包就沒有問題了
經本人測試包括cv2包也可以,但是本人對matlablib這個包沒有導入成功。
其他說明
1.qt生成的文件一定要注意他們的相對位置,是相對于哪一個文件的位置。
2.QT C++調用python 后,我沒有能夠進行調試,不知道是什么原因不能調試。
3.python 和C++ 的數據類型,有些QT C++數據類型傳到python中不好使用,主要我對QT和C++不了解。
4.帶有第三方打包的就比普通打包部署多一步,這一步需要pyinstaller生成動態鏈接庫等文件,復制進和QT C++ 生成的exe同一個文件夾中。
參考網頁
Qt調用Python詳細圖文過程記錄_python_腳本之家
在QT C++中調用 Python并將軟件打包發布(裸機可運行)_互聯網集市
C++調用python腳本 - 知乎
如何在C++中使用一個Python類-[PyImport_ImportModule、PyModule_GetDict、PyDict_GetItemString、PyObject_CallFuncti]-CSDN博客
Qt項目中C++調用Python函數傳多參問題_qt調用python_平頭猿小哥的博客-CSDN博客
C++調用Python(混合編程)函數整理總結_jindayue的博客-CSDN博客
PyObject_CallObject, PyObject_Call, PyObject_CallFunction使用例子-CSDN博客
Qt C++ Python 混合編程測試文檔 - 知乎