用ai编写的一些小工具分享

1、按键辅助

我玩游戏的时候有一些日常需要点点点,好麻烦,不然奖励领不完,下面纯源码直接跑就行,页面效果如下,觉得不好看的找ai在帮忙改改

复制代码
"""
鼠标精灵 v3.0  ---  鼠标连点 & 操作录制回放
UI: 录制列表改为卡片模块风格,每张卡片内嵌回放快捷键选择
全局 F7 紧急停止(最高优先级,停一切回放)
"""

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading, time, json, os, datetime, random, ctypes, shutil, sys

import pynput.mouse    as pmouse
import pynput.keyboard as pkeyboard

# ──────────────────────────────────────────
# 路径(兼容 PyInstaller 单文件模式)
# ──────────────────────────────────────────
if getattr(sys, 'frozen', False):
    APP_DIR = os.path.dirname(sys.executable)
else:
    APP_DIR = os.path.dirname(os.path.abspath(__file__))
RECORDS_DIR = os.path.join(APP_DIR, "records")
os.makedirs(RECORDS_DIR, exist_ok=True)

# ──────────────────────────────────────────
# 配色
# ──────────────────────────────────────────
C_GREEN      = "#3dbf76"
C_GREEN_DARK = "#2fa065"
C_GREEN_TAB  = "#45c97e"
C_PANEL      = "#f0faf4"
C_WHITE      = "#ffffff"
C_YELLOW     = "#f5a623"
C_YELLOW_H   = "#e09010"
C_RED        = "#e05555"
C_RED_DARK   = "#c04040"
C_GRAY       = "#888888"
C_TEXT       = "#1a1a1a"
C_SUBTEXT    = "#555555"
C_BORDER     = "#b8e4c8"
C_CARD_BG    = "#ffffff"
C_CARD_HOVER = "#e8f8ef"
C_CARD_SEL   = "#d0f0e0"
C_CARD_BD    = "#c0e8d0"
C_EMERGENCY  = "#d03030"   # 紧急停止按钮颜色

FN  = ("微软雅黑", 9)
FNB = ("微软雅黑", 9,  "bold")
FB  = ("微软雅黑", 12, "bold")
FBB = ("微软雅黑", 14, "bold")
FS  = ("微软雅黑", 8)
FC  = ("微软雅黑", 10, "bold")  # 卡片标题

HK_LIST  = ["F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12"]
# F7 保留为紧急停止,从可选列表里剔除(但不强制,提示即可)
HK_PLAY  = ["(无)"]+[x for x in HK_LIST if x != "F7"]


# ──────────────────────────────────────────
# 工具函数
# ──────────────────────────────────────────
def flat_btn(parent, text, cmd, bg=C_GREEN, fg=C_WHITE,
             font=FN, padx=10, pady=4, width=None):
    b = tk.Button(parent, text=text, command=cmd, bg=bg, fg=fg,
                  font=font, relief="flat", cursor="hand2",
                  activebackground=C_YELLOW_H, activeforeground=C_WHITE,
                  padx=padx, pady=pady, bd=0)
    if width: b.config(width=width)
    hbg = C_YELLOW_H if bg in (C_YELLOW,) else C_GREEN_DARK
    b.bind("<Enter>", lambda e: b.config(bg=hbg))
    b.bind("<Leave>", lambda e: b.config(bg=bg))
    return b

def yellow_btn(parent, text, cmd, font=FBB, padx=28, pady=10):
    b = tk.Button(parent, text=text, command=cmd,
                  bg=C_YELLOW, fg=C_WHITE, font=font,
                  relief="flat", cursor="hand2", bd=0,
                  activebackground=C_YELLOW_H, activeforeground=C_WHITE,
                  padx=padx, pady=pady)
    b.bind("<Enter>", lambda e: b.config(bg=C_YELLOW_H))
    b.bind("<Leave>", lambda e: b.config(bg=C_YELLOW))
    return b

def emergency_btn(parent, text, cmd, font=FB, padx=18, pady=8):
    """红色紧急停止按钮"""
    b = tk.Button(parent, text=text, command=cmd,
                  bg=C_EMERGENCY, fg=C_WHITE, font=font,
                  relief="flat", cursor="hand2", bd=0,
                  activebackground="#a02020", activeforeground=C_WHITE,
                  padx=padx, pady=pady)
    b.bind("<Enter>", lambda e: b.config(bg="#b02020"))
    b.bind("<Leave>", lambda e: b.config(bg=C_EMERGENCY))
    return b

def sep(parent, color=C_BORDER, pady=0):
    tk.Frame(parent, bg=color, height=1).pack(fill="x", padx=0, pady=pady)


# ──────────────────────────────────────────
# Settings
# ──────────────────────────────────────────
class Settings:
    _DEFAULTS = dict(
        click_button="left",
        click_interval=0.1,
        click_mode="efficient",
        clicker_hotkey="f9",
        rec_hotkey="f8",
        use_random_interval=False,
        random_max=0.5,
        press_release_interval=0.001,
        use_random_offset=False,
        offset_x=2, offset_y=2,
        use_fixed_pos=False,
        fixed_x=0, fixed_y=0,
        use_click_limit=False,
        click_limit=0,
        playback_times=0,
        use_play_interval=False,
        play_interval_min=0.5,
        play_interval_max=1.0,
        auto_hide=True,
        play_sound=False,
    )

    def __init__(self):
        self._path = os.path.join(APP_DIR, "settings.json")
        for k, v in self._DEFAULTS.items():
            setattr(self, k, v)
        self._load()

    def _load(self):
        if not os.path.exists(self._path): return
        try:
            d = json.load(open(self._path, encoding="utf-8"))
            for k, v in d.items():
                if k in self._DEFAULTS: setattr(self, k, v)
        except Exception: pass

    def save(self):
        d = {k: getattr(self, k) for k in self._DEFAULTS}
        json.dump(d, open(self._path, "w", encoding="utf-8"),
                  ensure_ascii=False, indent=2)


# ──────────────────────────────────────────
# AutoClicker
# ──────────────────────────────────────────
class AutoClicker:
    def __init__(self, s: Settings):
        self.s = s
        self._run = False
        self._cnt = 0
        self.on_change = None
        self._mc = pmouse.Controller()

    running = property(lambda self: self._run)

    def toggle(self):
        if self._run: self.stop()
        else:         self.start()

    def start(self):
        if self._run: return
        self._run = True; self._cnt = 0
        threading.Thread(target=self._loop, daemon=True).start()
        self._cb()

    def stop(self):
        self._run = False; self._cb()

    def _cb(self):
        if self.on_change: self.on_change(self._run, self._cnt)

    def _loop(self):
        s = self.s
        BM = {"left": pmouse.Button.left,
              "middle": pmouse.Button.middle,
              "right":  pmouse.Button.right}
        btn = BM.get(s.click_button, pmouse.Button.left)
        while self._run:
            if s.use_fixed_pos:
                self._mc.position = (s.fixed_x, s.fixed_y)
            if s.use_random_offset:
                x, y = self._mc.position
                self._mc.position = (x + random.randint(-s.offset_x, s.offset_x),
                                     y + random.randint(-s.offset_y, s.offset_y))
            self._mc.press(btn)
            time.sleep(max(0.0005, s.press_release_interval))
            self._mc.release(btn)
            self._cnt += 1; self._cb()
            if s.use_click_limit and s.click_limit > 0 and self._cnt >= s.click_limit:
                self._run = False; self._cb(); break
            if s.use_random_interval:
                time.sleep(random.uniform(0.05, max(0.06, s.random_max)))
            elif s.click_mode == "efficient":
                time.sleep(0.1)
            else:
                time.sleep(max(0.001, s.click_interval))


# ──────────────────────────────────────────
# MacroRecorder
# ──────────────────────────────────────────
class MacroRecorder:
    def __init__(self):
        self._rec = False
        self._evs = []
        self._t0  = 0
        self._ml = self._kl = None
        self.on_change = None
        self.name = ""

    recording = property(lambda self: self._rec)

    def start(self):
        if self._rec: return
        self._evs = []; self._t0 = time.time(); self._rec = True
        self._ml = pmouse.Listener(on_move=self._mv, on_click=self._cl, on_scroll=self._sc)
        self._kl = pkeyboard.Listener(on_press=self._kp, on_release=self._kr)
        self._ml.start(); self._kl.start(); self._cb()

    def stop(self):
        self._rec = False
        for l in (self._ml, self._kl):
            if l:
                try: l.stop()
                except: pass
        self._ml = self._kl = None
        self._cb()

    def _ts(self): return time.time() - self._t0
    def _cb(self):
        if self.on_change: self.on_change(self._rec, len(self._evs))

    def _mv(self, x, y):
        if self._rec: self._evs.append({"t":self._ts(),"type":"move","x":x,"y":y}); self._cb()
    def _cl(self, x, y, btn, pressed):
        if self._rec: self._evs.append({"t":self._ts(),"type":"click","x":x,"y":y,
                                         "button":str(btn).split(".")[-1],"pressed":pressed}); self._cb()
    def _sc(self, x, y, dx, dy):
        if self._rec: self._evs.append({"t":self._ts(),"type":"scroll","x":x,"y":y,"dx":dx,"dy":dy}); self._cb()
    def _kp(self, key):
        if self._rec:
            try: k = key.char
            except: k = str(key)
            self._evs.append({"t":self._ts(),"type":"key_press","key":k}); self._cb()
    def _kr(self, key):
        if self._rec:
            try: k = key.char
            except: k = str(key)
            self._evs.append({"t":self._ts(),"type":"key_release","key":k}); self._cb()

    def duration(self): return self._evs[-1]["t"] if self._evs else 0
    def events(self):   return list(self._evs)

    def save(self, path, play_hk=""):
        name = self.name or datetime.datetime.now().strftime("录制-%Y%m%d%H%M%S")
        json.dump({"name":name,"created":datetime.datetime.now().isoformat(),
                   "play_hk":play_hk,"events":self._evs},
                  open(path,"w",encoding="utf-8"), ensure_ascii=False, indent=2)

    def load(self, path):
        d = json.load(open(path, encoding="utf-8"))
        self.name  = d.get("name","")
        self._evs  = d.get("events",[])
        return d.get("play_hk","")


# ──────────────────────────────────────────
# MacroPlayer
# ──────────────────────────────────────────
class MacroPlayer:
    def __init__(self, s: Settings):
        self.s = s
        self._playing = False
        self._cnt = 0
        self._evs = []
        self.on_change = None
        self._mc = pmouse.Controller()
        self._kc = pkeyboard.Controller()

    playing = property(lambda self: self._playing)

    def start(self, events):
        if self._playing or not events: return
        self._playing = True; self._cnt = 0; self._evs = events
        threading.Thread(target=self._loop, daemon=True).start()
        self._cb()

    def stop(self):
        self._playing = False; self._cb()

    def _cb(self):
        if self.on_change: self.on_change(self._playing, self._cnt)

    def _play_once(self):
        BM = {"left":pmouse.Button.left,"middle":pmouse.Button.middle,"right":pmouse.Button.right}
        prev = 0
        for ev in self._evs:
            if not self._playing: break
            gap = ev["t"] - prev
            if gap > 0: time.sleep(gap)
            prev = ev["t"]
            t = ev["type"]
            if t == "move":
                self._mc.position = (ev["x"], ev["y"])
            elif t == "click":
                btn = BM.get(ev["button"], pmouse.Button.left)
                self._mc.press(btn) if ev["pressed"] else self._mc.release(btn)
            elif t == "scroll":
                self._mc.scroll(ev["dx"], ev["dy"])
            elif t in ("key_press", "key_release"):
                try:
                    k = ev["key"]
                    if k and k.startswith("Key."):
                        ko = getattr(pkeyboard.Key, k[4:], None)
                        if ko: (self._kc.press if t=="key_press" else self._kc.release)(ko)
                    elif k and len(k)==1:
                        (self._kc.press if t=="key_press" else self._kc.release)(k)
                except Exception: pass

    def _loop(self):
        s = self.s
        total = s.playback_times if s.playback_times > 0 else float("inf")
        n = 0
        while self._playing and n < total:
            self._play_once(); n += 1; self._cnt = int(n); self._cb()
            if self._playing and s.use_play_interval and n < total:
                time.sleep(random.uniform(s.play_interval_min, s.play_interval_max))
        self._playing = False; self._cb()


# ──────────────────────────────────────────
# HotkeyManager
# ──────────────────────────────────────────
class HotkeyManager:
    def __init__(self):
        self._cbs  = {}
        self._held = set()
        self._listener = None

    def register(self, name: str, cb):
        if name: self._cbs[name.lower()] = cb

    def unregister(self, name: str):
        self._cbs.pop(name.lower() if name else "", None)

    def start(self):
        if self._listener: return
        self._listener = pkeyboard.Listener(on_press=self._press, on_release=self._release)
        self._listener.start()

    def stop(self):
        if self._listener:
            try: self._listener.stop()
            except: pass
            self._listener = None

    @staticmethod
    def _kname(key):
        try:
            c = key.char
            return c.lower() if c else str(key).replace("Key.","").lower()
        except AttributeError:
            return str(key).replace("Key.","").lower()

    def _press(self, key):
        n = self._kname(key)
        if n in self._held: return
        self._held.add(n)
        cb = self._cbs.get(n)
        if cb:
            threading.Thread(target=cb, daemon=True).start()

    def _release(self, key):
        self._held.discard(self._kname(key))


# ══════════════════════════════════════════
#  录制卡片组件
# ══════════════════════════════════════════
class RecordCard(tk.Frame):
    """单条录制的卡片控件"""

    def __init__(self, parent, rec_info: dict, app, index: int, **kwargs):
        super().__init__(parent, bg=C_CARD_BG, bd=1, relief="groove",
                         highlightbackground=C_CARD_BD, highlightthickness=1,
                         **kwargs)
        self.rec  = rec_info
        self.app  = app
        self.idx  = index
        self._selected = False
        self._build()
        self.bind("<Button-1>", self._on_click)
        self._bind_children(self)

    def _bind_children(self, widget):
        for child in widget.winfo_children():
            child.bind("<Button-1>", self._on_click)
            self._bind_children(child)

    def _build(self):
        r = self.rec
        # ── 左侧编号 ──
        num_f = tk.Frame(self, bg=C_GREEN, width=34)
        num_f.pack(side="left", fill="y"); num_f.pack_propagate(False)
        tk.Label(num_f, text=f"{self.idx+1:02d}", bg=C_GREEN, fg=C_WHITE,
                 font=("微软雅黑", 12, "bold")).pack(expand=True)

        # ── 中间信息 ──
        info_f = tk.Frame(self, bg=C_CARD_BG)
        info_f.pack(side="left", fill="both", expand=True, padx=8, pady=4)

        # 第一行:名称 + 时长
        row1 = tk.Frame(info_f, bg=C_CARD_BG)
        row1.pack(fill="x")
        self._name_lbl = tk.Label(row1, text=r["name"], bg=C_CARD_BG,
                                   font=FC, fg=C_TEXT, anchor="w")
        self._name_lbl.pack(side="left")
        tk.Label(row1, text=f"  [{r['dur']}]", bg=C_CARD_BG,
                 font=FS, fg=C_SUBTEXT).pack(side="left")

        # 第二行:创建时间
        tk.Label(info_f, text=f"录制时间: {r['created']}", bg=C_CARD_BG,
                 font=FS, fg=C_GRAY, anchor="w").pack(fill="x")

        # ── 右侧控件区 ──
        ctrl_f = tk.Frame(self, bg=C_CARD_BG)
        ctrl_f.pack(side="right", padx=8, pady=4)

        # 快捷键选择
        hk_row = tk.Frame(ctrl_f, bg=C_CARD_BG)
        hk_row.pack(fill="x", pady=(0, 4))
        tk.Label(hk_row, text="播放键:", bg=C_CARD_BG, font=FS,
                 fg=C_SUBTEXT).pack(side="left")
        self._hk_var = tk.StringVar(value=(r.get("play_hk","") or "(无)").upper())
        hk_cb = ttk.Combobox(hk_row, textvariable=self._hk_var,
                              values=HK_PLAY, width=6, state="readonly",
                              font=FS)
        hk_cb.pack(side="left", padx=(3,0))
        hk_cb.bind("<<ComboboxSelected>>", self._on_hk_change)
        hk_cb.bind("<Button-1>", lambda e: "break")  # 阻止冒泡到卡片点击

        # 按钮行
        btn_row = tk.Frame(ctrl_f, bg=C_CARD_BG)
        btn_row.pack()

        pb = tk.Button(btn_row, text="▶ 播放", command=self._play,
                       bg=C_GREEN, fg=C_WHITE, font=FS, relief="flat",
                       cursor="hand2", bd=0, padx=8, pady=3)
        pb.pack(side="left", padx=2)
        pb.bind("<Enter>", lambda e: pb.config(bg=C_GREEN_DARK))
        pb.bind("<Leave>", lambda e: pb.config(bg=C_GREEN))

        db = tk.Button(btn_row, text="🗑", command=self._delete,
                       bg="#f5f5f5", fg=C_GRAY, font=FS, relief="flat",
                       cursor="hand2", bd=0, padx=6, pady=3)
        db.pack(side="left", padx=2)
        db.bind("<Enter>", lambda e: db.config(bg="#ffe0e0", fg=C_RED))
        db.bind("<Leave>", lambda e: db.config(bg="#f5f5f5", fg=C_GRAY))

    def set_selected(self, val: bool):
        self._selected = val
        bg = C_CARD_SEL if val else C_CARD_BG
        self.config(bg=bg)
        for child in self.winfo_children():
            self._set_bg_recursive(child, bg)
        # 编号栏保持绿色
        num_children = self.winfo_children()
        if num_children:
            num_children[0].config(bg=C_GREEN)
            for c in num_children[0].winfo_children():
                c.config(bg=C_GREEN)

    def _set_bg_recursive(self, widget, bg):
        try: widget.config(bg=bg)
        except: pass
        for child in widget.winfo_children():
            self._set_bg_recursive(child, bg)
        # combobox 不改
        if isinstance(widget, ttk.Combobox):
            pass

    def _on_click(self, e=None):
        self.app._card_select(self.idx)

    def _play(self):
        self.app._card_select(self.idx)
        self.app._play_record(self.rec)

    def _delete(self):
        if messagebox.askyesno("确认删除",
                               f"确定删除「{self.rec['name']}」吗?\n此操作不可恢复!",
                               parent=self.app):
            try: os.remove(self.rec["path"])
            except: pass
            self.app._load_records()
            self.app._rebuild_cards()

    def _on_hk_change(self, e=None):
        val = self._hk_var.get()
        hk = "" if val == "(无)" else val.lower()
        # 检查 F7 冲突
        if hk == "f7":
            messagebox.showwarning("热键冲突",
                "F7 已被保留为全局紧急停止键,请选择其他键。",
                parent=self.app)
            self._hk_var.set((self.rec.get("play_hk","") or "(无)").upper())
            return
        self.rec["play_hk"] = hk
        # 写回文件
        try:
            d = json.load(open(self.rec["path"], encoding="utf-8"))
            d["play_hk"] = hk
            json.dump(d, open(self.rec["path"],"w",encoding="utf-8"),
                      ensure_ascii=False, indent=2)
        except Exception as ex:
            messagebox.showerror("保存失败", str(ex), parent=self.app)
            return
        self.app._rebuild_play_hotkeys()


# ══════════════════════════════════════════
#  主窗口
# ══════════════════════════════════════════
class App(tk.Tk):

    def __init__(self):
        super().__init__()
        self.s        = Settings()
        self.clicker  = AutoClicker(self.s)
        self.recorder = MacroRecorder()
        self.player   = MacroPlayer(self.s)
        self.hkm      = HotkeyManager()

        self._records     = []
        self._cards       = []
        self._sel_idx     = -1
        self._cur_play_hks= []   # 当前所有录制的播放热键名

        self.title("鼠标精灵  v3.0")
        self.resizable(False, False)
        self.configure(bg=C_GREEN)

        self._build_ui()
        self._load_records()

        self.clicker.on_change  = lambda r,c: self.after(0, self._on_click_cb,  r, c)
        self.recorder.on_change = lambda r,c: self.after(0, self._on_rec_cb,    r, c)
        self.player.on_change   = lambda r,c: self.after(0, self._on_play_cb,   r, c)

        self._setup_hotkeys()
        self.protocol("WM_DELETE_WINDOW", self._quit)

        self.update_idletasks()
        W, H = 560, 580
        sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
        self.geometry(f"{W}x{H}+{(sw-W)//2}+{(sh-H)//2}")
        self.after(80, self._rebuild_cards)

    # ── 顶栏 & Tab ────────────────────────
    def _build_ui(self):
        # 标题栏
        title_bar = tk.Frame(self, bg=C_GREEN_DARK, height=46)
        title_bar.pack(fill="x"); title_bar.pack_propagate(False)

        logo_f = tk.Frame(title_bar, bg=C_GREEN_DARK)
        logo_f.pack(side="left", padx=10)
        tk.Label(logo_f, text="🖱", bg=C_GREEN_DARK, fg=C_WHITE,
                 font=("微软雅黑",16)).pack(side="left")
        tk.Label(logo_f, text=" 鼠标精灵", bg=C_GREEN_DARK, fg=C_WHITE,
                 font=("微软雅黑",13,"bold")).pack(side="left")

        right_f = tk.Frame(title_bar, bg=C_GREEN_DARK)
        right_f.pack(side="right", padx=8)
        flat_btn(right_f, "⚙ 设置", self._open_settings,
                 bg=C_GREEN_DARK, padx=8, pady=4).pack(side="left", padx=2)
        flat_btn(right_f, "✕", self._quit,
                 bg=C_GREEN_DARK, padx=8, pady=4).pack(side="left", padx=2)

        # Tab 栏
        tab_bar = tk.Frame(self, bg=C_GREEN, height=36)
        tab_bar.pack(fill="x"); tab_bar.pack_propagate(False)

        self._tabs  = {}
        self._pages = {}
        for key, icon in [("鼠标连点","🖱 鼠标连点"), ("鼠标录制","⏺ 鼠标录制")]:
            btn = tk.Button(tab_bar, text=icon, bg=C_GREEN, fg=C_WHITE,
                            font=("微软雅黑",10,"bold"), relief="flat",
                            cursor="hand2", bd=0, padx=18, pady=6,
                            activebackground=C_GREEN_TAB, activeforeground=C_WHITE,
                            command=lambda k=key: self._switch(k))
            btn.pack(side="left")
            self._tabs[key] = btn

        tk.Frame(self, bg=C_BORDER, height=1).pack(fill="x")

        self._body = tk.Frame(self, bg=C_PANEL)
        self._body.pack(fill="both", expand=True)

        self._build_clicker_page()
        self._build_recorder_page()
        self._switch("鼠标连点")

    def _switch(self, key):
        for k, btn in self._tabs.items():
            btn.config(bg=C_GREEN_TAB if k==key else C_GREEN)
        for k, pg in self._pages.items():
            if k == key: pg.pack(fill="both", expand=True)
            else:        pg.pack_forget()

    # ══════════════════════════════════════
    #  连点页
    # ══════════════════════════════════════
    def _build_clicker_page(self):
        pg = tk.Frame(self._body, bg=C_PANEL)
        self._pages["鼠标连点"] = pg

        box1 = tk.LabelFrame(pg, text="  点击类型  ", bg=C_PANEL,
                              font=FN, fg=C_GREEN_DARK, bd=1, relief="groove")
        box1.pack(fill="x", padx=14, pady=(10,4))
        self._cbtn_var = tk.StringVar(value=self.s.click_button)
        rbf = tk.Frame(box1, bg=C_PANEL); rbf.pack(fill="x", padx=6, pady=6)
        for val,lbl in [("left","🖱 左键"),("middle","⚙ 中键"),("right","🖱 右键")]:
            tk.Radiobutton(rbf, text=lbl, variable=self._cbtn_var, value=val,
                           bg=C_PANEL, font=FN, activebackground=C_PANEL,
                           selectcolor=C_PANEL, fg=C_TEXT,
                           command=self._apply_click_settings).pack(side="left", padx=14)

        box2 = tk.LabelFrame(pg, text="  间隔时间  ", bg=C_PANEL,
                              font=FN, fg=C_GREEN_DARK, bd=1, relief="groove")
        box2.pack(fill="x", padx=14, pady=4)
        intv_row = tk.Frame(box2, bg=C_PANEL); intv_row.pack(fill="x", padx=8, pady=6)
        self._cmode_var  = tk.StringVar(value=self.s.click_mode)
        mode_map = {"efficient":"高效模式 (10次/秒)", "custom":"自定义间隔"}
        display_vals = list(mode_map.values())
        self._cmode_disp = tk.StringVar(value=mode_map.get(self.s.click_mode, display_vals[0]))
        cb = ttk.Combobox(intv_row, textvariable=self._cmode_disp,
                          values=display_vals, width=20, state="readonly", font=FN)
        cb.pack(side="left", padx=4)
        cb.bind("<<ComboboxSelected>>", lambda e: self._on_mode_change())
        tk.Label(intv_row, text="自定义(秒):", bg=C_PANEL, font=FN, fg=C_SUBTEXT).pack(side="left",padx=(10,2))
        self._cintv_var = tk.StringVar(value=str(self.s.click_interval))
        tk.Entry(intv_row, textvariable=self._cintv_var, width=7,
                 font=FN, relief="solid", bd=1).pack(side="left")
        tk.Label(intv_row, text="秒", bg=C_PANEL, font=FN).pack(side="left", padx=2)

        box3 = tk.LabelFrame(pg, text="  启停热键  ", bg=C_PANEL,
                              font=FN, fg=C_GREEN_DARK, bd=1, relief="groove")
        box3.pack(fill="x", padx=14, pady=4)
        hk_row = tk.Frame(box3, bg=C_PANEL); hk_row.pack(fill="x", padx=8, pady=6)
        self._chk_var = tk.StringVar(value=self.s.clicker_hotkey.upper())
        hk_cb = ttk.Combobox(hk_row, textvariable=self._chk_var,
                              values=[x for x in HK_LIST if x!="F7"],
                              width=8, state="readonly", font=FN)
        hk_cb.pack(side="left", padx=4)
        hk_cb.bind("<<ComboboxSelected>>", lambda e: self._rebuild_clicker_hk())
        tk.Label(hk_row, text="← 按此键切换连点 启动/停止",
                 bg=C_PANEL, font=FS, fg=C_GRAY).pack(side="left", padx=6)

        box4 = tk.LabelFrame(pg, text="  次数限制  ", bg=C_PANEL,
                              font=FN, fg=C_GREEN_DARK, bd=1, relief="groove")
        box4.pack(fill="x", padx=14, pady=4)
        lim_row = tk.Frame(box4, bg=C_PANEL); lim_row.pack(fill="x", padx=8, pady=6)
        self._use_lim_var = tk.BooleanVar(value=self.s.use_click_limit)
        tk.Checkbutton(lim_row, text="启用,点击", variable=self._use_lim_var,
                       bg=C_PANEL, font=FN, activebackground=C_PANEL,
                       selectcolor=C_PANEL,
                       command=self._apply_click_settings).pack(side="left")
        self._lim_var = tk.StringVar(value=str(self.s.click_limit))
        tk.Entry(lim_row, textvariable=self._lim_var, width=7,
                 font=FN, relief="solid", bd=1).pack(side="left", padx=4)
        tk.Label(lim_row, text="次后自动停止", bg=C_PANEL, font=FN).pack(side="left")

        sep(pg)
        self._cstatus_var = tk.StringVar(value="就绪,按热键开始连点")
        tk.Label(pg, textvariable=self._cstatus_var,
                 bg=C_PANEL, font=FS, fg=C_GRAY, anchor="w", padx=14).pack(fill="x", pady=(4,0))

        btn_f = tk.Frame(pg, bg=C_PANEL); btn_f.pack(pady=10)
        self._click_big = yellow_btn(btn_f, self._click_btn_txt(), self._toggle_clicker)
        self._click_big.pack()
        self._ccount_var = tk.StringVar(value="点击次数: 0")
        tk.Label(pg, textvariable=self._ccount_var,
                 bg=C_PANEL, font=FS, fg=C_GRAY).pack()

    def _click_btn_txt(self):
        hk = self.s.clicker_hotkey.upper()
        bm = {"left":"左键","middle":"中键","right":"右键"}
        bn = bm.get(self.s.click_button,"左键")
        return f"按 {hk} 键{'停止' if self.clicker.running else '开始'} {bn} 连点"

    def _on_mode_change(self):
        disp = self._cmode_disp.get()
        self._cmode_var.set("efficient" if "高效" in disp else "custom")
        self._apply_click_settings()

    def _toggle_clicker(self):
        self._apply_click_settings()
        self.clicker.toggle()

    def _apply_click_settings(self):
        s = self.s
        s.click_button = self._cbtn_var.get()
        s.click_mode   = self._cmode_var.get()
        try: s.click_interval = float(self._cintv_var.get())
        except: pass
        s.use_click_limit = self._use_lim_var.get()
        try: s.click_limit = int(self._lim_var.get())
        except: pass
        s.save()

    def _rebuild_clicker_hk(self):
        self.hkm.unregister(self.s.clicker_hotkey)
        self.s.clicker_hotkey = self._chk_var.get().lower()
        self.s.save()
        self.hkm.register(self.s.clicker_hotkey, self._toggle_clicker)
        self._click_big.config(text=self._click_btn_txt())

    def _on_click_cb(self, running, cnt):
        self._ccount_var.set(f"点击次数: {cnt}")
        if running:
            self._cstatus_var.set(f"⚡ 连点中... 已点击 {cnt} 次")
            self._click_big.config(text=self._click_btn_txt(), bg=C_RED,
                                   activebackground=C_RED_DARK)
        else:
            self._cstatus_var.set(f"✔ 已停止,共点击 {cnt} 次")
            self._click_big.config(text=self._click_btn_txt(), bg=C_YELLOW,
                                   activebackground=C_YELLOW_H)

    # ══════════════════════════════════════
    #  录制页
    # ══════════════════════════════════════
    def _build_recorder_page(self):
        pg = tk.Frame(self._body, bg=C_PANEL)
        self._pages["鼠标录制"] = pg

        # ── 顶部工具栏 ──
        bar = tk.Frame(pg, bg=C_PANEL); bar.pack(fill="x", padx=12, pady=(8,4))
        for txt, cmd in [("⬇ 导入", self._import), ("⬆ 导出", self._export)]:
            flat_btn(bar, txt, cmd, bg=C_GREEN, padx=10, pady=4, font=FN).pack(side="left", padx=3)

        tk.Frame(bar, bg=C_BORDER, width=1).pack(side="left", fill="y", padx=8, pady=2)
        tk.Label(bar, text="录制键:", bg=C_PANEL, font=FN, fg=C_SUBTEXT).pack(side="left")
        self._rhk_var = tk.StringVar(value=self.s.rec_hotkey.upper())
        rhk_cb = ttk.Combobox(bar, textvariable=self._rhk_var,
                               values=[x for x in HK_LIST if x!="F7"],
                               width=5, state="readonly", font=FN)
        rhk_cb.pack(side="left", padx=4)
        rhk_cb.bind("<<ComboboxSelected>>", lambda e: self._rebuild_rec_hk())

        # ── 紧急停止提示 ──
        emg_bar = tk.Frame(pg, bg="#fff0f0", bd=1, relief="flat")
        emg_bar.pack(fill="x", padx=12, pady=(0,4))
        tk.Label(emg_bar, text="🛑  F7 = 全局紧急停止(任何回放立即中断)",
                 bg="#fff0f0", fg=C_EMERGENCY, font=FNB,
                 padx=10, pady=4).pack(side="left")
        emergency_btn(emg_bar, "⛔ 立即停止", self._emergency_stop,
                      font=FNB, padx=12, pady=3).pack(side="right", padx=8, pady=3)

        # ── 卡片列表区 ──
        list_label = tk.Frame(pg, bg=C_GREEN, height=26)
        list_label.pack(fill="x", padx=12); list_label.pack_propagate(False)
        tk.Label(list_label, text="  # 录制名称", bg=C_GREEN, fg=C_WHITE,
                 font=FNB, anchor="w").pack(side="left", padx=4)
        tk.Label(list_label, text="时长    播放快捷键    操作",
                 bg=C_GREEN, fg=C_WHITE, font=FNB, anchor="e").pack(side="right", padx=12)

        # 滚动容器
        outer = tk.Frame(pg, bg=C_PANEL, bd=1, relief="groove")
        outer.pack(fill="both", expand=True, padx=12, pady=(0,4))

        self._card_canvas = tk.Canvas(outer, bg=C_PANEL, highlightthickness=0)
        vsb = ttk.Scrollbar(outer, orient="vertical", command=self._card_canvas.yview)
        self._card_canvas.configure(yscrollcommand=vsb.set)
        vsb.pack(side="right", fill="y")
        self._card_canvas.pack(side="left", fill="both", expand=True)

        self._card_frame = tk.Frame(self._card_canvas, bg=C_PANEL)
        self._card_window = self._card_canvas.create_window(
            (0,0), window=self._card_frame, anchor="nw")

        self._card_frame.bind("<Configure>", self._on_card_frame_configure)
        self._card_canvas.bind("<Configure>", self._on_canvas_configure)
        self._card_canvas.bind("<MouseWheel>", self._on_mousewheel)

        # ── 状态 + 录制大按钮 ──
        sep(pg)
        self._rstatus_var = tk.StringVar(value="就绪,按 F8 开始录制")
        tk.Label(pg, textvariable=self._rstatus_var,
                 bg=C_PANEL, font=FS, fg=C_GRAY, anchor="w", padx=14).pack(fill="x", pady=(3,0))

        bf = tk.Frame(pg, bg=C_PANEL); bf.pack(pady=6)
        self._rec_big = yellow_btn(bf, self._rec_btn_txt(), self._toggle_rec)
        self._rec_big.pack()

    def _on_card_frame_configure(self, e):
        self._card_canvas.configure(scrollregion=self._card_canvas.bbox("all"))

    def _on_canvas_configure(self, e):
        self._card_canvas.itemconfig(self._card_window, width=e.width)

    def _on_mousewheel(self, e):
        self._card_canvas.yview_scroll(int(-1*(e.delta/120)), "units")

    # ── 卡片管理 ──────────────────────────
    def _rebuild_cards(self):
        for c in self._cards:
            try: c.destroy()
            except: pass
        self._cards = []
        for i, r in enumerate(self._records):
            card = RecordCard(self._card_frame, r, self, i)
            card.pack(fill="x", padx=4, pady=3)
            self._cards.append(card)
        if not self._records:
            tk.Label(self._card_frame,
                     text="暂无录制  ---  按 F8 开始录制第一条",
                     bg=C_PANEL, font=FN, fg=C_GRAY).pack(pady=30)
        self._rebuild_play_hotkeys()

    def _rebuild_play_hotkeys(self):
        """重新注册所有录制卡片的播放热键"""
        for hk in self._cur_play_hks:
            self.hkm.unregister(hk)
        self._cur_play_hks = []
        for r in self._records:
            hk = r.get("play_hk","")
            if hk and hk.lower() != "f7":
                self.hkm.register(hk.lower(), lambda rec=r: self.after(0, self._play_record, rec))
                self._cur_play_hks.append(hk.lower())

    def _card_select(self, idx):
        self._sel_idx = idx
        for i, card in enumerate(self._cards):
            card.set_selected(i == idx)

    # ── 回放 ─────────────────────────────
    def _play_record(self, rec):
        rec_obj = MacroRecorder()
        try: rec_obj.load(rec["path"])
        except Exception as e:
            messagebox.showerror("错误", str(e)); return
        self.player.stop()
        time.sleep(0.05)
        self.player.start(rec_obj.events())

    def _emergency_stop(self):
        """F7 紧急停止 --- 最高优先级"""
        self.clicker.stop()
        self.player.stop()
        self.after(0, lambda: self._rstatus_var.set("🛑 紧急停止!所有操作已中断"))

    # ── 录制控制 ─────────────────────────
    def _rec_btn_txt(self):
        hk = self._rhk_var.get() if hasattr(self,"_rhk_var") else self.s.rec_hotkey.upper()
        return f"按 {hk} 键{'停止' if self.recorder.recording else '开始'}录制"

    def _toggle_rec(self):
        if self.recorder.recording: self._stop_rec()
        else:                       self._start_rec()

    def _start_rec(self):
        self.recorder.start()
        self._rstatus_var.set("⏺ 录制中...")
        self._rec_big.config(text=self._rec_btn_txt(), bg=C_RED, activebackground=C_RED_DARK)

    def _stop_rec(self):
        self.recorder.stop()
        dur  = self.recorder.duration()
        name = datetime.datetime.now().strftime("录制-%Y%m%d%H%M%S")
        path = os.path.join(RECORDS_DIR, name+".json")
        self.recorder.name = name
        self.recorder.save(path, play_hk="")
        self._records.append(dict(
            name    = name, path=path,
            created = datetime.datetime.now().strftime("%Y/%m/%d %H:%M"),
            dur     = f"{dur:.1f}s",
            play_hk = "",
        ))
        self._rebuild_cards()
        self._rstatus_var.set(f"✔ 已保存: {name}  时长 {dur:.1f}s")
        self._rec_big.config(text=self._rec_btn_txt(), bg=C_YELLOW,
                             activebackground=C_YELLOW_H)

    def _on_rec_cb(self, recording, cnt):
        if recording: self._rstatus_var.set(f"⏺ 录制中... {cnt} 个事件")

    def _on_play_cb(self, playing, cnt):
        if playing:
            self._rstatus_var.set(f"▶ 回放中... 第 {cnt} 次")
        else:
            self._rstatus_var.set(f"✔ 回放结束,共 {cnt} 次")

    # ── 导入/导出 ─────────────────────────
    def _load_records(self):
        self._records = []
        for fn in sorted(os.listdir(RECORDS_DIR)):
            if not fn.endswith(".json"): continue
            path = os.path.join(RECORDS_DIR, fn)
            try:
                d = json.load(open(path, encoding="utf-8"))
                evs = d.get("events",[])
                dur = evs[-1]["t"] if evs else 0
                self._records.append(dict(
                    name    = d.get("name", fn),
                    path    = path,
                    created = d.get("created","")[:16].replace("T"," "),
                    dur     = f"{dur:.1f}s",
                    play_hk = d.get("play_hk",""),
                ))
            except Exception: pass

    def _import(self):
        path = filedialog.askopenfilename(title="导入录制",
                                          filetypes=[("JSON录制","*.json"),("所有","*.*")])
        if not path: return
        dst = os.path.join(RECORDS_DIR, os.path.basename(path))
        shutil.copy2(path, dst)
        self._load_records(); self._rebuild_cards()
        messagebox.showinfo("导入成功", os.path.basename(path))

    def _export(self):
        if self._sel_idx < 0 or self._sel_idx >= len(self._records):
            messagebox.showinfo("提示","请先点击选中一条录制"); return
        r = self._records[self._sel_idx]
        dst = filedialog.asksaveasfilename(title="导出录制",
                                           initialfile=os.path.basename(r["path"]),
                                           defaultextension=".json",
                                           filetypes=[("JSON录制","*.json")])
        if not dst: return
        shutil.copy2(r["path"], dst)
        messagebox.showinfo("导出成功", dst)

    # ── 热键注册 ──────────────────────────
    def _setup_hotkeys(self):
        self.hkm.register(self.s.clicker_hotkey, self._toggle_clicker)
        self.hkm.register(self.s.rec_hotkey,     self._toggle_rec)
        self.hkm.register("f7",                  self._emergency_stop)   # 最高优先级,固定不可改
        self.hkm.start()

    def _rebuild_rec_hk(self):
        self.hkm.unregister(self.s.rec_hotkey)
        self.s.rec_hotkey = self._rhk_var.get().lower()
        self.s.save()
        self.hkm.register(self.s.rec_hotkey, self._toggle_rec)
        self._rec_big.config(text=self._rec_btn_txt())

    # ══════════════════════════════════════
    #  设置窗口
    # ══════════════════════════════════════
    def _open_settings(self):
        w = tk.Toplevel(self)
        w.title("鼠标精灵 - 设置")
        w.resizable(False,False)
        w.configure(bg=C_PANEL)
        w.geometry("490x400")
        w.transient(self); w.grab_set()
        s = self.s

        nb = ttk.Notebook(w)
        nb.pack(fill="both", expand=True, padx=8, pady=8)

        def page(title):
            p = tk.Frame(nb, bg=C_PANEL); nb.add(p, text=title); return p

        p1 = page("点击设置")
        ri_v=tk.BooleanVar(value=s.use_random_interval)
        ri_m=tk.StringVar(value=str(s.random_max))
        r1=tk.Frame(p1,bg=C_PANEL); r1.pack(fill="x",padx=14,pady=5)
        tk.Checkbutton(r1,text="启用随机间隔,最长",variable=ri_v,
                       bg=C_PANEL,font=FN,activebackground=C_PANEL,selectcolor=C_PANEL).pack(side="left")
        tk.Entry(r1,textvariable=ri_m,width=7,font=FN,relief="solid",bd=1).pack(side="left",padx=4)
        tk.Label(r1,text="秒",bg=C_PANEL,font=FN).pack(side="left")

        pr_v=tk.StringVar(value=str(s.press_release_interval))
        r2=tk.Frame(p1,bg=C_PANEL); r2.pack(fill="x",padx=14,pady=5)
        tk.Label(r2,text="按下抬起间隔:",bg=C_PANEL,font=FN).pack(side="left")
        tk.Entry(r2,textvariable=pr_v,width=8,font=FN,relief="solid",bd=1).pack(side="left",padx=4)
        tk.Label(r2,text="秒  (建议 0.001~0.05)",bg=C_PANEL,font=FS,fg=C_GRAY).pack(side="left")

        ro_v=tk.BooleanVar(value=s.use_random_offset)
        ox_v=tk.StringVar(value=str(s.offset_x)); oy_v=tk.StringVar(value=str(s.offset_y))
        r3=tk.Frame(p1,bg=C_PANEL); r3.pack(fill="x",padx=14,pady=5)
        tk.Checkbutton(r3,text="坐标随机抖动",variable=ro_v,
                       bg=C_PANEL,font=FN,activebackground=C_PANEL,selectcolor=C_PANEL).pack(side="left")
        for lbl,var in [("X抖动:",ox_v),("Y抖动:",oy_v)]:
            tk.Label(r3,text=lbl,bg=C_PANEL,font=FN).pack(side="left",padx=(6,2))
            tk.Entry(r3,textvariable=var,width=4,font=FN,relief="solid",bd=1).pack(side="left")
            tk.Label(r3,text="px",bg=C_PANEL,font=FN).pack(side="left",padx=1)

        fp_v=tk.BooleanVar(value=s.use_fixed_pos)
        fx_v=tk.StringVar(value=str(s.fixed_x)); fy_v=tk.StringVar(value=str(s.fixed_y))
        r4=tk.Frame(p1,bg=C_PANEL); r4.pack(fill="x",padx=14,pady=5)
        tk.Checkbutton(r4,text="固定坐标点击",variable=fp_v,
                       bg=C_PANEL,font=FN,activebackground=C_PANEL,selectcolor=C_PANEL).pack(side="left")
        for lbl,var in [("X:",fx_v),("Y:",fy_v)]:
            tk.Label(r4,text=lbl,bg=C_PANEL,font=FN).pack(side="left",padx=(8,2))
            tk.Entry(r4,textvariable=var,width=6,font=FN,relief="solid",bd=1).pack(side="left")

        p2 = page("回放设置")
        pt_v=tk.StringVar(value=str(s.playback_times))
        rp1=tk.Frame(p2,bg=C_PANEL); rp1.pack(fill="x",padx=14,pady=8)
        tk.Label(rp1,text="回放次数 (0=无限循环):",bg=C_PANEL,font=FN).pack(side="left")
        tk.Entry(rp1,textvariable=pt_v,width=7,font=FN,relief="solid",bd=1).pack(side="left",padx=6)

        pi_v=tk.BooleanVar(value=s.use_play_interval)
        pi_mn=tk.StringVar(value=str(s.play_interval_min))
        pi_mx=tk.StringVar(value=str(s.play_interval_max))
        rp2=tk.Frame(p2,bg=C_PANEL); rp2.pack(fill="x",padx=14,pady=5)
        tk.Checkbutton(rp2,text="每次回放间隔  最小:",variable=pi_v,
                       bg=C_PANEL,font=FN,activebackground=C_PANEL,selectcolor=C_PANEL).pack(side="left")
        tk.Entry(rp2,textvariable=pi_mn,width=6,font=FN,relief="solid",bd=1).pack(side="left",padx=3)
        tk.Label(rp2,text="秒  最大:",bg=C_PANEL,font=FN).pack(side="left")
        tk.Entry(rp2,textvariable=pi_mx,width=6,font=FN,relief="solid",bd=1).pack(side="left",padx=3)
        tk.Label(rp2,text="秒",bg=C_PANEL,font=FN).pack(side="left")

        p3 = page("其他")
        ah_v=tk.BooleanVar(value=s.auto_hide)
        tk.Checkbutton(p3,text="启动连点/录制/回放后自动隐藏主窗口",
                       variable=ah_v,bg=C_PANEL,font=FN,
                       activebackground=C_PANEL,selectcolor=C_PANEL).pack(anchor="w",padx=14,pady=8)
        tk.Label(p3,text="⚠  F7 为固定紧急停止键,不可更改",
                 bg=C_PANEL,font=FS,fg=C_EMERGENCY).pack(anchor="w",padx=14)

        p4 = page("关于")
        tk.Label(p4,text="🖱  鼠标精灵  v3.0",bg=C_PANEL,
                 font=("微软雅黑",14,"bold"),fg=C_GREEN_DARK).pack(pady=22)
        tk.Label(p4,text="免费本地工具  ---  鼠标连点 & 操作录制回放\nF7 全局紧急停止  |  每条录制可绑定独立播放键\nPython + tkinter + pynput",
                 bg=C_PANEL,font=FN,fg=C_SUBTEXT,justify="center").pack()

        bf = tk.Frame(w, bg=C_PANEL); bf.pack(fill="x", padx=10, pady=8)

        def save():
            s.use_random_interval   = ri_v.get()
            s.use_random_offset     = ro_v.get()
            s.use_fixed_pos         = fp_v.get()
            s.use_play_interval     = pi_v.get()
            s.auto_hide             = ah_v.get()
            for attr,var,cast in [
                ("random_max",ri_m,float),("press_release_interval",pr_v,float),
                ("offset_x",ox_v,int),("offset_y",oy_v,int),
                ("fixed_x",fx_v,int),("fixed_y",fy_v,int),
                ("playback_times",pt_v,int),
                ("play_interval_min",pi_mn,float),("play_interval_max",pi_mx,float)]:
                try: setattr(s, attr, cast(var.get()))
                except: pass
            s.save(); w.destroy()

        flat_btn(bf,"  保 存  ",save,bg=C_GREEN,padx=18,pady=5,font=FNB).pack(side="right",padx=6)

    # ── 收尾 ──────────────────────────────
    def _quit(self):
        self.clicker.stop()
        self.recorder.stop()
        self.player.stop()
        self.hkm.stop()
        self.s.save()
        self.destroy()

    def run(self):
        self.mainloop()


# ──────────────────────────────────────────
if __name__ == "__main__":
    try: ctypes.windll.shcore.SetProcessDpiAwareness(1)
    except: pass
    App().run()
相关推荐
Wilbert Lee7 小时前
OpenClaw Windows 最新官方安装教程(超简单一键安装)
windows·openclaw
厌灵泽(后端小白)7 小时前
Windows11本地安装Zookeeper(最新)
大数据·windows·zookeeper·笔记本电脑
数据法师8 小时前
ZyperWin++技术深度解析:2MB的开源Windows优化工具,如何实现一键分级调优?
windows
ВаΙΙаd9 小时前
Windows文件夹共享
windows·经验分享
Loli_Wolf9 小时前
AI 原生研发闭环:从提需到线上监测,再自动回到提需
人工智能·深度学习·算法·microsoft·ai·ai编程·harness
岳麓丹枫0019 小时前
Windows 环境下创建 pgcrypto 扩展失败问题分析解决
windows·postgresql
青云计划10 小时前
Lambda与建造者模式:从回调地狱到流式编排的工程实践
网络·windows·建造者模式
小燚~10 小时前
MSVCR100.dII报错问题处理
c++·windows·qt
Kay_Liang11 小时前
VirtualBox NAT 网络实现三台虚拟机互联踩坑实录
网络·windows·笔记·ubuntu·网络安全