Tkinter中如何实现后台计时器与接硬币游戏逻辑的并行执行
解决Tkinter中计时器与游戏逻辑同时运行的问题
我明白你的困扰——现在计时器跑完才启动游戏,而且计时结束后也没法终止游戏循环。咱们一步步来修复这个问题,核心要解决线程创建错误、GUI操作必须在主线程以及线程间安全通信这几个关键点。
问题根源分析
- 线程创建方式错误:你写
Thread(target=countdown(int(timeEntry.get())))时,会先执行countdown函数,等它完全跑完才会创建线程,这就是为什么游戏要等计时器结束才启动的原因。 - 子线程操作GUI:
movingCoin里有大量Canvas的操作,Tkinter的GUI组件只能在主线程中修改,把它放到子线程会导致崩溃或异常。 - 死循环阻塞主线程:
movingCoin里的while True会卡住Tkinter的事件循环,应该用Tkinter自带的after方法实现非阻塞的游戏循环。 - 线程通信不可靠:用全局字符串
allowedTime判断计时结束,存在线程同步风险,用threading.Event来标记"时间到"的状态更安全。
修改后的完整代码
import tkinter import random import time from threading import Thread, Event window = tkinter.Tk() window.title("Dukaty") canvas = tkinter.Canvas(window, height=400, width=800) canvas.pack() timeLabel = tkinter.Label(text="Time:", font="Helvetica 15") timeLabel.pack() timeEntry = tkinter.Entry(bd=1) timeEntry.pack() mistakesLabel = tkinter.Label(text="Allowed mistakes:", font="Helvetica 15") mistakesLabel.pack() mistakesEntry = tkinter.Entry(bd=1) mistakesEntry.pack() timeEntry.insert(0, "0") mistakesEntry.insert(0, "0") speed = 250 score = 0 mistakes = 0 time_up_event = Event() # 用于标记计时结束的事件 current_coin = None # 先创建碗的图形(你可以替换成自己的图片) bowl = canvas.create_rectangle(300, 360, 380, 380, fill="brown") # 假设你有coinImg,这里先注释掉,实际使用时替换 # coinImg = tkinter.PhotoImage(file="coin.png") def countdown(t): global time_up_event while t and not time_up_event.is_set(): mins, secs = divmod(t, 60) timer = '{:02d}:{:02d}'.format(mins, secs) print(f"Remaining time: {timer}") time.sleep(1) t -= 1 time_up_event.set() # 设置时间到事件 window.after(0, end) # 确保在主线程执行结束操作 def update_score_and_mistakes(): # 统一更新分数和错误数,避免重复代码 canvas.delete("score") canvas.create_text(700, 50, text=str(score), font="Helvetica 20", fill="green", tags="score") canvas.delete("mistakes") canvas.create_text(700, 80, text=str(mistakes), font="Helvetica 20", fill="red", tags="mistakes") def move_coin(): global speed, score, mistakes, current_coin # 检查终止条件:时间到、错误数达标 if time_up_event.is_set(): return allowed_mistakes = int(mistakesEntry.get()) if allowed_mistakes != 0 and mistakes >= allowed_mistakes: end() return if current_coin is None: # 创建新硬币(如果有图片就替换成image=coinImg) current_coin = canvas.create_image(random.randint(15, 585), 18, tags="coin") update_score_and_mistakes() # 移动硬币 y_pos = canvas.coords(current_coin)[1] if y_pos < 360: canvas.move(current_coin, 0, 5) window.after(speed, move_coin) # 递归调用实现循环 else: # 判断是否接住硬币 bowl_x = canvas.coords(bowl)[0] coin_x = canvas.coords(current_coin)[0] if (bowl_x - 40) < coin_x < (bowl_x + 40): score += 1 else: mistakes += 1 # 删除旧硬币,准备新的 canvas.delete(current_coin) current_coin = None speed -= 10 # 加快速度 window.after(0, move_coin) # 立即创建新硬币 def end(): global time_up_event time_up_event.set() print(f"Game Over! Score: {score}, Mistakes: {mistakes}") window.destroy() def tap(): global time_up_event time_up_event.clear() # 重置时间到事件 # 启动计时器线程(daemon=True让线程随主线程退出) set_time = int(timeEntry.get()) if set_time > 0: Thread(target=countdown, args=(set_time,), daemon=True).start() # 在主线程启动游戏循环 move_coin() endButton = tkinter.Button(text="End", command=end) endButton.pack(side="right") startButton = tkinter.Button(text="Start", command=tap) startButton.pack(side="left") window.mainloop()
关键改动说明
- 用
threading.Event实现线程通信:time_up_event是线程安全的,计时器结束时调用set(),游戏循环里检查is_set()来判断是否终止。 - 修正线程创建:
Thread(target=countdown, args=(set_time,), daemon=True).start(),这里把函数名传给target,参数用args传递,不会提前执行函数;daemon=True确保主线程退出时子线程也跟着结束。 - 非阻塞游戏循环:用
window.after(speed, move_coin)递归调用代替死循环,不会阻塞Tkinter的事件循环,GUI能正常响应操作。 - GUI操作统一在主线程:所有Canvas的修改都在主线程中执行,避免子线程操作GUI的风险。
- 代码复用:提取
update_score_and_mistakes函数,减少重复代码,让逻辑更清晰。
内容的提问来源于stack exchange,提问作者Marek Maďar
相关产品推荐
相关产品推荐

