之前的两篇,只是记录了软件开发的一些思路,软件的功能也并不完善,没有干货。这篇文章计划将软件编写的全流程做个记录,也将把源码贴上,最后会罗列一下自己遇到的坑,希望可以帮到大家。
目录
1. 开发背景
2.开发思路
3.软件功能详细说明
3.0 软件启动及登录
3.1 选取excel文件地址
3.2 外购件料号excel文件填写
编辑
3.2.1 料号填写错误
3.2.2 料号重复
3.2.3 料号正确且不重复
3. 3 G2及BOM的填写
3.4 软件功能、帮助按钮及最终保存
3.4.1 软件功能及帮助按钮
3.4.1 保存料号
4. 软件源码
4.1 方案框架:
4.2 登录窗口
4.3 ※主窗口
4.3.1 主窗口构造
4.3.2 打开某个excel文件
4.3.3 清空重选按钮
4.3.4 生成料号按钮
4.3.5 保存最终结果:
5. ※遇到的坑与解决
5.1 PyImport_ImportModule or PyObject_GetAttrString 返回NULL
5.2 PyType_Slot *slots冲突问题
5.3 QString转为std::String中文显示为“???”
5.4 PyBytes_AsString无法将结果转为string,返回Null
5.5 PyImport_ImportModule无法重复调用,写入异常问题
1. 开发背景
公司开发新品时,如果需要采购其他公司生产的零部件,且该部件为第一次采购时,就需要手动完成一个外购件料号的登记流程。即需要填写三个Excle表格,分别是“外购件料号登记表”、“G2"登记表及“BOM登记清单”。这三个表格,用户手动填写比较费时间,于是开发一个软件,可以一次性完成三个表格的填写。
其中,外购件料号登记表为了防止其他同事的误编辑,设置了访问密码,打开后显示如下:
表格内容如图:
而G2表格没有密码,每个项目都有独立的G2表格,内容如下:
BOM表格内容如图:
需要研发部填写的只有“HSI料号”一栏,也就是料号表格中的料号。
2.开发思路明确了填写需求,开始构思开发思路。
我的思路就是,通过QT编写用户 *** 作界面,后台完成对excel文件的读写。
我构思的软件界面如下(关于V1.0及V2.0请参考前面两篇文章):
用户需要对应选择自己公盘下的excel文件地址,因为公盘使用one-drive上传,同一个excel文件,不同用户有不同的地址:某用户用户名\OneDrive...\公盘地址\xx.xlsx。
而需要填写在不同excel表格中的内容,用户在QT界面填写即可。最后填写完毕进行保存,下面对软件逻辑进行详细说明。
3.软件功能详细说明 3.0 软件启动及登录因为外购件料号软件需要密码进行编辑,为了防止无权用户通过软件进行编辑,软件设置了登录密码,用户需要登录成功后才能进入软件主界面:
3.1 选取excel文件地址当用户点选excel文件地址时,设置一个文件格式筛选,方便用户的同时也可以防止用户误选错误的文件。
而为了防止用户忘记选择,直接进行料号的生成等 *** 作,我设置了在选取到excel地址前,阻止用户输入:
当用户已经选取了正确地址后,如果不确定是否进行了这次 *** 作,我对按钮设置了提示:
会告诉用户,已经进行过选取 *** 作了。
当然,这样就存在一种可能,用户选取了非对应的excel地址,如外购件料号地址选择了G2文件等,因此用户可以点击清空重选按钮,重新选择。
3.2 外购件料号excel文件填写这里为了简化用户填写,将需要填写的分类通过下拉框进行选择,用户无需查阅手册即可填写。
填写完成,点击生成料号按钮获取填写的料号:
3.2.1 料号填写错误当用户填写规则不符合手册时,将无法生成料号:
3.2.2 料号重复因为只在第一次采购某种物料时才需要进行登记,那么就需要在用户登记时进行查重 *** 作。如果物料号重复,告知用户,防止重复登记。
3.2.3 料号正确且不重复
料号正确且不重复时,在料号预览窗中预览最终结果:
3. 3 G2及BOM的填写G2文件的填写,只需要选择应用领域、单位、原产国及发货国即可,填写过程和料号无异。唯一需要注意的就是,需要在料号成功生成后,即预览窗口正确显示料号后才可完成最终保存。
写到这也想到了一个优化方向,只有在用户正确生成料后,才允许在G2信息生成部分进行 *** 作。
而BOM填写,只需将生成的料号保存在excel中即可,无需任何填写。
3.4 软件功能、帮助按钮及最终保存 3.4.1 软件功能及帮助按钮这两个按钮,我直接设置了一个QMessageBox,用户点击跳出信息,很简单。
3.4.1 保存料号当用户成功生成料号后,点击保存料号按钮,软件后台会打开对应的三个excel文件进行读写保存的 *** 作,完成后d出保存结果成功的提示窗口。
4. 软件源码 4.1 方案框架: 4.2 登录窗口登录窗口,验证用户密码正确后,登录成功,跳转主软件窗口。密码我用明码保存在了源码中,很粗糙并且错误的方式。技术有限,只能用这种方式实现。或许后续学习一些用户名密码的数据库保存方式,进行优化。
loginWindow.h
-------------------------------
#ifndef LOGINWINDOW_H
#define LOGINWINDOW_H
#include
#include
#include "mainwindow.h"
QT_BEGIN_NAMESPACE
namespace Ui { class loginWindow; }
QT_END_NAMESPACE
class loginWindow : public QWidget
{
Q_OBJECT
public:
loginWindow(QWidget *parent = nullptr);
~loginWindow();
void switchToMainWindow();
void login_btn_clicked();
void keyPressEvent(QKeyEvent* keyEvent);
private slots:
void keyBoard_login_btn_clicked();
private:
Ui::loginWindow* ui;
MainWindow* mainWindow;
};
#endif // LOGINWINDOW_H
4.3 ※主窗口
主窗口中,定义了许多方法,在贴上所有代码前,分别介绍一下方法的功能。
4.3.1 主窗口构造通过QT代码,构造出我的主窗口界面布局,并将不同按钮与需要的功能进行绑定。
我的理解:QT的逻辑是每个部件,会有自己的信号发出方法,这些方法会激活对应的方法功能。例如按钮,当按钮按下后,会发出一个被按下的信号,将这个信号与预执行的动作进行绑定(connect),就可以在按钮按下后实现对应功能。
4.3.2 打开某个excel文件以打开外购件料号登记excel文件为例,方法如下。
我定义方法需要返回一个bool类型FLAG_IS_PMN_OPEN,用了标记是否已经选取到了地址。
bool MainWindow::openPMN()
{
if (FLAG_IS_PMN_OPEN == false)
{
pmn_excel_path = QFileDialog::getOpenFileName(this, "查找外购件料号文件", "./", "*.xlsx");
//excelPath = pmn_excel_path;
if (pmn_excel_path.isEmpty())
{
FLAG_IS_PMN_OPEN = false;
}
else
{
// qDebug() << excelPath;
FLAG_IS_PMN_OPEN = true;
}
}
else
{
QMessageBox::information(this, "提示", "已选取外购件料号文件地址,无需再次选取哦~");
}
return false;
}
而G2, BOM方法一模一样,不再赘述。
4.3.3 清空重选按钮方法很好理解,定义如下:
void MainWindow::clearChose()
{
if (pmn_excel_path.isEmpty() && g2_excel_path.isEmpty() && bom_excel_path.isEmpty())
{
QMessageBox::information(this, "提示", "地址均为空,无需清空重选");
}
else
{
FLAG_IS_PMN_OPEN = false;
FLAG_IS_G2_OPEN = false;
FLAG_IS_BOM_OPEN = false;
if (!pmn_excel_path.isEmpty())
{
pmn_excel_path.clear();
}
if (!g2_excel_path.isEmpty())
{
g2_excel_path.clear();
}
if (!bom_excel_path.isEmpty())
{
bom_excel_path.clear();
}
QMessageBox::information(this, "提示", "清空成功,请重选选取地址");
}
}
4.3.4 生成料号按钮
QString MainWindow::getFinalPMN()
{
//Py_SetPythonHome((wchar_t*)L"C:\\Users\\DSHAHKang\\Anaconda3\\envs\\py36");
Py_SetPythonHome((wchar_t*)L".\\Python36");
Py_Initialize();
if (FLAG_IS_PMN_OPEN == false)
{
QMessageBox::warning(this, "警告", "未选择正确的外购件料号文件地址!");
return NULL;
}
else
{
//MainWindow::openExcel(pmn_excel_path, 1);
MainWindow::openExcel(pmn_excel_path);
ui->PreviewLineEdit->clear();
if (ui->isUserSpecifycheckBox->isChecked())
{
if (ui->SMlineEdit->text().length() != 0 &&
ui->SNlineEdit->text().length() != 0)
{
finalPMN = (ui->PMcomboBox->currentText().section("-", 0, 0))
+ ui->FirstlLabel->text()
+ (ui->SMlineEdit->text())
+ ui->SecondLabel->text()
+ (ui->SNlineEdit->text()
+ ui->ThirdLabel->text()
+ ui->isUserSpecifylineEdit->text());
if (MainWindow::checkRepeat(finalPMN) == true)
{
QMessageBox::critical(this, "错误", "料号重复,无法生成,请修改。");
return NULL;
}
else
{
ui->PreviewLineEdit->setText(finalPMN);
FLAG_IS_GEN_PMN_SUCCEED = true;
return finalPMN;
}
}
else
{
ui->PreviewLineEdit->setText("无法生成料号,输入错误,请参阅手册");
finalPMN = "无法生成料号,输入错误,请参阅手册";
FLAG_IS_GEN_PMN_SUCCEED = false;
// return NULL;
return finalPMN;
}
}
else
{
if (ui->SMlineEdit->text().length() != 0 &&
ui->SNlineEdit->text().length() != 0)
{
finalPMN = (ui->PMcomboBox->currentText().section("-", 0, 0))
+ ui->FirstlLabel->text()
+ (ui->SMlineEdit->text())
+ ui->SecondLabel->text()
+ (ui->SNlineEdit->text());
if (MainWindow::checkRepeat(finalPMN) == true)
{
QMessageBox::critical(this, "错误", "料号重复,无法生成,请修改。");
return NULL;
}
else
{
ui->PreviewLineEdit->setText(finalPMN);
FLAG_IS_GEN_PMN_SUCCEED = true;
return finalPMN;
}
}
else
{
ui->PreviewLineEdit->setText("无法生成料号,输入错误,请参阅手册");
finalPMN = "无法生成料号,输入错误,请参阅手册";
FLAG_IS_GEN_PMN_SUCCEED = false;
// return NULL;
return finalPMN;
}
}
}
Py_Finalize();
}
这一步中,有几句与Python有关的代码:
Py_SetPythonHome((wchar_t*)L".\Python36");
Py_Initialize();
// TODO SOMETHING...
Py_Finalize();
原因放在踩坑部分讲述。
生成料号的逻辑,首先判断用户有没有勾选“用户指定”,是否勾选最终料号的组成方式不同。
随后,判断用户是否选择了excel文件地址,用户选择后,对用户输入进行判断,
A、B框有任意为空,告知用户生成错误。
A、B框内容均非空,我定义了一个openExcel方法,获取目前外购件料号中已存在的料号。
// -----------------------------------------------------------------------
// function: 通过Python打开excel文件,将文件的内容推入list,
// 通过Python传参返回给C++, 进而为判断重复做准备
// -----------------------------------------------------------------------
void MainWindow::openExcel(QString excelPath)
{
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./pythonScripts/')");
PyObject* pModule = PyImport_ImportModule("getAllPmn");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pFunc = PyDict_GetItemString(pDict, "getAllPmnFunc");
QByteArray _array_excelPath = excelPath.toLocal8Bit();
std::string _str_excelPath = (std::string)_array_excelPath;
QByteArray _array_PMcomboBox = ui->PMcomboBox->currentText().toLocal8Bit();
std::string _str_PMcomboBox = (std::string)_array_PMcomboBox;
PyObject* pArgs = PyTuple_New(2);
PyObject* pArgs1 = Py_BuildValue("O", StringToPy(_str_excelPath));
PyObject* pArgs2 = Py_BuildValue("O", StringToPy(_str_PMcomboBox));
PyTuple_SetItem(pArgs, 0, pArgs1);
PyTuple_SetItem(pArgs, 1, pArgs2);
PyObject* pReturn = PyObject_CallObject(pFunc, pArgs);
int size = PyList_Size(pReturn); // size = 11
for (int i = 0; i < size; i++)
{
PyObject* item = PyList_GetItem(pReturn, i);
const char* str_item = PyUnicode_AsUTF8(item);
//printf("%s\n", str_item);
existPMN->append(str_item);
}
}
在该方法中,额外调用了Python脚本来访问excel,Python脚本为getAllPmn.py.
我的方法是,通过Python的win32com访问excel,遍历外购件excel登记表的第二列内容,保存在一个list中。并将list返回给C++,而C++中,将返回list的内容,保存在一个QStringList类型中,最终用于判断是否存在重复料号。
getAllPmn.py
# -*- coding:utf-8 -*-
import win32com.client as win32
def getAllPmnFunc(excelPath, sheetName):
excel = win32.Dispatch('Excel.Application')
excel.Visible = False;
excel.DisplayAlerts = False;
password = "123456"
wb = excel.Workbooks.Open(excelPath, UpdateLinks=False, ReadOnly=False, Format=None, Password=password, WriteResPassword=password)
ws = wb.Worksheets(sheetName)
totalRow = ws.UsedRange.Rows.Count # max Rows
existPmn = []
i = 0
while i <= totalRow:
if i >= 4:
existPmn.append(str(ws.Cells(i,2)))
i += 1
wb.Close()
excel.Quit()
return existPmn;
查重方法:
// -----------------------------------------------------------------------
// function: 检查填写的外购件是否重复
// -----------------------------------------------------------------------
bool MainWindow::checkRepeat(QString finalPMN)
{
if (existPMN->isEmpty() == false && existPMN->contains(finalPMN))
{
return true;
}
return false;
}
4.3.5 保存最终结果:
如前所述,保存的前提是已经生成了正确料号,否则无法保存,方法如下:
// -----------------------------------------------------------------------
// function: 1.将用户生成的料号与已有料号进行比对,如果重复告知用户
// 2.料号未重复,将料号写入对应的excel文件中;
// 3.将用户填写的G2信息写入对应的excel文件中;
// 4.将该料号对应的产品信息写出对应的bom清单中;
// 5.完成上述步骤后告知用户写入结果。
// -----------------------------------------------------------------------
void MainWindow::saveResult()
{
//Py_SetPythonHome((wchar_t*)L"C:\Users\DSHAHKang\Anaconda3\envs\py36");
Py_SetPythonHome((wchar_t*)L".\Python36");
Py_Initialize();
if (FLAG_IS_GEN_PMN_SUCCEED == false)
{
QMessageBox::critical(this, "错误", "未能生成正确的料号,无法保存!");
}
else
{
savePMN(pmn_excel_path);
saveG2(g2_excel_path);
saveBOM(bom_excel_path);
QMessageBox::information(this, "保存成功", "已保存所有结果");
}
Py_Finalize();
}
而对应调用的每个保存方法,内容逻辑相似,以保存BOM举例:
// -----------------------------------------------------------------------
// function: 保存BOM
// -----------------------------------------------------------------------
void MainWindow::saveBOM(QString excelPath)
{
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./pythonScripts/')");
PyObject* pModule = PyImport_ImportModule("saveBom");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pFunc = PyDict_GetItemString(pDict, "saveBomFunc");
QByteArray _array_bomexcelPath = bom_excel_path.toLocal8Bit();
std::string _str_bomexcelPath = (std::string)_array_bomexcelPath;
QByteArray _array_finalPMN = finalPMN.toLocal8Bit();
std::string _str_finalPMN = (std::string)_array_finalPMN;
PyObject* pArgs = PyTuple_New(2);
PyObject* pArgs1 = Py_BuildValue("O", StringToPy(_str_bomexcelPath));
PyObject* pArgs2 = Py_BuildValue("O", StringToPy(_str_finalPMN));
PyTuple_SetItem(pArgs, 0, pArgs1);
PyTuple_SetItem(pArgs, 1, pArgs2);
PyObject_CallObject(pFunc, pArgs);
}
还是调用了Python的脚本,脚本内容如下,设置好格式,直接保存即可:
# -*- coding:utf-8 -*-
import win32com.client as win32
def saveBomFunc(bomPath, finalPmn):
excel = win32.Dispatch('Excel.Application')
excel.Visible = False;
excel.DisplayAlerts = False;
wb = excel.Workbooks.Open(bomPath, UpdateLinks=False, ReadOnly=False, Format=None, Password=None, WriteResPassword=False)
ws = wb.Worksheets('BOM清单')
totalRow = ws.UsedRange.Rows.Count # max Rows
ws.Cells(totalRow+1, 15).Value = finalPmn
ws.Cells(totalRow+1, 15).Borders.LineStyle = 1 # 边框1,表示实线
ws.Cells(totalRow+1, 15).Borders.Weight = 2 # 最细边框1 细2
ws.Cells(totalRow+1, 15).Borders.Color = 0 # 颜色 0 黑色
ws.Cells(totalRow+1, 15).HorizontalAlignment = -4131 # -4108居中, -4131居左, -4152居右
ws.Cells(totalRow+1, 15).VerticalAlignment = -4108 # -4160顶部, -4107底部 -4108居中
ws.Cells(totalRow+1, 15).Font.Size = 11
ws.Cells(totalRow+1, 15).Font.Name = 'DengXian'
wb.Close(True)
excel.Quit()
5. ※遇到的坑与解决
5.1 PyImport_ImportModule or PyObject_GetAttrString 返回NULL
这个问题是困扰我最久的,也是百度后发现大家普遍遇到的问题。
问题产生的第一个原因,
PyRun_SimpleString("sys.path.append('./pythonScripts/')");
这一句的地址要写对,在VS进行调试启动时,“./”表示当前工程所在目录。而通过Debug生成的exe文件启动软件时,“./”表示.exe文件所在的目录。
问题产生的第二个原因,Python代码有误。如果Python代码本身无法运行,PyImport_ImportModule也返回为空,要确保Python脚本可以正确运行。因此,如果import了第三方库,一定要确保库已经安装。
问题产生的第三个原因,这个坑我踩的很迷。我用了Python3.10,在单独进行功能测试时无误,但是引入我的软件中就一直无法获取到Python脚本内容。最终在朋友的指导下换用了Python3.6,用Anaconda管理环境,解决了上述问题。
5.2 PyType_Slot *slots冲突问题QT引入Python后,会与Python定义的Slot冲突。解决办法就是修改QT的Object.h文件,添加如下语句即可。
#undef slots //添加
PyType_Slot *slots; /* terminated by slot==0. */
#define slots Q_SLOTS // 添加
5.3 QString转为std::String中文显示为“???”
解决方法,使用toLocal8Bit()方法:
5.4 PyBytes_AsString无法将结果转为string,返回NullopenExcel方法中,我尝试通过
char* result = PyBytes_AsString(str);
将Python返回list中每一个元素转为std::string时,返回结果为Null。
这个问题也一度困扰我很久,最终我调试时发现,result的值为<
隐约感觉是win32com的坑,在Python打印了每个Cell的类型,果然如此:
最终在Python中通过类型强转,将str类型的结果推入Python List,再通过C++获取,问题解决。
5.5 PyImport_ImportModule无法重复调用,写入异常问题我的点击生成料号按钮需要调用openExcel()方法,第一次调用没有问题,不退出软件第二次点击会出现PyImport_ImportModule返回为Null的问题。Google后,应该是Python的引用计数问题。而百度到的解决方案就是把
Py_SetPythonHome((wchar_t*)L".\Python36");
Py_Initialize();
// TODO SOMETHING...
Py_Finalize();
语句,写在大循环外面。请看本文4.3.4及4.3.5的对应语句。
5.6 弃用C++ xlnt及Libxl为何通过C++编写QT,却要引入Python来访问excel。
这里我踩了两次坑,第一次使用了LibXL库,这个库是收费库,但是有PJ版可以用。但是这个库对于带密码的excel文件无法进行访问,遂弃用,浪费了3天时间;
第二次使用了xlnt库,开源免费,也可以访问带密码的excel文件,但是极不稳定,保存很可能导致excel文件崩溃,且不能访问后再次带密码存储。
随引入了Python,当作“胶水”,也是朋友给的建议。终于Python完美的解决了我的问题,访问带密码的excel,并且完成后保存,不破坏该文件。
至此,软件开发完成。通过QT的windepolyqt进行部署。添加了图标进行美化。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)