[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import time
from pynput import mouse, keyboard
from enum import Enum
import json
import pystray
from PIL import Image
import datetime
class EventType(Enum):
"""定义事件类型枚举"""
KEY_PRESS = 1
KEY_RELEASE = 2
MOUSE_MOVE = 3
MOUSE_CLICK = 4
MOUSE_SCROLL = 5
MOUSE_DOUBLE_CLICK = 6
class Event:
"""定义事件类,用于存储事件信息"""
def __init__(self, type, timestamp, **kwargs):
self.type = type
self.timestamp = timestamp
self.data = kwargs
def to_dict(self):
"""转换为字典,方便json序列化"""
return {
"type": self.type.value,
"timestamp": self.timestamp,
"data": self.data
}
@classmethod
def from_dict(cls,data):
"""从字典创建事件实例"""
return cls(EventType(data["type"]),data["timestamp"],**data["data"])
def __str__(self):
if self.type == EventType.KEY_PRESS or self.type == EventType.KEY_RELEASE:
return f"{self.type.name}: {self.data.get('key')}, 时间:{self.timestamp:.2f}"
elif self.type == EventType.MOUSE_MOVE:
return f"{self.type.name}: X={self.data.get('x')}, Y={self.data.get('y')}, 时间:{self.timestamp:.2f}"
elif self.type == EventType.MOUSE_CLICK:
return f"{self.type.name}: {self.data.get('button')}, X={self.data.get('x')}, Y={self.data.get('y')}, 时间:{self.timestamp:.2f}"
elif self.type == EventType.MOUSE_SCROLL:
return f"{self.type.name}: X={self.data.get('dx')}, Y={self.data.get('dy')}, 时间:{self.timestamp:.2f}"
else:
return "Unknown event"
class MacroRecorderApp:
def __init__(self, root):
self.root = root
self.root.title("宏录制器")
self.root.geometry("800x600")
# 最小化时隐藏主窗口
self.root.protocol('WM_DELETE_WINDOW', self.withdraw_window)
self.is_recording = False
self.is_paused = False # 暂停状态
self.recorded_events = []
self.start_time = None
self.pause_time = None # 记录暂停开始的时间
self.total_pause_duration = 0 # 记录总的暂停时长
self.mouse_listener = None
self.keyboard_listener = None
self.mouse_controller = mouse.Controller()
self.keyboard_controller = keyboard.Controller()
self.hotkey_listener = None
self.tray_icon = None
self.loop_count = tk.IntVar(value=1) # 循环次数
self.infinite_loop = tk.BooleanVar(value=False) # 是否无限循环
self.stop_playback = False # 停止播放的标志
self.scheduled_playback = False # 定时播放状态
self.scheduled_time = tk.StringVar(value="") # 定时播放时间
# 默认热键
self.record_hotkey = '<ctrl>+<alt>+r' # 录制热键
self.stop_record_hotkey = '<ctrl>+<alt>+s' # 停止录制热键
self.play_hotkey = '<ctrl>+<alt>+p' # 播放热键
self.pause_resume_hotkey = '<ctrl>+<alt>+a' # 暂停/恢复热键
self.stop_playback_hotkey = '<ctrl>+<alt>+x' # 停止播放热键
# 事件类型选择
self.record_mouse_move = tk.BooleanVar(value=True) # 记录鼠标移动事件
self.record_mouse_click = tk.BooleanVar(value=True) # 记录鼠标点击事件
self.record_mouse_scroll = tk.BooleanVar(value=True) # 记录鼠标滚轮事件
self.record_key_press = tk.BooleanVar(value=True) # 记录键盘按下事件
self.record_key_release = tk.BooleanVar(value=True) # 记录键盘释放事件
self._create_widgets()
self._apply_theme("clam")
self._setup_hotkeys()
self._register_hotkey_listener()
self._setup_tray_icon()
def _apply_theme(self,theme_name):
"""应用主题"""
style=ttk.Style(self.root)
style.theme_use(theme_name)
# 统一字体
style.configure(".", font=("Arial", 10))
# 设置按钮样式
style.configure("TButton", padding=5,relief="raised")
style.map("TButton", foreground=[('active', 'black')], background=[('active', 'lightgray')])
def _create_widgets(self):
# 顶部框架
top_frame = ttk.Frame(self.root, padding=10)
top_frame.pack(fill=tk.X)
# 录制按钮
self.record_button = ttk.Button(top_frame, text="录制", command=self.toggle_record)
self.record_button.pack(side=tk.LEFT, padx=5)
# 暂停/恢复按钮
self.pause_button = ttk.Button(top_frame, text="暂停", command=self.toggle_pause_resume, state=tk.DISABLED)
self.pause_button.pack(side=tk.LEFT, padx=5)
# 播放按钮
self.play_button = ttk.Button(top_frame, text="播放", command=self.play_macro, state=tk.DISABLED)
self.play_button.pack(side=tk.LEFT, padx=5)
# 保存按钮
self.save_button = ttk.Button(top_frame, text="保存", command=self.save_macro, state=tk.DISABLED)
self.save_button.pack(side=tk.LEFT, padx=5)
# 加载按钮
self.load_button = ttk.Button(top_frame, text="加载", command=self.load_macro)
self.load_button.pack(side=tk.LEFT, padx=5)
# 删除按钮
self.delete_button = ttk.Button(top_frame, text="删除", command=self._delete_event, state=tk.DISABLED)
self.delete_button.pack(side=tk.LEFT, padx=5)
# 热键设置按钮
self.hotkey_button = ttk.Button(top_frame, text="设置热键", command=self._open_hotkey_settings)
self.hotkey_button.pack(side=tk.LEFT, padx=5)
# 循环和定时播放设置框架 (合并在同一行)
settings_frame = ttk.Frame(self.root, padding=10)
settings_frame.pack(fill=tk.X)
# 循环设置框架
loop_frame = ttk.LabelFrame(settings_frame, text="循环设置", padding=10)
loop_frame.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
# 循环次数
ttk.Label(loop_frame, text="循环次数:").pack(side=tk.LEFT, padx=5)
loop_count_entry = ttk.Entry(loop_frame, textvariable=self.loop_count, width=5)
loop_count_entry.pack(side=tk.LEFT, padx=5)
# 无限循环
ttk.Checkbutton(loop_frame, text="无限循环", variable=self.infinite_loop).pack(side=tk.LEFT, padx=5)
# 定时播放框架
schedule_frame = ttk.LabelFrame(settings_frame, text="定时播放", padding=10)
schedule_frame.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
# 定时播放时间标签和输入框
ttk.Label(schedule_frame, text="定时时间(HH:MM:SS):").pack(side=tk.LEFT, padx=5)
self.schedule_entry = ttk.Entry(schedule_frame, textvariable=self.scheduled_time, width=10)
self.schedule_entry.pack(side=tk.LEFT, padx=5)
# 定时播放按钮
self.schedule_button = ttk.Button(schedule_frame, text="设置定时播放", command=self._schedule_playback, state=tk.DISABLED)
self.schedule_button.pack(side=tk.LEFT, padx=5)
# 事件类型选择框架
event_type_frame = ttk.LabelFrame(self.root, text="选择录制事件类型", padding=10)
event_type_frame.pack(fill=tk.X, padx=10, pady=5)
# 鼠标事件复选框
ttk.Checkbutton(event_type_frame, text="鼠标移动", variable=self.record_mouse_move).pack(side=tk.LEFT, padx=5)
ttk.Checkbutton(event_type_frame, text="鼠标点击", variable=self.record_mouse_click).pack(side=tk.LEFT, padx=5)
ttk.Checkbutton(event_type_frame, text="鼠标滚轮", variable=self.record_mouse_scroll).pack(side=tk.LEFT, padx=5)
# 键盘事件复选框
ttk.Checkbutton(event_type_frame, text="键盘按下", variable=self.record_key_press).pack(side=tk.LEFT, padx=5)
ttk.Checkbutton(event_type_frame, text="键盘释放", variable=self.record_key_release).pack(side=tk.LEFT, padx=5)
# 事件列表框架
list_frame = ttk.Frame(self.root, padding=10)
list_frame.pack(fill=tk.BOTH, expand=True)
# 事件列表
self.event_list = tk.Listbox(list_frame, selectmode=tk.SINGLE)
self.event_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.event_list.bind("<<ListboxSelect>>", self._enable_delete_button)
self.event_list.bind("<Double-Button-1>",self._edit_event)
# 垂直滚动条
scrollbar=ttk.Scrollbar(list_frame,orient=tk.VERTICAL,command=self.event_list.yview)
scrollbar.pack(side=tk.RIGHT,fill=tk.Y)
self.event_list.config(yscrollcommand=scrollbar.set)
def _setup_hotkeys(self):
"""设置热键组合"""
self.record_hotkey_combination = keyboard.HotKey.parse(self.record_hotkey)
self.stop_record_hotkey_combination = keyboard.HotKey.parse(self.stop_record_hotkey)
self.play_hotkey_combination = keyboard.HotKey.parse(self.play_hotkey)
self.pause_resume_hotkey_combination = keyboard.HotKey.parse(self.pause_resume_hotkey)
self.stop_playback_hotkey_combination = keyboard.HotKey.parse(self.stop_playback_hotkey)
def _register_hotkey_listener(self):
"""注册热键监听器"""
self.hotkey_listener = keyboard.GlobalHotKeys({
self.record_hotkey: self._start_recording, # 录制热键
self.stop_record_hotkey: self._stop_recording, # 停止录制热键
self.play_hotkey: self.play_macro, # 播放热键
self.pause_resume_hotkey: self.toggle_pause_resume, # 暂停/恢复热键
self.stop_playback_hotkey: self._stop_playback # 停止播放热键
})
self.hotkey_listener.start()
def _stop_playback(self):
"""停止播放"""
self.stop_playback = True
def _open_hotkey_settings(self):
"""打开热键设置对话框"""
settings_window = tk.Toplevel(self.root)
settings_window.title("热键设置")
settings_window.geometry("300x300") # 调整窗口大小
# 录制热键
record_label = ttk.Label(settings_window, text="录制热键:")
record_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.record_hotkey_entry = ttk.Entry(settings_window)
self.record_hotkey_entry.insert(0, self.record_hotkey)
self.record_hotkey_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
# 停止录制热键
stop_record_label = ttk.Label(settings_window, text="停止录制热键:")
stop_record_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.stop_record_hotkey_entry = ttk.Entry(settings_window)
self.stop_record_hotkey_entry.insert(0, self.stop_record_hotkey)
self.stop_record_hotkey_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
# 播放热键
play_label = ttk.Label(settings_window, text="播放热键:")
play_label.grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.play_hotkey_entry = ttk.Entry(settings_window)
self.play_hotkey_entry.insert(0, self.play_hotkey)
self.play_hotkey_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
# 暂停/恢复热键
pause_resume_label = ttk.Label(settings_window, text="暂停/恢复热键:")
pause_resume_label.grid(row=3, column=0, padx=5, pady=5, sticky="w")
self.pause_resume_hotkey_entry = ttk.Entry(settings_window)
self.pause_resume_hotkey_entry.insert(0, self.pause_resume_hotkey)
self.pause_resume_hotkey_entry.grid(row=3, column=1, padx=5, pady=5, sticky="ew")
# 停止播放热键
stop_playback_label = ttk.Label(settings_window, text="停止播放热键:")
stop_playback_label.grid(row=4, column=0, padx=5, pady=5, sticky="w")
self.stop_playback_hotkey_entry = ttk.Entry(settings_window)
self.stop_playback_hotkey_entry.insert(0, self.stop_playback_hotkey)
self.stop_playback_hotkey_entry.grid(row=4, column=1, padx=5, pady=5, sticky="ew")
# 保存按钮
save_button = ttk.Button(settings_window, text="保存", command=self._save_hotkey_settings)
save_button.grid(row=5, column=0, columnspan=2, padx=5, pady=10)
def _save_hotkey_settings(self):
"""保存热键设置"""
new_record_hotkey = self.record_hotkey_entry.get()
new_stop_record_hotkey = self.stop_record_hotkey_entry.get()
new_play_hotkey = self.play_hotkey_entry.get()
new_pause_resume_hotkey = self.pause_resume_hotkey_entry.get()
new_stop_playback_hotkey = self.stop_playback_hotkey_entry.get()
try:
# 验证热键
keyboard.HotKey.parse(new_record_hotkey)
keyboard.HotKey.parse(new_stop_record_hotkey)
keyboard.HotKey.parse(new_play_hotkey)
keyboard.HotKey.parse(new_pause_resume_hotkey)
keyboard.HotKey.parse(new_stop_playback_hotkey)
# 更新热键
self.record_hotkey = new_record_hotkey
self.stop_record_hotkey = new_stop_record_hotkey
self.play_hotkey = new_play_hotkey
self.pause_resume_hotkey = new_pause_resume_hotkey
self.stop_playback_hotkey = new_stop_playback_hotkey
self._setup_hotkeys()
# 停止并重新启动热键侦听器以应用更改
if self.hotkey_listener:
self.hotkey_listener.stop()
self._register_hotkey_listener()
messagebox.showinfo("成功", "热键设置已保存")
except ValueError:
messagebox.showerror("错误", "无效的热键组合")
def toggle_record(self):
"""
切换录制状态。
为了兼容使用界面按钮触发录制,此函数做判断
"""
if not self.is_recording:
self._start_recording()
else:
self._stop_recording()
def toggle_pause_resume(self):
"""切换暂停/恢复状态"""
if not self.is_recording:
return
if self.is_paused:
self._resume_recording()
else:
self._pause_recording()
def _start_recording(self):
"""开始录制"""
# 如果正在录制,则不执行任何操作
if self.is_recording:
return
self.is_recording = True
self.is_paused = False
self.record_button.config(text="停止")
self.pause_button.config(state=tk.NORMAL, text="暂停")
self.play_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
self.schedule_button.config(state=tk.DISABLED) # 禁用定时播放按钮
self.recorded_events = []
self.event_list.delete(0,tk.END) # 清空列表
self.start_time = time.time()
self.total_pause_duration = 0
self.mouse_listener = mouse.Listener(
on_move=self._on_mouse_move if self.record_mouse_move.get() else self._do_nothing,
on_click=self._on_mouse_click if self.record_mouse_click.get() else self._do_nothing,
on_scroll=self._on_mouse_scroll if self.record_mouse_scroll.get() else self._do_nothing
)
self.keyboard_listener = keyboard.Listener(
on_press=self._on_key_press if self.record_key_press.get() else self._do_nothing,
on_release=self._on_key_release if self.record_key_release.get() else self._do_nothing
)
self.mouse_listener.start()
self.keyboard_listener.start()
def _pause_recording(self):
"""暂停录制"""
if not self.is_recording or self.is_paused:
return
self.is_paused = True
self.pause_button.config(text="恢复")
self.pause_time = time.time()
# 通过修改回调函数为空函数来实现,无论是否选择事件类型都执行
self.mouse_listener.on_move = self._do_nothing
self.mouse_listener.on_click = self._do_nothing
self.mouse_listener.on_scroll = self._do_nothing
self.keyboard_listener.on_press = self._do_nothing
self.keyboard_listener.on_release = self._do_nothing
def _resume_recording(self):
"""恢复录制"""
if not self.is_recording or not self.is_paused:
return
self.is_paused = False
self.pause_button.config(text="暂停")
# 更新总的暂停时间
self.total_pause_duration += (time.time() - self.pause_time)
# 恢复监听鼠标和键盘事件,根据选择的事件类型执行
self.mouse_listener.on_move = self._on_mouse_move if self.record_mouse_move.get() else self._do_nothing
self.mouse_listener.on_click = self._on_mouse_click if self.record_mouse_click.get() else self._do_nothing
self.mouse_listener.on_scroll = self._on_mouse_scroll if self.record_mouse_scroll.get() else self._do_nothing
self.keyboard_listener.on_press = self._on_key_press if self.record_key_press.get() else self._do_nothing
self.keyboard_listener.on_release = self._on_key_release if self.record_key_release.get() else self._do_nothing
def _do_nothing(self, *args):
"""空函数,用于在暂停时替换事件回调函数"""
pass
def _stop_recording(self):
"""停止录制"""
# 如果没有录制,则不执行任何操作
if not self.is_recording:
return
self.is_recording = False
self.is_paused = False
self.record_button.config(text="录制")
self.pause_button.config(state=tk.DISABLED, text="暂停")
self.play_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
self.schedule_button.config(state=tk.NORMAL)
if self.mouse_listener:
self.mouse_listener.stop()
if self.keyboard_listener:
self.keyboard_listener.stop()
self.mouse_listener = None
self.keyboard_listener = None
def _on_mouse_move(self, x, y):
"""鼠标移动事件处理"""
if not self.is_recording or self.is_paused or not self.record_mouse_move.get():
return
timestamp = time.time() - self.start_time - self.total_pause_duration
event=Event(EventType.MOUSE_MOVE,timestamp,x=x,y=y)
self.recorded_events.append(event)
self.event_list.insert(tk.END,str(event))
def _on_mouse_click(self, x, y, button, pressed):
"""鼠标点击事件处理"""
if not self.is_recording or self.is_paused or not self.record_mouse_click.get():
return
timestamp = time.time() - self.start_time - self.total_pause_duration
if pressed:
event = Event(EventType.MOUSE_CLICK, timestamp, x=x, y=y, button=str(button))
self.recorded_events.append(event)
self.event_list.insert(tk.END, str(event))
else:
# 处理双击事件
if button == mouse.Button.left:
if self.event_list.size() > 0 and self.event_list.get(self.event_list.size() - 1).startswith("MOUSE_CLICK"):
last_event = self.recorded_events[-1]
if last_event.type == EventType.MOUSE_CLICK and last_event.data['button'] == str(button):
double_click_event = Event(EventType.MOUSE_DOUBLE_CLICK, timestamp, x=x, y=y, button=str(button))
self.recorded_events.append(double_click_event)
self.event_list.insert(tk.END, str(double_click_event))
def _on_mouse_scroll(self, x, y, dx, dy):
"""鼠标滚轮事件处理"""
if not self.is_recording or self.is_paused or not self.record_mouse_scroll.get():
return
timestamp = time.time() - self.start_time - self.total_pause_duration
event= Event(EventType.MOUSE_SCROLL,timestamp,dx=dx,dy=dy)
self.recorded_events.append(event)
self.event_list.insert(tk.END,str(event))
def _on_key_press(self, key):
"""键盘按键按下事件处理,增强版"""
if not self.is_recording or self.is_paused or not self.record_key_press.get():
return
timestamp = time.time() - self.start_time - self.total_pause_duration
try:
key_char = key.char if key.char else str(key) # 获取按键字符表示
except AttributeError:
key_char = str(key)
# 处理左右 Ctrl、Shift、Alt 键
if key in (keyboard.Key.ctrl_l, keyboard.Key.ctrl_r):
key_char = "Left Ctrl" if key == keyboard.Key.ctrl_l else "Right Ctrl"
elif key in (keyboard.Key.shift_l, keyboard.Key.shift_r):
key_char = "Left Shift" if key == keyboard.Key.shift_l else "Right Shift"
elif key in (keyboard.Key.alt_l, keyboard.Key.alt_r):
key_char = "Left Alt" if key == keyboard.Key.alt_l else "Right Alt"
event = Event(EventType.KEY_PRESS, timestamp, key=key_char)
self.recorded_events.append(event)
self.event_list.insert(tk.END, str(event))
def _on_key_release(self, key):
"""键盘按键释放事件处理,增强版"""
if not self.is_recording or self.is_paused or not self.record_key_release.get():
return
timestamp = time.time() - self.start_time - self.total_pause_duration
try:
key_char = key.char if key.char else str(key) # 获取按键字符表示
except AttributeError:
key_char = str(key)
# 处理左右 Ctrl、Shift、Alt 键
if key in (keyboard.Key.ctrl_l, keyboard.Key.ctrl_r):
key_char = "Left Ctrl" if key == keyboard.Key.ctrl_l else "Right Ctrl"
elif key in (keyboard.Key.shift_l, keyboard.Key.shift_r):
key_char = "Left Shift" if key == keyboard.Key.shift_l else "Right Shift"
elif key in (keyboard.Key.alt_l, keyboard.Key.alt_r):
key_char = "Left Alt" if key == keyboard.Key.alt_l else "Right Alt"
event = Event(EventType.KEY_RELEASE, timestamp, key=key_char)
self.recorded_events.append(event)
self.event_list.insert(tk.END, str(event))
def play_macro(self):
"""播放录制好的宏"""
if not self.recorded_events:
messagebox.showinfo("提示","没有录制任何宏")
return
self.play_button.config(state=tk.DISABLED)
self.record_button.config(state=tk.DISABLED)
self.pause_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
self.schedule_button.config(state=tk.DISABLED)
self.stop_playback = False # 重置停止播放标志
threading.Thread(target=self._playback_thread,daemon=True).start()
def _playback_thread(self):
"""回放宏操作的后台线程"""
print("Playback thread started")
try:
loop_counter = 0
while True:
if self.stop_playback:
break
if not self.infinite_loop.get():
loop_counter += 1
if loop_counter > self.loop_count.get():
break
print(f"Starting loop: {loop_counter if not self.infinite_loop.get() else 'Infinite'}")
start_time = time.time()
last_timestamp = 0
for event in self.recorded_events:
if self.stop_playback:
break # 如果停止标志被设置,则退出循环
print(f"Event: {event}")
delay = event.timestamp - last_timestamp
if delay > 0:
print(f"Delaying {delay:.2f} seconds")
# 使用循环来检查停止标志
delay_start_time = time.time()
while time.time() - delay_start_time < delay:
if self.stop_playback:
break
time.sleep(0.01) # 每次循环等待0.01s
if event.type == EventType.KEY_PRESS:
key_str = event.data.get('key')
print(f"Pressing key: {key_str}")
try:
# 尝试作为普通字符按下
self.keyboard_controller.press(key_str)
except (ValueError, AttributeError):
# 处理特殊键
try:
# 尝试使用keyboard.Key找到特殊键
special_key = getattr(keyboard.Key, key_str.replace("Key.", ""))
self.keyboard_controller.press(special_key)
except AttributeError:
print(f"无法识别的键:{key_str}")
elif event.type == EventType.KEY_RELEASE:
key_str = event.data.get('key')
print(f"Releasing key: {key_str}")
try:
# 尝试作为普通字符释放
self.keyboard_controller.release(key_str)
except (ValueError, AttributeError):
# 处理特殊键
try:
# 尝试使用keyboard.Key找到特殊键
special_key = getattr(keyboard.Key, key_str.replace("Key.", ""))
self.keyboard_controller.release(special_key)
except AttributeError:
print(f"无法识别的键:{key_str}")
elif event.type == EventType.MOUSE_MOVE:
x = event.data.get('x')
y = event.data.get('y')
print(f"Moving mouse to: x={x}, y={y}")
self.mouse_controller.position = (x, y)
elif event.type == EventType.MOUSE_CLICK:
x = event.data.get('x')
y = event.data.get('y')
button = event.data.get('button').replace("Button.", "")
print(f"Clicking mouse at: x={x}, y={y}, button={button}")
self.mouse_controller.position = (x, y)
self.mouse_controller.click(mouse.Button[button], 1)
elif event.type == EventType.MOUSE_SCROLL:
dx = event.data.get('dx')
dy = event.data.get('dy')
print(f"Scrolling mouse by dx={dx}, dy={dy}")
self.mouse_controller.scroll(dx, dy)
last_timestamp = event.timestamp
except Exception as e:
print(f"Error during playback: {e}")
finally:
self.play_button.config(state=tk.NORMAL)
self.record_button.config(state=tk.NORMAL)
self.pause_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
self.schedule_button.config(state=tk.NORMAL)
def save_macro(self):
"""保存宏到文件"""
file_path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON Files", "*.json")])
if file_path:
try:
with open(file_path, 'w') as f:
json.dump([event.to_dict() for event in self.recorded_events], f, indent=4)
messagebox.showinfo("成功", "宏已保存")
except Exception as e:
messagebox.showerror("错误", f"保存宏失败: {e}")
def load_macro(self):
"""从文件加载宏"""
file_path = filedialog.askopenfilename(defaultextension=".json", filetypes=[("JSON Files", "*.json")])
if file_path:
try:
with open(file_path, 'r') as f:
loaded_data = json.load(f)
self.recorded_events = [Event.from_dict(data) for data in loaded_data]
self.event_list.delete(0, tk.END) # 清空列表框
for event in self.recorded_events:
self.event_list.insert(tk.END,str(event))
self.play_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
self.schedule_button.config(state=tk.NORMAL) # 启用定时播放按钮
messagebox.showinfo("成功","宏已加载")
except Exception as e:
messagebox.showerror("错误", f"加载宏失败: {e}")
def _stop_recording(self):
"""停止录制"""
# 如果没有录制,则不执行任何操作
if not self.is_recording:
return
self.is_recording = False
self.is_paused = False
self.record_button.config(text="录制")
self.pause_button.config(state=tk.DISABLED, text="暂停")
self.play_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
self.schedule_button.config(state=tk.NORMAL) # 启用定时播放按钮
if self.mouse_listener:
self.mouse_listener.stop()
if self.keyboard_listener:
self.keyboard_listener.stop()
self.mouse_listener = None
self.keyboard_listener = None
def _delete_event(self):
"""删除选中的事件"""
selection = self.event_list.curselection()
if not selection:
return
index = selection[0]
del self.recorded_events[index]
self.event_list.delete(index)
if not self.recorded_events:
self.play_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
self.schedule_button.config(state=tk.DISABLED) # 禁用定时播放按钮
else:
# 确保如果列表框中还有项目,删除按钮保持启用状态
self.delete_button.config(state=tk.NORMAL)
def _edit_event(self, event = None):
"""编辑选中的事件"""
selection= self.event_list.curselection()
if not selection:
return
selected_index=selection[0]
selected_event=self.recorded_events[selected_index]
if selected_event.type == EventType.KEY_PRESS or selected_event.type == EventType.KEY_RELEASE:
self._edit_key_event(selected_index,selected_event)
elif selected_event.type == EventType.MOUSE_MOVE:
self._edit_mouse_move_event(selected_index,selected_event)
elif selected_event.type == EventType.MOUSE_CLICK:
self._edit_mouse_click_event(selected_index,selected_event)
elif selected_event.type == EventType.MOUSE_SCROLL:
self._edit_mouse_scroll_event(selected_index,selected_event)
def _edit_key_event(self,index,event):
"""编辑键盘事件"""
edit_window= tk.Toplevel(self.root)
edit_window.title("编辑键盘事件")
key_label= ttk.Label(edit_window, text="按键:")
key_label.grid(row=0,column=0,sticky="w",padx=5,pady=5)
key_entry = ttk.Entry(edit_window)
key_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
key_entry.insert(0, event.data.get("key",""))
timestamp_label = ttk.Label(edit_window, text="时间戳(秒):")
timestamp_label.grid(row=1, column=0, sticky="w", padx=5, pady=5)
timestamp_entry = ttk.Entry(edit_window)
timestamp_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
timestamp_entry.insert(0, f"{event.timestamp:.2f}")
def save_edit():
new_key= key_entry.get()
event.data["key"]=new_key
try:
new_timestamp = float(timestamp_entry.get())
if new_timestamp < 0:
raise ValueError("时间戳不能为负数")
event.timestamp = new_timestamp
self._update_event_list(index, event)
edit_window.destroy()
except ValueError as e:
messagebox.showerror("错误", f"无效的时间戳: {e}")
save_button = ttk.Button(edit_window,text="保存",command=save_edit)
save_button.grid(row=2,column=0,columnspan=2,pady=5)
def _edit_mouse_move_event(self,index, event):
"""编辑鼠标移动事件"""
edit_window= tk.Toplevel(self.root)
edit_window.title("编辑鼠标移动事件")
x_label = ttk.Label(edit_window,text="X:")
x_label.grid(row=0,column=0,sticky="w",padx=5,pady=5)
x_entry = ttk.Entry(edit_window)
x_entry.grid(row=0, column=1, sticky="ew", padx= 5, pady=5)
x_entry.insert(0,event.data.get("x",""))
y_label = ttk.Label(edit_window,text="Y:")
y_label.grid(row=1,column=0,sticky="w",padx=5,pady=5)
y_entry = ttk.Entry(edit_window)
y_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
y_entry.insert(0,event.data.get("y",""))
timestamp_label = ttk.Label(edit_window, text="时间戳(秒):")
timestamp_label.grid(row=2, column=0, sticky="w", padx=5, pady=5)
timestamp_entry = ttk.Entry(edit_window)
timestamp_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=5)
timestamp_entry.insert(0, f"{event.timestamp:.2f}")
def save_edit():
try:
new_x = float(x_entry.get())
new_y= float(y_entry.get())
event.data["x"] =new_x
event.data["y"] = new_y
new_timestamp = float(timestamp_entry.get())
if new_timestamp < 0:
raise ValueError("时间戳不能为负数")
event.timestamp = new_timestamp
self._update_event_list(index,event)
edit_window.destroy()
except ValueError as e:
messagebox.showerror("错误",f"无效的输入: {e}")
save_button = ttk.Button(edit_window,text="保存",command=save_edit)
save_button.grid(row=3,column=0,columnspan=2,pady=5)
def _edit_mouse_click_event(self,index,event):
"""编辑鼠标点击事件"""
edit_window= tk.Toplevel(self.root)
edit_window.title("编辑鼠标点击事件")
x_label = ttk.Label(edit_window,text="X:")
x_label.grid(row=0,column=0,sticky="w",padx=5,pady=5)
x_entry = ttk.Entry(edit_window)
x_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
x_entry.insert(0,event.data.get("x",""))
y_label = ttk.Label(edit_window,text="Y:")
y_label.grid(row=1,column=0,sticky="w",padx=5,pady=5)
y_entry = ttk.Entry(edit_window)
y_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
y_entry.insert(0,event.data.get("y",""))
button_label =ttk.Label(edit_window,text="按钮:")
button_label.grid(row=2,column=0,sticky="w",padx=5,pady=5)
button_combobox = ttk.Combobox(edit_window,values=[str(b) for b in mouse.Button])
button_combobox.grid(row=2,column=1,sticky="ew",padx=5,pady=5)
button_combobox.set(str(event.data.get("button")).replace("Button.",""))
timestamp_label = ttk.Label(edit_window, text="时间戳(秒):")
timestamp_label.grid(row=3, column=0, sticky="w", padx=5, pady=5)
timestamp_entry = ttk.Entry(edit_window)
timestamp_entry.grid(row=3, column=1, sticky="ew", padx=5, pady=5)
timestamp_entry.insert(0, f"{event.timestamp:.2f}")
def save_edit():
try:
new_x= float(x_entry.get())
new_y= float(y_entry.get())
new_button= button_combobox.get()
event.data["x"]=new_x
event.data["y"]=new_y
event.data["button"]=new_button
new_timestamp = float(timestamp_entry.get())
if new_timestamp < 0:
raise ValueError("时间戳不能为负数")
event.timestamp = new_timestamp
self._update_event_list(index,event)
edit_window.destroy()
except ValueError as e:
messagebox.showerror("错误",f"无效的输入: {e}")
save_button = ttk.Button(edit_window,text="保存",command=save_edit)
save_button.grid(row=4,column=0,columnspan=2,pady=5)
def _edit_mouse_scroll_event(self,index,event):
"""编辑鼠标滚动事件"""
edit_window= tk.Toplevel(self.root)
edit_window.title("编辑鼠标滚动事件")
dx_label=ttk.Label(edit_window,text="DX:")
dx_label.grid(row=0,column=0,sticky="w",padx=5,pady=5)
dx_entry=ttk.Entry(edit_window)
dx_entry.grid(row=0,column=1,sticky="ew",padx=5,pady=5)
dx_entry.insert(0,event.data.get("dx",""))
dy_label=ttk.Label(edit_window,text="DY:")
dy_label.grid(row=1,column=0,sticky="w",padx=5,pady=5)
dy_entry=ttk.Entry(edit_window)
dy_entry.grid(row=1,column=1,sticky="ew",padx=5,pady=5)
dy_entry.insert(0,event.data.get("dy",""))
timestamp_label = ttk.Label(edit_window, text="时间戳(秒):")
timestamp_label.grid(row=2, column=0, sticky="w", padx=5, pady=5)
timestamp_entry = ttk.Entry(edit_window)
timestamp_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=5)
timestamp_entry.insert(0, f"{event.timestamp:.2f}")
def save_edit():
try:
new_dx=float(dx_entry.get())
new_dy=float(dy_entry.get())
event.data["dx"]=new_dx
event.data["dy"]=new_dy
new_timestamp = float(timestamp_entry.get())
if new_timestamp < 0:
raise ValueError("时间戳不能为负数")
event.timestamp = new_timestamp
self._update_event_list(index,event)
edit_window.destroy()
except ValueError as e:
messagebox.showerror("错误",f"无效的输入: {e}")
save_button=ttk.Button(edit_window,text="保存",command=save_edit)
save_button.grid(row=3,column=0,columnspan=2,pady=5)
def _update_event_list(self,index,event):
"""更新列表框"""
self.recorded_events[index] =event
self.event_list.delete(index)
self.event_list.insert(index,str(event))
def _delete_event(self):
"""删除选中的事件"""
selection = self.event_list.curselection()
if not selection:
return
index = selection[0]
del self.recorded_events[index]
self.event_list.delete(index)
if not self.recorded_events:
self.play_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
self.schedule_button.config(state=tk.DISABLED)
else:
# 确保如果列表框中还有项目,删除按钮保持启用状态
self.delete_button.config(state=tk.NORMAL)
def _enable_delete_button(self, _):
"""根据列表选中项是否选中启用/禁用删除按钮"""
selection = self.event_list.curselection()
if selection:
self.delete_button.config(state=tk.NORMAL)
else:
self.delete_button.config(state=tk.DISABLED)
def _schedule_playback(self):
"""设置定时播放"""
if not self.recorded_events:
messagebox.showinfo("提示","没有录制任何宏")
return
schedule_time_str = self.scheduled_time.get()
try:
schedule_time = datetime.datetime.strptime(schedule_time_str, "%H:%M:%S").time()
now=datetime.datetime.now()
schedule_datetime = datetime.datetime.combine(now,schedule_time)
if schedule_datetime <= now:
schedule_datetime += datetime.timedelta(days=1) # 如果设定时间小于等于当前时间,则设置为第二天
delay_seconds = (schedule_datetime - now).total_seconds()
self.scheduled_playback =True
self.play_button.config(state=tk.DISABLED) # 禁用播放按钮
self.record_button.config(state=tk.DISABLED)
self.pause_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
self.schedule_button.config(text="取消定时",state=tk.DISABLED) # 禁用定时播放按钮
self.schedule_button.config(command=self._cancel_scheduled_playback)
messagebox.showinfo("提示",f"宏将在 {schedule_datetime.strftime('%Y-%m-%d %H:%M:%S')} 播放")
threading.Timer(delay_seconds, self._scheduled_playback_trigger).start()
except ValueError:
messagebox.showerror("错误","定时时间格式错误,正确格式为HH:MM:SS")
def _cancel_scheduled_playback(self):
"""取消定时播放"""
self.scheduled_playback = False
self.schedule_button.config(text="设置定时播放",state=tk.NORMAL) # 启用定时播放按钮
self.schedule_button.config(command=self._schedule_playback)
self.play_button.config(state=tk.NORMAL)
self.record_button.config(state=tk.NORMAL)
self.pause_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
messagebox.showinfo("提示","已取消定时播放")
def _scheduled_playback_trigger(self):
"""定时播放触发"""
if self.scheduled_playback:
self.scheduled_playback = False # 保证只触发一次
self.play_macro()
# 还原定时播放按钮状态
self.schedule_button.config(text="设置定时播放")
self.schedule_button.config(command=self._schedule_playback)
def _setup_tray_icon(self):
"""设置系统托盘图标"""
image = Image.open("icon.ico") # 替换为你的图标文件
menu = (
pystray.MenuItem('显示', self.show_window),
pystray.MenuItem('录制', self.toggle_record),
pystray.MenuItem('播放', self.play_macro),
pystray.MenuItem('退出', self.quit_window)
)
self.tray_icon = pystray.Icon("Macro Recorder", image, "宏录制器", menu)
self.tray_icon.run_detached()
def withdraw_window(self):
"""隐藏主窗口"""
self.root.withdraw()
def show_window(self, icon, item):
"""显示主窗口"""
self.root.deiconify()
def quit_window(self, icon, item):
"""退出程序"""
self.tray_icon.stop()
self.root.quit()
if __name__ == "__main__":
root = tk.Tk()
app = MacroRecorderApp(root)
root.mainloop()