Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
442 views
in Technique[技术] by (71.8m points)

python - Trying to fix tkinter GUI freeze-ups (using threads)

I have a Python 3.x report creator that is so I/O bound (due to SQL, not python) that the main window will "lock up" for minutes while the reports are being created.

All that is needed is the ability to use the standard window actions (move, resize/minimize, close, etc.) while the GUI is locked-up (everything else on the GUI can stay "frozen" until all reports have finished).

Added 20181129 : In other words, tkinter must only control the CONTENTS of the application window and leave handling of all standard (external) window controls to the O/S. If I can do that my problem disappears and I don't need to use threads/subprocesses all (the freezeups become acceptable behaviour similar in effect to disabling the "Do Reports" button).

What is the easiest/simplest way (= minimum disturbance to existing code) of doing this - ideally in a way that works with Python >= 3.2.2 and in a cross-platform way (i.e. works on at least Windows & linux).


Everything below is supporting information that explains the issue in greater detail, approaches tried, and some subtle issues encountered.

Things to consider:

  • Users choose their reports then push a "Create Reports" button on the main window (when the real work starts and the freezeup occurs). Once all reports are done the report creation code displays a (Toplevel) "Done" window. Closing this window enables everything in the main window, allowing users to exit the program or create more reports.

  • Added 20181129: At apparently random intervals (several seconds apart) I can move the window.

  • Except for displaying the "Done" window, the report creation code does not involve the GUI or tkinter in any way.

  • Some data produced by the report creation code must appear on the "Done" window.

  • There's no reason to "parallelize" report creation especially since the same SQL server & database is used to create all reports.

  • In case it affects the solution : I'll eventually need to display the report names (now shown on the console) on the GUI as each report gets created.

  • First time doing threading/subprocessing with python but am familiar with both from other languages.

  • Added 20181129 : Development environment is 64 bit Python 3.6.4 on Win 10 using Eclipse Oxygen (pydev plugin). Application must be portable to at least linux.


The simplest answer seems to be to use threads. Only one additional thread is needed (the one that creates the reports). The affected line:

DoChosenReports()  # creates all reports (and the "Done" window)

when changed to:

from threading import Thread

CreateReportsThread = Thread( target = DoChosenReports )
CreateReportsThread.start()
CreateReportsThread.join()  # 20181130: line omitted in original post, comment out to unfreeze GUI 

successfully produces the reports with their names being displayed on the console as they get created.
However, the GUI remains frozen and the "Done" window (now invoked by the new thread) never appears. This leaves the user in limbo, unable to do anything and wondering what, if anything, has happened (which is why I want to display the filenames on the GUI as they get created).

BTW, After the reports are done the report creation thread must quietly commit suicide before (or after) the Done window is shown.

I also tried using

from multiprocessing import Process

ReportCreationProcess = Process( target = DoChosenReports )
ReportCreationProcess.start()

but that fell afoul of the main programs "if (__name__ == '__main__) :' " test.


Added 20181129 : Just discovered the "waitvariable" universal widget method (see http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html). Basic idea is to launch the create report code as an do-forever thread (daemon?) controlled by this method (with execution controlled by the "Do reports" button on the GUI).


From web research I know that all tkinter actions should be made from within the main (parent) thread, meaning that I must move the "Done" window to that thread.
I also need that window to display some data (three strings) it receives from the "child" thread. I'm thinking of using use application-level globals as semaphores (only written to by the create report thread and only read by the main program) to pass the data. I'm aware that this can be risky with more than two threads but doing anything more (e.g. using queues?) for my simple situation seems like overkill.


To summarize: What's the easiest way to allow the user to manipulate (move, resize, minimize, etc.) an application's main window while the window is frozen for any reason. In other words, the O/S, not tkinter, must control the frame (outside) of the main window.
The answer needs to work on python 3.2.2+ in a cross-platform way (on at least Windows & linux)

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You'll need two functions: the first encapsulates your program's long-running work, and the second creates a thread that handles the first function. If you need the thread to stop immediately if the user closes the program while the thread is still running (not recommended), use the daemon flag or look into Event objects. If you don't want the user to be able to call the function again before it's finished, disable the button when it starts and then set the button back to normal at the end.

import threading
import tkinter as tk
import time

class App:
    def __init__(self, parent):
        self.button = tk.Button(parent, text='init', command=self.begin)
        self.button.pack()
    def func(self):
        '''long-running work'''
        self.button.config(text='func')
        time.sleep(1)
        self.button.config(text='continue')
        time.sleep(1)
        self.button.config(text='done')
        self.button.config(state=tk.NORMAL)
    def begin(self):
        '''start a thread and connect it to func'''
        self.button.config(state=tk.DISABLED)
        threading.Thread(target=self.func, daemon=True).start()

if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...