这个项目是我的一个课设,课设要求做出一款能实现Excel增删改查及对应图像增删改查功能的软件,最后要求打包成exe,因为数据保密的原因,代码里的相关图片及表格都被我删掉了,只留了两张测试图片。正所谓,做都做了,不能白做。之前从未正经接触过QtDesigner和python,对于python的认知也只是停留在函数,所以在程序的设计模式上有很多不足,包括个人认为整个项目会有点乱,详细都写在README.md里了,有很多人进不去GitHub,因此代码放在了码云上。发现网上没有一篇比较统一的入门贴,这次踩了不少坑,写得辛苦,感觉还是蛮有借鉴意义的,包括分页、加强鲁棒性等等,会以功能点的实现为线写出来给各位入个门。一文可能讲不完,所有开源给大家,具体功能实现去看代码更清晰。若有任何关于优化或错误的建议,请联系我!
|-data 数据 |-dist Excel文件输出位置 |-new.xls 用户操作后的新Excel文件 |-dist_images 存放原始数据图像,用户操作影像读取及输出位置 |-M0 正常样本图像 |-M1 异常样本图像 |-new 新增未分类图像 |-info.xlsx 示例Excel文件 |-detect 神经网络(*可替换,不可更改,替换时也应更改detail.py文件中的接口) |-logs |-best.pth 网络权重 |-model_data |-cls_classes.txt 类别 |-nets 网络主体 |-utils 工具函数文件夹 |-classification.py 分类网络类 |-icon 图标文件夹 |-cat.ico 程序icon图标 |-img 示例图像 |-Clinical_information 临床信息界面文件 |-detail 影像图片文件 |-Image_information Excel 表格影像信息文件 |-information 主界面布局文件 |-new_info 新增患者信息文件 |-main.py 程序入口文件 |-main.spec 打包的中间文件 |-requirements.txt 库版本要求 |-README.md 项目说明 |-.gitignore git忽略的文件 # 训练网络是resnet50 # 参考博主:https://blog.csdn.net/weixin_44791964/article/details/109160814
安装好合适的环境后,点击运行 main.py 即可。
在使用所有功能前,需先通过“Open Excel”打开Excel文件作为表格数据库,否则会有提示。
视图渲染:当用户选择完Excel文件时,表格视图会自动渲染,并自动计算当前页和总页数,下方状态栏会显示当前用户操作。
当关闭主界面窗口时,其他界面也会跟着关闭。
搜索患者:当用户输入ID号的前几位并点击“搜索”按钮时,会自动查找到第一个匹配的患者信息,并自动跳转定位到当前页和当前行。
页面切换功能:点击“上一页”跳转至上一页,点击“下一页”跳转至下一页,当在第一页和最后一页分别点击“上一页”和“下一页”的按钮时,会弹出提示框。
点击“查看”按钮,查看当前行患者的临床信息和影像信息。
修改功能:双击单元格,修改内容之后,按Enter键,修改的数据会重新写到dist文件夹下的new.xls文件中,这样不会污染原始数据。对于临床信息和影像信息,在修改完之后点击“确认修改”按钮。
删除功能:点击“删除”按钮,修改的数据会重新写到dist文件夹下的new.xls文件中,这样不会污染原始数据。
新增患者信息:点击“新增患者信息”按钮,填写完信息后,点击“提交”按钮,同样,添加的信息会写入到new.xls文件中,新增的影像图片会放在images/new文件夹下。当点击“+添加影像”选择完图片后,“影像信息”前的“”会消失。填写信息需要注意格式,比如日期格式为“YYYY-MM-DD”,ID号不可以超过九位。
查看患者对应的影像信息:点击“影像信息”列下的“查看”按钮,在弹出的界面中点击“查看影像”按钮。
点击7中界面的“添加”、“修改”、“删除”、“AI检测”按钮,实现对图片的增删改及人工智能诊断,诊断结果将显示在下方的输出栏,并将文件夹放在images对应的M0和M1文件夹下。
安装virtualenv后,在cmd中用命令virtualenv env_name 创建出一个虚拟环境,和anaconda的环境做隔离,减小包的体积。该项目打包之后有2GB多是因为torch这个包就占了2GB。
pycharm打开Terminal,切换到env_name/Scripts路径下,输入activate.bat激活环境。
查看requirements.txt,利用pip install安装完所有的包,直至不再报No module的错误。
在激活环境下运行下方命令:
pyinstaller main.spec
在生成的dist文件夹中新建以下文件夹:
|-data 数据 |-dist |-dist_images |-M0 以类别名命名的文件夹 |-M1 以类别名命名的文件夹 |-new 新增未分类图像(不可随意更改,若更改则需同步更改代码)
QtDesigner官方文档
我跳过了环境安装,请配好了QtDesigner和.ui转.py的工具之后再来(不是。
版本: pycharm 2017.3
import sys from information import Ui_MainWindow # 由information.ui转来的information.py文件,Ui_MainWindow是information.py里面的类名,这一步根据自己的类名去写。 class MyMainForm(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super(MyMainForm, self).__init__(parent) self.setupUi(self) # 槽函数 def MethodName(self): print("test") if __name__ == "__main__": #固定的,PyQt5程序都需要QApplication对象。sys.argv是命令行参数列表,确保程序可以双击运行 app = QApplication(sys.argv) #初始化 myWin = MyMainForm() #将窗口控件显示在屏幕上 myWin.show() #程序运行,sys.exit方法确保程序完整退出。 sys.exit(app.exec()) ### 如果这一步运行不出来,请检查一下main.py文件里有没有刚刚定义的与控件相关联的槽函数。
小结
到这一步,基本流程已经打通了,可以开始自己的页面布局啦!
3.2开始介绍特殊功能点,比较这是处理Excel文件的程序
当用户打开Excel文件时,将Excel文件里的信息渲染到QTableWidget上。分为三步:用xlrd库去读Excel文件,拿到每一行的信息,按行渲染到QTableWidget上。具体看main.py文件里的readExcel和getOnePage、information.py文件里的generateRow这三个方法。(注意导入相应的包哦!)
# 读Excel文件 def readExcel(self, fileName): workbook = xlrd.open_workbook(fileName) # 获得sheet_name self.sheet_name = workbook.sheet_names()[0] # 根据sheet索引或者名称获取sheet内容 self.sheet = workbook.sheet_by_index(0) # 从索引0开始 # 获得总行数 self.nrows = self.sheet.nrows # 获取总页数 self.pageCount = math.ceil((self.nrows - 2) / self.pageSize) self.setTotalPage(self.pageCount) # 获取最后一页的行数 self.lastPageCount = self.nrows - 2 - self.pageSize * (self.pageCount - 1) # 获取第一列的内容 self.ids = self.sheet.col_values(0) for i, v in enumerate(self.ids): # 跳过头两个item if i > 1: self.ids[i] = str(int(v)) # 逐行生成一页 def getOnePage(self): self.isEidt = False # isEdit是用来作阀的,因为接上了QTableWidget的cellChanged信号,前期我并不想触发,想等用户自己修改时再触发 self.changePageStatus(self.currentPage + 1) for i in range(self.pageSize): for j in range(self.infoCols): if self.lastPageFlag and i >= self.lastPageCount: val = '' else: index = self.currentPage * self.pageSize + i + 2 # 拿到value val = self.sheet.cell_value(index, j) if isinstance(val, float): if j == 0: # 去除小数点后面的数字 val = int(val) # TableWidget需要字符串格式才能正常显示 val = str(val) elif j == 1: # 做日期格式的转换,显示正确的日期格式 data_time = datetime(*xldate_as_tuple(val, 0)) val = data_time.strftime('%Y-%m-%d') else: val = str(val) # 除去表头 self.generateRow(i + 1, j, val, self.lastPageCount, self.lastPageFlag, index) self.isEidt = True # 生成表格的一行 def generateRow(self, row, col, val, lastPageNum, lastPageFlag, trueRow): # print('p', row, col, val) item = QtWidgets.QTableWidgetItem(val) self.tableWidget.setItem(row, col, item) if row <= lastPageNum or lastPageFlag == False: # 插入查看临床信息按钮 self.bedButton = QtWidgets.QPushButton('查看') self.bedButton.setStyleSheet('QPushButton{margin:3px};') self.bedButton.setObjectName("bedButton" + str(trueRow)) self.tableWidget.setCellWidget(row, 6, self.bedButton) # 插入查看影像信息按钮 self.imageButton = QtWidgets.QPushButton('查看') self.imageButton.setStyleSheet('QPushButton{margin:3px};') self.imageButton.setObjectName("imageButton" + str(trueRow)) self.tableWidget.setCellWidget(row, 7, self.imageButton) # 插入删除信息按钮 self.deleteButton = QtWidgets.QPushButton('删除') self.deleteButton.setStyleSheet('QPushButton{margin:3px};') self.deleteButton.setObjectName("deleteButton" + str(trueRow)) self.tableWidget.setCellWidget(row, 8, self.deleteButton) # lambda匿名函数用于传参 self.bedButton.clicked.connect(lambda: self.MainWindow.getBedInfo(trueRow)) self.imageButton.clicked.connect(lambda: self.MainWindow.getImageInfo(trueRow)) self.deleteButton.clicked.connect(lambda: self.MainWindow.deleInfo(trueRow)) else: self.tableWidget.removeCellWidget(row, 6) self.tableWidget.removeCellWidget(row, 7) self.tableWidget.removeCellWidget(row, 8)
传参:利用匿名函数lambda,场景为:当我点击按钮时,打开另一个Widget,此时我需要传入ID号。
# lambda匿名函数用于传参 self.bedButton.clicked.connect(lambda: self.goToNewWidget(user_id)) def goToNewWidget(self, user_id): print(user_id)
self.bedButton.clicked.connect(self.goToNewWidget()) def goToNewWidget(self): self.new_widget = QWidget() self.new_ui = Ui_New_Info() self.new_ui.setupUi(self.new_widget, self) self.new_widget.setWindowTitle('New Information') self.new_widget.show()
self.statusbar.showMessage(message)
重写closeEvent方法,具体看information.py文件里的closeEvent方法。
# --------------information.py------------------ # 重写关闭方法 def closeEvent(self, event): if self.MainWindow.clinical_widget: # print('close') self.MainWindow.clinical_widget.close() if self.MainWindow.image_widget: # print('close') self.MainWindow.image_widget.close() if self.MainWindow.new_widget: # print('close') self.MainWindow.new_widget.close() if self.MainWindow.detail_widget: # print('close') self.MainWindow.detail_widget.close() event.accept() ### 这里的所有widget都挂载在main.py文件里的QMainWindow对象中,为了在其他函数里拿到QMainWindow里的widget,我把QMainWindow挂载为当前类的属性,即self.MainWindow = MainWindow, 加多一层if判断是为了防止有些窗口没打开,如果这是执行关闭的话,程序会卡顿出错。 # ---------------main.py------------- def __init__(self, parent=None): super(MyMainForm, self).__init__(parent) self.setupUi(self) self.setWindowIcon(QIcon('icon\cat.ico')) # 四个窗口 self.clinical_widget = '' self.image_widget = '' self.new_widget = '' self.detail_widget = ''
self.setWindowIcon(QIcon('icon\cat.ico')
你会发现从Excel表格里读取到的日期是一串数字,需要利用datetime这个库进行转化才能正确显示。
from datetime import datetime from xlrd import xldate_as_tuple data_time = datetime(*xldate_as_tuple(val, 0)) val = data_time.strftime('%Y-%m-%d') # 在Excel中写入正确的日期格式 datetime.strptime(self.getTableWidgetItemContent(row, col), '%Y-%m-%d')
第一个参数为打开文件选择框的窗口父类,第二个参数为弹出的选择框的名称,第三个参数为打开的指定路径(可选),第四个为限制类型(注意:这里的类型限制的“无效”的)。
image_Name, imgType = QFileDialog.getOpenFileName(self, "选择图片", "", "*.jpg;;*.png;;All Files(*)")
from PyQt5.QtWidgets import * QMessageBox.information(self, "提示", "当前页是最后一页!") QMessageBox.warning(self, "警告", "请先打开Excel文件!") QMessageBox.error(self, "警告", "请先打开Excel文件!") ### 具体的弹出框类型看文档
具体看detail.py文件里的setPixMap方法。
def setPixMap(self, path): # 利用qlabel显示图片,show_image是QLabel的对象名,在3.1开始的添加控件有说明 png = QtGui.QPixmap(path).scaled(self.show_image.width(),self.show_image.height()) self.show_image.setPixmap(png) # 这里的setPixmap是QLabel的原生方法 self.show_image.setScaledContents(True)
思路:获取到Excel表格中的所有ID号放在list里,因为ID号是按行存储的,所以list的索引就是患者信息在Excel文件里的行数-1(list索引从零开始)。当用户在输入框输入完ID号,点击搜索按钮时,截取掉输入框值前后的空格,利用list的原生方法count和index查看当前患者是否存在,存在则定位至那一页和那一行,不存在则给予相应的提示。用户可以不必输入完整的ID号,会自动返回第一个匹配项。具体看main.py里的searchInfo方法。
# 搜索患者信息 def searchInfo(self): if self.fileName != '': search_id = self.getContent().strip() # 字符串格式 self.showStatusMessage("搜索ID号:" + search_id) # 清除搜索框内容 self.clearContect() # 使用 startswith,返回一个列表 res = [idx for idx in self.ids if idx.startswith(search_id)] if len(res) == 0: QMessageBox.information(self, "提示", "没有找到匹配项") else: # 第一个匹配项,index是Excel表格中的真实行数 index = self.ids.index(res[0]) - 2 # 获取匹配项所在页数 page = math.floor(index / self.pageSize) row = (index % self.pageSize) + 1 self.currentPage = page if self.currentPage == (self.pageCount - 1): self.lastPageFlag = True self.getOnePage() self.isEidt = False self.setLineColor(row) self.isEidt = True else: QMessageBox.warning(self, "警告", "请先打开Excel文件!")
思路:很好理解,增删改都可以理解为两步,读取和重新写入,利用xlrd库读取Excel文件,利用xlwt库写入Excel文件。对读取到的信息进行增删改的操作之后,再将操作之后的信息写回去。具体看main.py里的getNewXl方法
# 新建工作本并保存,有row则删除,有row和col则为修改,都没有则为新增, change_val为指定值,未传时为双击表格修改的内容,count为计数器,防止多次弹出提示框 def getNewXl(self, row = -1, col = -1, change_val = '', count_n = 0): try: # 新建工作簿 workbook = xlwt.Workbook(encoding='utf-8') # 新建sheet sheet_w = workbook.add_sheet(self.sheet_name) # 循环 count = 0 for x in range(self.sheet.nrows): if col == -1: # 删除 if x != row: for y in range(self.sheet.ncols): val = self.sheet.cell_value(x, y) if y == 1: dateFormat = xlwt.XFStyle() dateFormat.num_format_str = 'yyyy/mm/dd' sheet_w.write(count, y, val, dateFormat) else: sheet_w.write(count, y, val) count = count + 1 else: # 修改 for y in range(self.sheet.ncols): if x == row and y == col: if change_val == '': val = self.text else: val = change_val else: val = self.sheet.cell_value(x, y) if y == 1: dateFormat = xlwt.XFStyle() dateFormat.num_format_str = 'yyyy/mm/dd' sheet_w.write(x, y, val, dateFormat) else: sheet_w.write(x, y, val) if row == -1: new_row = self.sheet.nrows # 新增 for i, v in enumerate(self.forms): if i == 1: dateFormat = xlwt.XFStyle() dateFormat.num_format_str = 'yyyy/mm/dd' sheet_w.write(new_row, i, v, dateFormat) else: sheet_w.write(new_row, i, v) # 保存工作簿 path = self.dist_root + self.dist_name workbook.save(path) # 重新渲染视图 self.readExcel(path) self.getOnePage() except: if row == -1: QMessageBox.warning(self, "错误", "新增失败") elif col == -1: QMessageBox.warning(self, "错误", "删除失败") else: if count_n == 0: QMessageBox.warning(self, "错误", "修改失败") else: self._count = self._count + 1 if self._count == count_n: QMessageBox.warning(self, "错误", "修改失败") self._count = 0 else: if row == -1: self.showStatusMessage("新增成功") elif col == -1: self.showStatusMessage("删除成功") else: self.showStatusMessage("修改成功")
思路:实现对一个文件夹里的图片的增删改查,查就必须要有数据,因此我们需要利用os库遍历文件夹里的文件获取到每一个文件名,构成一个文件名的list,之后拿用户输入的信息在list里面找,方法和Excel的查一样。增是利用cv2这个库实现的。删是利用os.remove方法。改就是增+删。具体看detail.py文件里的逻辑。
# 获取文件夹图片目录元组 def getDirectTuple(self): self.fileList = [] for filepath, dirnames, filenames in os.walk(r'data\dist_images'): for filename in filenames: self.fileList.append(os.path.join(filepath, filename)) # print(self.fileList)
针对单个py文件打包成exe网上已经有很多教程了,针对整个项目的却很散。我们利用中间文件.spec来提高成功率。注意: 这里所讲的步骤最后打包出来是一个文件夹,不是单个exe。
步骤
a = Analysis(['main.py', 'Clinical_information.py', 'detail.py', 'Image_information.py', 'information.py', 'new_info.py', 'detect\\classification.py', 'detect\\utils\\utils.py', 'detect\\nets\\__init__.py', 'detect\\nets\\mobilenet.py', 'detect\\nets\\resnet50.py', 'detect\\nets\\vgg16.py', 'detect\\nets\\vit.py'], pathex=['D:\\AhhC_File\\CourseDesign\\smodel_mart-medical-system'], binaries=[], datas=[('detect\\logs','detect\\logs'),('detect\\model_data','detect\\model_data'),('icon','icon')], hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, [], exclude_binaries=True, name='main', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=True, icon='icon/cat.ico')
# 在生成的main.spec文件中加入 # -*- mode: python ; coding: utf-8 -*- import sys sys.setrecursionlimit(5000)
每天感慨自己写的什么垃圾代码,一两句话说不完,还是得看代码,虽然我不是正经python人,虽然我只是个前端。写这个程序大概四五天吧,之后就是自己做测试,优化,也让我学到了一些设计模式。真是one day day, write bug。最后还是发现了很多不足,比如打包之后在朋友的电脑上显出出来很丑且不能自适应,还有一些隐藏的bug。想学东西还是得自己动手呀!
欢迎批评指正。
https://gitee.com/Jesy_Hsu/smart-medical-system.git