使用来自第二个线程的数据更新 Tkinter-GUI 中的数据

Update data in a Tkinter-GUI with data from a second Thread

问题是我的解决方案是否是一种使用来自另一个线程的数据更新 Tkinter-GUI 的保存和 pythonic 方式? Lock 是必需的吗?或者 Queue 怎么能在这里提供帮助?此示例运行良好,但原始应用程序需要处理更复杂的数据。

请关注最小工作示例中的AsyncioThread.create_dummy_data()。该示例有 两个线程 。一个 运行 Tkinter-mainloop 和第二个线程 运行 asyncio-loop。异步循环模拟获取一些数据并用这些数据刷新一些tkinter.Label

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# restrict to Python3.5 or higher because of asyncio syntax

# based on <

from tkinter import *
import asyncio
import threading
import random


class AsyncioThread(threading.Thread):
    def __init__(self, asyncio_loop, theWindow):
        self.asyncio_loop = asyncio_loop
        self.theWindow = theWindow
        self.maxData = len(theWindow.varData)
        threading.Thread.__init__(self)


    def run(self):
        self.asyncio_loop.run_until_complete(self.do_data())


    async def do_data(self):
        """ Creating and starting 'maxData' asyncio-tasks. """
        tasks = [
            self.create_dummy_data(number)
            for number in range(self.maxData)
        ]
        completed, pending = await asyncio.wait(tasks)
        results = [task.result() for task in completed]
        print('\n'.join(results))


    async def create_dummy_data(self, number):
        """ One task. """
        sec = random.randint(1, 3)
        data = '{}:{}'.format(number, random.random())
        await asyncio.sleep(sec)

        # IS THIS SAVE?
        self.theWindow.varData[number].set(data)
        print('Thread-ID: {}\tsec: {}\n\t{}' \
               .format(threading.get_ident(), sec, data))

        return data


class TheWindow:
    def __init__(self, maxData):
        # asyncio loop will run in an extra Thread
        self.asyncio_loop = asyncio.get_event_loop()

        # the GUI main object
        self.root = Tk()

        # create the data variable
        self.varData = []
        for i in range(maxData):
            self.varData.append(StringVar())
            self.varData[i].set('<default>')

        # Button to start the asyncio tasks
        Button(master=self.root,
               text='Start Asyncio Tasks',
               command=lambda:self.do_asyncio()).pack()
        # Frames to display data from the asyncio tasks
        for i in range(maxData):
            Label(master=self.root, textvariable=self.varData[i]).pack()
        # Button to check if the GUI is freezed
        Button(master=self.root,
               text='Freezed???',
               command=self.do_freezed).pack()

    def do_freezed(self):
        """ Button-Event-Handler to see if a button on GUI works.
            The GOAL of this example is to make this button clickable
            while the other thread/asyncio-tasks are working. """
        print('Tkinter is reacting. Thread-ID: {}'
              .format(threading.get_ident()))


    def do_asyncio(self):
        """ Button-Event-Handler starting the asyncio part in a separate thread. """
        thread = AsyncioThread(self.asyncio_loop, self)
        thread.start()


if __name__ == '__main__':
    window = TheWindow(5)
    window.root.mainloop()

真正的应用

这个例子被简化了。真正的应用程序是从许多不同的网站下载(feedparser)数百个 xml 文件(新闻源)。 结果显示在 Tkinter.Treeview 中,其中每个 xml 文件在 TreeView 中都有一个条目。 e. G。 xml 文件中的条目数显示在 TreeView 的条目中(例如 "Time Magazine (12 entries)")。 每次 xml 文件下载完成并且 not 完成 all xml 文件下载后,都应执行此操作完成了。

此解决方案基于其他人的评论。它使用 queue.Queue 在两个线程之间共享数据。 Tkinter GUI/Thread 使用 1 秒计时器检查队列中是否有新数据并使用它来刷新其标签。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# based on <

from tkinter import *
import asyncio
import threading
import random
import queue


class AsyncioThread(threading.Thread):
    def __init__(self, the_queue, max_data):
        self.asyncio_loop = asyncio.get_event_loop()
        self.the_queue = the_queue
        self.max_data = max_data
        threading.Thread.__init__(self)

    def run(self):
        self.asyncio_loop.run_until_complete(self.do_data())

    async def do_data(self):
        """ Creating and starting 'maxData' asyncio-tasks. """
        tasks = [
            self.create_dummy_data(key)
            for key in range(self.max_data)
        ]
        await asyncio.wait(tasks)

    async def create_dummy_data(self, key):
        """ Create data and store it in the queue. """
        sec = random.randint(1, 10)
        data = '{}:{}'.format(key, random.random())
        await asyncio.sleep(sec)

        self.the_queue.put((key, data))


class TheWindow:
    def __init__(self, max_data):
        # thread-safe data storage
        self.the_queue = queue.Queue()

        # the GUI main object
        self.root = Tk()

        # create the data variable
        self.data = []
        for key in range(max_data):
            self.data.append(StringVar())
            self.data[key].set('<default>')

        # Button to start the asyncio tasks
        Button(master=self.root,
               text='Start Asyncio Tasks',
               command=lambda: self.do_asyncio()).pack()
        # Frames to display data from the asyncio tasks
        for key in range(max_data):
            Label(master=self.root, textvariable=self.data[key]).pack()
        # Button to check if the GUI is freezed
        Button(master=self.root,
               text='Freezed???',
               command=self.do_freezed).pack()

    def refresh_data(self):
        """
        """
        # do nothing if the aysyncio thread is dead
        # and no more data in the queue
        if not self.thread.is_alive() and self.the_queue.empty():
            return

        # refresh the GUI with new data from the queue
        while not self.the_queue.empty():
            key, data = self.the_queue.get()
            self.data[key].set(data)

        print('RefreshData...')

        #  timer to refresh the gui with data from the asyncio thread
        self.root.after(1000, self.refresh_data)  # called only once!

    def do_freezed(self):
        """ Button-Event-Handler to see if a button on GUI works.
            The GOAL of this example is to make this button clickable
            while the other thread/asyncio-tasks are working. """
        print('Tkinter is reacting. Thread-ID: {}'
              .format(threading.get_ident()))

    def do_asyncio(self):
        """
            Button-Event-Handler starting the asyncio part in a separate
            thread.
        """
        # create Thread object
        self.thread = AsyncioThread(self.the_queue, len(self.data))

        #  timer to refresh the gui with data from the asyncio thread
        self.root.after(1000, self.refresh_data)  # called only once!

        # start the thread
        self.thread.start()


if __name__ == '__main__':
    window = TheWindow(10)
    window.root.mainloop()

此示例基于 。 不确定这是否是一个优雅的解决方案。请随时对此进行编辑。我的目标是让其他人可以重复使用我的问题和答案。

我用另一种方式解决了。 我不是专家,但我发现这个解决方案对我有用。欢迎任何改进!

  1. main().
  2. 中创建 Tk 对象(通常称为 root
  3. main() 中创建一个线程 运行 您的应用程序(我假设它是 Class)并将 root 作为参数传递给它。
  4. main()
  5. 中执行root.mainloop()
  6. 在应用程序 __init__ 中定义 GUI 按钮、标签、条目... class
  7. 像往常一样使用 Tkinter 方法更新应用程序中的 GUI 按钮、标签、条目...class(get()set()、...)

这是一个获得灵感的模板:

from tkinter import *
import _thread


class App(threading.Thread):

    def __init__(self, root, my_param2, my_param3):

        # Creare GUI
        self.root = root

        # Your code here

    def method1(self):
        # Your code here



def main():

    # Create GUI
    root = Tk(className='MyApp')

    # Create 2 threads
    num_threads = 2
    for t in range(num_threads):
    
        try:
            _thread.start_new_thread(App, (root, my_param2, my_param3, ) )

        except:
            print('Error: can not create a thread')

    # tkinter main loop
    root.mainloop()

    print ('You don\'t see this message')


if __name__ == "__main__":

    main()