buhtz
buhtz

Reputation: 12202

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.

Upvotes: 1

Views: 14885

Answers (2)

Luca Urbinati
Luca Urbinati

Reputation: 49

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

Upvotes: 0

buhtz
buhtz

Reputation: 12202

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.

Upvotes: 7

Related Questions