My little program has a potentially long running process. That's not a problem when doing it from the console, but now I want to add a GUI. Ideally I want to use Tkinter (a) because it's simple, and (b) because it might be easier to implement across platforms. From what I've read and experienced, (almost) all GUIs suffer the same issue anyway.
Through all my reading on the subject of threading and GUI there seem to be two streams. 1 - where the underlying worker process is polling (eg waiting to fetch data), and 2 - where the worker process is doing a lot of work (eg copying files in a for loop). My program falls into the latter.
My code has a "hierarchy" of classes.
The MIGUI class handles the GUI and interacts with the interface class MediaImporter. The MediaImporter class is the interface between the user interface (console or GUI) and the worker classes. The Import class is the long-running worker. It does not know that the interface or GUI classes exist.
The problem: After clicking the Start button, the GUI is blocked, so I can't click the Abort button. It is as if I'm not using threading at all. I suspect the issue is with the way I am starting the threading in startCallback method.
I've also tried the approach of threading the entire MediaImporter class. See the commented-out lines.
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
import threading
import time
class MIGUI():
def __init__(self, master):
self.master = master
self.mediaImporter = MediaImporter()
self.startButton = ttk.Button(self.master, text='Start', command=self.startCallback)
self.startButton.pack()
self.abortButton = ttk.Button(self.master, text='Abort', command=self.abortCallback)
self.abortButton.state(['disabled'])
self.abortButton.pack()
def startCallback(self):
print('startCallback')
self.abortButton.state(['!disabled'])
self.startButton.state(['disabled'])
self.abortButton.update() # forcing the update seems unnecessary
self.startButton.update()
#print(self.startButton.state())
#print(self.abortButton.state())
self.x = threading.Thread(target=self.mediaImporter.startImport)
self.x.start()
self.x.join()
#self.mediaImporter.startImport()
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
self.abortButton.update()
self.startButton.update()
#print(self.startButton.state())
#print(self.abortButton.state())
def abortCallback(self):
print('abortCallback')
self.mediaImporter.abortImport()
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
class MediaImporter():
#class MediaImporter(threading.Thread):
""" Interface between user (GUI / console) and worker classes """
def __init__(self):
#threading.Thread.__init__(self)
self.Import = Import()
#other worker classes exist too
def startImport(self):
print('mediaImporter - startImport')
self.Import.start()
def abortImport(self):
print('mediaImporter - abortImport')
self.Import.abort()
class Import():
""" Worker
Does not know anything about other non-worker classes or UI.
"""
def __init__(self):
self._wantAbort = False
def start(self):
print('import - start')
self._wantAbort = False
self.doImport()
def abort(self):
print('import - abort')
self._wantAbort = True
def doImport(self):
print('doImport')
for i in range(0,10):
#actual code has nested for..loops
print(i)
time.sleep(.25)
if self._wantAbort:
print('doImport - abort')
return
def main():
gui = True
console = False
if gui:
root = tk.Tk()
app = MIGUI(root)
root.mainloop()
if console:
#do simple console output without tkinter - threads not necessary
pass
if __name__ == '__main__':
main()
The reason your GUI is blocked is because you call self.x.join()
, which blocks until the doImport
function is complete, see the join documentation. Instead I would call join()
in your abortCallback()
function, since that is what will cause the thread to stop running.
Thank you again XORNAND. The join() was definitely part of the problem. The other part of the problem was that there was no means of the MIGUI class knowing when the long-running process was complete (either because it had run its course, or because it was aborted.) An additional layer of messaging is required between the low-level worker, and the UI layer. I did try to use threading.Event without success, and did consider using Queues.
My solution is to use pubsub. ( https://github.com/schollii/pypubsub ) The worker layer can sendMessage on various topics, and the UI and interface layers can set up Listeners to perform actions with received data.
In my case, the Import.doImport method sends a STATUS message when it is completed. The MIGUI listener can then flip-flop the Start/Abort buttons accordingly.
To make sure the implementation of pubsub was going to work as planned I also set up a tkinter Progressbar. The doImport method sends a PROGESS message with the percent complete. This is reflected in the on-screen Progressbar.
A side note - in my original issue I had to use .update() on the buttons to get them to display. Now that we're not blocking anymore, this is not necessary.
Posting the complete working solution here, showing the pubsub implementation.
import tkinter as tk
from tkinter import ttk
import threading
import time
from pubsub import pub
class MIGUI():
def __init__(self, master):
self.master = master
self.mediaImporter = MediaImporter()
self.startButton = ttk.Button(self.master, text='Start', command=self.startCallback)
self.startButton.pack()
self.abortButton = ttk.Button(self.master, text='Abort', command=self.abortCallback)
self.abortButton.state(['disabled'])
self.abortButton.pack()
self.progress = ttk.Progressbar(self.master, length=300)
self.progress.pack()
pub.subscribe(self.statusListener, 'STATUS')
pub.subscribe(self.progressListener, 'PROGRESS')
def statusListener(self, status, value):
print('MIGUI', status, value)
if status == 'copying' and (value == 'finished' or value == 'aborted'):
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
def progressListener(self, value):
print('Progress %d' % value)
self.progress['maximum'] = 100
self.progress['value'] = value
def startCallback(self):
print('startCallback')
self.abortButton.state(['!disabled'])
self.startButton.state(['disabled'])
self.x = threading.Thread(target=self.mediaImporter.startImport)
self.x.start()
# original issue had join() here, which was blocking.
def abortCallback(self):
print('abortCallback')
self.mediaImporter.abortImport()
class MediaImporter():
""" Interface between user (GUI / console) and worker classes """
def __init__(self):
self.Import = Import()
#other worker classes exist too
pub.subscribe(self.statusListener, 'STATUS')
def statusListener(self, status, value):
#perhaps do something
pass
def startImport(self):
self.Import.start()
def abortImport(self):
self.Import.abort()
class Import():
""" Worker
Does not know anything about other non-worker classes or UI.
It does use pubsub to publish messages - such as the status and progress.
The UI and interface classes can subsribe to these messages and perform actions. (see listener methods)
"""
def __init__(self):
self._wantAbort = False
def start(self):
self._wantAbort = False
self.doImport()
def abort(self):
pub.sendMessage('STATUS', status='abort', value='requested')
self._wantAbort = True
def doImport(self):
self.max = 13
pub.sendMessage('STATUS', status='copying', value='started')
for i in range(1,self.max):
#actual code has nested for..loops
progress = ((i+1) / self.max * 100.0)
pub.sendMessage('PROGRESS', value=progress)
time.sleep(.1)
if self._wantAbort:
pub.sendMessage('STATUS', status='copying', value='aborted')
return
pub.sendMessage('STATUS', status='copying', value='finished')
def main():
gui = True
console = False
if gui:
root = tk.Tk()
app = MIGUI(root)
root.mainloop()
if console:
#do simple console output without tkinter - threads not necessary
pass
if __name__ == '__main__':
main()
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.