win10小程序(二十)循环键鼠操作程序

"""

循环粘贴工具 v1.0

通过 Ctrl+B 触发/停止执行命令序列

"""

import tkinter as tk

from tkinter import ttk, messagebox

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

import pyperclip

import pyautogui

import keyboard

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

_missing = []

try: import pyperclip

except ImportError: _missing.append("pyperclip")

try: import pyautogui

except ImportError: _missing.append("pyautogui")

try: import keyboard

except ImportError: _missing.append("keyboard")

if _missing:

import subprocess, sys

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

import importlib

for m in _missing:

importlib.import_module(m)

import pyperclip, pyautogui, keyboard

── 常量 ──────────────────────────────────────────────────────────────────────

BASE_W, BASE_H = 1920, 1080

SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "循环粘贴_设置.json")

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 = [

"单击左键","单击右键","双击左键","粘贴","Page Up","Page Down","延时","读取并粘贴"

]

CMD_HELP = (

"命令说明:\n"

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

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

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

" 粘贴 → 中间填文字 先点左键再粘贴内容(右侧备注)\n"

" Page Up → 中间忽略 执行 PageUp 翻页\n"

" Page Down → 中间忽略 执行 PageDown 翻页\n"

" 延时 → 中间填秒数 额外等待指定秒数\n"

" 读取并粘贴→ 从同目录xlsx第2列依次读取并粘贴\n"

"\n"

" 循环次数 → 顶部设置,整个命令序列重复执行的次数(默认1次)"

)

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

class App(tk.Tk):

def init(self):

super().init()

self.title("循环粘贴工具")

self._running = False

self._run_thread = None

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

self._drag_x = self._drag_y = 0

分辨率缩放比

sw = self.winfo_screenwidth()

sh = self.winfo_screenheight()

self.sx = sw / BASE_W

self.sy = sh / BASE_H

加载设置

self.cfg = self._load_cfg()

窗口尺寸/位置

列宽推算:Combobox≈15字符*8px + 中间100字符*8px + 右侧30字符*8px + 滚动条20 + 边距30 ≈ 1230

DEFAULT_WIDTH = 1230

geo = self.cfg.get("geometry", f"{DEFAULT_WIDTH}x800+100+50")

self.geometry(geo)

self.configure(bg=WIN_BG)

self.minsize(600, 400)

self._build_ui()

self._load_rows()

绑定 Ctrl+B

keyboard.add_hotkey("ctrl+b", self._toggle_run)

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

── 设置持久化 ────────────────────────────────────────────────────────────

def _load_cfg(self):

if os.path.exists(SETTINGS_FILE):

try:

with open(SETTINGS_FILE, "r", encoding="utf-8") as f:

return json.load(f)

except Exception:

pass

return {}

def _save_cfg(self):

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(SETTINGS_FILE, "w", encoding="utf-8") as f:

json.dump(self.cfg, f, ensure_ascii=False, indent=2)

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

def _build_ui(self):

── 顶部设置区 ────────────────────────────────────────────────────────

top = tk.Frame(self, bg=WIN_BG)

top.pack(fill="x", padx=6, pady=4)

tk.Label(top, text="命令间延时(s):", bg=WIN_BG, font=FONT).pack(side="left")

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.02"))

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_cfg)

save_btn.pack(side="left", padx=4)

Ctrl+B 提示

tk.Label(top, text=" 【Ctrl+B】启动/停止执行", bg=WIN_BG,

font=(FONT_NAME, 12, "bold"), fg="#E53935").pack(side="left", padx=10)

状态标签

self.lbl_status = tk.Label(top, text="●停止", bg=WIN_BG,

font=(FONT_NAME, 12, "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=15, 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 = [] # 每项: {frame, cmd, mid, rem}

── 行管理 ────────────────────────────────────────────────────────────────

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=14, font=FONT, state="normal")

cmb.pack(side="left", padx=1, pady=1)

中间输入框:宽100,无边框

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_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_after(self, target_rd):

idx = self._row_index(target_rd)

new_rd = self._make_row(idx + 1)

把 frame 移到正确位置

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):

saved = self.cfg.get("rows", [])

self._xlsx_row = self.cfg.get("xlsx_row", 0)

count = max(DEFAULT_ROWS, len(saved))

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("rem",""))

self.rows.append(rd)

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

def _toggle_run(self):

if self._running:

self._running = False

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.02

try:

loop_count = max(1, int(self.var_loop_count.get()))

except ValueError:

loop_count = 1

pyautogui.FAILSAFE = False

for _loop in range(loop_count):

if not self._running:

break

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.after(0, lambda: self.lbl_status.configure(text="●完成", fg="#1565C0"))

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

def parse_xy(s):

"""解析坐标字符串,返回 (x, y);若为空则返回 None(表示使用鼠标当前位置)"""

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:

pyautogui.click(xy[0], xy[1], button="left")

else:

pyautogui.click(button="left")

elif cmd in ("单击右键",):

xy = parse_xy(mid)

time.sleep(cmd_delay)

if xy:

pyautogui.click(xy[0], xy[1], button="right")

else:

pyautogui.click(button="right")

elif cmd in ("双击左键",):

xy = parse_xy(mid)

time.sleep(cmd_delay)

if xy:

pyautogui.doubleClick(xy[0], xy[1])

else:

pyautogui.doubleClick()

elif cmd == "粘贴":

time.sleep(cmd_delay)

pyperclip.copy(mid)

pyautogui.hotkey("ctrl", "v")

elif cmd == "Page Up":

time.sleep(cmd_delay)

pyautogui.press("pageup")

elif cmd == "Page Down":

time.sleep(cmd_delay)

pyautogui.press("pagedown")

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))

pyautogui.hotkey("ctrl", "v")

def _read_xlsx_cell(self):

"""读取同目录 xlsx 第一个工作表第一列,从上次记录行的下一行读取"""

try:

import openpyxl

except ImportError:

import subprocess, sys

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

self._save_cfg()

try:

keyboard.remove_hotkey("ctrl+b")

except Exception:

pass

self.destroy()

── 入口 ──────────────────────────────────────────────────────────────────────

if name == "main":

Windows 高 DPI 修正

if platform.system() == "Windows":

try:

from ctypes import windll

windll.shcore.SetProcessDpiAwareness(1)

except Exception:

pass

app = App()

app.mainloop()

相关推荐
Gerardisite1 小时前
CRM、ERP、OA 如何连接企业微信?QiWe 提供标准化解决方案
java·python·机器人·自动化·企业微信
weixin_444012931 小时前
CSS Flex布局中如何实现导航栏与Logo的左右分布_利用justify-content- space-between
jvm·数据库·python
彳亍1011 小时前
Less如何优化CSS文件大小_利用压缩配置去除冗余样式
jvm·数据库·python
m0_748554811 小时前
SQL如何防止JOIN查询导致数据库宕机_查询超时限制与资源管理
jvm·数据库·python
m0_748554811 小时前
React 中的渲染(Rendering)机制详解
jvm·数据库·python
2401_880071401 小时前
html怎么用jekyll转换_Jekyll博客如何导入传统HTML页面
jvm·数据库·python
wsj668882 小时前
03 | Ollama:本地大模型部署与调用
python
yaoxin5211232 小时前
405. Java 文件操作基础 - 装饰者模式与 I/O Streams
java·开发语言·python
Unbelievabletobe2 小时前
免费外汇api的响应时间在不同时段下的波动分析
大数据·开发语言·前端·python