上一章主要讲了如何使用Qt Designer设计一个漂亮的UI界面,这一章开始进入代码的分析。当然,这里不会一行一行分析,下载源码后里面的注释相信已经比较清晰了。我只写些重点和写代码过程中遇到的一些问题。
源码地址:https://github.com/zengjiawei/CommAssistant-for-PyQt5
在新建工程的时候我们一开始便创建了一个.py文件,如果还没有创建的现在也可以创建,创建之后我们先来调用UI界面的程序,模板代码如下:
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget
from CommAssistant_UI import Ui_Form
class CommAssistant(QWidget, Ui_Form):
def __init__(self, parent=None):
super(CommAssistant, self).__init__(parent)
self.setupUi(self) # 初始化界面
@QtCore.pyqtSlot()
def on_ClearSendButton_clicked(self): # 按键处理
return None
if __name__ == '__main__':
app = QApplication(sys.argv)
win = CommAssistant()
win.show()
sys.exit(app.exec_())
这段代码可以作为一个模板,这仅仅是实现调用界面程序,不同的工程需要修改的就是项目的名字和UI控件的名字,比如我这里的 "CommAssistant" ,要根据项目保存的名称为主,否则会报错。修改完之后,右键这个.py文件点击 "Run" ,大家就可以看到自己做的界面了,是不是有点小激动。不过,你点击任何东西都是没有反应的,因为这些控件还没有连接到我们的逻辑。下面开始进入程序部分。
讲程序之前有必要先讲一下程序的调试,调试是非常重要的,很多人都说程序不是写出来的,而是调出来的。在pycharm的右上角大家会看到一个 "甲虫" 一样的图标,点一下就会进入调试模式,因为我们没有设置断点,所以程序一进入Debug模式程序就直接跑完了,我们需要先在程序中设置断点,再进入Debug模式,或者点一下左下角 "绿色的箭头" Rerun一下。如下图。
Debug模式程序就会运行到我们设置断点的地方了,上方的图我还框了两个地方,一个是 "Console" 我们在调试的时候可能需要输出一些数据,如:print(),我们就可以通过这个窗口看到,还有右边的 "蓝色小箭头",写过C的都知道单步调试等等,原理一样的。调试用到的主要是这几个地方。
这里再分享一个调试的方法。在我们写代码的时候有时候代码本身没有报错,但是运行的时候往往会造成软件崩溃。这时,我们可以采用上述的方法,先定位是哪一句语句造成崩溃的,然后再用如下代码:
try:
# 这里是怀疑造成崩溃的语句
except Exception as e:
print(e)
这样,运行的时候就会在下方的输出框输出错误的原因。用以上两种调试方法差不多已经够用了。
学会调试之后,剩下的就是编写代码。编写代码大部分都是使用相应库的API函数,不用全部背,需要什么功能的API就去看用户指南或者百度,这里推荐Google,搜索的效率比较高。或者参考我的代码,我也是边查边写的。
这里主要分享几个比较困扰的算法,第一个就是十六进制和字符串的互相转化。下面这个算法是将字符串转换成十六进制。
for i in range(0, len(input_string)):
string_to_hex = string_to_hex + '{:02X}'.format(input_string[i]) + ' ' # 转换为十六进制
当然有字符串转换成十六进制就有十六进制转换成字符串。
hex_to_string = bytes.fromhex(input_data).decode('gbk') # 转换成字符串
因为我的串口助手支持中文字符显示,所以我用的是GBK的解码方式,就可以转化成十六进制了。
下面就是分享一个验证过的鼠标拉伸、拖拽算法(我也是baidu的),为什么我们需要重写这些算法呢?因为我们自定义标题栏,要先隐藏原有的标题栏,隐藏之后原本的边缘识别自然要重写。实现这个功能主要需要以下几个方法。
- 第一个重写resizeEvent,这个方法主要作用就是每次大小发生计算的时候都要计算以下边缘的位置,以实现拖拽放大缩小。
def resizeEvent(self, QResizeEvent): # 当窗口大小发生变化时,边框的位置需要重新计算,为边缘拉伸做准备
try:
self.corner_rect = [QPoint(x, y) for x in range(self.width() - self.padding, self.width() + 1)
for y in range(self.height() - self.padding, self.height() + 1)]
self.right_rect = [QPoint(x, y) for x in range(self.width() - self.padding, self.width() + 1)
for y in range(1, self.height() - self.padding)]
self.bottom_rect = [QPoint(x, y) for x in range(1, self.width() - self.padding)
for y in range(self.height() - self.padding, self.height() + 1)]
except Exception as e:
print(e)
- 第二个重写mousePressEvent,这个方法的作用是当鼠标点击特定边缘时,进入拖拽放大缩小状态。
def mousePressEvent(self, event): # 鼠标点击事件
if (event.button() == Qt.LeftButton) and (event.pos() in self.corner_rect): # 鼠标点击了右下角
self.corner_drag = True
event.accept()
elif (event.button() == Qt.LeftButton) and (event.pos() in self.right_rect): # 鼠标点击了右边缘
self.right_drag = True
event.accept()
elif (event.button() == Qt.LeftButton) and (event.pos() in self.bottom_rect): # 鼠标点击了下边缘
self.bottom_drag = True
event.accept()
elif (event.button() == Qt.LeftButton) and (event.y() < self.TitleBar.height()): # 鼠标点击在标题栏位置
self.move_drag = True
self.move_drag_position = event.globalPos() - self.pos()
event.accept()
- 第三个重写mouseMoveEvent,这个方法的作用主要是当鼠标移动到边缘时,光标的形状发生相应的改变。如果是进入到的放大缩小模式,即鼠标点击没有释放,则鼠标移动到哪,软件对应方向的大小也会发生改变。
def mouseMoveEvent(self, QMouseEvent):
# 这里主要设置鼠标放在特定位置时鼠标的图标发生变化
if QMouseEvent.pos() in self.corner_rect:
self.setCursor(Qt.SizeFDiagCursor)
elif QMouseEvent.pos() in self.right_rect:
self.setCursor(Qt.SizeHorCursor)
elif QMouseEvent.pos() in self.bottom_rect:
self.setCursor(Qt.SizeVerCursor)
else:
self.setCursor(Qt.ArrowCursor)
# 这里就是实现拉伸、拖拽
if Qt.LeftButton and self.corner_drag:
self.resize(QMouseEvent.pos().x(), QMouseEvent.pos().y()) # 窗口移动的位置
QMouseEvent.accept()
elif Qt.LeftButton and self.right_drag:
self.resize(QMouseEvent.pos().x(), self.height())
QMouseEvent.accept()
elif Qt.LeftButton and self.bottom_drag:
self.resize(self.width(), QMouseEvent.pos().y())
QMouseEvent.accept()
elif Qt.LeftButton and self.move_drag:
self.move(QMouseEvent.globalPos() - self.move_drag_position)
QMouseEvent.accept()
- 第四个mouseReleaseEvent,鼠标松开后复位所有的状态。
def mouseReleaseEvent(self, QMouseEvent): # 鼠标松开,复位所有状态位
self.corner_drag = False
self.right_drag = False
self.bottom_drag = False
self.move_drag = False
上面的四个函数已经可以实现边缘的放大缩小以及点击标题栏拖动的功能。当然还有一个比较常用的就是双击标题栏,可以实现最大化和恢复的功能。代码如下:
def mouseDoubleClickEvent(self, QMouseEvent): # 双击事件
if (QMouseEvent.button() == Qt.LeftButton) and (QMouseEvent.y() < self.TitleBar.height()): # 鼠标双击在标题栏位置
if self.isMaximized(): # 如果窗口已经最大化
self.showNormal() # 变为初始时大小
else:
self.showMaximized() # 实现窗口最大化
下面再分享一个琢磨比较久的问题。在设计UI的时候大家引用的可能都是相对路径/绝对路径(我这里用的是绝对路径),不管用哪种路径都好,有一个问题就是,这个路径只是我的电脑上的,如果我把这个软件移动到别人的电脑,那是不是就无法显示了?
我这里的方法是,先将图片编码成Base64,这个具体怎么编码,直接Baidu就可以了,上面有很多的在线工具可以转换,转换之后把编码用一个新的py文件放起来。
base64编码这里只列出了局部的编码图片,把图片都转换成base64之后,需要在主函数中调用,具体调用方法如下:
# base64
from ImgBase64 import close_first as close_first
from ImgBase64 import close_latter as close_latter
from ImgBase64 import maximize_first as maximize_first
from ImgBase64 import maximize_latter as maximize_latter
from ImgBase64 import normal_first as normal_first
from ImgBase64 import normal_latter as normal_latter
from ImgBase64 import minimize_first as minimize_first
from ImgBase64 import minimize_latter as minimize_latter
from ImgBase64 import title_icon as title_icon
from ImgBase64 import icon as icon
上方是调用的格式,当程序执行的时候需要将base64解码成图片,供程序调用。这里需要在头文件上 "import base64" ,并且还要事先确定好解码的路径,因为这个路径会根据实际用户的路径生成相应的文件(这个路径要跟UI中的路径对应起来!!,因为需要引用的),解码代码如下,picPath是需要解码的路径,name是图片的名字,这里的名字要跟UI设计的名字一样,否则会引用失败。比如,我UI设计的图像资源路径是 C:/images/close_first.png ,那么我解码的路径就是 LoadImg('C:/images/close_first.png', close_first)。这两个要对应起来。如过这样听起来比较抽象,请参考源代码。
# 加载图片资源
def LoadImg(picPath, name):
tmp = open(picPath, 'wb+') # 在指定目录下创建文件
tmp.write(base64.b64decode(name)) # 解码图片
tmp.close() # 关闭写
当然,我觉得这个方法好像有些复杂,在网上有人说可以把图片和程序一起打包成.exe,按照网上的方法我没有成功,不知道什么原因,有成功的欢迎在下方分享经验。
最后分享一个初始化的方法。我们在使用的时候都希望程序能够记录上一次使用过的参数,就不用每次都选择了,这里直接贴出代码,理解一下就好了,比较简单。
def InitSetting(self):
global config
if not os.path.exists('setting.ini'): # 检测是否存在配置文件
open('setting.ini', 'w') # 创建配置文件
config = configparser.ConfigParser() # 加载现有配置文件
config.read('setting.ini') # 读取ini文件
if not config.has_section('globals'): # 如果为空
config['globals'] = {'baudrate': '115200', 'stop': '1', 'data': '8', 'parity': '无校验', 'string': 'Hello'} # 向配置文件写入数据
with open('setting.ini', 'w') as configfile: # with用法,先打开数据再写
config.write(configfile)
# 把配置文件的数据加载到窗口
self.SerialBaudRateComboBox.setCurrentText(config.get('globals', 'baudrate'))
self.SerialStopBitsComboBox.setCurrentText(config.get('globals', 'stop'))
self.SerialDataBitsComboBox.setCurrentText(config.get('globals', 'data'))
self.SerialParityComboBox.setCurrentText(config.get('globals', 'parity'))
self.SendEdit.insertPlainText(config.get('globals', 'string'))
代码部分主要分享这几个算法,如果还有其他的疑问或者有更好的简介可以在下方评论区提出,我再补上去。
网友评论