【学习笔记】QT/C++/Python软件编程全记录

之前的两篇,只是记录了软件开发的一些思路,软件的功能也并不完善,没有干货。这篇文章计划将软件编写的全流程做个记录,也将把源码贴上,最后会罗列一下自己遇到的坑,希望可以帮到大家。 


目录

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登记清单”。这三个表格,用户手动填写比较费时间,于是开发一个软件,可以一次性完成三个表格的填写。

其中,外购件料号登记表为了防止其他同事的误编辑,设置了访问密码,打开后显示如下:

【学习笔记】QT/C++/Python软件编程全记录_第1张图片

 表格内容如图:

【学习笔记】QT/C++/Python软件编程全记录_第2张图片

而G2表格没有密码,每个项目都有独立的G2表格,内容如下:

【学习笔记】QT/C++/Python软件编程全记录_第3张图片

BOM表格内容如图:

 需要研发部填写的只有“HSI料号”一栏,也就是料号表格中的料号。

2.开发思路

明确了填写需求,开始构思开发思路。

我的思路就是,通过QT编写用户操作界面,后台完成对excel文件的读写。

我构思的软件界面如下(关于V1.0及V2.0请参考前面两篇文章):

【学习笔记】QT/C++/Python软件编程全记录_第4张图片

用户需要对应选择自己公盘下的excel文件地址,因为公盘使用one-drive上传,同一个excel文件,不同用户有不同的地址:某用户用户名\OneDrive...\公盘地址\xx.xlsx。

而需要填写在不同excel表格中的内容,用户在QT界面填写即可。最后填写完毕进行保存,下面对软件逻辑进行详细说明。

3.软件功能详细说明

3.0 软件启动及登录

因为外购件料号软件需要密码进行编辑,为了防止无权用户通过软件进行编辑,软件设置了登录密码,用户需要登录成功后才能进入软件主界面:

【学习笔记】QT/C++/Python软件编程全记录_第5张图片【学习笔记】QT/C++/Python软件编程全记录_第6张图片

【学习笔记】QT/C++/Python软件编程全记录_第7张图片

3.1 选取excel文件地址

【学习笔记】QT/C++/Python软件编程全记录_第8张图片

 当用户点选excel文件地址时,设置一个文件格式筛选,方便用户的同时也可以防止用户误选错误的文件。

【学习笔记】QT/C++/Python软件编程全记录_第9张图片

 而为了防止用户忘记选择,直接进行料号的生成等操作,我设置了在选取到excel地址前,阻止用户输入:

【学习笔记】QT/C++/Python软件编程全记录_第10张图片

 当用户已经选取了正确地址后,如果不确定是否进行了这次操作,我对按钮设置了提示:

【学习笔记】QT/C++/Python软件编程全记录_第11张图片

 会告诉用户,已经进行过选取操作了。

当然,这样就存在一种可能,用户选取了非对应的excel地址,如外购件料号地址选择了G2文件等,因此用户可以点击清空重选按钮,重新选择。

【学习笔记】QT/C++/Python软件编程全记录_第12张图片

3.2 外购件料号excel文件填写

 【学习笔记】QT/C++/Python软件编程全记录_第13张图片

这里为了简化用户填写,将需要填写的分类通过下拉框进行选择,用户无需查阅手册即可填写。

填写完成,点击生成料号按钮获取填写的料号:

3.2.1 料号填写错误

 当用户填写规则不符合手册时,将无法生成料号:

【学习笔记】QT/C++/Python软件编程全记录_第14张图片

3.2.2 料号重复

 因为只在第一次采购某种物料时才需要进行登记,那么就需要在用户登记时进行查重操作。如果物料号重复,告知用户,防止重复登记。

 【学习笔记】QT/C++/Python软件编程全记录_第15张图片

3.2.3 料号正确且不重复

料号正确且不重复时,在料号预览窗中预览最终结果: 

【学习笔记】QT/C++/Python软件编程全记录_第16张图片

3. 3 G2及BOM的填写

G2文件的填写,只需要选择应用领域、单位、原产国及发货国即可,填写过程和料号无异。唯一需要注意的就是,需要在料号成功生成后,即预览窗口正确显示料号后才可完成最终保存。

写到这也想到了一个优化方向,只有在用户正确生成料后,才允许在G2信息生成部分进行操作

而BOM填写,只需将生成的料号保存在excel中即可,无需任何填写。

3.4 软件功能、帮助按钮及最终保存

3.4.1 软件功能及帮助按钮

这两个按钮,我直接设置了一个QMessageBox,用户点击跳出信息,很简单。

【学习笔记】QT/C++/Python软件编程全记录_第17张图片

【学习笔记】QT/C++/Python软件编程全记录_第18张图片

 3.4.1 保存料号

当用户成功生成料号后,点击保存料号按钮,软件后台会打开对应的三个excel文件进行读写保存的操作,完成后弹出保存结果成功的提示窗口。

【学习笔记】QT/C++/Python软件编程全记录_第19张图片

4. 软件源码

4.1 方案框架:

【学习笔记】QT/C++/Python软件编程全记录_第20张图片

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 

【学习笔记】QT/C++/Python软件编程全记录_第21张图片

【学习笔记】QT/C++/Python软件编程全记录_第22张图片

这个问题是困扰我最久的,也是百度后发现大家普遍遇到的问题。 

问题产生的第一个原因,

 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中文显示为“???”

【学习笔记】QT/C++/Python软件编程全记录_第23张图片

解决方法,使用toLocal8Bit()方法:

 5.4 PyBytes_AsString无法将结果转为string,返回Null

openExcel方法中,我尝试通过

char* result = PyBytes_AsString(str);

将Python返回list中每一个元素转为std::string时,返回结果为Null。

这个问题也一度困扰我很久,最终我调试时发现,result的值为<>.

【学习笔记】QT/C++/Python软件编程全记录_第24张图片

 隐约感觉是win32com的坑,在Python打印了每个Cell的类型,果然如此:

【学习笔记】QT/C++/Python软件编程全记录_第25张图片

最终在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进行部署。添加了图标进行美化。

你可能感兴趣的:(学习笔记,学习,qt,c++,python)