2

My app contains some long-running tasks that are triggered by button clicks. I want to set the cursor to 'wait' when the button is clicked and reset it when the task is finished. This seems to be fairly straightforward but I can't get it working.

A sample (not the actual app) to illustrate my problem:

import tkinter as tk
import tkinter.ttk as ttk
import time

class App(ttk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.button = ttk.Button(master=self.master, text='Run task', command=self.onclick_button)
        self.button.grid(padx=25, pady=25)

    def set_cursor_busy(self):
        self.config(cursor='wait')

    def reset_cursor(self):
        self.config(cursor='')

    def onclick_button(self):
        self.set_cursor_busy()
        time.sleep(5)           # Simulate a long running task.
        self.reset_cursor()


root = tk.Tk()
app = App(master=root)
app.mainloop()

From what I have learned from searching the internet and reading numerous posts, I understand that the cursor won't be updated until execution returns to the mainloop. But by that time the task has finished and the cursor has already been reset to normal, so no cursor change is visible. The options I have seen are, to use:

  1. update_idletasks()
  2. update()
  3. after()
  4. threading
  5. your own mainloop

Apart from 5, I have tried to implement all options, but I can't get any of them working.

What is the simplest way to get my code working?

EDIT: The solution mentioned in this post is actually option 2 mentioned above, but adding self.update() or self.master.update() after the self.config(.) lines does not resolve my issue. The same holds for adding self.master.update_idletasks() (option 1).

Additional information about my setup: Python 3.7.3 on Windows 10 Enterprise

EDIT 2: So for absolute clarity, none of the following works:

    def set_cursor_busy(self):
        self.master.config(cursor='wait')
        self.master.update_idletasks()
        # or: self.update_idletasks()
        # or: self.update()
        # or: self.master.update()

    def reset_cursor(self):
        self.master.config(cursor='')
        self.master.update_idletasks()
        # or: self.update_idletasks()
        # or: self.update()
        # or: self.master.update()

EDIT 3: Neither does:

    def set_cursor_busy(self):
        self.master.config(cursor='watch')  # 'watch' instead of 'wait'
        self.master.update_idletasks()

EDIT 4: Delaying the long-running task using after() (as suggested in this post) does not help either:

    def set_cursor_busy(self):
        self.master.config(cursor='watch')

    def reset_cursor(self):
        self.master.config(cursor='')

    def onclick_button(self):
        self.set_cursor_busy()
        self.after(500, lambda: time.sleep(5))
        # Simulate a long running task with time.sleep(5)
        self.reset_cursor()

EDIT 5: Althoug it does not resolve the problem, a status bar could at least serve as an alternative in many cases, or may even be preferred because of the possibility to add a tailored status message. In contrast to the cursor (in TKInter on Windows), a status bar can be updated during a long-running task. Here's an example that works on Windows:

class App(ttk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.button = ttk.Button(master=self.master, text='Run task', command=self.onclick_button)
        self.button.grid(padx=25, pady=25)
        self.statusbar = ttk.Label(master=self.master, text='', background='white')
        self.statusbar.grid(row=1000, padx=1, sticky=tk.EW)
        # Use a high row number to force the statusbar to be the last item on the grid.

    def onclick_button(self):
        self.update_statusbar()

        time.sleep(3)                   # A long-running task.

        self.update_statusbar()

    def update_statusbar(self):
        if not self.statusbar['text']:
            self.statusbar['text'] = 'Busy...'
        else:
            self.statusbar['text'] = ''
        self.master.update()
  • 1
    Does this answer your question? [Python:Tkinter -- How do I get cursor to show busy status](https://stackoverflow.com/questions/20411978/pythontkinter-how-do-i-get-cursor-to-show-busy-status) – stovfl Jan 03 '20 at 11:32
  • No, unfortunately adding `self.update()` after the `self.config(.)` lines in the cursor methods, does not resolve the issue. Neither does adding `self.update_idletasks()` at the same place. – Jan-Willem Lankhaar Jan 03 '20 at 11:58
  • [This answer](https://stackoverflow.com/a/44143431/7414759) claims you have to delay your long running task doing: `self.after(500, lambda: time.sleep(3)` and **don't** use `.update_idletasks()`. It's a Windows issue. – stovfl Jan 03 '20 at 14:27
  • Your ***EDIT 4*** will not work, because `.after(...` return at once. See my answer. – stovfl Jan 03 '20 at 15:05

1 Answers1

1

Question: How to change the cursor= during a long-running task

Reference


Note: It makes no differnce, using .update(..., update_idletasks(... or .after(500, ... to force a timeslice to the .mainloop().

  1. Working, using root.config(... or self.master.config(...
    root.config(cursor='watch')
    root.update_idletasks()
    ...
    
  2. Working, using self.button.config(...
    self.button.config(cursor='watch')
    self.update_idletasks()
    
  3. NOT Working: using self.config(...
    It seems, man can't set a cursor= to a Frame object, no error shown.

    self.config(cursor='watch')
    self.update_idletasks()
    

The delayed variant, using .after(..., with the same results as above:

    def long_running_task(self):
        time.sleep(5)  # Simulate a long running task.
        self.button.config(cursor='no')

    def onclick_button(self):
        self.button.config(cursor='watch')
        self.after(500, self.long_running_task)

Tested with Linux - Python: 3.5 - 'TclVersion': 8.6 'TkVersion': 8.6

stovfl
  • 14,998
  • 7
  • 24
  • 51
  • 1
    I'm affraid I can't mark your answer as the solution, because you propose exactly the same solutions as mentioned in the posts I refer to. None of these works in my case as I outline in the original post. – Jan-Willem Lankhaar Jan 03 '20 at 16:15
  • @Jan-WillemL: I decided to write a answer to show which solution is working for me. The main difference is the used OS and setting a cursor is **Window Manager** dependant. – stovfl Jan 03 '20 at 18:14
  • Okay, fair enough. Thanks a lot for your input! OS-dependency is an aspect that wasn't that explicitly mentioned in the other posts I had read. – Jan-Willem Lankhaar Jan 03 '20 at 19:18