ArthurDent
ArthurDent

Reputation: 179

Controlling another application from within tkinter

I don't know if what I'm seeking to achieve is even possible. I have written a tkinter app which imports a method from an external class. This method runs a hill-climbing algorithm which will run perpetually and try to improve upon the "score" that it has calculated. After each pass, it presents the current output and score to the user and asks (on the command line) if they wish to continue.

The first challenge in getting this working was to implement threading. I have this working, but I don't don't know if I have done it correctly.

The algorithm will continue until the user signals that they have got the answer they were looking for, or loses the will to live and presses CTRL-C.

In my tkinter main app, this presents me with two problems:

  1. How to display the output from this external method. I tried to write a while loop that polled the output field periodically, (see commented out section of the "start_proc" method) but that was clearly never going to work, and besides, I would ideally like to see real-time output; and
  2. How to interact with the algorithm to either continue or stop (see commented out section of the "my_long_procedure" method). As you can see, I can inject a "stopped" attribute, and that does indeed halt the algorithm, but I can't take my eyes off the output because the desired answer may have gone past before I can press stop.

Below is, I hope, a simplified bare-bones example of what I am trying to do.

This is a learning exercise for me and I would be grateful of any help.

import tkinter as tk
from threading import Thread
from random import randint
import time

class MyTestClass: # This would actually be imported from another module
    def __init__(self):
        self.stopped = False

    def my_long_procedure(self):
        # Fake method to simulate actual algorithm
        count = 0
        maxscore = 0
        i = 0
        while count < 1000 and not self.stopped:
            i += 1
            score = randint(1,10000)
            if score > maxscore:
                maxscore = score
                self.message = f'This is iteration {i} and the best score is {maxscore}'
                print(self.message)
                # self.carry_on = input("Do you want to continue? ")
                # if self.carry_on.upper() != "Y":
                #     return maxscore
            time.sleep(2)
        print('OK - You stopped me...')

class MyMainApp(tk.Tk):
    def __init__(self, title="Sample App", *args, **kwargs):
        super().__init__()
        self.title(title)
        self.test_run = MyTestClass()
        self.frame1 = tk.LabelFrame(self, text="My Frame")
        self.frame1.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky=tk.NSEW)
        self.frame1.columnconfigure(0, weight=1)
        self.frame1.rowconfigure(0, weight=1)

        start_button = tk.Button(self.frame1, text="Start!",
                                command=self.start_proc).grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        stop_button = tk.Button(self.frame1, text="Stop!",
                                command=self.stop_proc).grid(row=0, column=2, padx=5, pady=5, sticky=tk.E)
        self.output_box = tk.Text(self.frame1, width=60, height=8, wrap=tk.WORD)
        self.output_box.grid(row=1, column=0, columnspan=3, sticky=tk.NSEW)

    def start_proc(self):
        self.test_run.stopped = False
        self.control_thread = Thread(target=self.test_run.my_long_procedure, daemon=True)
        self.control_thread.start()
        time.sleep(1)
        self.output_box.delete(0.0, tk.END)
        self.output_box.insert(0.0, self.test_run.message)
        # self.control_thread.join()
        # while not self.test_run.stopped:
        #     self.output_box.delete(0.0, tk.END)
        #     self.output_box.insert(0.0, self.test_run.message)
        #     time.sleep(0.5)

    def stop_proc(self):
        self.test_run.stopped = True

if __name__ == "__main__":
    MyMainApp("My Test App").mainloop()

Upvotes: 0

Views: 77

Answers (1)

Jean-Marc Volle
Jean-Marc Volle

Reputation: 3333

If you own the implementation of the algorithm you could pass a callback (a method of MyMainApp) so that the algorithm signals on his own whenever he has done some "work" worth notification to the user. This would look like:

def my_long_procedure(self,progress):

The callback prototype could be:

 def progress(self,iteration,result):

and instead of print(self.message) you could do progress(i,maxscore)

starting the thread:

self.control_thread = Thread(target=self.test_run.my_long_procedure, daemon=True,args=(self.progress,))

Unfortunatly you need to be aware that you cannot refresh the GUI from another thread than the main thread. This is a much discussed tkinter limitation. So in a nutshell you cannot directly call any of your GUI widgets from progress function. The workaround to this issue is to store the progress in the progress function and register a function to be executed whenever the tkinter main loop will be idle. you can do somethink like self.after_idle(self.update_ui) from progress method. update_ui() would be a new methode updating eg a progress bar or your graph using data transmitted by the progress callback and saved as MyMainApp properties. More on this "pattern" (using message queues instead of callback) here: https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html

Upvotes: 1

Related Questions