简体   繁体   中英

tkinter cursor not changing until after action despite update_idletasks()

I am trying to change the cursor in my tkinter program to show the program is working but the cursor only changes to the working cursor until after the work is done, this is as compressed as I can make the code

warning: to demonstrate working it will count to 99,999,999 when you press go to page one

import tkinter as tk                # python 3
from tkinter import font  as tkfont # python 3
#import Tkinter as tk     # python 2
#import tkFont as tkfont  # python 2

class SampleApp(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)
        self.frames = {}
        for F in (StartPage, PageOne):
            page_name = F.__name__
            frame = F(parent=container, controller=self)
            self.frames[page_name] = frame

            frame.grid(row=0, column=0, sticky="nsew")
        self.show_frame("StartPage")

    def show_frame(self, page_name):
        '''Show a frame for the given page name'''
        frame = self.frames[page_name]
        frame.tkraise()


class StartPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is the start page", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)

        button1 = tk.Button(self, text="Go to Page One",
                            command=self.go)
        button1.pack()

    def go(self):
        # do something for like 5 seconds to demonstrate working
        working(True)
        l = [x for x in range(99999999)]
        self.controller.show_frame('PageOne')


class PageOne(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is page 1", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        button = tk.Button(self, text="Go to the start page",
                           command=self.back)
        button.pack()

    def back(self):
        working(False)
        self.controller.show_frame('StartPage')


def working(yesorno):
    if yesorno==True:
        app.config(cursor='wait')
    else:
        app.config(cursor='')
    app.update_idletasks()


if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

Edit: I would like to thank Switch between two frames in tkinter for this app layout example

I suspect that all events need to be proceeded to change a cursor's look, because cursor depends on operating system and there're some events to handle (I assume that), since update_idletask has no effect - your cursor really change look only when code flow reaches a mainloop . Since you can treat an update as mainloop(1) (very crude comparison) - it's a good option if you know what you doing, because noone wants an endless loop in code.

Little snippet to represent idea:

try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk
import time


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.button = tk.Button(self, text='Toggle cursor', command=self.toggle_business)
        self.button.pack()

    def toggle_business(self):
        if self['cursor']:
            self.config(cursor='')
        else:
            self.config(cursor='wait')


        # self.update_idletasks()   # have no effect at all
        # self.update()             # "local" mainloop(1)

        # simulate work with time.sleep
        # time.sleep(3)

        # also your work can be scheduled so code flow can reach a mainloop
        # self.after(500, lambda: time.sleep(3))


app = App()
app.mainloop()

To overcome this problem you can use:

  • update method (note warnings)

  • after method for scheduled work (opportunity to reach a mainloop for a code flow)

  • threading for "threaded" work (another opportunity, but GUI is responsive, you can handle other events and even simulate unresponsiveness, in other hand threading adds complexity, so use it if you really need it).

Note: There's no difference in behaviour between universal and native cursors on Windows platform.

This code was tested in windows 10 and Python 3. I found the cursor would not change until control was returned to mainloop. The code here outlines how to consistently display the busy cursor during a long running task. Further, this code demonstrates how to retrieve the data from the long running task (like results from a database query).

#! python3
'''
Everything you need to run I/O in a separate thread and make the cursor show busy
Summary:
    1. Set up to call the long running task, get data from windows etc.
        1a. Issue a callback to the routine that will process the data
    2. Do the long running task - absolutely no tkinter access, return the data
    3. Get the data from the queue and process away. tkinter as you will
'''

import tkinter as tk
import tkinter.ttk as ttk
from threading import Thread
from threading import Event
import queue


class SimpleWindow(object):
    def __init__(self):

        self._build_widgets()

    def _build_widgets(self):
        # *************************************************************************************************
        # * Build buttons and some entry boxes
        # *************************************************************************************************
        g_col = 0
        g_row = 0
        WaiterFrame = ttk.Frame()
        WaiterFrame.pack( padx=50)

        i = 0
        g_row += 1
        longWaitButton = ttk.Button(WaiterFrame, text='Long Wait',command=self.setup_for_long_running_task)
        longWaitButton.grid(row = g_row, column = i, pady=4, padx=25)
        i += 1
        QuitButton = ttk.Button(WaiterFrame, text='Quit', command=self.quit)
        QuitButton.grid(row = g_row, column = i,pady=4, padx=25)
        i += 1
        self.Parm1Label = ttk.Label(WaiterFrame, text="Parm 1 Data")
        self.Parm1Label.grid(row = g_row-1, column = i, pady=4, padx=2)
        self.Parm1 = ttk.Entry(WaiterFrame)
        self.Parm1.grid(row = g_row, column = i, pady=4, padx=2)

        i += 1
        self.Parm2Label = ttk.Label(WaiterFrame, text="Parm 2 Data")
        self.Parm2Label.grid(row = g_row-1, column = i, pady=4, padx=2)
        self.Parm2 = ttk.Entry(WaiterFrame)
        self.Parm2.grid(row = g_row, column = i, pady=4, padx=2)

        i += 1
        self.Parm3Label = ttk.Label(WaiterFrame, text="Parm 3 Data")
        self.Parm3Label.grid(row = g_row-1, column = i, pady=4, padx=2)
        self.Parm3 = ttk.Entry(WaiterFrame)
        self.Parm3.grid(row = g_row, column = i, pady=4, padx=2)

        i += 1
        self.Parm4Label = ttk.Label(WaiterFrame, text="Parm 4 Data")
        self.Parm4Label.grid(row = g_row-1, column = i, pady=4, padx=2)
        self.Parm4 = ttk.Entry(WaiterFrame)
        self.Parm4.grid(row = g_row, column = i, pady=4, padx=2)

    def quit(self):
        root.destroy()
        root.quit()

    def setup_for_long_running_task(self):
        # ********************************************************************************************************
        # * Do what needs to be done before starting the long running task in a thread
        # ********************************************************************************************************
        Parm1, Parm2, Parm3, Parm4 = self.Get_Parms()

        root.config(cursor="wait")  # Set the cursor to busy

        # ********************************************************************************************************
        # * Set up a queue for thread communication
        # * Invoke the long running task (ie. database calls, etc.) in a separate thread
        # ********************************************************************************************************
        return_que = queue.Queue(1)
        workThread = Thread(target=lambda q, w_self, p_1, p_2, p_3, p_4: \
                            q.put(self.long_running_task(Parm1, Parm2, Parm3, Parm4)),
                            args=(return_que, self, Parm1, Parm2, Parm3, Parm4))
        workThread.start()
        # ********************************************************************************************************
        # * Busy cursor won't appear until this function returns, so schedule a callback to accept the data
        # * from the long running task. Adjust the wait time according to your situation
        # ********************************************************************************************************
        root.after(500,self.use_results_of_long_running_task,workThread,return_que)  # 500ms is half a second

    # ********************************************************************************************************
    # * This is run in a thread so the cursor can be changed to busy. NO tkinter ALLOWED IN THIS FUNCTION
    # ********************************************************************************************************
    def long_running_task(self, p1,p2,p3,p4):

        Event().wait(3.0)  # Simulate long running task

        p1_out = f'New {p1}'
        p2_out = f'New {p2}'
        p3_out = f'New {p3}'
        p4_out = f'New {p4}'

        return [p1_out, p2_out, p3_out, p4_out]

    # ********************************************************************************************************
    # * Waits for the thread to complete, then gets the data out of the queue for the listbox
    # ********************************************************************************************************
    def use_results_of_long_running_task(self, workThread,return_que):
        ThreadRunning = 1
        while ThreadRunning:
            Event().wait(0.1)  # this is set to .1 seconds. Adjust for your process
            ThreadRunning = workThread.is_alive()

        while not return_que.empty():
            return_list = return_que.get()

        self.LoadWindow(return_list)
        root.config(cursor="")  # reset the cursor to normal

    def LoadWindow(self, data_list):

        self.Parm1.delete(0, tk.END)
        self.Parm2.delete(0, tk.END)
        self.Parm3.delete(0, tk.END)
        self.Parm4.delete(0, tk.END)

        i=0; self.Parm1.insert(0,data_list[i])
        i+=1; self.Parm2.insert(0,data_list[i])
        i+=1; self.Parm3.insert(0,data_list[i])
        i+=1; self.Parm4.insert(0,data_list[i])

    # ********************************************************************************************************
    # * The long running task thread can't get to the tkinter self object, so pull these parms
    # * out of the window and into variables in the main process
    # ********************************************************************************************************
    def Get_Parms(self):
        p1 = self.Parm1Label.cget("text")
        p2 = self.Parm2Label.cget("text")
        p3 = self.Parm3Label.cget("text")
        p4 = self.Parm4Label.cget("text")
        return p1,p2,p3,p4

def WaitForBigData():
    global root

    root = tk.Tk()
    root.title("Wait with busy cursor")
    waitWindow = SimpleWindow()
    root.mainloop()

if __name__ == '__main__':

    WaitForBigData()

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM