统信小程序(十三)循环键鼠操作程序

复制代码
'''
pynput直接运行对linux的支持不好,尤其是组合键。安装 xdotool用户能访问 X11 显示,适配X11环境,能支持组合键了,但是按键释放不干净,经常只能执行一次。Wayland 模式安装设置麻烦,还没试过。
检测是否在临时目录中运行:如果检测到程序在 /tmp/ 或包含 onefile 的路径中运行
目录选择:优先使用当前工作目录:即用户启动程序时所在的目录;降级到用户主目录:如果当前工作目录不可用;再降级到桌面目录:作为最后的备选
'''
复制代码
import tkinter as tk

from tkinter import ttk, messagebox, filedialog

import json, os, sys, time, threading, platform, datetime, subprocess

import pyperclip

# ── 依赖检查 ──────────────────────────────────────────────────────────────────

_missing = []

try:
    import pyperclip

except ImportError:
    _missing.append("pyperclip")

# 使用 pynput 替代 pyautogui(Linux 兼容性更好)
try:
    from pynput import mouse, keyboard

    USE_PYNPUT = True
except ImportError:
    _missing.append("pynput")
    USE_PYNPUT = False

if _missing:

    subprocess.check_call([sys.executable, "-m", "pip", "install"] + _missing)

    import importlib

    for m in _missing:
        importlib.import_module(m)

    if 'pynput' in _missing:
        from pynput import mouse, keyboard

        USE_PYNPUT = True

# 检测 xdotool 是否可用(用于组合键)
HAS_XDOTOOL = False
XDOTOOL_VERSION = "unknown"
try:
    result = subprocess.run(['which', 'xdotool'], capture_output=True, text=True)
    if result.returncode == 0:
        HAS_XDOTOOL = True
        # 获取版本信息
        try:
            version_result = subprocess.run(['xdotool', '--version'],
                                            capture_output=True, text=True, timeout=2)
            if version_result.returncode == 0:
                XDOTOOL_VERSION = version_result.stdout.strip()
        except:
            pass
        print(f"[INFO] xdotool 已检测到 (版本: {XDOTOOL_VERSION}),将用于键盘控制")
    else:
        print("[WARNING] xdotool 未找到,建议安装: sudo apt-get install xdotool")
except Exception as e:
    print(f"[WARNING] 检测 xdotool 失败: {e}")

# ── 常量 ──────────────────────────────────────────────────────────────────────

BASE_W, BASE_H = 1920, 1080

# 兼容打包环境的路径获取
def get_program_dir():
    """获取程序所在目录,支持打包环境"""
    if getattr(sys, 'frozen', False):
        # 打包后的可执行文件
        exe_path = sys.executable
        
        # 检查是否在 /tmp 或其他临时目录中(onefile 模式)
        if exe_path.startswith('/tmp/') or 'onefile' in exe_path.lower():
            # 尝试从环境变量或命令行参数获取原始路径
            # 优先使用当前工作目录
            cwd = os.getcwd()
            if cwd and os.path.exists(cwd):
                return cwd
            
            # 其次尝试 HOME 目录
            home_dir = os.path.expanduser("~")
            if home_dir and os.path.exists(home_dir):
                return home_dir
            
            # 最后返回桌面目录
            desktop = os.path.join(home_dir, "Desktop")
            if os.path.exists(desktop):
                return desktop
            
            return home_dir
        else:
            # 正常打包(非 onefile 模式)
            return os.path.dirname(exe_path)
    else:
        # 普通 Python 脚本运行
        return os.path.dirname(os.path.abspath(__file__))

PROGRAM_DIR = get_program_dir()

DEFAULT_ROWS = 80

BG_COLORS = ["#FFFDE7", "#FFF9C4"]  # 间隔底色(淡黄系)

WIN_BG = "#FFFDE7"

BTN_COLORS = ["#FF8A80", "#FFD180", "#CCFF90", "#80D8FF", "#EA80FC"]  # 糖果色

FONT_NAME = "仿宋" if platform.system() == "Windows" else "Fangsong Ti"

FONT = (FONT_NAME, 12)

# 命令列表 - 扩展版,包含所有常用按键和组合键

CMD_LIST = [
    # 鼠标操作
    "单击左键", "单击右键", "双击左键",

    # 基础按键
    "Enter(回车)", "Space(空格)", "Tab(制表)", "Esc(退出)",
    "Backspace(退格)", "Delete(删除)",

    # 功能键
    "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",

    # 方向键
    "Up(上)", "Down(下)", "Left(左)", "Right(右)",
    "Home", "End", "Page Up", "Page Down",

    # 编辑键
    "Insert", "Print Screen", "Scroll Lock", "Pause",

    # 数字键盘
    "NumLock", "Numpad Enter",

    # 修饰键
    "Ctrl", "Alt", "Shift", "Win(徽标键)",

    # 组合键 - Windows系统
    "Alt+Tab(切换窗口)", "Alt+F4(关闭窗口)", "Ctrl+C(复制)", "Ctrl+V(粘贴)",
    "Ctrl+X(剪切)", "Ctrl+A(全选)", "Ctrl+S(保存)", "Ctrl+Z(撤销)",
    "Ctrl+Y(重做)", "Ctrl+F(查找)", "Ctrl+P(打印)", "Ctrl+N(新建)",
    "Ctrl+W(关闭标签)", "Ctrl+T(新标签)", "Ctrl+Shift+T(恢复标签)",
    "Alt+Enter(属性)", "Win+D(显示桌面)", "Win+E(资源管理器)",
    "Win+L(锁屏)", "Win+R(运行)", "Win+Tab(任务视图)",

    # 组合键 - 通用
    "Shift+Delete(永久删除)", "Ctrl+Shift+Esc(任务管理器)",
    "Alt+Space(窗口菜单)", "Ctrl+Home(文档开头)", "Ctrl+End(文档结尾)",
    "Ctrl+Left(单词左移)", "Ctrl+Right(单词右移)",
    "Shift+Home(选中到行首)", "Shift+End(选中到行尾)",

    # 特殊操作
    "粘贴", "读取并粘贴", "延时"
]

CMD_HELP = (

    "命令说明:\n"
    "═══════════════════════════════════════\n\n"

    "【鼠标操作】\n"
    "  单击左键  → 中间填 x,y  在该坐标左键单击(留空则在鼠标当前位置点击)\n"
    "  单击右键  → 中间填 x,y  在该坐标右键单击(留空则在鼠标当前位置点击)\n"
    "  双击左键  → 中间填 x,y  在该坐标左键双击(留空则在鼠标当前位置点击)\n\n"

    "【键盘按键】\n"
    "  单个按键  → 直接选择对应按键即可执行\n"
    "  如: F2, Enter, Space, Tab, Esc, Page Up等\n\n"

    "【组合键】\n"
    "  自动识别  → 选择组合键后自动执行,无需额外参数\n"
    "  如: Alt+Tab, Ctrl+C, Win+D等\n\n"

    "【特殊命令】\n"
    "  粘贴      → 中间填文字   先点左键再粘贴内容(右侧备注)\n"
    "  延时      → 中间填秒数   额外等待指定秒数(如: 1.5)\n"
    "  读取并粘贴→ 从同目录xlsx第2列依次读取并粘贴\n\n"

    "【全局设置】\n"
    "  命令间延时 → 每个命令执行后的基础延时(默认0.01秒)\n"
    "  循环次数   → 整个命令序列重复执行的次数(默认1次)\n"
    "  每行延时   → 每行命令之间的额外延时(默认0.02秒)\n\n"

    "【快捷键】\n"
    "  Ctrl+B    → 启动/停止执行\n"
    "  右键菜单  → 添加/删除行\n\n"

    "💡 提示:可以通过'延时'命令精确控制每一步的等待时间"

)


# ── 主应用 ────────────────────────────────────────────────────────────────────

class App(tk.Tk):

    def __init__(self):

        super().__init__()

        print("[DEBUG] 初始化应用...")

        self.title("循环粘贴工具 v2.0 (增强版)")

        self._running = False

        self._run_thread = None

        self._xlsx_row = 0  # xlsx 当前读取行(0-based)

        self._drag_x = self._drag_y = 0

        # 热键监听器
        self._hotkey_listener = None

        # 当前记录文件路径
        self.current_file = None

        # pynput 控制器
        print("[DEBUG] 创建 pynput 控制器...")
        self.mouse_ctrl = mouse.Controller()
        self.keyboard_ctrl = keyboard.Controller()
        print("[DEBUG] pynput 控制器创建成功")

        # 分辨率缩放比

        sw = self.winfo_screenwidth()

        sh = self.winfo_screenheight()

        self.sx = sw / BASE_W

        self.sy = sh / BASE_H

        print(f"[DEBUG] 屏幕分辨率: {sw}x{sh}, 缩放比: {self.sx:.2f}x{self.sy:.2f}")

        # 加载默认配置(空白)
        self.cfg = self._get_default_cfg()

        # 窗口尺寸/位置

        DEFAULT_WIDTH = 1230

        self.geometry(f"{DEFAULT_WIDTH}x800+100+50")

        self.configure(bg=WIN_BG)

        self.minsize(600, 400)

        print("[DEBUG] 构建 UI...")
        self._build_ui()

        print("[DEBUG] 加载行...")
        self._load_rows()

        # 绑定 Ctrl+B - 使用 pynput 或 tkinter 内置绑定
        print("[DEBUG] 设置热键...")
        self._setup_hotkey()

        self.protocol("WM_DELETE_WINDOW", self._on_close)

        # 启动时清理修饰键状态
        self._clear_modifiers()

        print("[DEBUG] 应用初始化完成")

    def _clear_modifiers(self):
        """清理所有修饰键状态,防止卡键"""
        print("[DEBUG] 清理修饰键状态...")
        
        if HAS_XDOTOOL:
            try:
                # 使用 xdotool 清除所有修饰键(多次确保释放)
                modifiers = ['Control_L', 'Control_R', 'Alt_L', 'Alt_R', 
                           'Shift_L', 'Shift_R', 'Super_L', 'Super_R']
                for _ in range(2):  # 重复两次确保释放
                    for mod in modifiers:
                        subprocess.run(['xdotool', 'keyup', mod],
                                     timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    time.sleep(0.02)
                print("[DEBUG] 修饰键清理完成 (xdotool)")
            except Exception as e:
                print(f"[WARNING] xdotool 清理修饰键失败: {e}")
        
        # 同时使用 pynput 清理(双重保障)
        try:
            from pynput.keyboard import Key
            modifier_keys = [
                Key.ctrl_l, Key.ctrl_r,
                Key.alt_l, Key.alt_r,
                Key.shift_l, Key.shift_r,
                Key.cmd_l, Key.cmd_r
            ]
            for key in modifier_keys:
                self.keyboard_ctrl.release(key)
            print("[DEBUG] 修饰键清理完成 (pynput)")
        except Exception as e:
            print(f"[WARNING] pynput 清理修饰键失败: {e}")

    def _ensure_key_released(self, key_name):
        """确保指定按键被释放(在执行组合键前调用)"""
        if HAS_XDOTOOL:
            xdotool_map = {
                'ctrl': ['Control_L', 'Control_R'],
                'alt': ['Alt_L', 'Alt_R'],
                'shift': ['Shift_L', 'Shift_R'],
                'win': ['Super_L', 'Super_R'],
            }
            
            keys_to_release = xdotool_map.get(key_name.lower(), [])
            # 重复两次确保释放
            for _ in range(2):
                for key in keys_to_release:
                    try:
                        subprocess.run(['xdotool', 'keyup', key],
                                     timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    except:
                        pass
                time.sleep(0.01)
        
        # 同时使用 pynput 清理
        try:
            from pynput.keyboard import Key
            release_map = {
                'ctrl': [Key.ctrl_l, Key.ctrl_r],
                'alt': [Key.alt_l, Key.alt_r],
                'shift': [Key.shift_l, Key.shift_r],
                'win': [Key.cmd_l, Key.cmd_r],
            }
            
            keys_to_release = release_map.get(key_name.lower(), [])
            for key in keys_to_release:
                self.keyboard_ctrl.release(key)
        except:
            pass

    def _get_default_cfg(self):
        """获取默认配置"""
        return {
            "geometry": f"{1230}x800+100+50",
            "cmd_delay": "0.01",
            "loop_count": "1",
            "line_delay": "0.05",  # 增加到 0.05 秒,防止按键被吞
            "xlsx_row": 0,
            "rows": []
        }

    def _get_pynput_key(self, key_name):
        """将按键名称转换为 pynput 按键对象"""
        special_keys = {
            'enter': keyboard.Key.enter,
            'space': keyboard.Key.space,
            'tab': keyboard.Key.tab,
            'esc': keyboard.Key.esc,
            'escape': keyboard.Key.esc,
            'backspace': keyboard.Key.backspace,
            'delete': keyboard.Key.delete,
            'up': keyboard.Key.up,
            'down': keyboard.Key.down,
            'left': keyboard.Key.left,
            'right': keyboard.Key.right,
            'home': keyboard.Key.home,
            'end': keyboard.Key.end,
            'pageup': keyboard.Key.page_up,
            'pagedown': keyboard.Key.page_down,
            'insert': keyboard.Key.insert,
            'printscreen': keyboard.Key.print_screen,
            'scrolllock': keyboard.Key.scroll_lock,
            'pause': keyboard.Key.pause,
            'numlock': keyboard.Key.num_lock,
            'ctrl': keyboard.Key.ctrl_l,
            'alt': keyboard.Key.alt_l,
            'shift': keyboard.Key.shift_l,
            'win': keyboard.Key.cmd,
            'f1': keyboard.Key.f1,
            'f2': keyboard.Key.f2,
            'f3': keyboard.Key.f3,
            'f4': keyboard.Key.f4,
            'f5': keyboard.Key.f5,
            'f6': keyboard.Key.f6,
            'f7': keyboard.Key.f7,
            'f8': keyboard.Key.f8,
            'f9': keyboard.Key.f9,
            'f10': keyboard.Key.f10,
            'f11': keyboard.Key.f11,
            'f12': keyboard.Key.f12,
        }

        return special_keys.get(key_name.lower(), key_name)

    def _setup_hotkey(self):
        """设置热键监听"""
        try:
            from pynput import keyboard

            def on_press(key):
                try:
                    if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
                        self._ctrl_pressed = True
                    elif hasattr(self, '_ctrl_pressed') and self._ctrl_pressed:
                        if hasattr(key, 'char') and key.char == 'b':
                            self._toggle_run()
                except AttributeError:
                    pass

            def on_release(key):
                try:
                    if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
                        self._ctrl_pressed = False
                except AttributeError:
                    pass

            self._ctrl_pressed = False
            self._hotkey_listener = keyboard.Listener(
                on_press=on_press,
                on_release=on_release
            )
            self._hotkey_listener.start()

        except Exception as e:
            print(f"pynput 热键监听失败: {e}")
            self._fallback_hotkey()

    def _fallback_hotkey(self):
        """降级方案:使用 tkinter 内置绑定(仅当窗口聚焦时有效)"""
        self.bind('<Control-b>', lambda e: self._toggle_run())
        self.bind('<Control-B>', lambda e: self._toggle_run())

        # 显示提示
        msg = "⚠️ 全局热键不可用\n\n"
        msg += "原因:Linux 系统下需要特殊权限\n\n"
        msg += "解决方案:\n"
        msg += "1. 保持本窗口处于激活状态\n"
        msg += "2. 使用 Ctrl+B 快捷键\n"
        msg += "3. 或直接点击'保 存'按钮旁的说明查看帮助\n\n"
        msg += "建议安装 pynput: pip install pynput"

        self.after(1000, lambda: messagebox.showwarning("热键提示", msg))

    # ── 文件管理 ────────────────────────────────────────────────────────────

    def _select_record_file(self):
        """选择记录文件"""
        file_path = filedialog.askopenfilename(
            title="选择记录文件",
            initialdir=PROGRAM_DIR,
            filetypes=[("JSON记录文件", "*.json"), ("所有文件", "*.*")]
        )

        if file_path:
            self._load_from_file(file_path)

    def _load_from_file(self, file_path):
        """从指定文件加载配置"""
        try:
            with open(file_path, "r", encoding="utf-8") as f:
                loaded_cfg = json.load(f)

            self.current_file = file_path
            self.cfg = loaded_cfg

            # 更新UI显示
            self.var_cmd_delay.set(self.cfg.get("cmd_delay", "0.01"))
            self.var_loop_count.set(self.cfg.get("loop_count", "1"))
            self.var_line_delay.set(self.cfg.get("line_delay", "0.05"))  # 修复:默认值改为 0.05
            self._xlsx_row = self.cfg.get("xlsx_row", 0)

            # 重新加载行
            self._clear_rows()
            self._load_rows_from_cfg()

            # 更新窗口标题
            filename = os.path.basename(file_path)
            self.title(f"循环粘贴工具 v2.0 - {filename}")

            self._update_status(f"✓ 已加载: {filename}")

        except Exception as e:
            messagebox.showerror("加载失败", f"无法加载文件:\n{str(e)}")

    def _save_to_current_file(self):
        """保存到当前文件,如果没有则创建新文件"""
        if self.current_file:
            # 保存到已有文件
            self._do_save(self.current_file)
        else:
            # 创建新文件(以日期命名)
            self._save_as_new_file()

    def _save_as_new_file(self):
        """保存为新文件(以日期时间命名)"""
        now = datetime.datetime.now()
        filename = now.strftime("记录_%Y%m%d_%H%M%S.json")
        file_path = os.path.join(PROGRAM_DIR, filename)

        # 如果文件已存在,添加序号
        counter = 1
        while os.path.exists(file_path):
            filename = now.strftime(f"记录_%Y%m%d_%H%M%S_{counter}.json")
            file_path = os.path.join(PROGRAM_DIR, filename)
            counter += 1

        self._do_save(file_path)
        self.current_file = file_path

        # 更新窗口标题
        self.title(f"循环粘贴工具 v2.0 - {filename}")
        self._update_status(f"✓ 已创建新文件: {filename}")

    def _save_as_dialog(self):
        """另存为对话框"""
        now = datetime.datetime.now()
        default_name = now.strftime("记录_%Y%m%d_%H%M%S.json")

        file_path = filedialog.asksaveasfilename(
            title="另存为",
            initialdir=PROGRAM_DIR,
            initialfile=default_name,
            defaultextension=".json",
            filetypes=[("JSON记录文件", "*.json"), ("所有文件", "*.*")]
        )

        if file_path:
            self._do_save(file_path)
            self.current_file = file_path

            # 更新窗口标题
            filename = os.path.basename(file_path)
            self.title(f"循环粘贴工具 v2.0 - {filename}")
            self._update_status(f"✓ 已保存: {filename}")

    def _do_save(self, file_path):
        """执行保存操作"""
        try:
            self.cfg["geometry"] = self.geometry()
            self.cfg["cmd_delay"] = self.var_cmd_delay.get()
            self.cfg["loop_count"] = self.var_loop_count.get()
            self.cfg["line_delay"] = self.var_line_delay.get()
            self.cfg["xlsx_row"] = self._xlsx_row

            # 保存每行数据
            rows = []
            for row in self.rows:
                rows.append({
                    "cmd": row["cmd"].get(),
                    "mid": row["mid"].get(),
                    "remark": row["rem"].get(),
                })
            self.cfg["rows"] = rows

            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(self.cfg, f, ensure_ascii=False, indent=2)

            filename = os.path.basename(file_path)
            self._update_status(f"✓ 已保存: {filename}")

        except Exception as e:
            messagebox.showerror("保存失败", f"保存文件时出错:\n{str(e)}")

    def _clear_rows(self):
        """清空所有行"""
        for rd in self.rows:
            rd["frame"].destroy()
        self.rows.clear()

    def _load_rows_from_cfg(self):
        """从配置加载行"""
        saved = self.cfg.get("rows", [])

        # 如果有保存的行,使用保存的数量;否则使用默认数量
        if saved:
            count = len(saved)
        else:
            count = DEFAULT_ROWS

        for i in range(count):
            d = saved[i] if i < len(saved) else {}
            rd = self._make_row(i,
                                cmd=d.get("cmd", ""),
                                mid=d.get("mid", ""),
                                rem=d.get("remark", ""))
            self.rows.append(rd)

    def _update_status(self, message):
        """更新状态标签"""
        if hasattr(self, 'lbl_status'):
            self.lbl_status.config(text=message)
            self.after(3000, lambda: self.lbl_status.config(
                text="●就绪", fg="#9E9E9E"
            ))

    # ── UI 构建 ───────────────────────────────────────────────────────────────

    def _build_ui(self):

        top = tk.Frame(self, bg=WIN_BG)
        top.pack(fill="x", padx=6, pady=4)

        file_btn = tk.Button(top, text="打开记录", font=FONT, bg=BTN_COLORS[3],
                             relief="raised", bd=2, command=self._select_record_file)
        file_btn.pack(side="left", padx=2)

        tk.Label(top, text="命令间延时(s):", bg=WIN_BG, font=FONT).pack(side="left", padx=(10, 0))
        self.var_cmd_delay = tk.StringVar(value=self.cfg.get("cmd_delay", "0.01"))
        tk.Entry(top, textvariable=self.var_cmd_delay, width=6, font=FONT).pack(side="left", padx=2)

        tk.Label(top, text=" 循环次数:", bg=WIN_BG, font=FONT).pack(side="left")
        self.var_loop_count = tk.StringVar(value=self.cfg.get("loop_count", "1"))
        tk.Entry(top, textvariable=self.var_loop_count, width=6, font=FONT).pack(side="left", padx=2)

        tk.Label(top, text="  每行延时(s):", bg=WIN_BG, font=FONT).pack(side="left")
        self.var_line_delay = tk.StringVar(value=self.cfg.get("line_delay", "0.05"))  # 修复:默认值改为 0.05
        tk.Entry(top, textvariable=self.var_line_delay, width=6, font=FONT).pack(side="left", padx=2)

        help_btn = tk.Button(top, text="命令说明", font=FONT, bg=BTN_COLORS[3],
                             relief="raised", bd=2,
                             command=lambda: messagebox.showinfo("命令说明", CMD_HELP))
        help_btn.pack(side="left", padx=8)

        save_btn = tk.Button(top, text="保 存", font=FONT, bg=BTN_COLORS[2],
                             relief="raised", bd=2, command=self._save_to_current_file)
        save_btn.pack(side="left", padx=4)

        saveas_btn = tk.Button(top, text="另存为", font=FONT, bg=BTN_COLORS[1],
                               relief="raised", bd=2, command=self._save_as_dialog)
        saveas_btn.pack(side="left", padx=4)

        hotkey_hint = "Ctrl+B"
        tk.Label(top, text=f"  【{hotkey_hint}】启动/停止", bg=WIN_BG,
                 font=(FONT_NAME, 11, "bold"), fg="#E53935").pack(side="left", padx=10)

        self.lbl_status = tk.Label(top, text="●就绪", bg=WIN_BG,
                                   font=(FONT_NAME, 11, "bold"), fg="#9E9E9E")
        self.lbl_status.pack(side="right", padx=6)

        hdr = tk.Frame(self, bg=WIN_BG)
        hdr.pack(fill="x", padx=6)
        tk.Label(hdr, text="命令", width=18, font=FONT, bg="#FFF176",
                 relief="groove").pack(side="left", padx=1)
        tk.Label(hdr, text="中间参数 / 粘贴内容", width=100, font=FONT, bg="#FFF176",
                 relief="groove").pack(side="left", padx=1)
        tk.Label(hdr, text="备注", width=30, font=FONT, bg="#FFF176",
                 relief="groove").pack(side="left", padx=1)

        container = tk.Frame(self, bg=WIN_BG)
        container.pack(fill="both", expand=True, padx=6, pady=2)

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

        self.inner = tk.Frame(canvas, bg=WIN_BG)
        self._canvas_win = canvas.create_window((0, 0), window=self.inner, anchor="nw")
        self.inner.bind("<Configure>",
                        lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.bind("<Configure>",
                    lambda e: canvas.itemconfig(self._canvas_win, width=e.width))

        self.inner.bind_all("<MouseWheel>",
                            lambda e: canvas.yview_scroll(-1 * (e.delta // 120), "units"))

        self._canvas = canvas
        self.rows = []

    def _make_row(self, idx, cmd="", mid="", rem=""):
        bg = BG_COLORS[idx % 2]
        frm = tk.Frame(self.inner, bg=bg)
        frm.pack(fill="x", pady=0)

        var_cmd = tk.StringVar(value=cmd if cmd else "单击左键")
        var_mid = tk.StringVar(value=mid)
        var_rem = tk.StringVar(value=rem)

        cmb = ttk.Combobox(frm, textvariable=var_cmd, values=CMD_LIST,
                           width=17, font=FONT, state="normal")
        cmb.pack(side="left", padx=1, pady=1)

        ent_mid = tk.Entry(frm, textvariable=var_mid, width=100, font=FONT,
                           bg=bg, relief="flat", bd=0,
                           highlightthickness=1, highlightbackground=bg,
                           highlightcolor="#BDBDBD")
        ent_mid.pack(side="left", padx=1)

        ent_rem = tk.Entry(frm, textvariable=var_rem, width=30, font=FONT,
                           bg=bg, relief="flat", bd=0,
                           highlightthickness=1, highlightbackground=bg,
                           highlightcolor="#BDBDBD")
        ent_rem.pack(side="left", padx=1, fill="x", expand=True)

        row_data = {"frame": frm, "cmd": var_cmd, "mid": var_mid, "rem": var_rem, "bg": bg}

        menu = tk.Menu(self, tearoff=0)
        menu.add_command(label="在上方添加行",
                         command=lambda rd=row_data: self._insert_before(rd))
        menu.add_command(label="在下方添加行",
                         command=lambda rd=row_data: self._insert_after(rd))
        menu.add_command(label="删除本行",
                         command=lambda rd=row_data: self._delete_row(rd))
        for widget in (frm, cmb, ent_mid, ent_rem):
            widget.bind("<Button-3>",
                        lambda e, m=menu: m.post(e.x_root, e.y_root))
        return row_data

    def _insert_before(self, target_rd):
        idx = self._row_index(target_rd)
        new_rd = self._make_row(idx)
        new_rd["frame"].pack_forget()
        self.rows.insert(idx, new_rd)
        self._repack_rows()

    def _insert_after(self, target_rd):
        idx = self._row_index(target_rd)
        new_rd = self._make_row(idx + 1)
        new_rd["frame"].pack_forget()
        self.rows.insert(idx + 1, new_rd)
        self._repack_rows()

    def _delete_row(self, target_rd):
        idx = self._row_index(target_rd)
        target_rd["frame"].destroy()
        self.rows.pop(idx)
        self._repack_rows()

    def _row_index(self, rd):
        return self.rows.index(rd)

    def _repack_rows(self):
        for i, rd in enumerate(self.rows):
            bg = BG_COLORS[i % 2]
            rd["bg"] = bg
            rd["frame"].configure(bg=bg)
            for child in rd["frame"].winfo_children():
                try:
                    child.configure(bg=bg)
                except Exception:
                    pass
                try:
                    child.configure(highlightbackground=bg)
                except Exception:
                    pass
            rd["frame"].pack_forget()
        for rd in self.rows:
            rd["frame"].pack(fill="x", pady=0)

    def _load_rows(self):
        self._xlsx_row = 0
        count = DEFAULT_ROWS
        for i in range(count):
            rd = self._make_row(i, cmd="", mid="", rem="")
            self.rows.append(rd)

    # ── 执行引擎 ──────────────────────────────────────────────────────────────

    def _toggle_run(self):
        if self._running:
            self._running = False
            self._clear_modifiers()
            self.after(0, lambda: self.lbl_status.configure(text="●停止", fg="#9E9E9E"))
        else:
            self._running = True
            self.after(0, lambda: self.lbl_status.configure(text="▶运行中", fg="#43A047"))
            self._run_thread = threading.Thread(target=self._run_commands, daemon=True)
            self._run_thread.start()

    def _run_commands(self):
        try:
            cmd_delay = float(self.var_cmd_delay.get())
            line_delay = float(self.var_line_delay.get())
        except ValueError:
            cmd_delay, line_delay = 0.01, 0.05  # 修复:line_delay 默认为 0.05

        try:
            loop_count = max(1, int(self.var_loop_count.get()))
        except ValueError:
            loop_count = 1

        for _loop in range(loop_count):
            if not self._running:
                break

            # 每次循环开始前清理修饰键
            if _loop > 0:
                self._clear_modifiers()
                time.sleep(0.05)

            for rd in self.rows:
                if not self._running:
                    break

                cmd = rd["cmd"].get().strip()
                mid = rd["mid"].get().strip()

                if not cmd:
                    continue

                try:
                    self._exec_one(cmd, mid, cmd_delay)
                except Exception as e:
                    print(f"[ERROR] {cmd} / {mid} => {e}")

                time.sleep(line_delay)

        self._running = False
        self._clear_modifiers()
        self.after(0, lambda: self.lbl_status.configure(text="●完成", fg="#1565C0"))

    def _exec_one(self, cmd, mid, cmd_delay):

        def exec_key_xdotool(key_str, max_retries=3, use_clearmodifiers=True):
            """使用 xdotool 执行按键(带重试机制)"""
            for attempt in range(max_retries):
                try:
                    if use_clearmodifiers:
                        xdotool_cmd = ['xdotool', 'key', '--clearmodifiers', '--delay', '20', key_str]
                    else:
                        xdotool_cmd = ['xdotool', 'key', '--delay', '20', key_str]
                    
                    result = subprocess.run(
                        xdotool_cmd,
                        timeout=2,
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL
                    )
                    if result.returncode == 0:
                        # 如果使用了 --clearmodifiers,执行后再次确保修饰键释放
                        if use_clearmodifiers:
                            time.sleep(0.02)
                            self._ensure_key_released('ctrl')
                            self._ensure_key_released('alt')
                            self._ensure_key_released('shift')
                        return True
                    else:
                        if attempt < max_retries - 1:
                            time.sleep(0.05)
                            continue
                except Exception:
                    if attempt < max_retries - 1:
                        time.sleep(0.05)
                        continue
            
            print(f"[WARNING] xdotool 执行失败 '{key_str}',切换到 pynput")
            return False

        def press_combo_with_xdotool(modifiers, key, max_retries=3):
            """使用 xdotool 执行组合键(带重试)"""
            if not HAS_XDOTOOL:
                return False

            for attempt in range(max_retries):
                try:
                    xdotool_map = {
                        'ctrl': 'Control_L', 'alt': 'Alt_L', 'shift': 'Shift_L', 'win': 'Super_L',
                        'enter': 'Return', 'tab': 'Tab', 'space': 'space', 'esc': 'Escape',
                        'delete': 'Delete', 'home': 'Home', 'end': 'End',
                        'left': 'Left', 'right': 'Right', 'up': 'Up', 'down': 'Down',
                        'f1': 'F1', 'f2': 'F2', 'f3': 'F3', 'f4': 'F4', 'f5': 'F5',
                        'f6': 'F6', 'f7': 'F7', 'f8': 'F8', 'f9': 'F9', 'f10': 'F10',
                        'f11': 'F11', 'f12': 'F12',
                        'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f',
                        'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l',
                        'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r',
                        's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x',
                        'y': 'y', 'z': 'z',
                    }

                    key_parts = [xdotool_map.get(m.lower(), m) for m in modifiers]
                    key_parts.append(xdotool_map.get(key.lower(), key))
                    combo_str = '+'.join(key_parts)

                    result = subprocess.run(
                        ['xdotool', 'key', '--clearmodifiers', '--delay', '20', combo_str],
                        timeout=2,
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL
                    )

                    if result.returncode == 0:
                        return True
                    else:
                        if attempt < max_retries - 1:
                            time.sleep(0.05)
                            continue
                except Exception:
                    if attempt < max_retries - 1:
                        time.sleep(0.05)
                        continue

            print(f"[WARNING] xdotool 组合键失败,切换到 pynput")
            return False

        def press_key(key_name):
            """执行单个按键 - 优化版本,保持窗口焦点"""
            xdotool_map = {
                'enter': 'Return', 'space': 'space', 'tab': 'Tab', 'esc': 'Escape',
                'backspace': 'BackSpace', 'delete': 'Delete',
                'up': 'Up', 'down': 'Down', 'left': 'Left', 'right': 'Right',
                'home': 'Home', 'end': 'End', 'pageup': 'Page_Up', 'pagedown': 'Page_Down',
                'insert': 'Insert', 'printscreen': 'Print', 'scrolllock': 'Scroll_Lock',
                'pause': 'Pause', 'numlock': 'Num_Lock',
                'f1': 'F1', 'f2': 'F2', 'f3': 'F3', 'f4': 'F4', 'f5': 'F5',
                'f6': 'F6', 'f7': 'F7', 'f8': 'F8', 'f9': 'F9', 'f10': 'F10',
                'f11': 'F11', 'f12': 'F12',
                'ctrl': 'Control_L', 'alt': 'Alt_L', 'shift': 'Shift_L', 'win': 'Super_L',
            }

            xdotool_key = xdotool_map.get(key_name.lower(), key_name)

            # 对于方向键等导航键,使用最简化的方式,绝不回退到 pynput
            navigation_keys = ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown']
            is_navigation = key_name.lower() in navigation_keys

            if HAS_XDOTOOL:
                if is_navigation:
                    # 导航键:使用最简单的 key 命令
                    # 不使用 windowactivate,避免时序问题导致按键被吞
                    try:
                        result = subprocess.run(['xdotool', 'key', xdotool_key],
                                                timeout=2,
                                                stdout=subprocess.DEVNULL,
                                                stderr=subprocess.DEVNULL)

                        if result.returncode != 0:
                            print(f"[WARNING] xdotool 导航键返回码异常: {result.returncode}")

                        # 等待确保按键被系统处理(关键!)
                        time.sleep(0.05)
                        return
                    except Exception as e:
                        print(f"[WARNING] xdotool 导航键失败 '{key_name}': {e}")

                # 其他键使用标准方式(但不使用 --clearmodifiers)
                success = exec_key_xdotool(xdotool_key, max_retries=3, use_clearmodifiers=False)
                if not success:
                    # 回退到 pynput
                    try:
                        k = self._get_pynput_key(key_name)
                        self.keyboard_ctrl.press(k)
                        time.sleep(0.05)
                        self.keyboard_ctrl.release(k)
                    except Exception as e:
                        print(f"[按键错误] {key_name}: {e}")
            else:
                try:
                    k = self._get_pynput_key(key_name)
                    self.keyboard_ctrl.press(k)
                    time.sleep(0.05)
                    self.keyboard_ctrl.release(k)
                except Exception as e:
                    print(f"[按键错误] {key_name}: {e}")

        def press_hotkey(*keys):
            """执行组合键"""
            if len(keys) < 2:
                return

            modifier_names = ['ctrl', 'alt', 'shift', 'win']
            modifiers = [k for k in keys[:-1] if k.lower() in modifier_names]
            main_key = keys[-1]

            # 执行前确保所有修饰键都是释放状态
            for mod in modifiers:
                self._ensure_key_released(mod)
            time.sleep(0.02)  # 短暂等待确保释放完成

            if HAS_XDOTOOL:
                success = press_combo_with_xdotool(modifiers, main_key, max_retries=3)
                if success:
                    # 执行后再次清理修饰键
                    for mod in modifiers:
                        self._ensure_key_released(mod)
                    return

            try:
                pynput_keys = [self._get_pynput_key(k) for k in keys]
                for key in pynput_keys:
                    self.keyboard_ctrl.press(key)
                    time.sleep(0.01)
                for key in reversed(pynput_keys):
                    self.keyboard_ctrl.release(key)
                    time.sleep(0.01)
                # 执行后再次清理修饰键
                for mod in modifiers:
                    self._ensure_key_released(mod)
            except Exception as e:
                print(f"[组合键错误] {'+'.join(keys)}: {e}")
                # 异常时也要清理
                for mod in modifiers:
                    self._ensure_key_released(mod)

        def press_alt_tab_xdotool(max_retries=3):
            """专门优化的 Alt+Tab 实现(带重试和清理)"""
            if not HAS_XDOTOOL:
                return False

            for attempt in range(max_retries):
                alt_pressed = False
                try:
                    # 1. 先确保 Alt 是释放状态(双重保障)
                    self._ensure_key_released('alt')
                    time.sleep(0.02)
                    
                    # 2. 再次用 xdotool 确保释放
                    subprocess.run(['xdotool', 'keyup', 'Alt_L'],
                                 timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    subprocess.run(['xdotool', 'keyup', 'Alt_R'],
                                 timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    time.sleep(0.02)

                    # 3. 按下 Alt
                    result1 = subprocess.run(['xdotool', 'keydown', 'Alt_L'],
                                           timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    if result1.returncode != 0:
                        raise Exception("keydown failed")
                    
                    alt_pressed = True
                    time.sleep(0.12)

                    # 4. 按 Tab
                    result2 = subprocess.run(['xdotool', 'key', '--delay', '20', 'Tab'],
                                           timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    if result2.returncode != 0:
                        raise Exception("key Tab failed")
                    
                    time.sleep(0.08)

                    # 5. 释放 Alt
                    result3 = subprocess.run(['xdotool', 'keyup', 'Alt_L'],
                                           timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    if result3.returncode != 0:
                        raise Exception("keyup failed")
                    
                    alt_pressed = False
                    
                    # 6. 再次确保 Alt 完全释放
                    self._ensure_key_released('alt')
                    
                    return True
                except Exception:
                    # 确保释放 Alt 键(防止卡住)
                    if alt_pressed:
                        try:
                            subprocess.run(['xdotool', 'keyup', 'Alt_L'],
                                         timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                            subprocess.run(['xdotool', 'keyup', 'Alt_R'],
                                         timeout=1, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                            self._ensure_key_released('alt')
                            print("[DEBUG] 异常处理:已释放 Alt 键")
                        except:
                            pass
                    
                    if attempt < max_retries - 1:
                        time.sleep(0.1)
                        continue
            
            print("[WARNING] xdotool Alt+Tab 失败,切换到 pynput")
            return press_alt_tab_pynput()

        def press_alt_tab_pynput():
            """pynput 版本的 Alt+Tab"""
            try:
                # 执行前确保 Alt 已释放
                self._ensure_key_released('alt')
                time.sleep(0.02)
                
                alt_key = keyboard.Key.alt_l
                tab_key = keyboard.Key.tab

                self.keyboard_ctrl.press(alt_key)
                time.sleep(0.15)

                self.keyboard_ctrl.press(tab_key)
                time.sleep(0.05)
                self.keyboard_ctrl.release(tab_key)
                time.sleep(0.05)

                self.keyboard_ctrl.release(alt_key)
                
                # 执行后再次确保 Alt 释放
                self._ensure_key_released('alt')
                
                return True
            except Exception as e:
                print(f"[Alt+Tab pynput 错误]: {e}")
                # 异常时也要确保释放
                try:
                    self._ensure_key_released('alt')
                except:
                    pass
                return False

        def parse_xy(s):
            s = s.strip()
            if not s:
                return None
            parts = s.replace(",", ",").split(",")
            if len(parts) < 2:
                return None
            try:
                x = int(float(parts[0].strip()) * self.sx)
                y = int(float(parts[1].strip()) * self.sy)
                return x, y
            except (ValueError, IndexError):
                return None

        # ── 鼠标操作 ──
        if cmd in ("单击左键",):
            xy = parse_xy(mid)
            time.sleep(cmd_delay)
            if xy:
                self.mouse_ctrl.position = (xy[0], xy[1])
                time.sleep(0.05)
            self.mouse_ctrl.click(mouse.Button.left)

        elif cmd in ("单击右键",):
            xy = parse_xy(mid)
            time.sleep(cmd_delay)
            if xy:
                self.mouse_ctrl.position = (xy[0], xy[1])
                time.sleep(0.05)
            self.mouse_ctrl.click(mouse.Button.right)

        elif cmd in ("双击左键",):
            xy = parse_xy(mid)
            time.sleep(cmd_delay)
            if xy:
                self.mouse_ctrl.position = (xy[0], xy[1])
                time.sleep(0.05)
            self.mouse_ctrl.click(mouse.Button.left, 2)

        # ── 基础按键 ──
        elif cmd == "Enter(回车)":
            time.sleep(cmd_delay)
            press_key('enter')
        elif cmd == "Space(空格)":
            time.sleep(cmd_delay)
            press_key('space')
        elif cmd == "Tab(制表)":
            time.sleep(cmd_delay)
            press_key('tab')
        elif cmd == "Esc(退出)":
            time.sleep(cmd_delay)
            press_key('esc')
        elif cmd == "Backspace(退格)":
            time.sleep(cmd_delay)
            press_key('backspace')
        elif cmd == "Delete(删除)":
            time.sleep(cmd_delay)
            press_key('delete')

        # ── 功能键 ──
        elif cmd == "F1":
            time.sleep(cmd_delay)
            press_key('f1')
        elif cmd == "F2":
            time.sleep(cmd_delay)
            press_key('f2')
        elif cmd == "F3":
            time.sleep(cmd_delay)
            press_key('f3')
        elif cmd == "F4":
            time.sleep(cmd_delay)
            press_key('f4')
        elif cmd == "F5":
            time.sleep(cmd_delay)
            press_key('f5')
        elif cmd == "F6":
            time.sleep(cmd_delay)
            press_key('f6')
        elif cmd == "F7":
            time.sleep(cmd_delay)
            press_key('f7')
        elif cmd == "F8":
            time.sleep(cmd_delay)
            press_key('f8')
        elif cmd == "F9":
            time.sleep(cmd_delay)
            press_key('f9')
        elif cmd == "F10":
            time.sleep(cmd_delay)
            press_key('f10')
        elif cmd == "F11":
            time.sleep(cmd_delay)
            press_key('f11')
        elif cmd == "F12":
            time.sleep(cmd_delay)
            press_key('f12')

        # ── 方向键 ──
        elif cmd == "Up(上)":
            time.sleep(cmd_delay)
            press_key('up')
        elif cmd == "Down(下)":
            time.sleep(cmd_delay)
            press_key('down')
        elif cmd == "Left(左)":
            time.sleep(cmd_delay)
            press_key('left')
        elif cmd == "Right(右)":
            time.sleep(cmd_delay)
            press_key('right')
        elif cmd == "Home":
            time.sleep(cmd_delay)
            press_key('home')
        elif cmd == "End":
            time.sleep(cmd_delay)
            press_key('end')
        elif cmd == "Page Up":
            time.sleep(cmd_delay)
            press_key('pageup')
        elif cmd == "Page Down":
            time.sleep(cmd_delay)
            press_key('pagedown')

        # ── 其他按键 ──
        elif cmd == "Insert":
            time.sleep(cmd_delay)
            press_key('insert')
        elif cmd == "Print Screen":
            time.sleep(cmd_delay)
            press_key('printscreen')
        elif cmd == "Scroll Lock":
            time.sleep(cmd_delay)
            press_key('scrolllock')
        elif cmd == "Pause":
            time.sleep(cmd_delay)
            press_key('pause')
        elif cmd == "NumLock":
            time.sleep(cmd_delay)
            press_key('numlock')
        elif cmd == "Numpad Enter":
            time.sleep(cmd_delay)
            press_key('enter')

        # ── 修饰键 ──
        elif cmd == "Ctrl":
            time.sleep(cmd_delay)
            press_key('ctrl')
        elif cmd == "Alt":
            time.sleep(cmd_delay)
            press_key('alt')
        elif cmd == "Shift":
            time.sleep(cmd_delay)
            press_key('shift')
        elif cmd == "Win(徽标键)":
            time.sleep(cmd_delay)
            press_key('win')

        # ── 组合键 ──
        elif cmd == "Alt+Tab(切换窗口)":
            time.sleep(cmd_delay)
            if HAS_XDOTOOL:
                press_alt_tab_xdotool(max_retries=3)
            else:
                press_alt_tab_pynput()
        elif cmd == "Alt+F4(关闭窗口)":
            time.sleep(cmd_delay)
            press_hotkey('alt', 'f4')
        elif cmd == "Ctrl+C(复制)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'c')
        elif cmd == "Ctrl+V(粘贴)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'v')
        elif cmd == "Ctrl+X(剪切)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'x')
        elif cmd == "Ctrl+A(全选)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'a')
        elif cmd == "Ctrl+S(保存)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 's')
        elif cmd == "Ctrl+Z(撤销)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'z')
        elif cmd == "Ctrl+Y(重做)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'y')
        elif cmd == "Ctrl+F(查找)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'f')
        elif cmd == "Ctrl+P(打印)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'p')
        elif cmd == "Ctrl+N(新建)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'n')
        elif cmd == "Ctrl+W(关闭标签)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'w')
        elif cmd == "Ctrl+T(新标签)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 't')
        elif cmd == "Ctrl+Shift+T(恢复标签)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'shift', 't')
        elif cmd == "Alt+Enter(属性)":
            time.sleep(cmd_delay)
            press_hotkey('alt', 'enter')
        elif cmd == "Win+D(显示桌面)":
            time.sleep(cmd_delay)
            press_hotkey('win', 'd')
        elif cmd == "Win+E(资源管理器)":
            time.sleep(cmd_delay)
            press_hotkey('win', 'e')
        elif cmd == "Win+L(锁屏)":
            time.sleep(cmd_delay)
            press_hotkey('win', 'l')
        elif cmd == "Win+R(运行)":
            time.sleep(cmd_delay)
            press_hotkey('win', 'r')
        elif cmd == "Win+Tab(任务视图)":
            time.sleep(cmd_delay)
            press_hotkey('win', 'tab')
        elif cmd == "Shift+Delete(永久删除)":
            time.sleep(cmd_delay)
            press_hotkey('shift', 'delete')
        elif cmd == "Ctrl+Shift+Esc(任务管理器)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'shift', 'esc')
        elif cmd == "Alt+Space(窗口菜单)":
            time.sleep(cmd_delay)
            press_hotkey('alt', 'space')
        elif cmd == "Ctrl+Home(文档开头)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'home')
        elif cmd == "Ctrl+End(文档结尾)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'end')
        elif cmd == "Ctrl+Left(单词左移)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'left')
        elif cmd == "Ctrl+Right(单词右移)":
            time.sleep(cmd_delay)
            press_hotkey('ctrl', 'right')
        elif cmd == "Shift+Home(选中到行首)":
            time.sleep(cmd_delay)
            press_hotkey('shift', 'home')
        elif cmd == "Shift+End(选中到行尾)":
            time.sleep(cmd_delay)
            press_hotkey('shift', 'end')

        # ── 特殊命令 ──
        elif cmd == "粘贴":
            time.sleep(cmd_delay)
            pyperclip.copy(mid)
            press_hotkey('ctrl', 'v')
        elif cmd == "延时":
            try:
                extra = float(mid)
                time.sleep(extra)
            except ValueError:
                pass
        elif cmd == "读取并粘贴":
            text = self._read_xlsx_cell()
            if text is not None:
                time.sleep(cmd_delay)
                pyperclip.copy(str(text))
                press_hotkey('ctrl', 'v')

    def _read_xlsx_cell(self):
        try:
            import openpyxl
        except ImportError:
            subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
            import openpyxl

        base_dir = os.path.dirname(os.path.abspath(__file__))
        xlsx_files = [f for f in os.listdir(base_dir) if f.endswith(".xlsx")]

        if not xlsx_files:
            return None

        wb = openpyxl.load_workbook(os.path.join(base_dir, xlsx_files[0]), read_only=True, data_only=True)
        ws = wb.worksheets[0]

        cells = []
        for row in ws.iter_rows(min_col=2, max_col=2, values_only=True):
            cells.append(row[0])
        wb.close()

        start = 0
        for i, v in enumerate(cells):
            if v is not None and str(v).strip():
                start = i
                break

        target = max(start, self._xlsx_row)
        if target >= len(cells):
            target = start

        while target < len(cells) and (cells[target] is None or str(cells[target]).strip() == ""):
            target += 1

        if target >= len(cells):
            return None

        value = cells[target]
        self._xlsx_row = target + 1
        return value

    def _on_close(self):
        self._running = False
        if self._hotkey_listener:
            try:
                self._hotkey_listener.stop()
            except:
                pass
        self._clear_modifiers()
        self.destroy()

if __name__ == "__main__":
    if platform.system() == "Windows":
        try:
            from ctypes import windll

            windll.shcore.SetProcessDpiAwareness(1)
        except Exception:
            pass

    try:
        print("[INFO] 正在启动循环粘贴工具...")
        app = App()
        print("[INFO] 程序界面已创建,进入主循环...")
        app.mainloop()
        print("[INFO] 程序正常退出")
    except Exception as e:
        print(f"[ERROR] 程序启动失败: {e}")
        import traceback

        traceback.print_exc()
        sys.exit(1)
相关推荐
AI视觉网奇7 小时前
blender底部对齐
开发语言·python·blender
lunzi_08267 小时前
【学习笔记】《Python编程 从入门到实践》第2章:变量命名规则、字符串操作与数值类型详解
笔记·python·学习
E_ICEBLUE7 小时前
Python 教程:快速复制 Excel 工作表
python·excel
是三旬老汉。7 小时前
宇树G1-D机器人端推理开发记录
python·机器人
databook7 小时前
轨迹的蓝图:方程求解与交点计算
python·数学·动效
隔壁大炮7 小时前
MNE-Python 第1天学习笔记:环境搭建与数据初探
python·eeg·bci·mne·脑电数据处理
晚烛7 小时前
CANN 模型热更新:不停机模型切换与无缝更新实战指南
开发语言·python
ZPC82107 小时前
单物体最优抓取轨迹生成
python·opencv·计算机视觉
若兰幽竹8 小时前
【大模型应用】抖音爆款视频深度分析系统:流水线式AI逆向拆解流量密码,精准预测播放量!
人工智能·python·音视频·抖音爆款分析