You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Tkinter按键事件循环与Canvas更新无延迟同步运行

问题:钢琴块游戏动画与事件循环不同步,按键卡顿

我正在开发一款类似钢琴块的游戏,目前遇到游戏动画与事件循环无法同步运行的问题。希望分离检测KeyPress的事件循环与游戏动画逻辑,实现流畅运行,避免狂按按键时出现卡顿。想知道能否通过asyncio处理事件循环以避免动画卡顿?

尝试用asyncio方案时,游戏无法加载,出现如下错误:

DeprecationWarning: There is no current event loop
  loop = asyncio.get_event_loop()

见过不少将Tkinter与非Tkinter函数通过asyncio结合的方案,但未找到针对事件循环相关的解决办法。


相关代码

game.py

import threading
import tkinter
import main
import time
from playsound import playsound


def open_file(music_file):
    """Open a music file."""
    with open(music_file, 'r') as file:
        return file.read()


class Game:
    def __init__(self, root):
        self.game_menu_sc = None
        self.place_to_hit = []
        self.lines = []
        self.number_of_keys = 12
        self.items = None
        self.points = 0
        self.root = root
        self.clickable_area = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9, 'k': 10, 'l': 11}


    def game_screen(self):
        """Create the game screen of the application."""
        for widget in self.root.winfo_children():
            widget.destroy()

        game_menu_sc = tkinter.Canvas(self.root)
        game_menu_sc.configure(bg='white')
        screen_width, screen_height = main.get_screen_size(self.root)
        width, height = main.scale_to_screen_size(screen_width, screen_height)
        game_menu_sc.bind('<KeyPress>', self.keyup)

        game_menu_sc.place(width=width, height=height)
        game_menu_sc.focus_set()
        for i in range(self.number_of_keys):
            line = game_menu_sc.create_line(i * int(width / self.number_of_keys), 0,
                                            i * int(width / self.number_of_keys), height, fill='black')
            self.lines.append(line)
            place_to_hit = game_menu_sc.create_rectangle(i * int(width / self.number_of_keys), int(height - 50),
                                                         i * int(width / self.number_of_keys) + int(
                                                             width / self.number_of_keys), int(height), fill='black')
            self.place_to_hit.append(place_to_hit)
        self.game_menu_sc = game_menu_sc
        self.animate_game(game_menu_sc, width)
        return True

    def animate_game(self, canvas, width):
        """Animate the game."""
        self.items = self.play_music('music1.txt', width)
        while True:
            for item in self.items:
                canvas.move(item, 0, 1)
            canvas.update()
            time.sleep(0.01)
            if not self.items:
                print('points  {}'.format(self.points))
                break

    def keyup(self, event):
        """Handle keyup events."""
        list_of_keycodes = [65, 83, 68, 70, 71, 72, 74, 75, 76, 186, 222, 13]
        for index, keycode in enumerate(list_of_keycodes):
            if keycode == event.keycode:
                threading.Thread(target=playsound, args=(f'assets/{index + 1 }.wav',), daemon=True).start()
                # winsound.PlaySound(f'assets/{index + 1}.wav', winsound.SND_FILENAME | winsound.SND_ASYNC)
                if self.get_collision(index):
                    self.points += 1
                    print('collision')

    def get_collision(self, index):
        """Check if there is a collision and remove the object if there's a collision."""
        p = self.game_menu_sc.coords(self.place_to_hit[index])
        collision = self.game_menu_sc.find_overlapping(p[0], p[1], p[2], p[3])
        collision = list(collision)
        collision_new = list(collision)

        for item in collision_new:
            if item in self.place_to_hit or item in self.lines:
                collision.remove(item)
        if len(collision) != 0:
            self.items.remove(collision[0])
            self.game_menu_sc.delete(collision[0])
            return True
        else:
            return False

    def play_music(self, music_file, width):
        """Create the rectangles based on the music file."""
        music = open_file(music_file)
        notes = music.split(',')
        list_of_items = []
        for note in notes:
            index, keys = note.split(':')
            for items in keys:
                i = self.clickable_area[items]
                rectangles = self.create_rectangle(i * int(width / self.number_of_keys) + 1, -float(index) * 50,
                                                   i * int(width / self.number_of_keys) +
                                                   int(width / self.number_of_keys) - 1, -float(index) * 50 + 50)
                list_of_items.append(rectangles)
        return list_of_items

    def create_rectangle(self, x1, y1, x2, y2):
        """Create rectangles."""
        place_to_hit = self.game_menu_sc.create_rectangle(x1, y1, x2, y2, fill='black')
        return place_to_hit

music1.txt

1:a,2:b,3:ab,4:df,5:dl,6:ab,7:ah,8:hj,8:la,9:ga,10:la,11:fd

解决方案

核心问题分析

卡顿的根源不是按键检测和动画的同步问题,而是animate_game方法里的while True + time.sleep阻塞了Tkinter的主事件循环。Tkinter所有的UI事件(包括按键、界面更新)都依赖主循环处理,一旦被阻塞,按键响应就会延迟,出现卡顿。

方案1:用Tkinter原生after方法(推荐,最简单)

替换animate_game里的死循环,用Tkinter的after方法实现非阻塞动画,让主事件循环始终能处理按键事件:

修改animate_game方法如下:

def animate_game(self, canvas, width):
    """Animate the game with non-blocking after method."""
    self.items = self.play_music('music1.txt', width)
    
    def update_frame():
        if self.items:
            for item in self.items:
                canvas.move(item, 0, 1)
            # 递归调用,每10ms更新一帧
            canvas.after(10, update_frame)
        else:
            print('points  {}'.format(self.points))
    
    update_frame()

这个方案不需要额外引入asyncio,完全符合Tkinter的运行机制,能彻底解决卡顿问题。

方案2:结合asyncio(适合需要异步任务的场景)

如果一定要用asyncio,需要正确整合Tkinter主循环和asyncio事件循环,同时解决DeprecationWarning(Python 3.10+中get_event_loop()已被弃用,需显式创建或获取运行中的循环)。

步骤1:修改动画逻辑为异步函数

import asyncio

async def async_animate(self, canvas):
    while self.items:
        for item in self.items:
            canvas.move(item, 0, 1)
        canvas.update()
        # 用asyncio.sleep替代time.sleep,不阻塞事件循环
        await asyncio.sleep(0.01)
    print('points  {}'.format(self.points))

步骤2:正确启动asyncio循环

在game_screen方法末尾,替换原有的self.animate_game调用,改为:

# 创建asyncio事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 在asyncio循环中运行动画任务
loop.create_task(self.async_animate(game_menu_sc))
# 用Tkinter的after定期轮询asyncio循环,避免阻塞主循环
def poll_asyncio():
    loop.run_until_complete(asyncio.sleep(0))
    game_menu_sc.after(10, poll_asyncio)

poll_asyncio()

步骤3:修复DeprecationWarning

原错误是因为没有当前运行的事件循环,改用asyncio.new_event_loop()显式创建,再用set_event_loop绑定,就不会触发警告。


内容的提问来源于stack exchange,提问作者Robert

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 03:30:54