在tkinter中如何在长时间运行的任务期间更改光标。

huangapple go评论69阅读模式
英文:

How to change the cursor during a long-running task in tkinter

问题

我的应用程序包含一些由按钮点击触发的长时间运行的任务。我希望在点击按钮时将光标设置为'等待',并在任务完成后将其重置。这似乎相当简单,但我无法使其工作。

一个示例(不是实际应用程序)来说明我的问题:

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)           # 模拟长时间运行的任务。
        self.reset_cursor()
    

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

根据我从互联网搜索和阅读众多帖子中了解到的,光标在执行返回到主循环之前不会被更新。但是到那时,任务已经完成,并且光标已经被重置为正常状态,因此看不到光标更改。我看到的选项有:

  1. update_idletasks()
  2. update()
  3. after()
  4. 线程
  5. 您自己的主循环

除了第5种,我尝试实现了所有选项,但没有任何一个可以正常工作。

什么是让我的代码正常工作的最简单方法?

编辑此帖中提到的解决方案实际上是上面提到的选项2,但在self.config(.)行之后添加self.update()self.master.update()并未解决我的问题。对于添加self.master.update_idletasks()(选项1)也是一样的情况。

关于我的设置的其他信息:Windows 10 Enterprise上的Python 3.7.3

编辑 2:所以为了绝对明确,以下任何一种方式都不起作用:

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

编辑 3:也不起作用:

    def set_cursor_busy(self):
        self.master.config(cursor='watch')  # 使用'watch'而不是'wait'
        self.master.update_idletasks()

编辑 4:使用after()延迟长时间运行的任务(如此帖建议的)也无济于事:

    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))
        # 模拟长时间运行的任务,使用time.sleep(5)
        self.reset_cursor()

编辑 5:尽管它不能解决问题,但状态栏至少可以作为许多情况下的替代方法,或者甚至可能更受欢迎,因为它可以在长时间运行的任务期间更新。与光标(在Windows上的TKInter中)不同,状态栏可以在长时间运行的任务期间更新。以下是在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)
        # 使用较高的行号强制状态栏成为网格中的最后一个项目。
        
    def onclick_button(self):
        self.update_statusbar()

        time.sleep(3)                   # 一个长时间运行的任务。

        self.update_statusbar()
    
    def update_statusbar(self):
        if not self.statusbar['text']:
            self.statusbar['text'] = '忙碌中...'
        else:
            self.statusbar['text'] = ''
        self.master.update()
英文:

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

得分: 1

问题: 如何在长时间运行的任务期间更改cursor=

参考资料


注意: 使用.update(...update_idletasks(....after(500, ...来强制.mainloop()执行时间切片没有任何区别。

  1. 工作中,使用root.config(...self.master.config(...
    root.config(cursor='watch')
    root.update_idletasks()
    ...
    
  2. 工作中,使用self.button.config(...
    self.button.config(cursor='watch')
    self.update_idletasks()
    
  3. 不工作:使用self.config(...
    似乎不能为Frame对象设置cursor=, 但不会显示错误。

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

使用延迟变体,使用.after(...,与上述结果相同:

def long_running_task(self):
    time.sleep(5)  # 模拟长时间运行的任务。
    self.button.config(cursor='no')

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

在Linux上测试 - Python版本: 3.5 - 'TclVersion': 8.6 'TkVersion': 8.6

英文:

>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

huangapple
  • 本文由 发表于 2020年1月3日 18:53:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/59577283.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定