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

如何在Tkinter GUI中检测按钮长按/抬起及键盘按键状态?

树莓派机器人GUI控制:实现按键/按钮长按检测方案

需求背景

我正在基于Tkinter(使用customtkinter库)构建树莓派机器人的控制GUI,核心功能是通过W/S/A/D按键或对应GUI按钮实现前进/后退/左转/右转。目前已完成基础界面布局,但需要完善逻辑,实现长按按键或按钮时持续输出对应方向,释放后停止(后续将替换为电机控制逻辑)。

当前代码如下:

import tkinter
import tkinter.messagebox
import customtkinter

# Setting up theme
customtkinter.set_appearance_mode("Dark")  # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue")  # Themes: "blue" (standard), "green", "dark-blue"


class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        # configure window
        self.title("Cool Blue")
        self.geometry(f"{1200}x{700}")

        # configure grid layout (4x4)
        self.grid_columnconfigure(1, weight=1)
        self.grid_columnconfigure((2, 3), weight=0)
        self.grid_rowconfigure((0, 1, 2), weight=1)

        # create sidebar frame for controls
        self.sidebar_frame = customtkinter.CTkFrame(self, width=200)
        self.sidebar_frame.grid(row=0, column=0, rowspan=4, padx=(5, 5), pady=(10, 10), sticky="nsew")
        self.sidebar_frame.grid_rowconfigure(4, weight=1)
        self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="Controls",  font=customtkinter.CTkFont(size=20, weight="bold"))
        self.logo_label.grid(row=0, column=1, padx=20, pady=(10, 10))
        self.button_up = customtkinter.CTkButton(self.sidebar_frame, text="W", height=10, width=10, command=self.motion_event)
        self.button_up.grid(row=1, column=1, padx=20, pady=10, ipadx=10, ipady=10)
        self.button_down = customtkinter.CTkButton(self.sidebar_frame, text="S", height=10, width=10, command=self.motion_event)
        self.button_down.grid(row=3, column=1, padx=20, pady=10, ipadx=10, ipady=10)
        self.button_left = customtkinter.CTkButton(self.sidebar_frame, text="A", height=10, width=10, command=self.motion_event)
        self.button_left.grid(row=2, column=0, padx=10, pady=10, ipadx=10, ipady=10)
        self.button_right = customtkinter.CTkButton(self.sidebar_frame, text="D", height=10, width=10, command=self.motion_event)
        self.button_right.grid(row=2, column=2, padx=10, pady=10, ipadx=10, ipady=10)
        self.button_stop = customtkinter.CTkButton(self.sidebar_frame, text="Stop", height=10, width=10, command=self.motion_event)
        self.button_stop.grid(row=2, column=1, padx=10, pady=10, ipadx=10, ipady=10)

        # create Video Canvas
        self.picam = customtkinter.CTkCanvas(self, width=800, background="gray")
        self.picam.grid(row=0, column=1, rowspan=2, padx=(5, 5), pady=(20, 20), sticky="nsew")
        self.picam_label = customtkinter.CTkLabel(master=self.picam, text="Live Video", font=customtkinter.CTkFont(size=20, weight="bold"))
        self.picam_label.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")

        # create sidebar frame for Environmental Variable
        self.measurements = customtkinter.CTkFrame(self, width=200)
        self.measurements.grid(row=0, column=3, rowspan=4, padx=(5, 5), pady=(10, 10), sticky="nsew")
        self.measurements.grid_rowconfigure(4, weight=1)
        self.label_measurements = customtkinter.CTkLabel(master=self.measurements, text="Environment:", font=customtkinter.CTkFont(size=20, weight="bold"))
        self.label_measurements.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")


    def motion_event(self):
        # if button_up pressed down or physical key "w" on keyboard pressed and held down, print "Forward" until key released or button not being clicked anymore.
        # else if button_down pressed down or physical key "s" on keyboard pressed and held down, print "Backward" until key released or button not being clicked anymore.
        # else if button_left pressed down or physical key "a" on keyboard pressed and held down, print "Left" until key released or button not being clicked anymore.
        # else if button_right pressed down or physical key "d" on keyboard pressed and held down, print "Right" until key released or button not being clicked anymore.


if __name__ == "__main__":
    app = App()
    app.mainloop()

GUI截图:
GUI截图


实现方案

核心思路

要实现长按持续触发的效果,需要解决两个关键问题:

  • 追踪按键/按钮的按下/释放状态
  • 定时检查状态并执行对应动作(避免阻塞GUI主循环)

修改后的完整代码

import customtkinter

# Setting up theme
customtkinter.set_appearance_mode("Dark")
customtkinter.set_default_color_theme("blue")


class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        # configure window
        self.title("Cool Blue")
        self.geometry(f"{1200}x{700}")

        # 状态追踪:记录每个方向是否处于激活状态
        self.motion_states = {
            "forward": False,
            "backward": False,
            "left": False,
            "right": False
        }

        # configure grid layout (4x4)
        self.grid_columnconfigure(1, weight=1)
        self.grid_columnconfigure((2, 3), weight=0)
        self.grid_rowconfigure((0, 1, 2), weight=1)

        # create sidebar frame for controls
        self.sidebar_frame = customtkinter.CTkFrame(self, width=200)
        self.sidebar_frame.grid(row=0, column=0, rowspan=4, padx=(5, 5), pady=(10, 10), sticky="nsew")
        self.sidebar_frame.grid_rowconfigure(4, weight=1)
        self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="Controls", font=customtkinter.CTkFont(size=20, weight="bold"))
        self.logo_label.grid(row=0, column=1, padx=20, pady=(10, 10))
        
        # 绑定按钮的按下/释放事件(替代command参数)
        self.button_up = customtkinter.CTkButton(self.sidebar_frame, text="W", height=10, width=10)
        self.button_up.grid(row=1, column=1, padx=20, pady=10, ipadx=10, ipady=10)
        self.button_up.bind("<ButtonPress-1>", lambda e: self.set_motion_state("forward", True))
        self.button_up.bind("<ButtonRelease-1>", lambda e: self.set_motion_state("forward", False))

        self.button_down = customtkinter.CTkButton(self.sidebar_frame, text="S", height=10, width=10)
        self.button_down.grid(row=3, column=1, padx=20, pady=10, ipadx=10, ipady=10)
        self.button_down.bind("<ButtonPress-1>", lambda e: self.set_motion_state("backward", True))
        self.button_down.bind("<ButtonRelease-1>", lambda e: self.set_motion_state("backward", False))

        self.button_left = customtkinter.CTkButton(self.sidebar_frame, text="A", height=10, width=10)
        self.button_left.grid(row=2, column=0, padx=10, pady=10, ipadx=10, ipady=10)
        self.button_left.bind("<ButtonPress-1>", lambda e: self.set_motion_state("left", True))
        self.button_left.bind("<ButtonRelease-1>", lambda e: self.set_motion_state("left", False))

        self.button_right = customtkinter.CTkButton(self.sidebar_frame, text="D", height=10, width=10)
        self.button_right.grid(row=2, column=2, padx=10, pady=10, ipadx=10, ipady=10)
        self.button_right.bind("<ButtonPress-1>", lambda e: self.set_motion_state("right", True))
        self.button_right.bind("<ButtonRelease-1>", lambda e: self.set_motion_state("right", False))

        self.button_stop = customtkinter.CTkButton(self.sidebar_frame, text="Stop", height=10, width=10, command=self.stop_all_motion)
        self.button_stop.grid(row=2, column=1, padx=10, pady=10, ipadx=10, ipady=10)

        # create Video Canvas
        self.picam = customtkinter.CTkCanvas(self, width=800, background="gray")
        self.picam.grid(row=0, column=1, rowspan=2, padx=(5, 5), pady=(20, 20), sticky="nsew")
        self.picam_label = customtkinter.CTkLabel(master=self.picam, text="Live Video", font=customtkinter.CTkFont(size=20, weight="bold"))
        self.picam_label.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")

        # create sidebar frame for Environmental Variable
        self.measurements = customtkinter.CTkFrame(self, width=200)
        self.measurements.grid(row=0, column=3, rowspan=4, padx=(5, 5), pady=(10, 10), sticky="nsew")
        self.measurements.grid_rowconfigure(4, weight=1)
        self.label_measurements = customtkinter.CTkLabel(master=self.measurements, text="Environment:", font=customtkinter.CTkFont(size=20, weight="bold"))
        self.label_measurements.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")

        # 绑定键盘事件
        self.bind("<KeyPress-w>", lambda e: self.set_motion_state("forward", True))
        self.bind("<KeyRelease-w>", lambda e: self.set_motion_state("forward", False))
        self.bind("<KeyPress-s>", lambda e: self.set_motion_state("backward", True))
        self.bind("<KeyRelease-s>", lambda e: self.set_motion_state("backward", False))
        self.bind("<KeyPress-a>", lambda e: self.set_motion_state("left", True))
        self.bind("<KeyRelease-a>", lambda e: self.set_motion_state("left", False))
        self.bind("<KeyPress-d>", lambda e: self.set_motion_state("right", True))
        self.bind("<KeyRelease-d>", lambda e: self.set_motion_state("right", False))

        # 启动动作检查循环
        self.check_motion_loop()

    def set_motion_state(self, direction, state):
        """更新指定方向的激活状态"""
        self.motion_states[direction] = state

    def stop_all_motion(self):
        """停止所有方向的动作"""
        for direction in self.motion_states:
            self.motion_states[direction] = False

    def check_motion_loop(self):
        """定时检查状态并执行对应动作"""
        # 按优先级处理(可根据需求调整)
        if self.motion_states["forward"]:
            print("Forward")
        elif self.motion_states["backward"]:
            print("Backward")
        elif self.motion_states["left"]:
            print("Left")
        elif self.motion_states["right"]:
            print("Right")
        # 每100ms检查一次,可根据需求调整间隔
        self.after(100, self.check_motion_loop)


if __name__ == "__main__":
    app = App()
    app.mainloop()

关键修改说明

  • 状态追踪:新增self.motion_states字典,记录每个方向的激活状态(True表示按下/长按,False表示释放)
  • 按钮事件绑定:将按钮的command参数替换为<ButtonPress-1>和<ButtonRelease-1>事件,分别设置状态为激活/关闭
  • 键盘事件绑定:绑定<KeyPress>和<KeyRelease>事件,对应更新方向状态
  • 循环检查机制:用after()方法实现非阻塞的定时检查,每隔100ms输出当前激活的方向(间隔可根据需求调整)
  • 停止功能:点击Stop按钮重置所有状态,立即停止输出

后续只需将print()语句替换为电机控制的代码即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 16:46:14