利用线程和事件系统更新 tkinter 的进度条,在同步的时候卡住了,求大佬帮看下。。。

2021-02-11 20:51:59 +08:00
 seventhbible

最近在用 python 写工具给小组的其他成员使用,因为需要可以在 GUI 上输入不同的变量值做不同的判断处理所以最后写了一个 gui 。现在问题是,最后我希望写一个进度条,可以在主线程的 for-loop 中,每处理完一个任务后发送一个事件,给在另外一个线程的事件管理器,事件管理器可以立刻更新响应 GUI 的进度条的进度。 下面是我写的代码:

from queue import Queue, Empty
from threading import *
from tkinter import *
import time
from tkinter import ttk

EVENT_TYPE_1 = "Count"
MAX_NUMBER = 10
CUR_NUMBER = 0

class event_manager:
    def __init__(self):
        self._eventQueue = Queue()
        self._thread = Thread(target=self.Run)
        self._handlers = {}
        self._active = False

    def Start(self):
        self._active = True
        self._thread.start()

    def Run(self):
        while self._active is True:
            try:
                event = self._eventQueue.get(block=True, timeout=1)
                self.Process(event)
            except Empty:
                pass

    def Process(self, event):
        if event.type in self._handlers:
            for handler in self._handlers[event.type]:
                handler()
        else:
            pass

    def Stop(self):
        self._active = False
        self._thread.join()

    def addEventListenter(self, type_, handler):
        try:
            handlerList = self._handlers[type_]

        except KeyError:
            handlerList = []
            self._handlers[type_] = handlerList

        if handler not in handlerList:
            handlerList.append(handler)

    def removeEventListenter(self, type_, handler):
        try:
            handlerList = self._handlers[type_]
            if handler in handlerList:
                handlerList.remove(handler)
            if not handlerList:
                del self._handlers[type_]
        except KeyError:
            pass

    def sendEvent(self, event):
        self._eventQueue.put(event)


class Event:
    def __init__(self, event_event_name, cur_done_task, type_=None):  
        self.type = type_
        self._event_name = event_event_name
        self._curDoneTask = cur_done_task


class EventSource:
    def __init__(self, event_name, event_mgr, max_number, type):
        self._event_name = event_name
        self._event_manager = event_mgr
        self._type = type
        self._max_number = max_number

    def count(self):
        global CUR_NUMBER
        for i in range(self._max_number):
            CUR_NUMBER = i + 1
            print("************ main thread start:now start process {} - count : {}".format(self._event_name, CUR_NUMBER))
            event = Event("test", CUR_NUMBER, type_=self._type)
            self._event_manager.sendEvent(event)
            time.sleep(1)

class GUIListener(Tk):
    def __init__(self):
        super(GUIListener, self).__init__()

        self.title("Progress GUI")
        self.geometry("1200x805+600+100")
        self.config(bg="#535353")
        self.resizable(True, True)

        self.progressBar = ttk.Progressbar(master=self, orient=HORIZONTAL, maximum=MAX_NUMBER, length=300)
        self.progressBar.pack()
        self.button = ttk.Button(self, text="Run", command=lambda: self.button_function(MAX_NUMBER))
        self.button.pack()

    def update_progress_value(self):
        print("************Sub thread start: detect progress bar value is now...{}".format(self.progressBar['value']))
        self.progressBar['value'] = CUR_NUMBER
        self.progressBar.update_idletasks()
        print("************Sub thread start: update progress bar value to...{}".format(CUR_NUMBER))

    def button_function(self,max_number):
        es = EventSource("eventSource", eventMgr, max_number, EVENT_TYPE_1)
        es.count()

if __name__ == '__main__':
    gui = GUIListener()

    eventMgr = event_manager()
    eventMgr.addEventListenter(EVENT_TYPE_1, gui.update_progress_value)
    eventMgr.Start()

    gui.mainloop()

现在的问题是,返回的结果很奇怪,是这样的:

************ main thread start:now start process eventSource - count : 1
************ main thread start:now start process eventSource - count : 2
************ main thread start:now start process eventSource - count : 3
************ main thread start:now start process eventSource - count : 4
************ main thread start:now start process eventSource - count : 5
************ main thread start:now start process eventSource - count : 6
************ main thread start:now start process eventSource - count : 7
************ main thread start:now start process eventSource - count : 8
************ main thread start:now start process eventSource - count : 9
************ main thread start:now start process eventSource - count : 10
************Sub thread start: detect progress bar value is now...0.0
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10
************Sub thread start: detect progress bar value is now...10
************Sub thread start: update progress bar value to...10

请大佬指点一下,因为我希望的结果是类似下面这样立刻响应插入的顺序:

************ main thread start:now start process eventSource - count : 1
************Sub thread start: detect progress bar value is now...0.0
************Sub thread start: update progress bar value to...1
************ main thread start:now start process eventSource - count : 2
************Sub thread start: detect progress bar value is now...1.0
************Sub thread start: update progress bar value to...2
************ main thread start:now start process eventSource - count : 3
************Sub thread start: detect progress bar value is now...2.0
************Sub thread start: update progress bar value to...3
...
etc

真的这个问题困扰了我蛮久了,希望大佬可以不吝赐教,指点问题发生在哪里,如何修改可以达到预期目的,感激不尽!

1703 次点击
所在节点    Python
14 条回复
ec0
2021-02-12 21:41:57 +08:00
我改了 2 个地方

1. class event_manager 里面的 __init__
从 self._thread = Thread(target=self.Run)
改成 self._thread = Thread(target=self.Run, daemon=True)

2. class GUIListener 里面的 button_function
从 es.count()
改成 Thread(target=es.count).start()

count 被 UI 线程调用,而 count 里面有 sleep,不要在 UI 线程写 sleep,另外开一个线程
seventhbible
2021-02-12 21:58:26 +08:00
@ec0 我的天,大佬非常非常感谢您的指导!终于解决了这个困扰我很久的问题,谢谢谢谢谢谢!另外可以请大佬解答一下 daemon 这个是啥意思么?
ec0
2021-02-12 22:12:53 +08:00
默认情况下 daemon=False
这样当你的窗口关闭时,UI 线程关闭了,但是 event_manager 里面的线程还在运行,程序没有关闭
你可以试试:运行程序,关闭窗口,但是在任务管理器里面可以看到 python 还在运行,只是没有显示窗口
daemon=True 时,主线程关闭,daemon 线程也会关闭
seventhbible
2021-02-12 22:53:18 +08:00
@ec0 原来如此,再次感谢大佬指教!
seventhbible
2021-02-13 18:07:19 +08:00
@ec0 大佬可以追问一个问题嘛!就是现在的做法是把具体的订阅者的业务逻辑放到另外一个子线程去跑了,主线程只管 GUI 。那么如何做到我在 GUI 上点击一个按钮之后,在子线程开始处理具体的业务逻辑(譬如 count )且没有处理结束之前,锁住整个 GUI 的其他按钮模块不允许进行其他业务线程的处理?
ec0
2021-02-13 23:55:09 +08:00
button["state"] = "disabled" # 按钮不可点击
button["state"] = "normal" # 按钮可以点击

比如,你可以在之前的 Thread(target=es.count).start() 前面加上
self.button["state"] = "disabled"

在 class EventSource 的 count 函数 for 语句的后面加上
for i in range(self._max_number):
...
gui.button["state"] = "normal"
seventhbible
2021-02-14 00:45:39 +08:00
@ec0 如果我的 GUI 上有 N 个按钮的话,就是每个按钮都要这样设定么
seventhbible
2021-02-14 00:58:20 +08:00
其实不只是按钮,我的 gui 上还有其他的类似 combobox,text input entry 框等等可以互动的组件,如果想要一起锁住,请问有比较通用的方法么?
ec0
2021-02-14 01:04:55 +08:00
是啊,很麻烦,不过你可以给 button 取有序的名字,比如 button0,button1,然后
for i in range(n):
(缩进)getattr(gui, "button"+str(i))["state"] = "disabled"

批量 disable 按钮
ec0
2021-02-14 02:06:38 +08:00
我找到了这段代码,可以批量 disable TButton,更通用一些,改造一下 if 语句可以 disable 其他类型的控件,不过不是所有控件都有 state 属性的

for child in gui.winfo_children():
(缩进)if child.winfo_class() == 'TButton':
(缩进)(缩进)child['state'] = 'disabled'
geekzhu
2021-02-14 11:26:28 +08:00
@ec0 我只想给这么有耐心的人点赞!
seventhbible
2021-02-14 14:06:27 +08:00
@ec0 非常感谢大佬这么有耐心帮助我!我马上就去试试!
dblpx
2021-02-15 09:23:07 +08:00
@ec0 点赞👍!
AlexShui
2021-03-11 11:54:11 +08:00
@ec0 大佬真厉害

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/752981

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX