一、背景
要用 python 做一个小工具,需要在子线程任务中更新界面,例如更新进度条,如果用 Pyqt5,Pyside2 等,可以通过在 QThread 里用pyqtSignal 来发射信号更新 UI,但是由于这俩框架做出来的程序打包成 .exe 后,包体积实在太大,这小工具界面又未复杂到非要使用 Pyqt5 的地步,以包体积换方便用户就不方便了,就选择使用 Tkinter 来实现好了。
最终效果:
二、问题分析
问题一:TKinter 里原生控件太少,连进度条控件也没有找着,只能自己画一个了;
问题二:TKinter 里没有 pyqtSignal 这种东西可用,只能自己做子线程和 UI 线程通信来实现 UI 刷新;
第一个并不是很麻烦,可以用 canvas 自己画一个进度条;
对于第二个问题,我想到了 android 里的 handler 消息机制,android 上也是在子线程做耗时操作,完成后通知 UI 线程更新界面,android 原生 api 自带的 handler 就主要来干这个事情,另外 Rxjava 也可以干这个事情,所以 Rxpy 可能也行,但是在 python 里用 Rx,不会感觉有点怪怪的吗?还是模拟 android 的消息机制在 python 里搞个简易的实现好了。
三、消息流程
这套消息机制有如下几个分工对象:
Handler:外层消息操作对象
Queue:消息队列
Message:消息
另外 android 还有 HandlerThread,Looper,这里通过 tKinter 的 after 实现 looper 的循环。
预期结果是在 tKinter 的 UI 线程创建一个 Handler 实例,并定义一个 handle_msg 方法,子线程可以通过这个 Handler 实例发送消息到 UI 线程的 handle_msg 里,统一在这里做更新界面操作。
四、编码实现
1.进度条
from tkinter import Label, Canvas, StringVar, Frame
class ProgressBar(Frame):
def __init__(self, parent, width=300, height=30, border_width=2):
super(ProgressBar, self).__init__(parent)
self.width = width
self.height = height
self.border_width = border_width
self.canvas = Canvas(self, width=self.width, height=self.height, bg="white")
self.progress_text = StringVar()
self.x1 = self.border_width + 1
self.y1 = self.border_width + 1
self.x2 = self.width - 1
self.y2 = self.height - 1
self.x11 = self.x1 + self.border_width / 2
self.y11 = self.y1 + self.border_width / 2
self.progress_width = self.width - self.x11 - self.border_width / 2 - 1
print(self.x1, self.y1, self.x2, self.y2)
self.init()
def init(self):
self.canvas.grid(row=0, column=0)
# 进度条背景框
self.canvas.create_rectangle(self.x1, self.y1, self.x2, self.y2, outline="green", width=self.border_width)
# 进度条进度
self.fill_rec = self.canvas.create_rectangle(self.x11, self.y11, self.x11, self.y2 - self.border_width / 2,
outline="", width=0, fill="green")
Label(self, textvariable=self.progress_text, width=5).grid(row=0, column=1)
def progress(self, current, total):
self.canvas.coords(self.fill_rec,
(self.x11, self.y11,
self.x11 + (current / total) * self.progress_width,
self.y2 - self.border_width / 2))
f = round(current / total * 100, 2)
self.progress_text.set(str(f) + '%')
self.update()
if f == 100.00:
self.progress_text.set("完成")
2.消息操作相关类
from queue import Queue
class MainQueue(Queue):
def __init__(self):
super(MainQueue, self).__init__()
class Message(object):
def __init__(self, what, obj):
super(Message, self).__init__()
self.what = what
self.obj = obj
class Handler(object):
def __init__(self, view, handle_msg):
self.queue = MainQueue()
self.view = view
self.handle_msg = handle_msg
self.loop()
def loop(self):
while not self.queue.empty():
content = self.queue.get()
self.handle_msg(content)
self.view.after(30, self.loop)
def send_msg(self, msg):
self.queue.put(msg)
def send_obj(self, what, obj):
self.queue.put(Message(what, obj))
3.程序界面
import threading
import time
from tkinter import Button, Tk, TOP
from util.HandlerMessage import Handler
from util.ProgressBar import ProgressBar
class GUI(object):
def __init__(self, root):
self.root = root
self.init_view(root)
# 消息处理
def handle_msg(self, msg):
if msg.what == "key1":
# 更新UI
self.change_progress(msg.obj, 100)
return
def init_view(self, root):
self.root.title("test")
self.root.geometry("400x200+700+500")
self.root.resizable = False
self.progress_bar = ProgressBar(root)
self.progress_bar.pack(anchor="center",pady=20)
self.button_1 = Button(root, text="download", width=10, command=self.start_download)
self.button_1.pack(anchor="center")
# 创建handler
self.handle = Handler(self.root, self.handle_msg)
self.root.mainloop()
def start_download(self):
BackTask(self.handle).run()
def change_progress(self, current, total):
self.progress_bar.progress(current, total)
class BackTask(object):
def __init__(self, handler):
self.handler = handler
def _task(self):
count = 0
while count < 100:
count += 1
# 在子线程发送消息
self.handler.send_obj("key1", count)
time.sleep(30 / 1000)
def run(self):
thread = threading.Thread(target=self._task)
thread.setDaemon(True)
thread.start()
if __name__ == "__main__":
root = Tk()
GUI(root)
到此已完成,使用时如果需要在子线程更新 UI,就在 UI 线程创建一个 Handler 实例,并创建一个处理消息的方法就完了。