在 TKinter 中显示倒数计时器,同时使代码阻塞但不冻结 GUI

Display a countdown timer in TKinter while making the code BLOCKING but do not freeze the GUI

请仔细阅读我的问题 - 我知道有很多方法可以在不冻结 window 的情况下在 Tkinter 上实现倒数计时器,但是所有现有的解决方案也会导致代码成为非阻塞的。对于我的用例,我需要在时间结束后自动将任务安排到 运行,同时保持 GUI 处于活动状态(未冻结)。我的猜测是我需要以某种方式阻止下一个任务的执行,但这也会冻结 GUI window。那么有什么出路吗?

我目前拥有的:

root = Tk.Tk()
def countdown(time, msg='Counting down'):
    def tick():
        nonlocal time
        status(f'{msg} ({60 - time}sec)')
        time += 1
    root.after(1000, tick)

其中 status() 只是一个更新某些按钮文本的函数。 当前倒计时功能无法自行运行,因为我无法在超时期限后停止 after()

程序的其他部分如下:

countdown(10)  # I need this line to be blocking or somehow prevents the code from going to next line
print('starting scheduled job...')
job()

我曾尝试使用线程,但正如我之前所说,这会导致代码成为非阻塞的,当我使用 Thread.join() 时,整个 GUI 再次冻结。

注意:我可能想多了,另一个答案实际上可能更简单

根据我的理解,你想创建一个程序,在一定时间后 运行 另一个任务,但任务和倒计时都不应干扰 GUI(但任务必须 运行仅在倒计时之后),代码注释中的解释:

# import what is needed
from tkinter import Tk, Button, Label
from threading import Thread
from queue import Queue, Empty
import time


# the function that the button will call to start the countdown
# and after that the task
def start_countdown():
    # disable button so not to accidentally run the task again
    # before it has even started
    button.config(state='disabled')
    # create a queue object
    queue = Queue()
    update_label(queue)
    # set daemon=True to kill thread if the main thread exits
    Thread(target=countdown, args=(queue, ), daemon=True).start()


# the task you want to do after countdown
def do_task():
    for _ in range(10):
        print('doing task...')
        time.sleep(0.5)


# the actual countdown (btw using `time.sleep()` is more precise
# and only the thread will sleep)
# put data in queue so that it can easily be accessed
# from the main thread
def countdown(queue):
    seconds = 10
    for i in range(1, seconds + 1):
        queue.put(f'Seconds left: {seconds + 1 - i}')
        time.sleep(1)
    queue.put('Starting task')
    # place a sentinel to tell the reading part
    # that it can stop
    queue.put('done')
    # do the task, this will run it in the same thread
    # so it won't block the main thread
    do_task()


# function to update the label that shows the users how many seconds left
def update_label(queue):
    try:
        # since block=False it will raise an exception
        # if nothing is in queue
        data = queue.get(block=False)
    except Empty:  # therefore except it and simply pass
        pass
    else:
        # if no error was raised check if data is sentinel,
        # if it is, stop this loop and enable the button (if needed)
        if data == 'done':
            button.config(state='normal')
            return
        # otherwise just update the label with data in the queue
        label.config(text=data)
    finally:
        # and (almost) no matter what happens (nothing much should) loop this again
        root.after(100, update_label, queue)


# basic tkinter setup
root = Tk()
root.geometry('300x200')

button = Button(root, text='Start countdown', command=start_countdown)
button.pack(expand=True)

label = Label(root)
label.pack(expand=True)

root.mainloop()

目前,你的问题对我来说意义不大。据我了解,您希望在倒计时后调用 job() 函数。

不需要为此使用线程。您可以在计时器到达 0 之后使用,然后调用 job() 函数。

这是一个最小的例子

import tkinter as tk


def job():
    status.config(text="starting job")


def countdown(time, msg='Counting down'):
    
    time -= 1
    status.config(text=f'{msg} ({time}sec)')

    if time != 0:
        root.after(1000, countdown, time)

    else:
        job()  # if job is blocking then create a thread


root = tk.Tk()

status = tk.Label(root)
status.pack()

countdown(20)

root.mainloop()