开完一坑又一坑,这应该是GUI
系列的完结稿了。时至今日,很多方法和函数已经忘记,一部分整理到了仓库,用时查阅;一部分学会了查官方文档。比如布局中的addSpacing
和addStretch
填充,以及不同空间该如何Qt.Align
,需要大量的经验。用过一次就会知道功能,所以,官方文档永远的神。
背景 在之前的制作倒车雷达 中,已经说过了多线程的应用背景。那会儿是项目驱动,现在来彻底了结。一个软件中,如果制作一个计数功能。那么主进程进入计数函数,没有进程负责界面的显示,就会导致软件卡死。如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import sys, timefrom PyQt5.QtWidgets import (QMainWindow, QWidget, QHBoxLayout, QApplication, QPushButton) class mainwindow (QMainWindow ): def __init__ (self ): super (mainwindow, self).__init__() layout = QHBoxLayout() w = QWidget() w.setLayout(layout) self.setCentralWidget(w) btn = QPushButton("点击" ) layout.addWidget(btn) btn.clicked.connect(self.count) def count (self ): num = 1 while num <= 12000 : time.sleep(0.1 ) num += 1 if __name__ == '__main__' : app = QApplication([]) m = mainwindow() m.show() sys.exit(app.exec ())
需要额外注意的是,在Qt的开发中,一定不能使用time.sleep()这种方法。Qt是框架是基于事件循环的,time.sleep()
因为它会阻塞事件的循环,导致窗口冻结,直接跳到sleep
后的程序,从而阻止了GUI的重新绘制,并没有中间的过程。所以在Qt中,可以考虑使用多线程来解决这些问题,如:分为显示线程和工作线程,显示事件负责GUI的显示,工作事件负责刷新物体的位置。
QTimer() 这是一个实现多线程的最简单的工具,通常用于周期性检测,比sleep()
这种强行停止窗口事件的循环要好上很多。它有常用的两个函数:
start(int n)
,表示n毫秒后,定时器会发出信号。我们只需要把发出的信号绑定到对应函数就可以工作了
timeout
就是发出的信号,将它绑定到槽函数上
stop
,停止计时器
我们来写一个最简单却常用的功能。当软件卡顿时,一般会出现转圈圈的图标,表示正在加载。假设就让它转5秒,5秒后通过另一个线程让它消失,不影响主窗口的显示。如果直接time.sleep(5)
,那么圆圈不会转动的,界面会直接到在这5秒内静止,然后一步跳到5秒后。所以以下程序是错误的:
1 2 3 self.move() time.sleep(5 ) self.stop()
Qt是循环的框架,所以应该考虑用 QTimer 代替 sleep 正确姿势如下,图片文件自己去下载一个吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import sys, timefrom PyQt5.QtWidgets import (QApplication, QLabel, QMainWindow, QWidget, QHBoxLayout) from PyQt5.QtGui import QMoviefrom PyQt5.QtCore import Qt, QTimerclass MainWidget (QWidget ): def __init__ (self, parent=None ): super (MainWidget, self).__init__(parent) self.setAttribute(Qt.WA_StyledBackground, True ) self.setStyleSheet('background-color: white' ) self.label = QLabel() self.label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) self.loading = QMovie("images/loading.gif" ) self.label.setMovie(self.loading) self.layout = QHBoxLayout() self.layout.addWidget(self.label) self.loading_start() self.setLayout(self.layout) self.t = QTimer() self.t.start(5000 ) self.t.timeout.connect(self.loading_end) def loading_end (self ): self.t.stop() self.loading.stop() time.sleep(0.2 ) self.label.clear() def loading_start (self ): self.loading.start() class MainWindow (QMainWindow ): def __init__ (self ): super (MainWindow, self).__init__() W = MainWidget() self.setCentralWidget(W) if __name__ == '__main__' : app = QApplication([]) window = MainWindow() window.show() sys.exit(app.exec ())
效果展示,你看多好,嘿嘿:
QThread() 这个实现多线程就比较强大了,可以完成更为复杂的业务。只需要继承这个类,并重写run()
函数就可以了。外部实例化这个类,并调用start()
函数,会启动线程;线程启动后,会自动调用实现的 run()
函数。
此外还有started, finised等信号来完成资源的加载与释放;
isRunning(), isfinished()来检测线程是否还在执行;
同样,也可以定义自己的信号;
线程执行完毕后,可以调用quit(), exit()来退出线程;
如果不是十分有把握,请不要使用terminate
来终止线程,因为它不是线程安全的,会导致资源、锁紊乱。
给个例子,包含以上所有提到的常用方法。主窗口启动一个子线程,线程每隔一秒发送数据给主窗口,主窗口显示数据。发送到一定量后,停止子线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import sys, timefrom PyQt5.QtWidgets import (QMainWindow, QTextEdit, QWidget, QApplication, QHBoxLayout) from PyQt5.QtCore import QThread, pyqtSignalclass Worker (QThread ): signal_out = pyqtSignal(str ) def __init__ (self, working ): super (Worker, self).__init__() self.working = working self.num = 0 self.finished.connect(self.finish) def finish (self ): print ('finish' ) def run (self ): while self.working == True : if self.num != 0 and self.num % 2 == 0 : self.signal_out.emit("stop" ) string = "Index " + str (self.num) self.num += 1 self.signal_out.emit(string) self.sleep(1 ) class MainWindow (QMainWindow ): def __init__ (self ): super (MainWindow, self).__init__() self.text = QTextEdit() layout = QHBoxLayout() layout.addWidget(self.text) w = QWidget() w.setLayout(layout) self.setCentralWidget(w) self.worker = Worker(working=True ) self.start() self.worker.signal_out.connect(self.display) def display (self, string ): if string == 'stop' : self.text.clear() self.worker.working = False self.worker.quit() else : QApplication.processEvents() self.text.append(string) def start (self ): self.worker.start() if __name__ == "__main__" : q = QApplication([]) m = MainWindow() m.show() sys.exit(q.exec ())
循环注意 治理需要注意的是,quit()
方法会退出当前事件的循环。所以,如果线程处理的事件是死循环时,即使调用quit()
是无法退出的。
1 2 3 4 5 6 7 8 9 while True : string = "Index " + str (self.num) self.num += 1 self.signal_out.emit(string) self.sleep(1 ) if self.num != 0 and self.num % 2 == 0 : self.signal_out.emit("stop" ) self.worker.quit()
但是,一般程序并不需要quit()
,因为这个线程根本就不需要事件循环。如以下程序虽然没有出错,但不够优雅:
1 2 3 4 5 6 7 8 9 10 11 while self.working == True : if self.num != 0 and self.num % 2 == 0 : self.signal_out.emit("stop" ) string = "Index " + str (self.num) self.num += 1 self.signal_out.emit(string) self.sleep(1 ) self.worker.working = False self.worker.quit()
wait
一般放在start
和terminate
之后,前者是等待线程结束,所以配合下文提到的QApplication.processEvents()
使用更加。后者是线程被强行杀死后可能没有立刻死亡,这取决于系统的调度策略,等待相关资源回收完毕。
processEvents() 最终呈现的UI界面,要持续不断地循环刷新,以保证显示流畅、能及时响应用户输入。一般要有一个良好的帧率,比如每秒刷新60帧, 即经常说的FPS 60, 换算一下 1000 ms/ 60 ≈ 16 ms,也就是每隔16毫秒刷新一次。而我们有时候又需要做一些复杂的计算,这些计算的耗时远远超过了16毫秒。
在没有计算完成之前,主线程不会退出计算任务,相当于显示被阻塞,事件循环得不到及时处理,就会发生UI卡住的现象。这种场景下,就可以使用Qt为我们提供的接口,立即处理一次事件循环,来保证UI的流畅。所以,在容易卡顿的地方调用processEvents()
函数即可。
MWE 既然都是完结稿了,留个Qt
的最小实例在这里吧,以后方便做简单的测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import sys, timefrom PyQt5.QtWidgets import (QMainWindow, QWidget, QHBoxLayout, QApplication, QPushButton) class mainwindow (QMainWindow ): def __init__ (self ): super (mainwindow, self).__init__() layout = QHBoxLayout() w = QWidget() w.setLayout(layout) self.setCentralWidget(w) btn = QPushButton("点击" ) layout.addWidget(btn) btn.clicked.connect(self.count) def count (self ): pass if __name__ == '__main__' : app = QApplication([]) m = mainwindow() m.show() sys.exit(app.exec ())
其实,学的越多才发现,我对真正的Qt
一无所知。而以后的成长,只能靠阅读文档、阅读一些经验性博客来提升自己了。
参考
qthread官方文档,想要的一切函数都有
processEvents文档
https://zhuanlan.zhihu.com/p/72758194
让Qt更快的响应 ,如果不忙,且需要开发软件,我会考虑翻译这篇文章。