「Python」结合PyQt5和PySerial实现串口助手
一、概要
1.主要模块介绍
PyQt5
PyQt5是一个用于创建 GUI应用程序的跨平台的工具包,它将 Python编程语言和Qt库成功融合在一起(Qt库是目前最强大的GUI库之一)。PyQt5可以运行在所有主流的操作系统上,包括UNIX、Windows和Mac OS。
特点:
- 基于高性能的Qt的GUI控件集。
- 能够跨平台运行在Windows、Linux和Mac OS等系统上。
- 使用信号/槽(signal/slot)机制进行通信。
- 对Qt库的完全封装。
- 可以使用Qt成熟的IDE(如Qt Designer)进行图形界面设计,并自动生成可执行的Python代码。
- 提供了一整套种类繁多的窗口控件。
pyqt5的类别分为几个模块,包括以下:
将会详细列出本案例所用到的模块和组件,下同
- QtCore(包含了核心的非GUI功能。此模块用于处理时间、文件和目录、各种数据类型、流、URL、MIME类型、线程或进程。)
- QDateTime(日期操作)、QTimer(定时器)、QRegExp(正则表达式类)
- QtGui(包含类窗口系统集成、事件处理、二维图形、基本成像、字体和文本)
- QIcon(图像)、QRegExpValidator(内置正则表达式检验器)、QColor(颜色)、QTextCursor(操作指针)
- QtWidgets(包含创造经典桌面风格的用户界面提供了一套UI元素的类)
- QWidget(基础窗口控件)、QApplication(控制流和主要设置)、QDesktopWidget(提供屏幕信息的访问)、QMessageBox(弹出式对话框)、QFileDialog(文件目录对话框)
- QGroupBox(组合框)、QGridLayout(表格布局)、QVBoxLayout(垂直布局)、QHBoxLayout(水平布局)、QFormLayout(表单布局)
- QLabel(标签)、QPushButton(按钮)、QComboBox(下拉列表)、QCheckBox(复选框)、QTextEdit(多行文本框)、QLineEdit(单行文本框)、QTextBrowser(富文本浏览器)
- QtMultimedia、QtBluetooth、QtNetwork、QtPositioning、Enginio、QtWebSockets、QtWebKit、QtWebKitWidgets、QtXml、QtSvg、QtSql、QtTest
一些组件的基本用法
QLabel(标签)
方法名 | 用途 |
---|---|
setText() | 设置QLabel的文本内容 |
text() | 获得QLabel的文本内容 |
QPushButton(按钮)
方法名 | 用途 |
---|---|
setIcon() | 设置按钮上的图标 |
setEnabled() | 设置按钮是否可用 False不可用 点击不会发射信号 |
setText() | 设置按钮的显示文本 |
text() | 返回按钮的显示文本 |
QComboBox(下拉列表)
方法名 | 用途 |
---|---|
addItem() | 添加一个下拉选项 |
addItems() | 从列表中添加下拉选项 |
clear() | 删除下拉列表中所有的选项 |
currentText() | 返回选中选项的文本 |
currentIndex() | 返回选中项的索引 |
setCurrentText() | 设置当前选择的文本 |
QCheckBox(复选框)
方法 | 用途 |
---|---|
setChecked() | 设置复选框的状态,设置为True表示选中,False表示取消选中的复选框 |
setText() | 设置复选框的显示文本 |
text() | 返回复选框的显示文本 |
isChecked() | 检查复选框是否被选中 |
QLineEdit(单行文本框)
方法名 | 用途 |
---|---|
setText() | 设置文本框的文本内容 |
text() | 获得文本框的文本内容 |
clear() | 清除文本框内容 |
QTextEdit(多行文本框)
方法名 | 用途 |
---|---|
setPlainText() | 设置多行文本框的文本内容 |
toPlainText() | 返回文本框的文本内容 |
clear() | 清除文本框内容 |
QTextBrowser(富文本浏览器)
方法名 | 用途 |
---|---|
append() | 追加文本,添加新行 |
insertPlainText() | 添加什么就显示什么 |
QMessageBox(弹出式对话框)
方法名 | 用途 |
---|---|
information(QWidget parent, title, text, buttons, defaultButton) | 弹出消息对话框:QWidget parent指定的父窗口控件;title对话框标题;text度花开文本;buttons多个标准按钮,默认为OK按钮;defaultButton默认选中的标准按钮,默认是第一个标准按钮 |
question(QWidget parent, title, text, buttons, defaultButton) | 弹出问答对话框 |
warning(QWidget parent, title, text, buttons, defaultButton) | 弹出警告对话框 |
critical(QWidget parent, title, text, buttons, defaultButton) | 弹出严重错误对话框 |
about(QWidget parent, title, text, buttons, defaultButton) | 弹出关于对话框 |
setTitle() | 设置标题 |
setText() | |
setIcon() | 设置弹出对话框的图片 |
QTimer(定时器)
方法名 | 用途 |
---|---|
start(milliseconds) | 启动或重新启动定时器,时间间隔为毫秒,如果定时器已经运行,他将停止并重新启动,如果singleSlot信号为真,定时器仅被激活一次 |
stop() | 停止定时器 |
QFileDialog(文件目录对话框)
方法名 | 用途 |
---|---|
getOpenFileName() | 返回用户所选择文件的名称,并打开该文件 |
getSaveFileName() | 使用用户选择的文件名保存文件 |
setFileMode() | 可以选择的文件类型,枚举常量是: |
QFileDialog.AnyFile:任何文件 | |
QFileDialog.ExistingFile:已存在的文件 | |
QFileDialog.Directory:文件目录 | |
QFileDialog.ExistingFiles:已经存在的多个文件 | |
setFilter() | 设置过滤器,只显示过滤器允许的文件类型 |
信号与槽
PyQt5有一个独一无二的信号和槽机制来处理事件。信号和槽用于对象之间的通信。当指定事件发生,一个事件信号会被发射。槽可以被任何Python脚本调用。当和槽连接的信号被发射时,槽会被调用。
PyQt中所有继承自QWidget的控件(这些都是QObject的子对象)都支持信号与槽机制。当信号发射时,连接的槽函数将会自动执行。在PyQt 5中信号与槽通过object.signal.connect()
方法连接。
PyQt的窗口控件类中有很多内置信号,开发者也可以添加自定义信号。信号与槽具有如下特点。
- 一个信号可以连接多个槽。
- 一个信号可以连接另一个信号。
- 信号参数可以是任何Python类型。
- 一个槽可以监听多个信号。
- 信号与槽的连接方式可以是同步连接,也可以是异步连接。
- 信号与槽的连接可能会跨线程。
- 信号可能会断开。
PySerial
PySerial模块封装了对串口的访问。
特性:
- 为多平台提供了统一的接口。
- 通过python属性访问串口设置。
- 支持不同的字节大小、停止位、校验位和流控设置。
- 可以有或者没有接收超时。
- 类似文件的API,例如read和write,也支持readline等。
初始化及对象常用方法
ser = serial.Serial("com1",9600,timeout=0.5) winsows系统使用com1口连接串行口
ser.isOpen() 查看端口是否被打开。
ser.open() 打开端口‘。
ser.close() 关闭端口。
ser.read() 从端口读字节数据。默认1个字节。
ser.read_all() 从端口接收全部数据。
ser.write("hello") 向端口写数据。
ser.readline() 读一行数据。
ser.readlines() 读多行数据。
in_waiting() 返回接收缓存中的字节数。
configparser
ConfigParser是python中一个用来读取配置文件的模块。该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。
配置文件一般格式:
[db]
db_host = 127.0.0.1
db_port = 69
db_user = root
db_pass = root
host_port = 69
[concurrent]
thread = 10
processor = 20
括号“[ ]”内包含的为section。紧接着section 为类似于key-value的options的配置内容。
配置文件的一般使用:
使用ConfigParser 首选需要初始化实例,并读取配置文件:
import configparser
config = configparser.ConfigParser()
1、获取指定section下指定option的值
config.read("ini", encoding="utf-8")
r = config.get("db", "db_host")
print(r) # 运行结果 127.0.0.1
2、添加section 和 option
if not config.has_section("default"): # 检查是否存在section
config.add_section("default")
if not config.has_option("default", "db_host"): # 检查是否存在该option
config.set("default", "db_host", "1.1.1.1")
3、写入文件
config.write(open("ini", "w"))
2.主要功能规划区分
串口助手主要有五个功能区域,串口设置、串口状态、单条发送、多条发送、接收区。
- 串口设置:主要用于刷新/检测串口,显示串口名称,选择串口,设置串口的波特率、数据位、校验位、停止位等,打开和关闭串口,以及设置接收窗口的文字及背景颜色。
- 串口状态:显示当前串口已接收和已发送的字节数,实时显示当前时间,以及串口助手版本号及作者姓名。
- 单条发送:单条发送数据,单条循环发送,清除窗口及保存窗口,设置是否以HEX进行接收和发送。
- 多条发送:多条发送数据,多条循环发送,清除接收窗口。
- 接收区:实时动态显示接收到的数据。
其余具体的设计参照下图:
3.原型图设计
利用任意原型图设计网站设计原型图,可以根据自己的喜好调整。
在这里简单设计了一下串口助手大致的布局,大小等,方便一会儿创建界面。
二、界面实现
1.总体布局
通过设计原型图,大致将串口助手的五大功能区划分为了五个组合框/分组框GroupBox,组合框/分组框的好处在于每个分组框都自带标题和边框,方便区域划分,可以在每个组合框中设置想要的布局结构,各个组合框内部的分组结构互不影响,在一定程度上简化了整个界面的布局。
每个组合框按照表格布局进行排列。首先创建一个用于编写界面的类SerialUi类继承QWidget,在init方法中会调用unit_ui方法来进行初始化UI,在unit_ui方法中对整个窗口进行表格布局,设置窗口大小、图标、名称和位置:
在表格布局中要按照位置关系将每个组合框添加进去
set_serial_setting_groupbox()、set_serial_state_groupbox()、set_receive_groupbox()、set_mul_sent_groupbpx()、set_single_sent_groupbox()都是稍后会写的方法,均会返回一个QGroupBox对象
class SerialUi(QWidget):
def __init__(self):
super().__init__()
# 初始化UI
self.unit_ui()
# 初始化UI
def unit_ui(self):
grid_layout = QGridLayout() # 设置总体布局为表格布局 2行3列
# addWidget用于添加组件
grid_layout.addWidget(self.set_serial_setting_groupbox(), 0, 0)
grid_layout.addWidget(self.set_serial_state_groupbox(), 1, 0)
grid_layout.addWidget(self.set_receive_groupbox(), 0, 1)
grid_layout.addWidget(self.set_mul_sent_groupbpx(), 0, 2)
grid_layout.addWidget(self.set_single_sent_groupbox(), 1, 1, 1, 2)
# 设置布局为grid_layout
self.setLayout(grid_layout)
# resize()方法调整窗口的大小
self.resize(760, 450)
# 设置窗口的图标
self.setWindowIcon(QIcon('title_icon.png'))
# 将窗口显示到中心
self.center()
# 设置窗口名
self.setWindowTitle('串口助手')
# 显示
self.show()
将窗口显示到屏幕中间的方法:
# 控制窗口显示在屏幕中心的方法
def center(self):
# 获得窗口
qr = self.frameGeometry()
# 获得屏幕中心点
cp = QDesktopWidget().availableGeometry().center()
# 显示到屏幕中心
qr.moveCenter(cp)
self.move(qr.topLeft())
2.串口设置区
在串口设置的界面实现中,根据需要,每一个下拉菜单或者按钮都有一个对应的标签,鉴于这种情况,可以使用表单布局(QFormLayout),QFormLayout是以表单的形式管理界面组件,其中表单中的标签和组件是相对应的关系:
首先创建串口设置区的分组框serial_setting_gb,并且将其中的布局管理器设置为表单布局:
# 串口设置 区
def set_serial_setting_groupbox(self):
# 设置一个 串口设置 分组框
serial_setting_gb = QGroupBox('串口设置')
# 创建 串口设置 分组框内的布局管理器
serial_setting_formlayout = QFormLayout()
创建“检测串口”按钮,addRow是添加表单项的方法,第一个参数为标签Label,第二个参数是组件Field:
# 检测串口 按钮
self.sset_btn_detect = QPushButton('检测串口')
serial_setting_formlayout.addRow('串口选择:', self.sset_btn_detect)
创建后“串口选择”标签,对应“检测串口”组件:
创建串口下拉菜单,将其添加到表单布局的第二行,没有标签则省略第一个参数,直接添加组件即可:
# 选择串口 下拉菜单
self.sset_cb_choose = QComboBox(serial_setting_gb)
# 添加一个下拉列表 由于没有标签 直接省略即可
serial_setting_formlayout.addRow(self.sset_cb_choose)
创建完成后:
创建“波特率”、“数据位”、“校验位”、“停止位”、“窗口配色”下拉菜单及对应的标签:
# 波特率 下拉菜单
self.sset_cb_baud = QComboBox(serial_setting_gb)
self.sset_cb_baud.addItems(['100', '300', '600', '1200', '2400', '4800', '9600', '14400', '19200',
'38400', '56000', '57600', '115200', '128000', '256000'])
serial_setting_formlayout.addRow('波特率:', self.sset_cb_baud)
# 数据位 下拉菜单
self.sset_cb_data = QComboBox(serial_setting_gb)
self.sset_cb_data.addItems(['8', '7', '6', '5'])
serial_setting_formlayout.addRow('数据位:', self.sset_cb_data)
# 校验位 下拉菜单
self.sset_cb_parity = QComboBox(serial_setting_gb)
self.sset_cb_parity.addItems(['N', 'E', 'O']) # 校验位N-无校验,E-偶校验,O-奇校验
serial_setting_formlayout.addRow('校验位:', self.sset_cb_parity)
# 停止位 下拉菜单
self.sset_cb_stop = QComboBox(serial_setting_gb)
self.sset_cb_stop.addItems(['1', '1.5', '2'])
serial_setting_formlayout.addRow('停止位:', self.sset_cb_stop)
# 窗口配色 下拉菜单
self.sset_cb_color = QComboBox(serial_setting_gb)
self.sset_cb_color.addItems(['whiteblack', 'blackwhite', 'blackgreen'])
serial_setting_formlayout.addRow('窗口配色:', self.sset_cb_color)
创建后:
最后创建打开/关闭串口的按钮:
# 打开串口 按钮
self.sset_btn_open = QPushButton('打开串口')
self.sset_btn_open.setIcon(QIcon('open_button.png'))
self.sset_btn_open.setEnabled(False)
serial_setting_formlayout.addRow(self.sset_btn_open)
最后调整表单布局里每项之间的间距,将组合框serial_setting_gb的布局设置为表单布局serial_setting_formlayout:
serial_setting_formlayout.setSpacing(11)
serial_setting_gb.setLayout(serial_setting_formlayout)
最终串口设置组合框的效果如下:
3.串口状态区
串口状态区和之前的串口设置区类似,首先创建“串口状态”组合框,并设置布局为表单布局:
# 串口状态区
def set_serial_state_groupbox(self):
self.serial_state_gb = QGroupBox('串口状态', self)
serial_state_formlayout = QFormLayout()
创建“已发送”和“已接收”标签,初始显示值均为0:
# 已发送 标签
self.sent_count_num = 0
self.ssta_lb_sent = QLabel(str(self.sent_count_num))
serial_state_formlayout.addRow('已发送:', self.ssta_lb_sent)
# 已接收 标签
self.receive_count_num = 0
self.ssta_lb_receive = QLabel(str(self.receive_count_num))
serial_state_formlayout.addRow('已接收:', self.ssta_lb_receive)
添加当前时间标签,这里创建了一个timer定时器对象,用于动态显示时间,将它的timeout()信号连接到showtime函数上,然后调用其start()函数开启定时器:
# 当前时间 标签
self.ssta_lb_timer = QLabel(self)
timer = QTimer(self)
timer.timeout.connect(self.showtime)
timer.start()
serial_state_formlayout.addRow(self.ssta_lb_timer)
在showtime函数中,获取当前的系统时间并转化为适当的格式的字符串,将当前时间的标签设置为该字符串:
def showtime(self):
time_display = QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss dddd')
self.ssta_lb_timer.setText(time_display)
添加版本及作者标签:
# 版本标签
ssta_lb_version = QLabel('version:V1.0.0')
serial_state_formlayout.addRow(ssta_lb_version)
ssta_lb_coder = QLabel('author:wong')
serial_state_formlayout.addRow(ssta_lb_coder)
最后设置行间距和布局:
serial_state_formlayout.setSpacing(13)
self.serial_state_gb.setLayout(serial_state_formlayout)
4.接收区
接收区的布局较之前的布局较为简单,只需使用垂直布局,在接收区组合框中添加一个富文本浏览器QTextBrowser即可:
# 接收区
def set_receive_groupbox(self):
# 设置一个接收区分组框
receive_gb = QGroupBox('接收区', self)
# 添加显示接收日志的文本框
self.receive_log_view = QTextBrowser(receive_gb)
# 在这里设置了最小宽度 防止接收窗口因拉动伸缩而变换的过小
self.receive_log_view.setMinimumWidth(350)
self.receive_log_view.append('Hello,欢迎使用串口助手!\n')
# 设置布局并添加文本框
vbox = QVBoxLayout()
vbox.addWidget(self.receive_log_view)
# 设置接收区分组框的布局
receive_gb.setLayout(vbox)
return receive_gb
5.单条发送区
单条发送区的结构较为复杂,在组合框中总体布局为水平布局,要在其中嵌套一个垂直布局和一个表格布局,垂直布局中添加单条发送文本框,表格布局中添加其余组件,之后将两种布局嵌套进入水平布局中:
# 单条发送区
def set_single_sent_groupbox(self):
single_sent_gb = QGroupBox('单条发送', self)
sins_overall_hlayout = QHBoxLayout(single_sent_gb)
vlayout_1 = QVBoxLayout()
glayout_1 = QGridLayout()
创建单条发送文本框,并添加到垂直布局中:
# 单条命令 文本框
self.sins_te_send = QTextEdit()
self.sins_te_send.setMinimumWidth(350)
vlayout_1.addWidget(self.sins_te_send)
创建循环发送复选框及输入文本框、HEX发送复选框、HEX接收复选框、清除发送、保存窗口、发送按钮,都添加在表格布局中:
# 循环发送 HEX发送 HEX接收 复选框
self.sins_cb_loop_send = QCheckBox('循环发送')
self.sins_le_loop_text = QLineEdit('1000')
reg = QRegExp('^(?!0)(?:[0-9]{1,6}|1000000)$') # ^(?!0)(?:[0-9]{1,4}|10000)$
reg_validator = QRegExpValidator(reg)
self.sins_le_loop_text.setValidator(reg_validator)
sins_lb_loop_label = QLabel('ms/次')
self.sins_cb_hex_receive = QCheckBox('HEX接收')
self.sins_cb_hex_send = QCheckBox('HEX发送')
glayout_1.addWidget(self.sins_cb_loop_send, 0, 0)
glayout_1.addWidget(self.sins_le_loop_text, 0, 1)
glayout_1.addWidget(sins_lb_loop_label, 0, 2)
glayout_1.addWidget(self.sins_cb_hex_receive, 1, 0)
glayout_1.addWidget(self.sins_cb_hex_send, 2, 0)
# 保存窗口 清除发送 发送 按钮
self.sins_btn_save = QPushButton('保存窗口')
self.sins_btn_clear = QPushButton('清除发送')
self.sins_btn_send = QPushButton('发送')
glayout_1.addWidget(self.sins_btn_save, 1, 1, 1, 2)
glayout_1.addWidget(self.sins_btn_clear, 2, 1, 1, 2)
glayout_1.addWidget(self.sins_btn_send, 3, 0, 1, 3)
其中,在循环时间输入文本框中,使用了一个校验器来对输入的文字进行校验,文本框中可输入的有效时间范围为1-1000000的数字,如果不符合输入的范围,例如输入了汉字、字母、特殊符号等,则不显示,或者输入了超出范围的数字,例如0、负数、大于1000000的数字,也不予显示。校验用的是正则表达式:
reg = QRegExp('^(?!0)(?:[0-9]{1,6}|1000000)$') # 创建一个正则表达式 reg_validator = QRegExpValidator(reg) # QRegExpValidator类用于根据正则表达式检查字符串,创建一个根据reg正则表达式检查字符串的对象reg_validator self.sins_le_loop_text.setValidator(reg_validator) # 将文本框的输入限制设置为reg_validator校验器来进行校验 # 其中正则表达式解释如下 ^(?!0)(?:[0-9]{1,6}|1000000)$ ^ # 匹配字符串的开始 (?!0) # 匹配不是0开头的任意数字 零宽度负预测先行断言(?!exp),断言此位置的后面不能匹配表达式exp (?:[0-9]{1,6}|1000000) # (?:exp) 匹配exp表达式但不获取结果 匹配 0-999999 或者 1000000 这些数字 [0-9]{1,6} 0-9 # 这几个数字可以出现至少1次 至多6次 | # 或者 $ # 匹配字符串的结束
最后将垂直布局和表格布局添加到水平布局中,再将组合框的布局设置为水平布局:
sins_overall_hlayout.addLayout(vlayout_1)
sins_overall_hlayout.addLayout(glayout_1)
single_sent_gb.setLayout(sins_overall_hlayout)
6.多条发送区
多条发送中的布局也较为复杂,多条发送组合框总体的布局方式为垂直布局,但是在其中要嵌套两个表格布局,第一个表格布局中放置“清除接收”按钮和“多条循环”复选框、输入文本框和标签,第二个表格布局中放置7组由复选框、单行文本框、按钮组成的多条发送界面:
# 多条发送区
def set_mul_sent_groupbpx(self):
mul_send_gb = QGroupBox('多条发送', self)
mul_send_vlayout = QVBoxLayout()
mul_send_gridlayout1 = QGridLayout()
mul_send_gridlayout2 = QGridLayout()
在第一个表格布局中添加“清除接收”和“多条循环”复选框及单行文本框和“ms”标签,其中时间单行输入文本框的输入限制校验器的设置同上:
# 清除接收 按钮
self.muls_btn_clear = QPushButton('清除接收')
mul_send_gridlayout1.addWidget(self.muls_btn_clear, 0, 0, 1, 3)
# 多条循环发送
self.mul_cb_loop_send = QCheckBox('多条循环')
mul_send_gridlayout1.addWidget(self.mul_cb_loop_send, 1, 0)
self.mul_le_loop_text = QLineEdit('1000')
reg = QRegExp('^(?!0)(?:[0-9]{1,6}|1000000)$')
reg_validator = QRegExpValidator(reg)
self.mul_le_loop_text.setValidator(reg_validator)
mul_send_gridlayout1.addWidget(self.mul_le_loop_text, 1, 1)
self.mul_lb_loop_lable = QLabel('ms/次')
mul_send_gridlayout1.addWidget(self.mul_lb_loop_lable, 1, 2)
在第二个表格布局中为了使代码简洁,利用两组for循环添加复选框、文本框和按钮,由于后续的使用需要,给每个复选框、文本框和按钮添加唯一的对象名称objectName,在之后功能实现的过程中如果需要利用这7组组件,则可以直接用findChild通过objectName来查找:
# 多条发送 区域
self.mul_btn_list = []
for i in range(1, 8):
for j in range(3):
if j == 0:
self.checkbox = QCheckBox()
self.checkbox.setObjectName('mul_cb_{}'.format(i))
mul_send_gridlayout2.addWidget(self.checkbox, i, j)
elif j == 1:
self.textedit = QLineEdit()
self.textedit.setObjectName('mul_le_{}'.format(i))
mul_send_gridlayout2.addWidget(self.textedit, i, j)
else:
self.button = QPushButton(str(i))
self.button.setFixedSize(25, 22)
self.button.setObjectName('mul_btn_{}'.format(i))
self.mul_btn_list.append(self.button.objectName())
mul_send_gridlayout2.addWidget(self.button, i, j)
最后将两组表格布局嵌套进垂直布局中:
mul_send_vlayout.addLayout(mul_send_gridlayout1)
mul_send_vlayout.addLayout(mul_send_gridlayout2)
mul_send_gb.setLayout(mul_send_vlayout)
mul_send_gb.setFixedWidth(180)
最终完成效果:
三、功能实现
首先创建一个SerialAssistant类继承SerialUi,在这个类里实现串口助手所需的功能,在init方法里初始化串口通信的serial对象,初始化串口配置文件以及给各个信号绑定的槽函数,由于显示的调用了父类的init方法,所以在实例化的时候会自动生成UI界面,不用再次调用生成函数:
class SerialAssistant(SerialUi):
def __init__(self):
super().__init__()
# 初始化serial对象 用于串口通信
self.ser = serial.Serial()
# 初始化串口配置文件
self.serial_cfg()
# 初始化串口 绑定槽
self.unit_serial()
检测串口
首先在unit_serial()方法中给串口检测按钮点击clicked信号绑定槽函数port_detect,当点击按钮时就会调用port_detect方法:
# 初始化串口 给各个信号绑定槽
def unit_serial(self):
# 串口检测按钮
self.sset_btn_detect.clicked.connect(self.port_detect)
在port_detect方法中,调用serial.tools.list_ports.comports()
接口可以返回计算机上所有的port口信息,将其储存在列表中并显示在下拉列表中。
返回的列表信息是ListPortInfo类的列表,存储类似于 [ [ COM1, xxxx ], [ COM2, yyyy ] ] 利用for循环读取每个串口的COM口数和串口名称,将其添加到下拉列表中,如果读取到的列表长度为0,则显示无串口:
# 串口检测
def port_detect(self):
# 检测所有存在的串口 将信息存在字典中
self.port_dict = {}
# serial.tools.list_ports.comports()返回计算机上所有的port口信息
# 将其存在列表中
port_list = list(serial.tools.list_ports.comports())
# 清除下拉列表中已有的选项
self.sset_cb_choose.clear()
for port in port_list:
# 添加到字典里
self.port_dict["%s" % port[0]] = "%s" % port[1]
# 添加到下拉列表选项
self.sset_cb_choose.addItem(port[0] + ':' + port[1])
if len(self.port_dict) == 0:
self.sset_cb_choose.addItem('无串口')
self.sset_btn_open.setEnabled(True)
检测串口-无串口
检测串口-有串口
打开/关闭串口
首先在unit_serial()方法中给串口打开/关闭按钮点击clicked信号绑定槽函数port_open_close,当点击按钮时就会调用port_open_close方法:
# 初始化串口 给各个信号绑定槽
def unit_serial(self):
# 打开/关闭串口 按钮
self.sset_btn_open.clicked.connect(self.port_open_close)
在port_open_close方法中,对按钮文字和是否有串口进行判断,如果打开串口无异常且串口状态是打开,则改变按钮的文字和图标,将串口的波特率、校验位、数据位、停止位下拉列表设置为不可修改:
# 打开/关闭 串口
def port_open_close(self):
# 按打开串口按钮 且 串口字典里有值
if (self.sset_btn_open.text() == '打开串口') and self.port_dict:
self.ser.port = self.get_port_name() # 设置端口
self.ser.baudrate = int(self.sset_cb_baud.currentText()) # 波特率
self.ser.bytesize = int(self.sset_cb_data.currentText()) # 数据位
self.ser.parity = self.sset_cb_parity.currentText() # 校验位
self.ser.stopbits = int(self.sset_cb_stop.currentText()) # 停止位
# 捕获 串口无法打开的异常
try:
self.ser.open()
except serial.SerialException:
QMessageBox.critical(self, 'Open Port Error', '此串口不能正常打开!')
return None
# 打开串口接收定时器 周期为2ms
self.serial_receive_timer.start(2)
# 判断 串口的打开状态
if self.ser.isOpen():
self.sset_btn_open.setText('关闭串口')
self.sset_btn_open.setIcon(QIcon('close_button.png'))
self.serial_state_gb.setTitle('串口状态(已开启)')
self.set_setting_enable(False)
# 按打开串口按钮 但 没有读取到串口
elif (self.sset_btn_open.text() == '打开串口') and (self.sset_cb_choose.currentText() == '无串口'):
QMessageBox.warning(self, 'Open Port Warning', '没有可打开的串口!')
return None
# 点击关闭串口按钮
elif self.sset_btn_open.text() == '关闭串口':
# 停止定时器
self.serial_receive_timer.stop()
try:
self.ser.close()
except:
QMessageBox.critical(self, 'Open Port Error', '此串口不能正常关闭!')
return None
self.sset_btn_open.setText('打开串口')
self.sset_btn_open.setIcon(QIcon('open_button.png'))
self.serial_state_gb.setTitle('串口状态')
self.set_setting_enable(True)
# 更改已发送和已接收标签
self.sent_count_num = 0
self.ssta_lb_sent.setText(str(self.sent_count_num))
self.receive_count_num = 0
self.ssta_lb_receive.setText(str(self.receive_count_num))
获取端口号的方法:
# 获取端口号(串口选择界面想显示完全 但打开串口只需要串口号COMX) def get_port_name(self): full_name = self.sset_cb_choose.currentText() # rfind会找到:的位置 com_name = full_name[0:full_name.rfind(':')] return com_name
使能的设置串口设置的可用与禁用:
# 设置 串口设置区 可用与禁用 def set_setting_enable(self, enable): self.sset_cb_choose.setEnabled(enable) self.sset_cb_baud.setEnabled(enable) self.sset_cb_data.setEnabled(enable) self.sset_cb_parity.setEnabled(enable) self.sset_cb_stop.setEnabled(enable)
打开串口-有串口
打开串口-无串口
改变窗口颜色
根据窗口配色下拉列表中选择的选项的不同,改变窗口配色,在unit_serial()方法中给根据下拉列表文字改变的信号绑定槽函数change_color,当点选择不同的下拉列表选项时就会调用change_color方法:
def unit_serial(self):
# 更改窗口颜色下拉菜单
self.sset_cb_color.currentTextChanged.connect(self.change_color)
在change_color方法中通过if判断和设置styleSheet即可更改窗口颜色:
使用setStyleSheet可以设置图形界面的外观,setStyleSheet("QPushButton{color:red}")
设定前景颜色,就是字体颜色,setStyleSheet("QPushButton{background-color:yellow}")
设定背景颜色为红色,setStyleSheet("QPushButton{color:rgb(255,255,255);background-color:black}")
同时设定字体和背景颜色
# 改变窗口颜色
def change_color(self):
if self.sset_cb_color.currentText() == 'whiteblack':
self.receive_log_view.setStyleSheet("QTextEdit {color:black;background-color:white}")
elif self.sset_cb_color.currentText() == 'blackwhite':
self.receive_log_view.setStyleSheet("QTextEdit {color:white;background-color:black}")
elif self.sset_cb_color.currentText() == 'blackgreen':
self.receive_log_view.setStyleSheet("QTextEdit {color:rgb(0,255,0);background-color:black}")
改变窗口颜色
发送
分析发送的基本实现代码可知,单条发送和多条发送在基础实现上是差不多的,为了提高代码复用率,先定一个公用的发送函数,根据是否勾选了HEX发送来判断是否要转换为16进制发送:
# 发送
def send_text(self, send_string):
if self.ser.isOpen():
# 非空字符串
if send_string != '':
# 如果勾选了HEX发送 则以HEX发送 String到Int再到Byte
if self.sins_cb_hex_send.isChecked():
# 移除头尾的空格或换行符
send_string = send_string.strip()
sent_list = []
while send_string != '':
# 检查是否是16进制 如果不是则抛出异常
try:
# 将send_string前两个字符以16进制解析成整数
num = int(send_string[0:2], 16)
except ValueError:
QMessageBox.critical(self, 'Wrong Data', '请输入十六进制数据,以空格分开!')
self.sins_cb_hex_send.setChecked(False)
return None
else:
send_string = send_string[2:].strip()
# 将需要发送的字符串保存在sent_list里
sent_list.append(num)
# 转化为byte
single_sent_string = bytes(sent_list)
# 否则ASCII发送
else:
single_sent_string = (send_string + '\r\n').encode('utf-8')
# 获得发送字节数
sent_num = self.ser.write(single_sent_string)
self.sent_count_num += sent_num
self.ssta_lb_sent.setText(str(self.sent_count_num))
else:
QMessageBox.warning(self, 'Port Warning', '没有可用的串口,请先打开串口!')
return None
单条发送
单条发送要通过点击“发送”按钮来触发,在unit_serial()方法中给“发送”按钮的点击信号绑定槽函数single_send,当点选择不同的下拉列表选项时就会调用single_send方法:
def unit_serial(self):
# 单行发送数据 按钮
self.sins_btn_send.clicked.connect(self.single_send)
单条发送就是从单条发送文本框中获取到需要发送的文本后调用send_text方法:
# 单条发送
def single_send(self):
# 获取已输入的字符串
single_sent_string = self.sins_te_send.toPlainText()
self.send_text(single_sent_string)
单条发送-ASCII
单条发送-HEX
多条发送
多条发送要根据所点击命令对应的按钮来触发,在unit_serial方法中根据唯一的objectname来循环给每个多条发送按钮绑定对应的槽函数multi_send_general:
def unit_serial(self):
# 多行发送数据 按钮
for i in range(1, 8):
self.child_button = self.findChild(QPushButton, 'mul_btn_{}'.format(i))
self.child_button.clicked.connect(self.multi_send_general)
在multi_send_general中首先要找到发送点击信号的发送者(Sender),判断是哪个按钮发送的信号,然后再获取按钮对应的文本框中的文本,之后调用send_text方法:
# 多行发送————普通 涉及sender 点击数字按钮发送对应的命令
def multi_send_general(self):
# 获取到发送信号的按钮序号
sent = self.sender().text()
# 找到对应的文本框
mul_te_sent = self.findChild(QLineEdit, 'mul_le_{}'.format(sent))
# 获取内容
multi_sent_string = mul_te_sent.text()
self.send_text(multi_sent_string)
多条发送
接收数据
接收数据需要定时刷新串口接收缓存区,所以需要定义一个定时器,在打开串口后定时刷新self.serial_receive_timer.start(2),timeout代表计时结束后调用data_receive,也就是每2ms调用一次data_receive:
def unit_serial(self):
# 定时器接受数据
self.serial_receive_timer = QTimer(self)
self.serial_receive_timer.timeout.connect(self.data_receive)
接收函数中获取接收缓存中的字节数后根据是否勾选HEX接收来判断如何显示接收数据:
# 接收数据
def data_receive(self):
try:
# inWaiting():返回接收缓存中的字节数
num = self.ser.inWaiting()
except:
pass
else:
# 接收缓存中有数据
if num > 0:
# 读取所有的字节数
data = self.ser.read(num)
receive_num = len(data)
# HEX显示
if self.sins_cb_hex_receive.isChecked():
receive_string = ''
for i in range(0, len(data)):
# {:X}16进制标准输出形式 02是2位对齐 左补0形式
receive_string = receive_string + '{:02X}'.format(data[i]) + ' '
self.receive_log_view.append(receive_string)
# 让滚动条随着接收一起移动
self.receive_log_view.moveCursor(QTextCursor.End)
else:
self.receive_log_view.insertPlainText(data.decode('utf-8'))
self.receive_log_view.moveCursor(QTextCursor.End)
# 更新已接收字节数
self.receive_count_num += receive_num
self.ssta_lb_receive.setText(str(self.receive_count_num))
else:
pass
循环发送
单条循环发送
单条循环发送需要创建一个定时器来充当循环的时钟,由定时器控制循环时长,根据循环发送复选框是否被勾选的状态信号链接到single_loop_send方法:
def unit_serial(self):
# 循环发送数据——单条
self.loop_single_send_timer = QTimer()
self.loop_single_send_timer.timeout.connect(self.single_send)
self.sins_cb_loop_send.stateChanged.connect(self.single_loop_send)
当单条循环复选框的勾选状态改变之后,调用single_loop_send方法后,进行相应的判断“单条输入文本框是否有内容”→“串口是否处于打开状态”→“如果单条循环复选框被勾选”→“打开定时器”:
# 循环发送——单条
def single_loop_send(self):
# 单挑输入框中有内容
if self.sins_te_send.toPlainText():
# 串口打开
if self.ser.isOpen():
# 循环发送复选框被勾选
if self.sins_cb_loop_send.isChecked():
# 循环时间文本框中输入了内容
if self.sins_le_loop_text.text():
# 打开定时器 按输入的数字计时
self.loop_single_send_timer.start(int(self.sins_le_loop_text.text()))
# 将循环时间文本框和打开串口按钮设为不可选中
self.sins_le_loop_text.setEnabled(False)
self.sset_btn_open.setEnabled(False)
else:
QMessageBox.warning(self, 'Value Error', '请输入1-1000000的值!')
self.sins_le_loop_text.setText('1000')
self.sins_cb_loop_send.setChecked(False)
else:
# 停止定时器
self.loop_single_send_timer.stop()
self.sins_le_loop_text.setEnabled(True)
self.sset_btn_open.setEnabled(True)
else:
QMessageBox.warning(self, 'Port Warning', '没有可用的串口,请先打开串口!')
self.sins_cb_loop_send.setChecked(False)
打开定时器后,定时器计时结束,会调用单行发送的方法single_send将数据发送出去:
# 单行发送
def single_send(self):
# 获取已输入的字符串
single_sent_string = self.sins_te_send.toPlainText()
self.send_text(single_sent_string)
单条循环发送
多条循环发送(功能尚不完善)
多条循环发送与单纯的多条发送不一样,多条发送只需要判断发送信号的按钮是哪个,然后调用multi_send_general即可,但是多条循环发送需要根据是否勾选多条循环复选框,是否勾选多条发送输入框前的复选框,勾选的多条发送输入框是否由内容来判断能否实施发送操作。首先在unit_serial中给信号绑定槽函数,这里的逻辑可以理解为,多条循环复选框的勾选状态改变后将会调用mul_loop_send方法,在mul_loop_send方法中当判断的先决条件都成立时则打开start定时器loop_mul_sent_timer,定时器multi_send_special计时结束timeout后会调用multi_send_special方法发送多条数据,每当定时器multi_send_special计时结束后都会调用multi_send_special方法,以此来形成多条循环:
def unit_serial(self):
# 循环发送数据——多条
self.loop_mul_sent_timer = QTimer()
self.loop_mul_sent_timer.timeout.connect(self.multi_send_special)
self.mul_cb_loop_send.stateChanged.connect(self.mul_loop_send)
当勾选/取消勾选了多条输入复选框后,会进入mul_loop_send方法,在其中进行判断,然后打开定时器,定时时间与多条循环文本框中输入的一致:
# 循环发送——多条
def mul_loop_send(self):
# 串口状态打开
if self.ser.isOpen():
# 多条循环复选框被勾选
if self.mul_cb_loop_send.isChecked():
# 多条循环时间输入框由内容
if self.mul_le_loop_text.text():
# 打开定时器
self.loop_mul_sent_timer.start(int(self.mul_le_loop_text.text()))
self.mul_le_loop_text.setEnabled(False)
self.sset_btn_open.setEnabled(False)
else:
QMessageBox.warning(self, 'Value Error', '请输入1-1000000的值!')
self.mul_le_loop_text.setText('1000')
self.mul_cb_loop_send.setChecked(False)
else:
self.loop_mul_sent_timer.stop()
self.mul_le_loop_text.setEnabled(True)
self.sset_btn_open.setEnabled(True)
else:
QMessageBox.warning(self, 'Port Warning', '1没有可用的串口,请先打开串口!')
self.mul_cb_loop_send.setChecked(False)
定时器loop_mul_sent_timer计时结束timeout后会调用multi_send_special方法,在其中获取到发送命令列表后利用send_text进行发送:
# 多行发送————特殊 不用点击按钮 根据勾选发送
def multi_send_special(self):
# 将待发送列表中的每一条循环发送出去
self.send_list = self.get_mul_send_list()
for string in self.send_list:
self.send_text(string)
其中get_mul_send_list方法用于获取一个多条发送命令的列表,里面包含了所有被勾选的命令:
# 获取一个多条发送列表 里面包含所有打钩的命令 def get_mul_send_list(self): self.mul_send_list = [] for i in range(1, 8): mul_te_sent = self.findChild(QLineEdit, 'mul_le_{}'.format(i)) multi_sent_string = mul_te_sent.text() mul_cb_check = self.findChild(QCheckBox, 'mul_cb_{}'.format(i)) if multi_sent_string and mul_cb_check.isChecked(): self.mul_send_list.append(multi_sent_string) return self.mul_send_list
多条循环
缺陷
目前多条循环发送,只能一次性同时发送多条,不能定时发送一条后再发送一条,这里仍需改进。
保存窗口
保存窗口用到了QFileDialog.getSaveFileName
,这个方法不会帮你创建文件,只返回一个元组。元组第一项为你的文件路径(包括你给的文件名),第二项为该文件的类型。
# 保存窗口
def save_receive_to_file(self):
file_name = QFileDialog.getSaveFileName(self, '保存窗口为txt文件','SaveWindow' +
time.strftime('%Y_%m_%d_%H-%M-%S', time.localtime()) + '.txt')
if file_name[1]:
with open(file_name[0], 'w') as file:
my_text = self.receive_log_view.toPlainText()
file.write(my_text)
else:
pass
保存窗口
清除
清除接收
当“清除接收”按钮被点击之后,执行清除操作:
def unit_serial(self):
# 清除接收按钮
self.muls_btn_clear.clicked.connect(self.clear_receive)
清除窗口就是直接将接收文本框中的文本设置为空:
# 清除接收
def clear_receive(self):
self.receive_log_view.setText('')
清除接收
清除发送
当“清除发送”按钮被点击之后,执行清除操作:
def unit_serial(self):
# 清除发送按钮
self.sins_btn_clear.clicked.connect(self.clear_send)
清除发送就是直接将发送文本框中的文本设置为空:
# 清除发送
def clear_send(self):
self.sins_te_send.setText('')
清除发送
读写配置文件
有效利用配置文件可以帮助我们记忆串口设置和输入的命令,减少重复工作,提高串口助手的效率。
每次使用配置文件都要在SerialAssistant类中的init方法中对串口配置进行初始化,包括初始化一个用于保存串口配置信息的字典,一个用于保存多条命令的字典,一个用于保存单条命令的字典,获取当前的目录用于保存配置文件,以及配置文件所在的目录名和配置文件的名称,之后要初始化一个configparser对象用于后续的配置操作:
# 初始化串口配置 定义串口设置信息 保存和读取
def serial_cfg(self):
self.cfg_serial_dic = {} # 用于保存串口设置信息的字典
self.cfg_command_dic = {}
self.cfg_single_dic = {}
self.current_path = os.path.dirname(os.path.realpath(__file__)) # 当前目录
self.cfg_path = '' # 配置文件的路径
self.cfg_dir = 'settings' # 配置文件目录
self.cfg_file_name = 'cfg.ini' # 配置文件名
self.conf_parse = configparser.ConfigParser() # 配置文件解析ConfigParser对象
# 读取串口配置
self.read_cfg()
# 将读取到的串口配置信息显示到界面上
self.display_cfg()
读取串口配置的方法read_cfg是要查看配置文件和所需的section是否存在,如果不存在则需要新建目录setting和目录中的文件cfg.ini,如果文件cfg.ini已存在但其中所需要的的section不存在则需要新建section:
# 读取串口配置————配置文件和section是否存在
def read_cfg(self):
self.cfg_path = os.path.join(self.current_path, self.cfg_dir, self.cfg_file_name) # 获取配置文件路径 join用于连接两个或更多的路径
# 判断读取配置文件是否正常 如果读取文件正常
if self.conf_parse.read(self.cfg_path):
# 判断读取section是否正常
try:
# 获取serial_setting section 返回一个配置字典
serial_items = self.conf_parse.items('serial_setting')
self.cfg_serial_dic = dict(serial_items)
print(self.cfg_serial_dic)
# 如果没有找到section
except configparser.NoSectionError:
self.conf_parse.add_section('serial_setting') # 添加section
self.conf_parse.write(open(self.cfg_path, 'w')) # 保存到配置文件
try:
command_items = self.conf_parse.items('mul_sent_command')
self.cfg_command_dic = dict(command_items)
print(self.cfg_command_dic)
except configparser.NoSectionError:
self.conf_parse.add_section('mul_sent_command')
self.conf_parse.write(open(self.cfg_path, 'w'))
try:
command_items = self.conf_parse.items('single_sent_command')
self.cfg_single_dic = dict(command_items)
print(self.cfg_single_dic)
except configparser.NoSectionError:
self.conf_parse.add_section('single_sent_command')
self.conf_parse.write(open(self.cfg_path, 'w'))
# 读取文件异常
else:
# 判断setting目录是否存在 不存在的话新建目录
if not os.path.exists(os.path.join(self.current_path, self.cfg_dir)):
os.mkdir(os.path.join(self.current_path, self.cfg_dir))
self.conf_parse.add_section('serial_setting') # 添加section
self.conf_parse.add_section('mul_sent_command')
self.conf_parse.add_section('single_sent_command')
self.conf_parse.write(open(self.cfg_path, 'w')) # 保存到配置文件
保存串口配置信息的方法save_cfg将每个需要保存的信息保存在配置文件中,供下一次启动时读取,每次在退出串口助手是调用保存方法,保存此次串口助手的配置:
# 保存串口配置信息
def save_cfg(self):
# 保存每一项到配置文件
self.conf_parse.set('serial_setting', 'baudrate', str(self.ser.baudrate))
self.conf_parse.set('serial_setting', 'data', str(self.ser.bytesize))
self.conf_parse.set('serial_setting', 'stopbits', str(self.ser.stopbits))
self.conf_parse.set('serial_setting', 'parity', self.ser.parity)
for i in range(1, 8):
self.conf_parse.set('mul_sent_command', 'command_{}'.format(i),
self.findChild(QLineEdit, 'mul_le_{}'.format(i)).text())
self.conf_parse.set('single_sent_command', 'command', self.sins_te_send.toPlainText())
self.conf_parse.write(open(self.cfg_path, 'w'))
每次启动串口助手时,需要读取串口配置信息然后将其显示在串口助手中对应的位置上:
# 将读取到的串口配置信息显示到界面上
def display_cfg(self):
self.sset_cb_baud.setCurrentText(self.conf_parse.get('serial_setting', 'baudrate'))
self.sset_cb_data.setCurrentText(self.conf_parse.get('serial_setting', 'data'))
self.sset_cb_stop.setCurrentText(self.conf_parse.get('serial_setting', 'stopbits'))
self.sset_cb_parity.setCurrentText(self.conf_parse.get('serial_setting', 'parity'))
for i in range(1, 8):
command = self.conf_parse.get('mul_sent_command', 'command_{}'.format(i))
if command:
self.findChild(QLineEdit, 'mul_le_{}'.format(i)).setText(command)
self.sins_te_send.setText(self.conf_parse.get('single_sent_command', 'command'))
配置文件格式
结合配置文件启动程序
关闭窗口
关闭窗口需要重写closeEvent方法,在点击窗口右上角的×以后,发出询问对话框,询问是否确认退出,一旦选择是则保存当前串口的配置后退出串口助手,否则不做任何操作:
def closeEvent(self, event):
reply = QMessageBox.question(self, 'Message', "确定要退出吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# 判断返回值,如果点击的是Yes按钮,我们就关闭组件和应用,否则就忽略关闭事件
if reply == QMessageBox.Yes:
self.save_cfg()
event.accept()
else:
event.ignore()
关闭窗口
main函数
在main函数中实例化应用程序和对象即可运行串口助手:
if __name__ == '__main__':
# 每一PyQt5应用程序必须创建一个应用程序对象
app = QApplication(sys.argv)
# 创建一个SerialAssistant对象
su = SerialAssistant()
# 系统exit()方法确保应用程序干净的退出
# exec_()方法的作用是“进入程序的主循环直到exit()被调用”,
# 如果没有这个方法,运行的时候窗口会闪退
sys.exit(app.exec_())
四、其他
依赖库版本
python——3.8.3
PyQt5——5.15.0
pyserial——3.5
configparser——5.0.1
以上模块除python外都可以通过pip install 模块名
来下载