Search code examples
pythonasynchronoustkinterpython-asynciopython-multithreading

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


The question is if my solution is a save and pythonic way to update a Tkinter-GUI with data from another thread? Are Locks required? Or how could a Queue help here? This example is working fine but the original application has much more complex data to deal with.

Please focus on AsyncioThread.create_dummy_data() in the minimal working example. The example has two threads. One run the Tkinter-mainloop and the second thread run a asyncio-loop. The asyncio-loop simulates getting some data and refreshing some tkinter.Label with this data.

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

# based on <https://stackoverflow.com/a/47920128/4865723>

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()

The real application

This example is simplified. The real application is downloading (with feedparser) hundreds of xml-files (Newsfeeds) from just as many different websites. The results are displayed in a Tkinter.Treeview where each xml-file has one entry in the TreeView. e. g. the count of entries in the xml-files is shown in the entries of the TreeView (e. g. "Time Magazine (12 entries)"). This should be done everytime one download of an xml-file has finished and not after all xml-file downloads are finished.


Solution

  • This solution is based on comments from other person. It use queue.Queue to share data between the two threads. The Tkinter GUI/Thread use a 1-second-timer to check if new data is in the Queue and use it to refresh its Labels.

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    # based on <https://stackoverflow.com/a/47920128/4865723>
    
    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()
    

    This example is based on https://stackoverflow.com/a/47920128/4865723. Not sure if this is an elegant solution. Please feel free to edit this. It is my goal to make my question and the answer reusable by others.