0

Windows 10 with Python 2.7.13 (32 bit)

I have a Python tkinter program with a progress bar. The program performs a long-running operation, and then waits for a set amount of time before repeating it.

To provide feedback to the user, a progress bar is updated in derterminate mode when counting down to the next operation, and then switches to an indeterminate bar during the operation (of unknown duration).

After 15-30mins, during a countdown using the code below, Windows pops up a python.exe has stopped working dialog, offering only the option to close the program, and the program is unresponsive (presumably because Windows has halted it).

I cannot figure out why this is occurring. During this time, in the GUI the following segment of code is running:

def wait_nextop(self, delay):
    end = time.time() + delay

    self.progress_bar.stop()
    self.progress_bar.config(mode = 'determinate', max = delay)

    while time.time() < end:
        remaining = (end - time.time()) + 1
        self.progress_bar.config(value = (delay - remaining))
        remaining_text = str(datetime.timedelta(seconds = int(remaining)))
        self.progress_text.config(text = 'Next operation in ' + remaining_text)
        time.sleep(0.1)

    self.progress_text.config(text = 'Operation in progress')    
    self.progress_bar.config(mode = 'indeterminate')
    self.progress_bar.start()

In this, delay is an integer delay in seconds. self.progress_bar is an instance of ttk.ProgressBar and self.progress_text is an instance of tkinter.Label. time and datetime are from the standard libraries.

Python offers no stack trace, and I do not currently have other systems available to test this on. It should be noted that this GUI function is called by another thread, but is intended to execute within the main thread.

I've seen these similar questions, but couldn't find a working resolution:

toxicantidote
  • 168
  • 2
  • 10
  • 1
    You should never call a tkinter function from any thread other than the one that created the widget. – Bryan Oakley Mar 18 '20 at 06:50
  • 1
    Follow this approach [threaded `Progressbar`](https://stackoverflow.com/a/60685778/7414759). – stovfl Mar 18 '20 at 07:46
  • @stovfl :The approach you posted seems to have resolved the issue, thanks! I've posted a working example using this system as an answer below for Python 2.7, as that example seemed to be for Python 3 – toxicantidote Mar 19 '20 at 02:56

1 Answers1

0

Based on advice from the comments, it seems that I should be using tkinter's events system, rather than calling functions in the GUI thread directly.

Here is a working example for Python2.7 using the described concepts. It creates the GUI with progress bar, and then updates it from another thread using the event system.

## Import the required modules
import threading
import Tkinter as tk
import ttk
import time
import random

## Class for creating the GUI
class GUI():
    def __init__(self):
        ## Make the GUI
        self.root = tk.Tk()        
        self.progress = ttk.Progressbar(self.root, orient = 'horizontal', length = 100, mode = 'determinate', max = 10)

        ## Bind an event to trigger the update
        self.root.bind('<<MoveIt>>', self.update_progress)

        ## Pack the progress bar in to the GUI
        self.progress.pack()

    def run(self):
        ## Enter the main GUI loop. this blocks the main thread
        self.root.mainloop()

    ## Updates the progress bar
    def update_progress(self, event):
        ## Work out what the new value will be
        new_value = self.progress['value'] + 1

        ## If the new value is less than 10..
        if new_value < 10:
            print('[main thread] New progress bar value is ' + str(new_value))
            ## Update the progress bar value
            self.progress.config(value = new_value)
            ## Force the GUI to update
            self.root.update()
        ## If the new value is more than 10
        else:
            print('[main thread] Progress bar done. Exiting!')
            ## Exit the GUI (and terminate the script)
            self.root.destroy()

## Class for creating the worker thread
class Worker(threading.Thread):
    def __init__(self, ui):
        ## Run the __init__ function from our threading.Thread parent
        threading.Thread.__init__(self)

        ## Save a local reference to the GUI object
        self.ui = ui

        ## Set as a daemon so we don't block the program from exiting
        self.setDaemon(True)

    def run(self):        
        print('[new thread] starting')
        ## Count to 10
        for i in range(10):
            ## Generate an event to trigger the progress bar update
            ui.root.event_generate('<<MoveIt>>', when = 'tail')

            ## Wait between one and three seconds before doing it again
            wait = random.randint(1,3)
            print('[new thread] Incrementing progress bar again in ' + str(wait) + ' seconds')
            time.sleep(wait)

## Create an instance of the GUI class            
ui = GUI()
## Create an instance of the Worker class and tell it about the GUI object
work = Worker(ui)
## Start the worker thread
work.start()
## Start the GUI thread (blocking)
ui.run()
toxicantidote
  • 168
  • 2
  • 10