dakov
dakov

Reputation: 1139

Run thread from Tkinter and wait until it's finished

I have a tkinter application (runs as main thread), within it I open new top-level window - it is a log windows printing result of tests (the test are performed with selenium webdriver). This dialog is also a caller of all the tests.

So I want to display the dialog (as top-level, there is one more window for whole application), run a test, wait until the test is done and print the result, then do the same for another test unit. But I don't want to make the window freeze during the tests.

I've tried to use threads, but it obviously it can work just like that. In this case the dialog doesn't even start till tests are done.

Here's the code of the dialog window.

class TestDialog(tkinter.Toplevel):

    def __init__(self, parent, tester, url):
        super().__init__(parent)        
        self.parent = parent
        self.webtester = tester;

        self.__initComponents()

        self.run(url)            

        self.wait_window(self)

    def __initComponents(self): 
        self.transient(self.parent)

        frame = tkinter.Frame(self)

        self._tarea = tkinter.Text(frame, state='disabled',wrap='none', width=55, height=25)

        vsb = tkinter.Scrollbar(frame, orient=tkinter.VERTICAL, command=self._tarea.yview)
        self._tarea.configure(yscrollcommand=vsb.set)


        self._tarea.grid(row=1, column=0, columnspan=4, sticky="NSEW", padx=3, pady=3)
        vsb.grid(row=1, column=4, sticky='NS',pady=3)
        frame.grid(row=0, column=0, sticky=tkinter.NSEW)

        frame.columnconfigure(0, weight=2)
        frame.rowconfigure(1, weight=1)

        window = self.winfo_toplevel()
        window.columnconfigure(0, weight=1) 
        window.rowconfigure(0, weight=1) 

        self.bind("<Escape>", self.close)

        self.protocol("WM_DELETE_WINDOW", self.close)
        self.grab_set()

    def appendLine(self, msg):
        self._tarea['state'] = 'normal'
        self._tarea.insert("end", msg+'\n')
        self._tarea['state'] = 'disabled'

    def run(self, url):

        self.appendLine("Runneing test #1...")

        try:
            thr = threading.Thread(target=self.webtester.urlopen, args=(url,))
            thr.start() 
        except:
            pass

        thr.join()

        self.webtester.urlopen(url)

        self.appendLine("Running test #2")        
        try: 
            thr = threading.Thread(target=self.webtester.test2)
            thr.start() 
        except:
            pass          

    def close(self, event=None):
        self.parent.setBackgroundScheme(DataTreeView.S_DEFAULT)
        self.parent.focus_set()
        self.destroy()

This dialog is opened from parent window simply by:

testDialog = TestDialog(self.parent, self._webtester, url)

Thank you for any advice.

Upvotes: 5

Views: 3438

Answers (1)

unutbu
unutbu

Reputation: 879271

To prevent the GUI from freezing you need self.run() to end quickly. It needs to spawn a thread, start the thread, then end:

import Queue
sentinel = object()
root = tkinter.Tk()

...
def run(self, url):
    outqueue = Queue.Queue()
    thr = threading.Thread(target=self.run_tests, args=(url, outqueue))
    thr.start()
    root.after(250, self.update, outqueue)

Now the function this thread runs can run for a long time:

def run_tests(self, url, outqueue):
    outqueue.put("Running test #1...")
    self.webtester.urlopen(url)
    outqueue.put("Running test #2")        
    self.webtester.test2()
    outqueue.put(sentinel)

But because Tkinter expects all GUI calls to originate from a single thread, this spawned thread must not make any GUI calls. For it to interact with the GUI, you can send output (such as a status update message) through a Queue.Queue and concurrently let the main Tkinter thread monitor this Queue.Queue periodically (through calls to root.after):

def update(self, outqueue):
    try:
        msg = outqueue.get_nowait()
        if msg is not sentinel:
            self.appendLine(msg)
            root.after(250, self.update, outqueue)
        else:
            # By not calling root.after here, we allow update to
            # truly end
            pass
    except Queue.Empty:
        root.after(250, self.update, outqueue)

Upvotes: 6

Related Questions