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_COLORS3,

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_COLORS2,

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_COLORSidx % 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_COLORSi % 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 = savedi 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(parts0.strip()) * self.sx)

y = int(float(parts1.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(xy0, xy1, button="left")

else:

pyautogui.click(button="left")

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

xy = parse_xy(mid)

time.sleep(cmd_delay)

if xy:

pyautogui.click(xy0, xy1, button="right")

else:

pyautogui.click(button="right")

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

xy = parse_xy(mid)

time.sleep(cmd_delay)

if xy:

pyautogui.doubleClick(xy0, xy1)

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_files0), read_only=True, data_only=True)

ws = wb.worksheets0

找第一列所有非空单元格

cells = \[\]

for row in ws.iter_rows(min_col=2, max_col=2, values_only=True):

cells.append(row0)

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 (cellstarget is None or str(cellstarget).strip() == ""):

target += 1

if target >= len(cells):

return None

value = cellstarget

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

相关推荐
青 春 记 忆7 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO37 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
临沂GEO9 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白10 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
荷蒲11 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
数据狐(Datafox)13 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
量化吞吐机13 小时前
2026年下半年手工交易规则走向量化表达,产品该先接住哪一步
人工智能·python
Metaphor69213 小时前
使用 Python 读取和删除 Excel 文件属性
python·excel
迷迭香yy14 小时前
Python构建转融券多空识别系统从接口到因子的全流程 IG50免费开源股票数据API接口
开发语言·python·开源
月光船幽幽15 小时前
真实即粗糙,光滑是伪造
人工智能·python