首页 > 解决方案 > 使用线程和队列将记录器消息重定向到 python 中的 tkinter 小部件

问题描述

问候社区。我面临以下问题:我有一个简单的 tkinter GUI,带有一个按钮,该按钮启动许多计算,同时在 tkinter 小部件中打印与计算进度相关的各种消息。

我已经阅读了一些帖子,据我了解,最有效的方法是在主线程中创建一个队列,将队列绑定到记录器,在单独的线程中运行计算,并将其消息重定向到队列由主线程使用间隔轮询。

但我显然做错了什么。消息写在我的滚动框架小部件中,但在线程操作完成后一次全部写入。

这是我的代码(我会尽可能简化它):

class GFTGUI(ttk.Frame):

    def __init__(self, parent):
        ttk.Frame.__init__(self, parent)
        self.app = parent

        # create widget to store messages  
        logger_pane = ttk.PanedWindow(self.app, orient=VERTICAL)
        logger_pane.grid(row=2, column=0, sticky=(N, W, E, S), padx=10, pady=10)
        self.logger_frame = ttk.Labelframe(logger_pane, text="Message Board")
        logger_pane.add(self.logger_frame, weight=1)

        self.scrolled_text = ScrolledText(self.logger_frame, state='disabled')
        self.scrolled_text.grid(row=0, column=0, sticky=(N, S, W, E))
        self.scrolled_text.configure(font='TkFixedFont')

        # configure GUI logger
        ConfigureTkFrameLogger(self.logger_frame, self.scrolled_text, self.gft)

        # create the run button
        self.run_button = Button(self.gft, text="Run!", command=self.run_command, height=35)
        self.run_button.grid(column=0, row=1, padx=3, pady=4)

    def run_command(self): 
        th = threading.Thread(target=run_calculations)  # see final block of code
        th.start()
        th.join()

def start_gui():
    app = Tk()
    GFTGUI(app)
    app.mainloop()

if __name__ == '__main__': start_gui()

然后我有一个单独的文件(称为 helpers.py)来存储记录器的类:

logger_user = logging.getLogger('user')

class ConfigureTkFrameLogger:

    def __init__(self, logger_frame, scrolled_text):

        self.logger_frame = logger_frame
        self.scrolled_text = scrolled_text

        # Create a logging handler using a queue
        self.log_queue = queue.Queue()
        self.queue_handler = QueueHandler(self.log_queue)
        formatter = logging.Formatter('%(asctime)s: %(message)s', datefmt='%H:%M:%S')
        self.queue_handler.setFormatter(formatter)
        self.queue_handler.set_name('gui')
        logger_user.addHandler(self.queue_handler)

        # Start polling messages from the queue
        self.logger_frame.after(100, self.poll_log_queue)

    # Check every 100ms if there is a new message in the queue to display
    def poll_log_queue(self):

       while True:
          try: record = self.log_queue.get(block=False)
          except queue.Empty: break
          else: self.display(record)

       self.logger_frame.after(100, self.poll_log_queue)

    def display(self, record):
        msg = self.queue_handler.format(record)
        self.scrolled_text.configure(state='normal')
        self.scrolled_text.insert(END, msg + '\n', record.levelname)
        self.scrolled_text.configure(state='disabled')
        self.scrolled_text.yview(END)  # Autoscroll to the bottom

class QueueHandler(logging.Handler):

   def __init__(self, log_queue):
       super().__init__()
       self.log_queue = log_queue

   def emit(self, record): self.log_queue.put(record)

最后,我的计算是在存储在单独文件中的方法中完成的:

logger = logging.getLogger('user')

def run_calculations(some_args...):

    # this message should be printed as soon as the calculations begin
    logger.info('Reading input file')
    
    do other stuff here 

我想我已经接近让它发挥作用了。我只需要一点推动!

标签: pythonmultithreadingloggingtkintermessage-queue

解决方案


这将是一个评论,但我“必须有 50 名声望才能发表评论”,所以。
不看其他任何东西,我看到了这个:

th.start()
th.join()

不要开始并立即加入(== 等待线程完成)。


推荐阅读