Havanapple
Havanapple

Reputation: 35

Running a time consuming script without disrupting the update of the GUI in tkinter

I have hit a wall with my tkinter built GUI wherein I am trying to have a time consuming function run on a button click that also updates several elements in my GUI at the same time. At the moment the function hasn't been built/implemented, so I am using a placeholder function that essentially just counts up to 1,000,000 (I have also used time.sleep(10) in other attempts).

The program is essentially designed to allow the user to choose an operation at the menu, and once chosen, the window changes to the operation screen and begins running the first function of that operation. Once that has completed, the user should be able to click a next button to run the next function. An indicator on the screen lets the user know which function they are on.

When I run from the menu screen however, the GUI hangs and does not update to the operation screen until the first function is complete. When I click the next button, the indicator does not update to the correct function until said function has completed.

From reading up on this, I figure my solution is going to probably involve using .after() or threading, however I have attempted to use both these options and I cant seem to get either of them working.

Bare in mind this is minimally functional code, so its pretty scrappy, but it demonstrates the issue I am running into. The chainMeta list is an external JSON list that will contain details for external python scripts that will be designed to boot up and operate functions within docker containers.

self.test() is essentially a placeholder for the time consuming scripts that will be specific to each node. node1.txt in the chainMeta is a placeholder for one of these scripts.

import tkinter as tk
bgCol = '#241e20'
windowCol = '#1c191b'

chainMeta = [
    {
        "name": "Chain 1",
        "description": "This is the first chain.",
        "nodes": [
            {
                "name": "Node 1",
                "phase": "Reconnaissance",
                "phase_img": "img/phases/phase_recon.png",
                "description": "This is a recon node.",
                "filename": "node1.txt"
            }
        ]
    }]
class RootGUI(tk.Tk):
    # Op screen, it builds it based on the selected Op.
    def bodySwitch(self, origin):
        self.opContent = OpPage(self.center)
        self.opContent.configure(bg=bgCol, highlightthickness=1, highlightcolor='#FFFFFF')
        origin.grid_forget()
        self.opContent.grid(row=0, column=0, sticky='nsew')

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        # Window attributes
        self.title('test')
        self.geometry('600x400')
        self.configure(highlightthickness=0, border=0)

        self.center = tk.Frame(self.master, bg=bgCol, padx=12, pady=12)
        self.center.grid(row=1, sticky='nsew')

        # Center Widgets
        self.menuContent = MenuPage(self.center, self)
        self.menuContent.configure(bg=bgCol, highlightthickness=1, highlightcolor='#FFFFFF')

        # Center Layout
        self.center.grid_rowconfigure(0, weight=1)
        self.center.grid_columnconfigure(0, weight=1)
        self.menuContent.grid(row=0, column=0, sticky="nsew")

class MenuPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        # MenuPage Widgets
        self.buttonFrame = tk.Frame(self, bg=bgCol)

        self.runOpBorder = tk.Label(self.buttonFrame, bg='#FFFFFF')
        self.runOpBtn = tk.Button(self.buttonFrame)
        self.runOpBtn.configure(text='run op', borderwidth=0, bg='white', anchor='nw',
                                    command=lambda: controller.bodySwitch(controller.menuContent))

        self.buttonFrame.grid(row=2, columnspan=3, stick='ew')
        self.runOpBorder.grid(row=0, column=0, sticky='nsew')
        self.runOpBtn.grid(row=0, column=0, sticky='nsew')

class OpPage(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        # OpPage widgets
        self.activeOp = ActiveOp(0)
        self.OpBox = tk.Frame(self, bg=bgCol)

        # Context Frame
        self.contextBorder = tk.Label(self.OpBox, bg='#FFFFFF')
        self.context = tk.Label(self.OpBox, text='this is some contextual information that should appear immediately after the run op button was pressed ')

        # OpPage layout
        self.OpBox.grid(row=0, column=1)
        self.contextBorder.grid(row=0, column=1, sticky='nsew', padx=(6, 12), pady=(12, 6))
        self.context.grid(row=0, column=1, rowspan=2, sticky='nsew', padx=(7, 13), pady=(13, 6))

class ActiveOp:
    def __init__(self, select):
        self.nodes = chainMeta[select]['nodes']
        self.nodeState = []
        self.activeNode = tk.IntVar()
        self.activeNode.set(0)
        for i in range(len(self.nodes)):
            state = 'queued'
            self.nodeState.append(state)
        self.run()

    def run(self):
        # Node is flagged as active
        self.nodeState[self.activeNode.get()] = 'active'
        # Node script is run
        print(self.nodes[self.activeNode.get()]['filename'], 'is running')

        self.test()

        # Node is flagged as complete
        self.nodeState[self.activeNode.get()] = 'active_done'

    def next(self, controller):
        # Flag current node as done and move to next node
        self.nodeState[self.activeNode.get()] = 'done'
        self.activeNode.set(self.activeNode.get() + 1)
        self.selectedNode.set(self.activeNode.get())
        controller.controlBtn.config(state='normal')
        self.run()

    def test(self):
        for x in range(1000000):
            print(x)

root = RootGUI()
root.mainloop()

Upvotes: 1

Views: 115

Answers (1)

Novel
Novel

Reputation: 13729

I'll assume you need threading. The only other thing you need to know is that in event driven programming you need to make a new function for every step. So that means you need a function for whatever action you want to run when the process ends, instead of just adding that action to the end of the run function.

import threading

# ... stuff ... 

def run(self):
    # Node is flagged as active
    self.nodeState[self.activeNode.get()] = 'active'
    # Node script is run
    print(self.nodes[self.activeNode.get()]['filename'], 'is running')

    # ideally you'd use the current Frame instead of default_root
    tk._default_root.bind('<<TaskDone>>', self.task_done)
    t = threading.Thread(target=self.test, daemon=True)
    t.start()

def task_done(self, event=None):
    print(self.nodes[self.activeNode.get()]['filename'], 'is done')
    # Node is flagged as complete
    self.nodeState[self.activeNode.get()] = 'active_done'

def test(self):
    for x in range(1000000):
        print(x)
    tk._default_root.event_generate('<<TaskDone>>')

Upvotes: 1

Related Questions