import tkinter as tk from tkinter import simpledialog, messagebox import json import os from pynput import keyboard from PIL import Image, ImageTk # 配置文件路径 CONFIG_FILE = "key_config.json" class KeyOverlayApp: def __init__(self): self.root = tk.Tk() self.root.title("KeyOverlay Ultra") # 初始默认配置 self.config = { "alpha": 0.8, "position": "+100+100", "monitored_keys": ["w", "a", "s", "d", "space"], "counts": {}, "ui_mode": "list" # "list" 或 "keyboard" } self.load_config() # 窗口基础设置 self.root.overrideredirect(True) self.root.attributes("-topmost", True) self.root.attributes("-alpha", self.config["alpha"]) self.root.geometry(self.config["position"]) self.root.configure(bg='#282c34') # 状态变量 self.key_labels = {} # 存储按键 UI 组件 self.char_image_label = None self.idle_photo = None self.pressed_photo = None self.active_keys = set() # 当前按下的键 # 加载图片 self.load_images() # 初始化 UI self.setup_ui() # 拖动功能 self._drag_data = {"x": 0, "y": 0} self.root.bind("", self.start_drag) self.root.bind("", self.do_drag) # 右键菜单 self.menu = tk.Menu(self.root, tearoff=0) self.menu.add_command(label="切换样式 (列表/键盘)", command=self.toggle_ui_mode) self.menu.add_separator() self.menu.add_command(label="添加按键", command=self.add_key_dialog) self.menu.add_command(label="删除按键", command=self.remove_key_dialog) self.menu.add_command(label="设置透明度", command=self.set_alpha_dialog) self.menu.add_command(label="重置计数", command=self.reset_counts) self.menu.add_separator() self.menu.add_command(label="退出并保存", command=self.quit_app) self.root.bind("", self.show_menu) # 启动监听 (包含按下和松开) self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release) self.listener.start() self.root.mainloop() def load_images(self): """加载人物图片,如果不存在则显示占位符""" try: # 假设图片大小调整为 150x150 size = (150, 150) if os.path.exists("idle.png"): img = Image.open("idle.png").convert("RGBA").resize(size) self.idle_photo = ImageTk.PhotoImage(img) if os.path.exists("pressed.png"): img = Image.open("pressed.png").convert("RGBA").resize(size) self.pressed_photo = ImageTk.PhotoImage(img) except Exception as e: print(f"图片加载失败: {e}") def load_config(self): if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, "r") as f: try: self.config.update(json.load(f)) except: pass for key in self.config["monitored_keys"]: if key not in self.config["counts"]: self.config["counts"][key] = 0 def save_config(self): self.config["position"] = f"+{self.root.winfo_x()}+{self.root.winfo_y()}" with open(CONFIG_FILE, "w") as f: json.dump(self.config, f, indent=4) def setup_ui(self): """核心 UI 构建,支持两种模式""" # 清空当前界面 for widget in self.root.winfo_children(): if widget != self.menu: widget.destroy() self.key_labels = {} # 1. 人物显示区域 (始终在最上方) self.char_image_label = tk.Label(self.root, bg='#282c34') if self.idle_photo: self.char_image_label.config(image=self.idle_photo) else: self.char_image_label.config(text="(No Image)", fg="gray") self.char_image_label.pack(pady=5) # 2. 按键显示区域 container = tk.Frame(self.root, bg='#282c34') container.pack(padx=10, pady=5) if self.config["ui_mode"] == "keyboard": # 键盘样式:网格/横向排列 for i, key in enumerate(self.config["monitored_keys"]): frame = tk.Frame(container, bg='#3e4451', highlightbackground="gray", highlightthickness=1) frame.pack(side=tk.LEFT, padx=2) name_lbl = tk.Label(frame, text=key.upper(), fg="white", bg='#3e4451', font=("Consolas", 10)) name_lbl.pack() count_lbl = tk.Label(frame, text=str(self.config["counts"].get(key, 0)), fg="#61afef", bg='#3e4451', font=("Consolas", 8)) count_lbl.pack() self.key_labels[key] = {"frame": frame, "name": name_lbl, "count": count_lbl} else: # 列表样式 for key in self.config["monitored_keys"]: lbl = tk.Label(container, text=f"{key.upper()}: {self.config['counts'].get(key, 0)}", fg="white", bg='#282c34', font=("Consolas", 12, "bold")) lbl.pack(fill=tk.X) self.key_labels[key] = {"label": lbl} def on_press(self, key): k = self._get_key_str(key) if k in self.config["monitored_keys"]: # 状态更新 if k not in self.active_keys: self.config["counts"][k] += 1 self.active_keys.add(k) # UI 实时反馈 self.root.after(0, lambda: self.update_key_ui(k, True)) def on_release(self, key): k = self._get_key_str(key) if k in self.active_keys: self.active_keys.remove(k) # UI 恢复反馈 self.root.after(0, lambda: self.update_key_ui(k, False)) def _get_key_str(self, key): try: if hasattr(key, 'char') and key.char: return key.char.lower() return key.name.lower() except: return None def update_key_ui(self, key, is_pressed): """更新按键高亮和人物图片""" # 更新人物图片 if self.char_image_label and self.idle_photo and self.pressed_photo: if len(self.active_keys) > 0: self.char_image_label.config(image=self.pressed_photo) else: self.char_image_label.config(image=self.idle_photo) # 更新按键方块 if key in self.key_labels: if self.config["ui_mode"] == "keyboard": data = self.key_labels[key] color = "#98c379" if is_pressed else "#3e4451" # 按下变绿 data["frame"].config(bg=color) data["name"].config(bg=color) data["count"].config(bg=color, text=str(self.config["counts"][key])) else: lbl = self.key_labels[key]["label"] lbl.config(text=f"{key.upper()}: {self.config['counts'][key]}", fg="#98c379" if is_pressed else "white") # 基础交互函数 def start_drag(self, event): self._drag_data["x"], self._drag_data["y"] = event.x, event.y def do_drag(self, event): x = self.root.winfo_x() - self._drag_data["x"] + event.x y = self.root.winfo_y() - self._drag_data["y"] + event.y self.root.geometry(f"+{x}+{y}") def show_menu(self, event): self.menu.post(event.x_root, event.y_root) def toggle_ui_mode(self): self.config["ui_mode"] = "keyboard" if self.config["ui_mode"] == "list" else "list" self.setup_ui() def add_key_dialog(self): nk = simpledialog.askstring("添加", "按键名:") if nk: nk = nk.lower() if nk not in self.config["monitored_keys"]: self.config["monitored_keys"].append(nk) self.config["counts"][nk] = 0 self.setup_ui() def remove_key_dialog(self): rk = simpledialog.askstring("删除", "按键名:") if rk and rk.lower() in self.config["monitored_keys"]: self.config["monitored_keys"].remove(rk.lower()) self.setup_ui() def set_alpha_dialog(self): a = simpledialog.askfloat("透明度", "0.1-1.0:", minvalue=0.1, maxvalue=1.0) if a: self.root.attributes("-alpha", a); self.config["alpha"] = a def reset_counts(self): if messagebox.askyesno("重置", "清空所有计数?"): for k in self.config["counts"]: self.config["counts"][k] = 0 self.setup_ui() def quit_app(self): self.save_config(); self.root.destroy(); os._exit(0) if __name__ == "__main__": KeyOverlayApp()