V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
seventhbible
V2EX  ›  Python

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

  •  
  •   seventhbible · 2021-02-11 20:51:59 +08:00 · 1881 次点击
    这是一个创建于 1373 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在用 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
    

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

    14 条回复    2021-03-11 11:54:11 +08:00
    ec0
        1
    ec0  
       2021-02-12 21:41:57 +08:00   ❤️ 5
    我改了 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
        2
    seventhbible  
    OP
       2021-02-12 21:58:26 +08:00
    @ec0 我的天,大佬非常非常感谢您的指导!终于解决了这个困扰我很久的问题,谢谢谢谢谢谢!另外可以请大佬解答一下 daemon 这个是啥意思么?
    ec0
        3
    ec0  
       2021-02-12 22:12:53 +08:00   ❤️ 1
    默认情况下 daemon=False
    这样当你的窗口关闭时,UI 线程关闭了,但是 event_manager 里面的线程还在运行,程序没有关闭
    你可以试试:运行程序,关闭窗口,但是在任务管理器里面可以看到 python 还在运行,只是没有显示窗口
    daemon=True 时,主线程关闭,daemon 线程也会关闭
    seventhbible
        4
    seventhbible  
    OP
       2021-02-12 22:53:18 +08:00
    @ec0 原来如此,再次感谢大佬指教!
    seventhbible
        5
    seventhbible  
    OP
       2021-02-13 18:07:19 +08:00
    @ec0 大佬可以追问一个问题嘛!就是现在的做法是把具体的订阅者的业务逻辑放到另外一个子线程去跑了,主线程只管 GUI 。那么如何做到我在 GUI 上点击一个按钮之后,在子线程开始处理具体的业务逻辑(譬如 count )且没有处理结束之前,锁住整个 GUI 的其他按钮模块不允许进行其他业务线程的处理?
    ec0
        6
    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
        7
    seventhbible  
    OP
       2021-02-14 00:45:39 +08:00
    @ec0 如果我的 GUI 上有 N 个按钮的话,就是每个按钮都要这样设定么
    seventhbible
        8
    seventhbible  
    OP
       2021-02-14 00:58:20 +08:00
    其实不只是按钮,我的 gui 上还有其他的类似 combobox,text input entry 框等等可以互动的组件,如果想要一起锁住,请问有比较通用的方法么?
    ec0
        9
    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
        10
    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
        11
    geekzhu  
       2021-02-14 11:26:28 +08:00
    @ec0 我只想给这么有耐心的人点赞!
    seventhbible
        12
    seventhbible  
    OP
       2021-02-14 14:06:27 +08:00
    @ec0 非常感谢大佬这么有耐心帮助我!我马上就去试试!
    dblpx
        13
    dblpx  
       2021-02-15 09:23:07 +08:00
    @ec0 点赞👍!
    AlexShui
        14
    AlexShui  
       2021-03-11 11:54:11 +08:00
    @ec0 大佬真厉害
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2736 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 14:52 · PVG 22:52 · LAX 06:52 · JFK 09:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.