1

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.

buhtz
  • 10,774
  • 18
  • 76
  • 149
  • 1
    You need this answer: https://stackoverflow.com/questions/17847869/python-tkinter-label-redrawing-every-10-seconds To summarize: UI is updated from the main thread, but you can make a Queue or a deque to communicate with this ui updater from any other threads. Queue and deque are thread-safe generally. – leovp Dec 22 '17 at 10:11
  • @leovp You mean __(1)__ I should store the data (from the asyncio coroutines) in a `Queue` object. And __(2)__ I should use `after()` to set a tmer in Tkinter that periodicly check that queue? When I think about it: My main question here is how does the Tkinter-Thread knows when to look into the Queue or how to prevent Tkinter to look to often or parallel into it? – buhtz Dec 22 '17 at 11:03
  • 1
    I'd choose some interval like 1 second. Almost-real-time. Other than that you shouldn't worry about access to the Queue resulting in anything out of the ordinary - it's designed for this. – leovp Dec 22 '17 at 11:21

2 Answers2

6

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.

CommonSense
  • 4,232
  • 2
  • 14
  • 38
buhtz
  • 10,774
  • 18
  • 76
  • 149
0

I solved it in another way. I'm not an expert, but I found this solution working for me. Any improvement is welcomed!

  1. Create the Tk object (usually called root) in the main().
  2. Create a thread to run your application (I assume it is a Class) in the main() and pass root to it as a parameter.
  3. Execute root.mainloop() in the main()
  4. Define your GUI buttons, labels, entries, ... in the __init__ of your application class
  5. Update your GUI buttons, labels, entries, ... in your application class with Tkinter methods as usual (get(), set(), ...)

This is a template to get the idea:

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