引言

使用pyqt5,Qt Designer,PyUIC,pycharm,ZBar练习python GUI开发,扫描条形码案例。无聊写写。最近学习做python GUI开发, 感觉比网页落后好多。我只是为了完成老师布置的任务, 做一个配合ZBar扫描条形码的小程序, 不打算过多深究二维码什么的, 感觉没什么意义。由于pyqt5貌似不是很火, 没多少成系统的教程。我能找到的就是http://code.py40.com/pyqt5/这个网站, 但是没有配合qt designer使用前期给了我很大的困惑。滑到最下面show you the code
先上一张图, 看看效果

pyqt5入门练习-扫描条形码 Python 第1张

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

简单得不能再简单了, 但第一次写有点磕磕绊绊的

  • 工具:pycharm + qt Designer + pyUIC + ZBar
  • qt Designer的主要功能就是把图形界面做出来
  • pyUIC的主要功能就是把图形界面翻译成.py文件
  • ZBar的功能是扫描图片上的条形码

主要实现的功能有: 用外部程序ZBar扫描图片、用xlwt库导出数据到excel, pyinstaller打包python文件, 其它的就是pyqt5的一些操作比如选择文件、换ico图标之类的

一、工具的安装

  • 安装pyqt5
    pip install pyqt5
  • 安装pyqt5-tools
    pip install pyqt5-tools
    完成了之后qt designer就安装在你的python根目录下的\Lib\site-packages\pyqt5_tools\Qt\bin\
  • 配置pycharm
    File->settings->Tools->External Tools->+

配置qt designer
pyqt5入门练习-扫描条形码 Python 第2张

配置pyUIC
pyqt5入门练习-扫描条形码 Python 第3张
参数:-m PyQt5.uic.pyuic $FileName$ -o $FileNameWithoutExtension$.py

工具的位置:
pyqt5入门练习-扫描条形码 Python 第4张

二、运行第一个Helloworld

pycharmnew一个python项目出来
Tools->External Tools->Qt Designer
创建一个Main Window模板(我第一次创建了个对话框, 怎么也运行不了)
左边工具栏Display Widgets拖一个Label到窗口中间, 安静地敲下Helloworld

pyqt5入门练习-扫描条形码 Python 第6张

右击HelloWorld->change styleSheet

pyqt5入门练习-扫描条形码 Python 第7张
这应该是你熟悉的css, 我们看到字有点超出边界了, 这里有个小技巧
右击Helloword->Layout->Adjust Size
这时候Helloworld饱满了
Ctrl+S保存到项目文件夹
左键选中helloworld.ui(一定要选中) 寻找工具pyUIC 点击 自动生成了helloworld.py文件
在项目目录中新建main.py文件作为主入口, 与图形界面的代码分离(试过就知道很方便), 并敲下如下代码:

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
import helloworld

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mw = QMainWindow()
    ui = helloworld.Ui_MainWindow()
    ui.setupUi(mw)
    mw.show()
    # 逻辑代码
    
    sys.exit(app.exec_())

运行main.py
pyqt5入门练习-扫描条形码 Python 第8张

三、界面制作

Tools->External Tools->Qt Desginer
创建一个Main Window 先把界面摆好

  • Buttons->Push Button拖出两个按钮
  • Item Widgets->Table Widget拖出两个表格框
  • Display Widgets->Label拖出标签
    pyqt5入门练习-扫描条形码 Python 第9张

文件数:0是两个不同标签
接下来开始为每个组件命名, 要做到见名知意
选中导入按钮命名为importBtn

pyqt5入门练习-扫描条形码 Python 第10张

依次地

pyqt5入门练习-扫描条形码 Python 第11张
保存到项目目录为barcode.ui->左键点击选中->Tools->External Tools->pyUIC
得到barcode.py 图形界面的代码

四、触发按钮

首先在main.py文件中进口barcode.py模块, 更改barcode模块生成ui对象
有两个对象ui对象和MainWindow对象
通过ui对象可以管理按钮、标签等组件, 通过MainWindow可以进行关闭窗口等操作
通过点击按钮 触发事件
先写最简单的退出按钮

# main.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
import barcode

def exitEvent(mw):
    mw.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mw = QMainWindow()
    ui = barcode.Ui_MainWindow()
    ui.setupUi(mw)
    mw.show()
    # 逻辑代码
    ui.exitBtn.clicked.connect(lambda: exitEvent(mw))
    sys.exit(app.exec_())

ui.exitBtn.clicked.connect(lambda: exitEvent(mw))这里使用lambda表达式主要是为了解决传参问题,还有一种partial传参方式没lambda好用。把mw主窗口对象传递到exitEvent()函数中, 对mw对象进行操作, 实现操作的模块化, 比如加个退出的确认框呀什么的
运行main.py 点击退出按钮 退出界面

五、选择文件

打开barcode.ui 双击第一个表格框 写出各个字段
pyqt5入门练习-扫描条形码 Python 第12张
pyqt5入门练习-扫描条形码 Python 第13张
pyqt5入门练习-扫描条形码 Python 第14张
然后点击保存, 直接用pyUIC转换成barcode.py文件, 这就是分离的好处, 只管界面, 不管逻辑。
我们可以通过观察barcode.py学习这个表格是怎么注入数据的, 如这一段

# def setupUi()
item = QtWidgets.QTableWidgetItem()
self.fileList.setItem(0, 0, item)
# 在(0,0)位置处生成一个单元格
# def retranslateUi()
item = self.fileList.item(0, 0)
item.setText(_translate("MainWindow", "1"))
# 在(0,0)处注入1这个字符

可以看到setupUi()方法专注于创建, 而retranslateUi()方法专注于注入数据, 就像盖楼一样, 我们先把框架搭好, 然后再在里面添砖加瓦

from PyQt5 import QtCore, QtGui
import barcode
import sys
import time
import os
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox, QTableWidgetItem
# 文件集合, 保存选择的文件的绝对路径, 确保元素不重复
fileSet = set()

# method
def importEvent(mw, ui):
    files = QFileDialog.getOpenFileNames(mw, '打开文件', './', ("Images (*.png *.jpg *.bmp *.gif *.raw *.tif *.xpm)"))
    for i in files[0]:
        fileSet.add(i)
    ui.totalFileNum.setText(str(len(fileSet)))  # 文件总数
    ui.fileList.clearContents()
    ui.fileList.setRowCount(0)
    for file in fileSet:
        row = ui.fileList.rowCount()  
        ui.fileList.insertRow(row)
        # 文件名
        fileNameItem = QTableWidgetItem(os.path.basename(file))
        fileNameItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件大小
        fileSizeItem = QTableWidgetItem(str("%.2f" % (os.path.getsize(file) / 1024)) + "KB")
        fileSizeItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件绝对路径
        filePathItem = QTableWidgetItem(file)
        filePathItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件创建时间
        createTimeItem = QTableWidgetItem(timeStampToStrTime(os.path.getctime(file)))
        createTimeItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件修改时间
        modifyTimeItem = QTableWidgetItem(timeStampToStrTime(os.path.getmtime(file)))
        modifyTimeItem.setTextAlignment(QtCore.Qt.AlignCenter)
        ui.fileList.setItem(row, 0, fileNameItem)
        ui.fileList.setItem(row, 1, fileSizeItem)
        ui.fileList.setItem(row, 2, filePathItem)
        ui.fileList.setItem(row, 3, createTimeItem)
        ui.fileList.setItem(row, 4, modifyTimeItem)

def exitEvent(mw):
    """退出事件"""
    mw.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mw = QMainWindow()
    ui = barcode.Ui_MainWindow()
    ui.setupUi(mw)
    mw.show()
    # 主体代码

    # 字段显示宽度
    ui.fileList.setColumnWidth(0, 150)
    ui.fileList.setColumnWidth(1, 150)
    ui.fileList.setColumnWidth(2, 500)
    ui.fileList.setColumnWidth(3, 200)
    ui.fileList.setColumnWidth(4, 200)
    ui.resultList.setColumnWidth(0, 150)
    ui.resultList.setColumnWidth(1, 150)
    ui.resultList.setColumnWidth(2, 400)
    ui.resultList.setColumnWidth(3, 400)

    # 按钮
    ui.importBtn.clicked.connect(lambda: importEvent(mw, ui))  # 导入按钮
    ui.exitBtn.clicked.connect(lambda: exitEvent(mw))  # 退出按钮
    sys.exit(app.exec_())
  • 这里用到文件选择器, 要用QFileDialog.getOpenFileNames(), 按住Ctrl可以选择多个文件。 别用getOpenfileName()这个一次只能选择一个文件。

  • 返回的files是一个元组, ([文件1绝对路径, 文件2绝对路径, ...], 文件类型)
    files[0]返回的是选中的文件的绝对路径, 有了绝对路径就好办事了, 文件名、大小、创建时间等属性可以由os.path得到

  • 创建一个集合fileSet用于保存文件的绝对路径, 这样就不怕选择了重复的文件

  • ("Images (*.png *.jpg *.bmp *.gif *.raw *.tif *.xpm)")是对文件类型的限制, 去掉这个参数就是可以添加任何文件
    每次新加入文件就把原来的表格内容清空同时行号置0

ui.fileList.clearContents()
ui.fileList.setRowCount(0)

运行main.py
pyqt5入门练习-扫描条形码 Python 第15张

六、扫描事件

打开barcode.ui 双击第二个表格 依次写出各个字段
pyqt5入门练习-扫描条形码 Python 第16张
pyqt5入门练习-扫描条形码 Python 第17张
pyqt5入门练习-扫描条形码 Python 第18张

# main.py
from PyQt5 import QtCore, QtGui
import barcode
import sys
import time
import os
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox, QTableWidgetItem
# 文件集合, 保存选择的文件的绝对路径, 确保元素不重复
fileSet = set()

# method
def importEvent(mw, ui):
    files = QFileDialog.getOpenFileNames(mw, '打开文件', './', ("Images (*.png *.jpg *.bmp *.gif *.raw *.tif *.xpm)"))
    if !files:
        return
    for i in files[0]:
        fileSet.add(i)
    ui.totalFile.setText(str(len(fileSet)))  # 文件总数
    ui.fileList.clearContents()
    ui.fileList.setRowCount(0)
    for file in fileSet:
        row = ui.fileList.rowCount()
        ui.fileList.insertRow(row)
        # 文件名
        fileNameItem = QTableWidgetItem(os.path.basename(file))
        fileNameItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件大小
        fileSizeItem = QTableWidgetItem(str("%.2f" % (os.path.getsize(file) / 1024)) + "KB")
        fileSizeItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件绝对路径
        filePathItem = QTableWidgetItem(file)
        filePathItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件创建时间
        createTimeItem = QTableWidgetItem(timeToStrTime(os.path.getctime(file)))
        createTimeItem.setTextAlignment(QtCore.Qt.AlignCenter)
        # 文件修改时间
        modifyTimeItem = QTableWidgetItem(timeToStrTime(os.path.getmtime(file)))
        modifyTimeItem.setTextAlignment(QtCore.Qt.AlignCenter)
        ui.fileList.setItem(row, 0, fileNameItem)
        ui.fileList.setItem(row, 1, fileSizeItem)
        ui.fileList.setItem(row, 2, filePathItem)
        ui.fileList.setItem(row, 3, createTimeItem)
        ui.fileList.setItem(row, 4, modifyTimeItem)

def timeToStrTime(timestamp):
    """将时间戳转化为格式时间字符串"""
    timeStruct = time.localtime(timestamp)
    return time.strftime('%Y-%m-%d %H:%M', timeStruct)

def startScan(mw, ui):
    if len(fileSet) == 0:
        QMessageBox.information(mw, '提示', '你没有选择任何条形码图片文件!', QMessageBox.Ok, QMessageBox.Ok)
        return
    successCount = 0  # 成功检测数
    failCount = 0  # 失败检测数
    cmd = ".\\ZBar\\bin\\zbarimg.exe " 
    ui.resultList.clearContents()
    ui.resultList.setRowCount(0)
    for file in fileSet:
        array = execCmd(cmd + file) # 执行命令.\zbarimg.exe imgPath
        row = ui.resultList.rowCount()
        ui.resultList.insertRow(row)
        fileNameItem = QTableWidgetItem(os.path.basename(file))
        fileNameItem.setTextAlignment(QtCore.Qt.AlignCenter)
        if len(array) == 0:
            # 设置失败红色字体
            statusItem = QTableWidgetItem("扫描失败")
            statusItem.setTextAlignment(QtCore.Qt.AlignCenter)
            brush = QtGui.QBrush(QtGui.QColor(234, 31, 42))
            brush.setStyle(QtCore.Qt.NoBrush)
            statusItem.setForeground(brush)

            typeItem = QTableWidgetItem("请调整图片清晰度")
            typeItem.setTextAlignment(QtCore.Qt.AlignCenter)
            brush = QtGui.QBrush(QtGui.QColor(234, 31, 42))
            brush.setStyle(QtCore.Qt.NoBrush)
            typeItem.setForeground(brush)

            valueItem = QTableWidgetItem("请调整图片清晰度")
            valueItem.setTextAlignment(QtCore.Qt.AlignCenter)
            brush = QtGui.QBrush(QtGui.QColor(234, 31, 42))
            brush.setStyle(QtCore.Qt.NoBrush)
            valueItem.setForeground(brush)
            failCount += 1
        else:
            # 设置成功绿色字体
            statusItem = QTableWidgetItem("扫描成功")
            statusItem.setTextAlignment(QtCore.Qt.AlignCenter)
            brush = QtGui.QBrush(QtGui.QColor(35, 177, 32))
            brush.setStyle(QtCore.Qt.NoBrush)
            statusItem.setForeground(brush)

            result = array[0].split(":") # 分离字符串得到条形码格式和内容
            codebarType = result[0]  # 条形码格式
            codebarValue = result[1].strip() # 条形码内容, 去掉两端的空格、制表符trim()函数
            typeItem = QTableWidgetItem(codebarType)
            typeItem.setTextAlignment(QtCore.Qt.AlignCenter)

            valueItem = QTableWidgetItem(codebarValue)
            valueItem.setTextAlignment(QtCore.Qt.AlignCenter)
            successCount += 1
        ui.resultList.setItem(row, 0, fileNameItem)
        ui.resultList.setItem(row, 1, statusItem)
        ui.resultList.setItem(row, 2, typeItem)
        ui.resultList.setItem(row, 3, valueItem)
        ui.successScanNum.setText(str(successCount)) # 设置成功扫描的文件数
        ui.failScanNum.setText(str(failCount))

def exitEvent(mw):
    """退出事件"""
    mw.close()

def execCmd(cmd):
    """执行控制台命令"""
    result = os.popen(cmd)
    text = result.readlines()
    result.close()
    return text

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mw = QMainWindow()
    ui = barcode.Ui_MainWindow()
    ui.setupUi(mw)
    mw.show()
    # 主体代码

    # 字段显示宽度
    ui.fileList.setColumnWidth(0, 150)
    ui.fileList.setColumnWidth(1, 150)
    ui.fileList.setColumnWidth(2, 500)
    ui.fileList.setColumnWidth(3, 200)
    ui.fileList.setColumnWidth(4, 200)
    ui.resultList.setColumnWidth(0, 150)
    ui.resultList.setColumnWidth(1, 150)
    ui.resultList.setColumnWidth(2, 400)
    ui.resultList.setColumnWidth(3, 400)

    # 按钮
    ui.scanBtn.clicked.connect(lambda: startScan(mw, ui))  # 扫描按钮
    ui.importBtn.clicked.connect(lambda: importEvent(mw, ui))  # 导入按钮
    ui.exitBtn.clicked.connect(lambda: exitEvent(mw))  # 退出按钮
    sys.exit(app.exec_())

通过execCmd()方法执行控制台指令, cmd = ".\\ZBar\\bin\\zbarimg.exe "即执行main.py运行的当前目路下的ZBar/bin/zbarimg.exe程序, 只要后面追加一个图片的绝对路径就可以实现扫描操作, 要注意的是zbarimg.exe与图片绝对路径之间有个空格。execCmd()通过执行命令得到结果, 通过把结果加载到内存中, 然后再分离各个字符串, 得到扫描结果return的text是一个列表, 里面要么只有1个元素就是扫描结果, 要么就没有元素未扫描出结果, 很简单的就可以判定
运行main.py
pyqt5入门练习-扫描条形码 Python 第19张

七、导出到excel

使用xlwt模块pip install xlwt

# main.py
def exportEvent(mw, ui):
    """保存到excel"""
    if len(fileSet) == 0:
        QMessageBox.information(mw, '提示', '你没有选择任何条形码图片文件!', QMessageBox.Ok, QMessageBox.Ok)
        return
    # 创建一个workbook 设置编码
    workbook = xlwt.Workbook(encoding='utf-8')
    # 创建一个worksheet
    worksheet = workbook.add_sheet('sheet1')
    # 样式
    t = xlwt.Font()
    t.colour_index = 10  # 红色字体
    redColorFont = xlwt.XFStyle()
    redColorFont.font = t

    t = xlwt.Font()
    t.colour_index = 57  # 绿色字体
    greenColorFont = xlwt.XFStyle()
    greenColorFont.font = t

    # 参数对应 行, 列, 值
    worksheet.write(0, 0, label='文件名')
    worksheet.col(0).width = 256 * 20
    worksheet.write(0, 1, label='扫描状态')
    worksheet.write(0, 2, label='格式')
    worksheet.write(0, 3, label='值')
    worksheet.col(3).width = 256 * 20
    worksheet.write(0, 4, label='文件路径')
    worksheet.col(4).width = 256 * 80

    cmd = ".\\ZBar\\bin\\zbarimg.exe "
    row = 1
    for file in fileSet:
        worksheet.write(row, 0, label=os.path.basename(file))
        array = execCmd(cmd + file)
        if len(array) == 0:
            worksheet.write(row, 1, u'扫描失败', redColorFont)
        else:
            worksheet.write(row, 1, u'扫描成功', greenColorFont)
            result = array[0].split(":")
            worksheet.write(row, 2, label=result[0])
            worksheet.write(row, 3, label=result[1].strip())
        worksheet.write(row, 4, label=file)
        row += 1
    # 保存
    workbook.save('.\\data.xls')

八、pyinstaller打包

使用pyinstaller把.py文件打包成.exe文件, 其中有很多坑, 比如图片资源没法打包, 路径问题等
进入main.py文件所在目录
由于没法直接打包图片, 所以需要将图片解码成.py文件然后再当成模块在python中使用
准备一个图标pyqt5入门练习-扫描条形码 Python 第20张
先在qt designer中设置windowIcon
pyqt5入门练习-扫描条形码 Python 第21张
然后保存, 使用pyUIC转化一下barcode.ui文件
窗口的图标就设置好了
打包图片资源:

  1. 创建resource.qrc文件
  2. 输入以下内容(要保证图片和.qrc文件在同一目录)
<RCC>
  <qresource prefix="/" >
    <file>favicon.png</file>
  </qresource>
</RCC>
  1. pyrcc5 -o resource.py resource.qrc转换得到resource.py模块
  2. 在barcode.py文件中引入resource.py模块
  3. 修改用到的图片路径, 在前面加引号:
    pyqt5入门练习-扫描条形码 Python 第22张

即使pycharm提示没使用也没关系, 打包后可以自动引用,虽然pycharm运行的时候窗口没有显示图标,但打包后就可以看到
将favicon.png复制一份, 改后缀为ico文件
打包:
pyinstaller -w main.py -i favicon.ico -n "barcodeKing"
-w窗口运行伴随控制台, 方便调试
-i参数是设置.exe文件的图标
-F参数, 把文件打包成单个.exe文件, 不推荐, 因为ZBar是外部程序, 不能被打包进.exe, 会造成路径问题闪退
-n为.exe文件命名

pyqt5入门练习-扫描条形码 Python 第23张

打包成功
进入dist->barcodeKing, 把ZBar目录复制到与barcodeKing.exe同级的目录下

pyqt5入门练习-扫描条形码 Python 第24张
运行良好

九、小结

没有很深入的研究, 只是pyqt5入门的操作, 写得不清楚也请见谅, 我是很水的
源码在这里https://github.com/CaseyFu/python/tree/master/src/GUI/barcode
如果你也是为了完成任务可以交我这个/doge哈哈哈哈

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄