前言
诚如标题所见,我在使用Pyqt5进行开发时,先后遇到了上面几个问题。本篇博客就用来记录遇到问题/解决问题的过程,希望能给遇到相同问题的读者一些参考。
项目背景
我的项目是构建一个可视化的交互界面,通过界面上的按钮可调用后台的爬虫程序。因此,需要通过添加一个进度条来反映当前的爬取进度。
进度条
Pyqt5设有进度条控件QProgressBar,官方文档提供了一个按钮驱动定时器加载进度条的例子。本次应用和官方例程略有不同。
进度条创建与样式设定
首先创建一个进度条,设定位置,并用QSS设定样式。
代码语言:javascript复制self.pb = QtWidgets.QProgressBar(self.centralwidget)
self.pb.setGeometry(QtCore.QRect(230, 690, 1021, 41))
self.pb.setStyleSheet("QProgressBar {border: 2px solid grey; border-radius: 5px; background-color: #FFFFFF; text-align:center; font-size:20px}")
这里的样式主要是对进度条外框进行修改,默认情况进度字体显示在进度条右侧,设置后将字体居中在进度条内,进度条则使用默认情况的绿色进度条,自带了动态加载光效,效果如图所示。
之后,设置进度条的范围[0,100],并将进度条在默认情况下进行隐藏。
代码语言:javascript复制self.pb.setRange(0, 100)
self.pb.hide()
进度条更新
使用pyqt5独特的信号与槽函数可进行进度条的更新。进度条设置函数setvalue()
由于进度条总长度是未知的,因此首先在进度条开始更新之前,需要先获取总任务量的数据,然后将完成任务量/总任务量,映射到[0,100]的区间内进行更新。
此外,还需要获取一个信号来标记是否结束,如果结束,则立刻将进度条设置为100%,同时弹出提示信息(本程序是弹出一个提示框)
定义两个信号:
progressBarValue
:用来回传当前换算后的进度数值
signal_done
:用来回传完成标记(由于pyqtSignal无法回传bool型数据,采用int型来进行区分。0表示未完成,1表示完成)
对应两个槽函数:
callback
:接收progressBarValue信号
callback_done
:接收signal_done信号
更新逻辑:初始进度条为隐藏状态,点击按钮,进度条进行显示,并设定初始值为0。当所有链接获取完之后,进度条开始逐渐更新(每间隔十个数据进行一次进度条更新)。若全部爬取完成(signal_done发送1信号),进度条填满,并弹出提示框。 核心代码如下:
代码语言:javascript复制 # 两个参数初始化
self.pb.setValue(0) # 设置进度条为0
self.is_done = 0 # 设置完成标记 完成/未完成 1/0
self.progressBarValue.connect(self.callback)
self.signal_done.connect(self.callback_done)
# 回传进度条参数
def callback(self, i):
self.pb.setValue(i)
# 回传结束信号
def callback_done(self, i):
self.is_done = i
if self.is_done == 1:
self.messageDialog1()
progressBarValue = pyqtSignal(int) # 更新进度条
signal_done = pyqtSignal(int) # 是否结束信号
Linklist_sum = self.pro_name.get_crawler_link()
length = len(Linklist_sum)
for start in range(0, length, 10):
self.pro_name.run_crawlertask(start, Linklist_sum)
self.progressBarValue.emit(int(start / length * 100)) # 发送进度条的值信号
self.signal_done.emit(1) # 发送结束信号
def messageDialog1(self):
msg_box = QMessageBox(QMessageBox.Information, '通知', '信息爬取已结束')
self.pb.setValue(100) # 如果爬取成功
msg_box.exec_()
多线程更新
直接将进度条更新的程序段和要调用的程序段放在一起会出现一个问题。当调用程序段运行时,qt界面会卡住不动,造成“假死”现象。 因此,要解决这个问题,就要引入多线程。将后台程序放入到一个子线程中运行,同时将数值传递给主线程,在主线程中进行UI的更新。 修改后的进度条更新程序段如下:
代码语言:javascript复制# 封装调用子线程执行程序name
def run_py(self, name):
# 两个参数初始化
self.pb.setValue(0) # 设置进度条为0
self.is_done = 0 # 设置完成标记 完成/未完成 1/0
self.thread_1 = Runthread(pro_name=name)
self.thread_1.progressBarValue.connect(self.callback)
self.thread_1.signal_done.connect(self.callback_done)
self.thread_1.start()
# 回传进度条参数
def callback(self, i):
self.pb.setValue(i)
# 回传结束信号
def callback_done(self, i):
self.is_done = i
if self.is_done == 1:
self.messageDialog1()
# Runthread子线程
class Runthread(QThread):
progressBarValue = pyqtSignal(int) # 更新进度条
signal_done = pyqtSignal(int) # 是否结束信号
def __init__(self, pro_name):
super(Runthread, self).__init__()
self.pro_name = pro_name
def run(self):
Linklist_sum = self.pro_name.get_crawler_link()
length = len(Linklist_sum)
for start in range(0, length, 10):
self.pro_name.run_crawlertask(start, Linklist_sum)
self.progressBarValue.emit(int(start / length * 100)) # 发送进度条的值信号
self.signal_done.emit(1) # 发送结束信号
按钮美化
甲方要求我做一个科技风格的按钮,然而没给我设计贴图,于是我采用QSS的qlineargradient实现渐变填充,先看效果。 常规状态:
鼠标悬浮状态:边框变红
鼠标按下状态:字体下沉
相关代码:
代码语言:javascript复制font = QtGui.QFont()
font.setFamily("方正粗黑宋简体")
font.setPointSize(18)
self.pushButton.setFont(font)
self.pushButton.setObjectName("pushButton")
self.pushButton.setStyleSheet("QPushButton{background:qlineargradient(spread:reflect, x1:0, y1:1, x2:0, y2:0, "
"stop:0 #0a4a83, stop:0.5 #186e99, stop:1 #239cd8);"
"border:2px solid qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:0,"
"stop:0 #002aff, stop:0.5 #00aeff, stop:1 #00e6fc); border-radius:1px; "
"color:#d0f2f5} "
"QPushButton:hover:!pressed {border:1px solid #f8878f;}"
"QPushButton:pressed {padding-left:6px;padding-top:6px;border:1px solid #f8878f;}")
另外提供另一种美化示例:
相关代码
代码语言:javascript复制self.pushButton.setStyleSheet("QPushButton{background:qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, "
"stop:0 #00ffff, stop:0.5 #1D2B56, stop:1 "
"#00ffff);border:0px;border-radius:1px;color:white;}")
选择框美化
顺带一提选择框美化,我的界面风格是暗黑系的,有一个开源的QSS风格qdarkstyle
可以直接套用。
import qdarkstyle
self.comboBox = QtWidgets.QComboBox(self.centralwidget)
self.comboBox.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) # 设置暗黑风格
当然,也可以全局配置该风格。
代码语言:javascript复制app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
但是我觉得该风格的按钮设计没上面自己设计的靓眼,因此局部应用了该风格。
图片编码
如果在pyqt中的界面使用了贴图,在用pyinstaller打包成exe文件后,如果贴图和exe文件不在同一路径下,则会无法显示。
为了解决这一问题,可以使用pyqt自带的Pyqrc
将图片资源编码成二进制数据,从而能够一起打包进exe文件。
首先建立文件img.qrc
将用到的图片写进去,比如,我用到了四张png图片。
<RCC>
<qresource prefix="png">
<file>images/title_bg.png</file>
<file>images/button_bg.png</file>
<file>images/bg.png</file>
<file>images/2.png</file>
</qresource>
</RCC>
之后,在pycharm中配置Pyqrc,配置方法可以参见这篇博客PyCharm中配置与PyQT5相关的External tools 然后,就能在pycharm中快速使用pyqrc进行转换。
转换之后,会生成img_rc.py文件。 在引用贴图的py文件中,引入该文件即可。
代码语言:javascript复制import img_rc
再次进行打包,生成的exe即包含图片信息。
开机自启动
exe文件完成了,甲方又给我提了最后一条需求,要求能够设置开机自启动。 这里,我提供两种方法。
添加注册表方法
第一种方式稍微复杂一些,通过python程序,将生成的exe添加到系统的启动注册表内。
代码语言:javascript复制import win32api
import win32con
import sys
import os
sys.setrecursionlimit(1000000)
name = 'auto_run'
cur_path = os.getcwd()
path = cur_path '\' '你的程序名.exe'
KeyName = r'Software\Microsoft\Windows\CurrentVersion\Run'
try:
key = win32api.RegOpenKey(win32con.HKEY_CURRENT_USER, KeyName, 0, win32con.KEY_ALL_ACCESS)
win32api.RegSetValueEx(key, name, 0, win32con.REG_SZ, path)
win32api.RegCloseKey(key)
except:
print('error!')
print('success!')
运行之后输出success即添加成功。 打开系统注册表和任务管理器,可以看到添加的内容。
如果需要关闭,在任务管理器内设置禁用即可。
bat脚本方法
正常来说,上面那种方法能够实现开机自启动,但是如果exe有个功能是打开当前程序文件夹,该方法会出现问题。开机启动后,打开当前文件夹会诡异地定位到C盘的system32文件夹里。 因此,有了下面这个方式,不仅可以解决这个问题,并且更加方便。
Windows在C盘中提供了一个启动文件夹(win R:输入shell:startup
即可进入),程序放入该文件夹中后,开机就能自动启动程序。由于我的程序涉及打开当前文件夹的操作,因此不能直接将程序放进去,而是将程序的快捷方式放进去。
首先创建快捷方式,命名为"shortcut"。 然后创建bat脚本,输入
代码语言:javascript复制xcopy "shortcut.lnk" "C:Users%username%AppDataRoamingMicrosoftWindowsStart MenuProgramsStartup" /Y
这里使用xcopy
命令进行快捷方式的复制,其中/Y
表示目标存在同名文件的情况取消提示以确认要覆盖。xcopy的更多参数设置可参考批处理(bat)xcopy详解
另外,创建快捷方式后的扩展名lnk
不会直接显示,但在写脚本中需要进行补充。
如果快捷方式中有中文,需要将bat脚本内容改成ANSI编码,修改方式可参考Bat批处理命令执行中文路径方法。