Skip to content

Commit 9565b18

Browse files
[feat] 增加按键显示
1. UI上增加显示最近几个按键
1 parent 372d018 commit 9565b18

File tree

3 files changed

+196
-2
lines changed

3 files changed

+196
-2
lines changed

src/app.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
import tkinter as tk
33
from tkinter import filedialog, messagebox, ttk
44
from typing import List, Optional
5+
import threading
6+
import time
7+
from collections import deque
58

69
from src.player import Player
710
from src.event import Event
811

912

1013
class BaseApp:
11-
def __init__(self, root: tk.Tk, title: str):
14+
def __init__(self, root: tk.Tk, title: str, create_key_display: bool = True):
1215
self.root = root
1316
self.root.title(title)
1417
self.score_text: Optional[str] = None
@@ -21,6 +24,18 @@ def __init__(self, root: tk.Tk, title: str):
2124
self._create_params_frame()
2225
self._create_control_frame()
2326
self._create_tips_frame()
27+
28+
# 根据参数决定是否创建按键显示框架
29+
if create_key_display:
30+
self._create_key_display_frame()
31+
32+
# 初始化按键显示相关变量
33+
self.keys = deque(maxlen=14) # 显示最近maxlen个按键
34+
self.last_press_time = {}
35+
self.running = True
36+
37+
# 启动按键监听
38+
self._start_key_listener()
2439

2540
def _create_file_bar(self):
2641
file_bar = tk.Frame(self.frm)
@@ -89,6 +104,85 @@ def _create_tips_frame(self):
89104
"4) 如无响应尝试以管理员身份运行。"
90105
)).pack(fill="x")
91106

107+
def _create_key_display_frame(self):
108+
"""创建按键显示框架"""
109+
key_frame = tk.LabelFrame(self.frm, text="按键显示")
110+
key_frame.pack(fill="x", pady=8)
111+
112+
# 创建按键显示标签
113+
self.lbl_keys = tk.Label(
114+
key_frame,
115+
text="等待按键...",
116+
font=("Consolas", 16, "bold"),
117+
fg="white",
118+
bg="#C0C0C0", # 使用深灰色模拟半透明效果
119+
height=2,
120+
anchor="center",
121+
relief="flat", # 去掉边框,让背景更平滑
122+
borderwidth=0
123+
)
124+
self.lbl_keys.pack(fill="x", padx=4, pady=4)
125+
126+
# 添加说明文字
127+
tk.Label(key_frame, text="实时显示当前按下的按键", font=("微软雅黑", 9), fg="gray").pack(anchor="w", padx=4)
128+
129+
def _start_key_listener(self):
130+
"""启动按键监听线程"""
131+
try:
132+
from pynput import keyboard
133+
134+
def on_press(key):
135+
"""键盘按下事件"""
136+
try:
137+
k = key.char.upper()
138+
except AttributeError:
139+
k = str(key).replace("Key.", "").upper()
140+
141+
self.keys.append(k)
142+
self.last_press_time[k] = time.time()
143+
self._update_key_display()
144+
145+
# 启动键盘监听器
146+
self.key_listener = keyboard.Listener(on_press=on_press)
147+
self.key_listener.start()
148+
149+
# 启动清理过期按键的线程
150+
threading.Thread(target=self._cleanup_keys_loop, daemon=True).start()
151+
152+
except ImportError:
153+
# 如果没有安装pynput,显示提示信息
154+
self.lbl_keys.config(
155+
text="需要安装 pynput 模块\npip install pynput",
156+
font=("微软雅黑", 10),
157+
fg="red",
158+
bg="lightgray"
159+
)
160+
161+
def _update_key_display(self):
162+
"""更新按键显示"""
163+
if hasattr(self, 'lbl_keys'):
164+
if self.keys:
165+
display_text = " ".join(self.keys)
166+
self.lbl_keys.config(text=display_text)
167+
else:
168+
self.lbl_keys.config(text="等待按键...")
169+
170+
def _cleanup_keys_loop(self):
171+
"""后台循环,清理过期按键"""
172+
while self.running:
173+
now = time.time()
174+
removed = False
175+
for k in list(self.keys):
176+
if now - self.last_press_time.get(k, 0) > 2.0: # 2秒后自动清除
177+
try:
178+
self.keys.remove(k)
179+
removed = True
180+
except ValueError:
181+
pass
182+
if removed:
183+
self._update_key_display()
184+
time.sleep(0.2)
185+
92186
def update_progress(self, current: int, total: int):
93187
"""更新进度条和进度标签"""
94188
if total > 0:
@@ -207,6 +301,15 @@ def toggle_play_pause(self):
207301
except Exception:
208302
pass
209303

304+
def __del__(self):
305+
"""析构函数,清理资源"""
306+
self.running = False
307+
if hasattr(self, 'key_listener'):
308+
try:
309+
self.key_listener.stop()
310+
except:
311+
pass
312+
210313

211314
if __name__ == '__main__':
212315
pass

src/app_multi.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class MultiApp(BaseApp):
1313
def __init__(self, root: tk.Tk):
14-
super().__init__(root, "多人模式 - 自动弹琴 (去和弦+分散)")
14+
super().__init__(root, "多人模式 - 自动弹琴 (去和弦+分散)", create_key_display=False)
1515
self.raw_events: List[Event] = []
1616
self.play_events: List[SimpleEvent] = []
1717

@@ -25,6 +25,9 @@ def __init__(self, root: tk.Tk):
2525
'3) 偏移不修改原谱文件,仅运行时生效。\n'
2626
'4) 适度调整偏移可减少漏音(建议 -20~20 范围内微调)。'
2727
)).pack(fill="x")
28+
29+
# 在说明框架之后添加独立的按键显示框架
30+
self._create_key_display_frame()
2831

2932
# 添加多人模式特有的参数 - 使用正确的行数
3033
params = self.frm.winfo_children()[1] # 获取第二个子元素(params)

utils/key_cast_overlay_demo.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import tkinter as tk
2+
from pynput import keyboard
3+
from collections import deque
4+
import threading
5+
import time
6+
7+
MAX_KEYS = 5 # 显示最近几个按键
8+
DISPLAY_TIME = 2.0 # 每个按键显示多久(秒)
9+
10+
11+
class KeyCastOverlay:
12+
def __init__(self):
13+
self.root = tk.Tk()
14+
self.root.overrideredirect(True) # 去掉窗口边框
15+
self.root.attributes("-topmost", True) # 窗口置顶
16+
self.root.attributes("-alpha", 0.7) # 半透明
17+
18+
# 设置窗口大小和位置(屏幕底部居中)
19+
sw = self.root.winfo_screenwidth()
20+
sh = self.root.winfo_screenheight()
21+
w, h = 400, 80
22+
x, y = (sw - w) // 2, sh - h - 50
23+
self.root.geometry(f"{w}x{h}+{x}+{y}")
24+
25+
# 标签显示按键
26+
self.label = tk.Label(
27+
self.root,
28+
text="",
29+
font=("Consolas", 24, "bold"),
30+
fg="white",
31+
bg="black"
32+
)
33+
self.label.pack(expand=True, fill="both")
34+
35+
# 按键缓存
36+
self.keys = deque(maxlen=MAX_KEYS)
37+
self.last_press_time = {}
38+
39+
# 键盘监听器
40+
self.listener = keyboard.Listener(on_press=self.on_press)
41+
self.listener.start()
42+
43+
# 后台线程定时清理过期按键
44+
self.running = True
45+
threading.Thread(target=self.cleanup_loop, daemon=True).start()
46+
47+
self.root.protocol("WM_DELETE_WINDOW", self.close)
48+
self.root.mainloop()
49+
50+
def on_press(self, key):
51+
"""键盘按下事件"""
52+
try:
53+
k = key.char.upper()
54+
except AttributeError:
55+
k = str(key).replace("Key.", "").upper()
56+
57+
self.keys.append(k)
58+
self.last_press_time[k] = time.time()
59+
self.update_display()
60+
61+
def update_display(self):
62+
"""更新显示内容"""
63+
self.label.config(text=" ".join(self.keys))
64+
65+
def cleanup_loop(self):
66+
"""后台循环,清理过期按键"""
67+
while self.running:
68+
now = time.time()
69+
removed = False
70+
for k in list(self.keys):
71+
if now - self.last_press_time.get(k, 0) > DISPLAY_TIME:
72+
try:
73+
self.keys.remove(k)
74+
removed = True
75+
except ValueError:
76+
pass
77+
if removed:
78+
self.update_display()
79+
time.sleep(0.2)
80+
81+
def close(self):
82+
self.running = False
83+
self.listener.stop()
84+
self.root.destroy()
85+
86+
87+
if __name__ == "__main__":
88+
KeyCastOverlay()

0 commit comments

Comments
 (0)