TKinter在子线程更新进度条UI

一、背景

要用 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 实例,并创建一个处理消息的方法就完了。

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值