import
tkinter as tk
from
tkinter
import
ttk, messagebox, Toplevel
import
time
import
threading
import
sys
import
os
import
platform
from
typing
import
Optional,
List
,
Set
,
Callable
,
Any
,
Tuple
, Union,
Dict
import
traceback
import
json
from
pathlib
import
Path
try
:
from
pynput.keyboard
import
Key, KeyCode
from
pynput.mouse
import
Button
PynputKeyType
=
Union[Key, KeyCode, Button]
try
:
import
pystray
from
PIL
import
Image, ImageDraw
except
ImportError:
pystray
=
None
Image, ImageDraw
=
None
,
None
except
ImportError:
class
Key:
pass
class
KeyCode:
pass
class
Button:
pass
pystray
=
None
Image, ImageDraw
=
None
,
None
PynputKeyType
=
Union[
Any
]
mouse_controller: Optional[
'MouseController'
]
=
None
keyboard_controller: Optional[
'KeyboardController'
]
=
None
DEFAULT_AUTOMATION_KEYS
=
6
MAX_AUTOMATION_KEYS
=
50
APP_NAME
=
"AdvancedAutoClicker"
CONFIG_FILENAME
=
"config.json"
def
get_config_dir()
-
> Path:
system
=
platform.system()
app_name_safe
=
APP_NAME.replace(
" "
,
"_"
)
try
:
if
system
=
=
"Windows"
:
path_str
=
os.getenv(
'APPDATA'
)
or
os.getenv(
'LOCALAPPDATA'
)
or
os.path.expanduser(
'~'
)
base_path
=
Path(path_str)
if
path_str
=
=
os.path.expanduser(
'~'
):
app_name_safe
=
"."
+
app_name_safe
elif
system
=
=
"Darwin"
:
base_path
=
Path.home()
/
"Library"
/
"Application Support"
else
:
xdg_config_home
=
os.getenv(
'XDG_CONFIG_HOME'
)
base_path
=
Path(xdg_config_home)
if
xdg_config_home
else
Path.home()
/
".config"
config_dir
=
base_path
/
app_name_safe
return
config_dir
except
Exception as e:
print
(f
"Error determining config directory, falling back to current: {e}"
)
return
Path(
"."
)
/
f
".{app_name_safe}_config"
def
get_config_path()
-
> Path:
config_dir
=
get_config_dir()
try
:
config_dir.mkdir(parents
=
True
, exist_ok
=
True
)
except
OSError as e:
print
(f
"Warning: Could not create config directory '{config_dir}': {e}"
)
return
Path(CONFIG_FILENAME)
except
Exception as e:
print
(f
"Unexpected error creating config directory '{config_dir}': {e}"
)
return
Path(CONFIG_FILENAME)
return
config_dir
/
CONFIG_FILENAME
class
AutoClickerApp:
def
__init__(
self
, root: tk.Tk):
self
.root
=
root
self
.system_name
=
platform.system()
self
.root.title(f
"高级连点器 v2.6.2 - 开源免费"
)
self
.root.resizable(
True
,
True
)
global
mouse_controller, keyboard_controller
if
mouse_controller
is
None
or
keyboard_controller
is
None
:
messagebox.showerror(
"初始化错误"
,
"输入控制器未能正确初始化。"
, parent
=
self
.root)
self
.root.after(
50
,
self
.root.destroy)
raise
RuntimeError(
"Input controllers not initialized"
)
self
.click_interval
=
tk.DoubleVar(value
=
0.01
)
self
.status_text
=
tk.StringVar(value
=
"状态: 初始化中..."
)
self
.is_capturing
=
False
self
._shutting_down
=
False
self
.window_hidden_by_automation
=
False
self
.selected_click_type
=
tk.StringVar(value
=
"left"
)
self
.mouse_hotkey_str
=
tk.StringVar(value
=
"Key.f8"
)
self
.parsed_mouse_hotkey: Optional[
Set
[PynputKeyType]]
=
set
()
self
.is_running_mouse
=
False
self
.mouse_click_thread: Optional[threading.Thread]
=
None
self
.mouse_stop_event
=
threading.Event()
self
.mouse_duration
=
tk.DoubleVar(value
=
0.0
)
self
.mouse_duration_after_id: Optional[
str
]
=
None
self
.keyboard_target_keys:
List
[tk.StringVar]
=
[]
self
.keyboard_target_delays:
List
[tk.DoubleVar]
=
[]
self
.valid_target_key_objects:
List
[
Tuple
[PynputKeyType,
float
]]
=
[]
self
.keyboard_hotkey_str
=
tk.StringVar(value
=
"Key.f7"
)
self
.parsed_keyboard_hotkey: Optional[
Set
[PynputKeyType]]
=
set
()
self
.keyboard_mode
=
tk.StringVar(value
=
"sequential"
)
self
.keyboard_cycle_delay
=
tk.DoubleVar(value
=
0.1
)
self
.is_running_keyboard
=
False
self
.keyboard_thread: Optional[threading.Thread]
=
None
self
.keyboard_stop_event
=
threading.Event()
self
.keyboard_duration
=
tk.DoubleVar(value
=
0.0
)
self
.keyboard_duration_after_id: Optional[
str
]
=
None
self
.current_key_row_count
=
0
self
.hotkey_listener_thread: Optional[threading.Thread]
=
None
self
.hotkey_stop_event
=
threading.Event()
self
.current_pressed_keys:
Set
[PynputKeyType]
=
set
()
self
._listener_keyboard_instance: Optional[
'KeyboardListener'
]
=
None
self
._listener_mouse_instance: Optional[
'MouseListener'
]
=
None
self
.tray_icon: Optional[
'pystray.Icon'
]
=
None
self
.tray_thread: Optional[threading.Thread]
=
None
self
.status_label
=
None
self
.interval_entry
=
None
self
.click_type_radios
=
[]
self
.mouse_hotkey_entry
=
None
self
.mouse_hotkey_capture_button
=
None
self
.mouse_hotkey_apply_button
=
None
self
.mouse_side_button_x1
=
None
self
.mouse_side_button_x2
=
None
self
.mouse_duration_entry
=
None
self
.mouse_duration_label
=
None
self
.mouse_duration_unit_label
=
None
self
.mouse_start_button
=
None
self
.mouse_stop_button
=
None
self
.keyboard_hotkey_entry
=
None
self
.keyboard_hotkey_capture_button
=
None
self
.keyboard_hotkey_apply_button
=
None
self
.keyboard_side_button_x1
=
None
self
.keyboard_side_button_x2
=
None
self
.key_row_frames
=
[]
self
.key_entries
=
[]
self
.key_capture_buttons
=
[]
self
.key_apply_buttons
=
[]
self
.key_delay_entries
=
[]
self
.key_delay_labels
=
[]
self
.keyboard_mode_radios
=
[]
self
.cycle_delay_label
=
None
self
.cycle_delay_entry
=
None
self
.cycle_delay_unit_label
=
None
self
.keyboard_duration_entry
=
None
self
.keyboard_duration_label
=
None
self
.keyboard_duration_unit_label
=
None
self
.keyboard_start_button
=
None
self
.keyboard_stop_button
=
None
self
.keys_canvas
=
None
self
.scrollable_frame
=
None
self
.key_list_frame
=
None
self
.add_key_button
=
None
self
.remove_key_button
=
None
self
.setup_gui()
self
.load_config()
self
._parse_initial_hotkeys()
self
.setup_tray()
self
.root.protocol(
"WM_DELETE_WINDOW"
,
self
.on_close_requested)
self
.root.after(
200
,
self
.start_hotkey_listener)
self
.root.after(
50
,
self
.update_ui_state)
if
self
.status_text.get()
=
=
"状态: 初始化中..."
:
self
.status_text.
set
(
"状态: 已停止"
)
def
_parse_initial_hotkeys(
self
):
if
'Key'
not
in
globals
():
print
(
"Warning: Pynput classes not loaded, cannot parse initial hotkeys."
)
self
.parsed_mouse_hotkey
=
set
()
self
.parsed_keyboard_hotkey
=
set
()
return
try
:
self
.parsed_mouse_hotkey
=
self
.parse_hotkey_str(
self
.mouse_hotkey_str.get())
except
(ValueError, RuntimeError) as e:
print
(f
"Warning: Invalid initial mouse hotkey '{self.mouse_hotkey_str.get()}': {e}"
)
self
.mouse_hotkey_str.
set
("")
self
.parsed_mouse_hotkey
=
set
()
try
:
self
.parsed_keyboard_hotkey
=
self
.parse_hotkey_str(
self
.keyboard_hotkey_str.get())
except
(ValueError, RuntimeError) as e:
print
(f
"Warning: Invalid initial keyboard hotkey '{self.keyboard_hotkey_str.get()}': {e}"
)
self
.keyboard_hotkey_str.
set
("")
self
.parsed_keyboard_hotkey
=
set
()
def
setup_gui(
self
):
main_frame
=
ttk.Frame(
self
.root, padding
=
"5"
)
main_frame.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
)
self
.root.columnconfigure(
0
, weight
=
1
)
self
.root.rowconfigure(
0
, weight
=
1
)
notebook
=
ttk.Notebook(main_frame)
notebook.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
, pady
=
3
)
main_frame.columnconfigure(
0
, weight
=
1
)
main_frame.rowconfigure(
0
, weight
=
1
)
mouse_tab
=
ttk.Frame(notebook, padding
=
"5"
)
keyboard_tab
=
ttk.Frame(notebook, padding
=
"5"
)
mouse_tab.columnconfigure(
1
, weight
=
1
)
keyboard_tab.columnconfigure(
0
, weight
=
1
)
keyboard_tab.rowconfigure(
1
, weight
=
1
)
notebook.add(mouse_tab, text
=
'鼠标连点'
)
notebook.add(keyboard_tab, text
=
'键盘自动化'
)
self
.root.minsize(
400
,
300
)
self
.setup_mouse_tab(mouse_tab)
self
.setup_keyboard_tab(keyboard_tab)
self
.status_label
=
ttk.Label(main_frame, textvariable
=
self
.status_text, relief
=
tk.SUNKEN, anchor
=
tk.W)
self
.status_label.grid(row
=
1
, column
=
0
, sticky
=
"ew"
, pady
=
(
3
,
0
))
def
setup_mouse_tab(
self
, tab: ttk.Frame):
settings_frame
=
ttk.Frame(tab)
settings_frame.grid(row
=
0
, column
=
0
, columnspan
=
2
, sticky
=
"ew"
, pady
=
3
)
settings_frame.columnconfigure(
1
, weight
=
1
)
interval_frame
=
ttk.LabelFrame(settings_frame, text
=
"鼠标连点设置"
, padding
=
"3"
)
interval_frame.pack(side
=
tk.LEFT, padx
=
(
0
,
3
), fill
=
tk.X, expand
=
True
)
ttk.Label(interval_frame, text
=
"间隔:"
).grid(row
=
0
, column
=
0
, sticky
=
tk.W, padx
=
1
)
vcmd_interval
=
(
self
.root.register(
self
.validate_interval),
'%P'
)
self
.interval_entry
=
ttk.Entry(interval_frame, textvariable
=
self
.click_interval, width
=
5
, validate
=
"key"
, validatecommand
=
vcmd_interval)
self
.interval_entry.grid(row
=
0
, column
=
1
, sticky
=
tk.W, padx
=
1
)
ttk.Label(interval_frame, text
=
"秒 (≥0.01)"
).grid(row
=
0
, column
=
2
, sticky
=
tk.W, padx
=
1
)
click_type_frame
=
ttk.LabelFrame(settings_frame, text
=
"类型"
, padding
=
"3"
)
click_type_frame.pack(side
=
tk.LEFT, padx
=
(
3
,
0
), fill
=
tk.X, expand
=
True
)
self
.click_type_radios
=
[]
types
=
[(
"左键"
,
"left"
), (
"中键"
,
"middle"
), (
"右键"
,
"right"
)]
for
txt, val
in
types:
rb
=
ttk.Radiobutton(click_type_frame, text
=
txt, variable
=
self
.selected_click_type, value
=
val)
rb.pack(side
=
tk.LEFT, padx
=
2
)
self
.click_type_radios.append(rb)
m_hotkey_frame
=
ttk.LabelFrame(tab, text
=
"鼠标连点 启动/停止 热键(支持组合键但部分无法识别,连点功能未做游戏界面内通用测试,请自行尝试,间隔较短时热键停止可能反应不灵敏并不是失效可重试)"
, padding
=
"5"
)
m_hotkey_frame.grid(row
=
1
, column
=
0
, columnspan
=
2
, sticky
=
"ew"
, pady
=
3
)
m_hotkey_frame.columnconfigure(
1
, weight
=
1
)
ttk.Label(m_hotkey_frame, text
=
"热键:"
).grid(row
=
0
, column
=
0
, sticky
=
tk.W, padx
=
3
, pady
=
1
)
self
.mouse_hotkey_entry
=
ttk.Entry(m_hotkey_frame, textvariable
=
self
.mouse_hotkey_str, width
=
20
)
self
.mouse_hotkey_entry.grid(row
=
0
, column
=
1
, sticky
=
"ew"
, padx
=
3
, pady
=
1
)
btn_frame_hk
=
ttk.Frame(m_hotkey_frame)
btn_frame_hk.grid(row
=
0
, column
=
2
, sticky
=
tk.E, padx
=
3
, pady
=
1
)
self
.mouse_hotkey_capture_button
=
ttk.Button(btn_frame_hk, text
=
"采集"
, command
=
lambda
:
self
.open_capture_dialog(
'mouse'
), width
=
5
)
self
.mouse_hotkey_capture_button.pack(side
=
tk.LEFT, padx
=
(
0
,
2
))
self
.mouse_hotkey_apply_button
=
ttk.Button(btn_frame_hk, text
=
"应用"
, command
=
lambda
:
self
.apply_hotkey(
'mouse'
), width
=
5
)
self
.mouse_hotkey_apply_button.pack(side
=
tk.LEFT)
side_btn_frame
=
ttk.Frame(m_hotkey_frame)
side_btn_frame.grid(row
=
1
, column
=
0
, columnspan
=
3
, sticky
=
tk.W, pady
=
(
3
,
0
))
ttk.Label(side_btn_frame, text
=
"快捷应用鼠标侧键:"
).pack(side
=
tk.LEFT, padx
=
3
)
if
'Button'
in
globals
()
and
hasattr
(Button,
'x1'
):
self
.mouse_side_button_x1
=
ttk.Button(side_btn_frame, text
=
"侧键1"
, command
=
lambda
:
self
.apply_specific_hotkey(
'mouse'
, Button.x1))
self
.mouse_side_button_x1.pack(side
=
tk.LEFT, padx
=
(
0
,
2
))
self
.mouse_side_button_x2
=
ttk.Button(side_btn_frame, text
=
"侧键2"
, command
=
lambda
:
self
.apply_specific_hotkey(
'mouse'
, Button.x2))
self
.mouse_side_button_x2.pack(side
=
tk.LEFT)
else
:
print
(
"Warning: pynput.mouse.Button not available for side button widgets."
)
ttk.Label(m_hotkey_frame, text
=
"格式: 'a', Key.f8, Button.x1, Key.ctrl_l + 'c' (建议用单一按键,部分组合键无法正常采集和识别。不会输入的点击采集键盘后按下需要的热键)"
, font
=
("",
8
)).grid(row
=
2
, column
=
0
, columnspan
=
3
, sticky
=
tk.W, padx
=
3
, pady
=
(
1
,
0
))
bottom_frame
=
ttk.Frame(tab)
bottom_frame.grid(row
=
2
, column
=
0
, columnspan
=
2
, sticky
=
"ew"
, pady
=
3
)
bottom_frame.columnconfigure(
0
, weight
=
1
)
m_duration_frame
=
ttk.LabelFrame(bottom_frame, text
=
"持续时间"
, padding
=
"3"
)
m_duration_frame.pack(side
=
tk.LEFT, padx
=
(
0
,
3
), fill
=
tk.X, expand
=
True
)
self
.mouse_duration_label
=
ttk.Label(m_duration_frame, text
=
"持续:"
)
self
.mouse_duration_label.grid(row
=
0
, column
=
0
, sticky
=
tk.W, padx
=
1
)
vcmd_pos_float
=
(
self
.root.register(
self
.validate_positive_float),
'%P'
)
self
.mouse_duration_entry
=
ttk.Entry(m_duration_frame, textvariable
=
self
.mouse_duration, width
=
5
, validate
=
"key"
, validatecommand
=
vcmd_pos_float)
self
.mouse_duration_entry.grid(row
=
0
, column
=
1
, sticky
=
tk.W, padx
=
1
)
self
.mouse_duration_unit_label
=
ttk.Label(m_duration_frame, text
=
"秒 (0=无限)"
)
self
.mouse_duration_unit_label.grid(row
=
0
, column
=
2
, sticky
=
tk.W, padx
=
1
)
m_control_frame
=
ttk.Frame(bottom_frame, padding
=
"3 0 0 0"
)
m_control_frame.pack(side
=
tk.LEFT, padx
=
(
3
,
0
))
self
.mouse_start_button
=
ttk.Button(m_control_frame, text
=
"启动"
, command
=
self
.toggle_clicking)
self
.mouse_start_button.pack(side
=
tk.LEFT, padx
=
(
0
,
3
))
self
.mouse_stop_button
=
ttk.Button(m_control_frame, text
=
"停止"
, command
=
self
.stop_clicking, state
=
tk.DISABLED)
self
.mouse_stop_button.pack(side
=
tk.LEFT)
def
setup_keyboard_tab(
self
, tab: ttk.Frame):
k_hotkey_frame
=
ttk.LabelFrame(tab, text
=
"键盘自动化 启动/停止 热键(支持组合键但部分无法识别,连点功能未做游戏界面内通用测试,请自行尝试,间隔较短时热键停止可能反应不灵敏并不是失效可重试)"
, padding
=
"5"
)
k_hotkey_frame.grid(row
=
0
, column
=
0
, sticky
=
"ew"
, pady
=
3
)
k_hotkey_frame.columnconfigure(
1
, weight
=
1
)
ttk.Label(k_hotkey_frame, text
=
"热键:"
).grid(row
=
0
, column
=
0
, sticky
=
tk.W, padx
=
3
, pady
=
1
)
self
.keyboard_hotkey_entry
=
ttk.Entry(k_hotkey_frame, textvariable
=
self
.keyboard_hotkey_str, width
=
20
)
self
.keyboard_hotkey_entry.grid(row
=
0
, column
=
1
, sticky
=
"ew"
, padx
=
3
, pady
=
1
)
k_btn_frame_hk
=
ttk.Frame(k_hotkey_frame)
k_btn_frame_hk.grid(row
=
0
, column
=
2
, sticky
=
tk.E, padx
=
3
, pady
=
1
)
self
.keyboard_hotkey_capture_button
=
ttk.Button(k_btn_frame_hk, text
=
"采集"
, command
=
lambda
:
self
.open_capture_dialog(
'keyboard'
), width
=
5
)
self
.keyboard_hotkey_capture_button.pack(side
=
tk.LEFT, padx
=
(
0
,
2
))
self
.keyboard_hotkey_apply_button
=
ttk.Button(k_btn_frame_hk, text
=
"应用"
, command
=
lambda
:
self
.apply_hotkey(
'keyboard'
), width
=
5
)
self
.keyboard_hotkey_apply_button.pack(side
=
tk.LEFT)
k_side_btn_frame
=
ttk.Frame(k_hotkey_frame)
k_side_btn_frame.grid(row
=
1
, column
=
0
, columnspan
=
3
, sticky
=
tk.W, pady
=
(
3
,
0
))
ttk.Label(k_side_btn_frame, text
=
"快捷应用侧键:"
).pack(side
=
tk.LEFT, padx
=
3
)
if
'Button'
in
globals
()
and
hasattr
(Button,
'x1'
):
self
.keyboard_side_button_x1
=
ttk.Button(k_side_btn_frame, text
=
"侧键1"
, command
=
lambda
:
self
.apply_specific_hotkey(
'keyboard'
, Button.x1))
self
.keyboard_side_button_x1.pack(side
=
tk.LEFT, padx
=
(
0
,
2
))
self
.keyboard_side_button_x2
=
ttk.Button(k_side_btn_frame, text
=
"侧键2"
, command
=
lambda
:
self
.apply_specific_hotkey(
'keyboard'
, Button.x2))
self
.keyboard_side_button_x2.pack(side
=
tk.LEFT)
else
:
print
(
"Warning: Mouse side buttons (X1/X2) not available for keyboard hotkey."
)
ttk.Label(k_hotkey_frame, text
=
"格式: 'a', Key.f8, Button.x1, Key.ctrl_l + 'c' (建议用单一按键,部分组合键无法正常采集和识别。不会输入的点击采集键盘后按下需要的热键)"
, font
=
("",
8
)).grid(row
=
2
, column
=
0
, columnspan
=
3
, sticky
=
tk.W, padx
=
3
, pady
=
(
1
,
0
))
self
.key_list_frame
=
ttk.LabelFrame(tab, text
=
"自动化按键列表 (可增减)"
, padding
=
"5"
)
self
.key_list_frame.grid(row
=
1
, column
=
0
, sticky
=
"nsew"
, pady
=
3
)
self
.key_list_frame.rowconfigure(
1
, weight
=
1
)
self
.key_list_frame.columnconfigure(
0
, weight
=
1
)
info_frame
=
ttk.Frame(
self
.key_list_frame)
info_frame.grid(row
=
0
, column
=
0
, columnspan
=
2
, sticky
=
"ew"
, pady
=
(
0
,
3
))
ttk.Label(info_frame, text
=
"添加要按下(循环/依次)的 单个 键盘/鼠标 按键:"
, font
=
("",
9
)).pack(side
=
tk.LEFT, padx
=
3
)
ttk.Label(info_frame, text
=
"格式: 'a', Key.enter, Button.left, Key.numpad1 (不支持组合键,num打开后小数字键盘无法识别)"
, font
=
("",
8
)).pack(side
=
tk.LEFT, padx
=
3
)
self
.keys_canvas
=
tk.Canvas(
self
.key_list_frame, borderwidth
=
0
, highlightthickness
=
0
)
keys_scrollbar
=
ttk.Scrollbar(
self
.key_list_frame, orient
=
"vertical"
, command
=
self
.keys_canvas.yview)
self
.scrollable_frame
=
ttk.Frame(
self
.keys_canvas)
self
.scrollable_frame.bind(
"<Configure>"
,
lambda
e:
self
.update_canvas_scrollregion())
self
.scrollable_frame.columnconfigure(
0
, weight
=
1
)
canvas_window
=
self
.keys_canvas.create_window((
0
,
0
), window
=
self
.scrollable_frame, anchor
=
"nw"
, tags
=
"frame"
)
def
_configure_frame_width(event):
if
self
.keys_canvas
and
self
.scrollable_frame:
self
.keys_canvas.itemconfig(canvas_window, width
=
event.width)
self
.keys_canvas.bind(
"<Configure>"
, _configure_frame_width, add
=
'+'
)
self
.keys_canvas.configure(yscrollcommand
=
keys_scrollbar.
set
)
self
.keys_canvas.grid(row
=
1
, column
=
0
, sticky
=
"nsew"
)
keys_scrollbar.grid(row
=
1
, column
=
1
, sticky
=
"ns"
)
def
_on_mousewheel(event):
if
not
self
.keys_canvas:
return
delta
=
0
local_system
=
self
.system_name
try
:
if
local_system
=
=
"Linux"
:
if
event.num
=
=
4
: delta
=
-
1
elif
event.num
=
=
5
: delta
=
1
elif
local_system
=
=
"Windows"
:
delta_val
=
getattr
(event,
'delta'
,
0
)
delta
=
-
1
if
delta_val >
0
else
(
1
if
delta_val <
0
else
0
)
elif
local_system
=
=
"Darwin"
:
delta_val
=
getattr
(event,
'delta'
,
0
)
delta
=
-
int
(delta_val)
if
delta_val !
=
0
else
0
else
:
if
hasattr
(event,
'delta'
)
and
event.delta !
=
0
:
delta
=
-
1
if
event.delta >
0
else
1
except
AttributeError as e:
print
(f
"Scroll event missing attrs: {e}"
); delta
=
0
except
Exception as e:
print
(f
"Error processing scroll delta: {e}"
); delta
=
0
if
delta !
=
0
:
try
:
self
.keys_canvas.yview_scroll(
int
(delta
*
1
),
"units"
)
except
tk.TclError:
pass
def
bind_recursive_scroll(widget):
if
self
.system_name
=
=
"Linux"
:
widget.bind(
"<Button-4>"
, _on_mousewheel, add
=
'+'
)
widget.bind(
"<Button-5>"
, _on_mousewheel, add
=
'+'
)
else
:
widget.bind(
"<MouseWheel>"
, _on_mousewheel, add
=
'+'
)
widget.bind(
"<Enter>"
,
lambda
e, w
=
widget: w.focus_set(), add
=
'+'
)
for
child
in
widget.winfo_children():
bind_recursive_scroll(child)
widgets_to_bind_scroll
=
[
self
.keys_canvas,
self
.scrollable_frame]
for
widget
in
widgets_to_bind_scroll:
if
widget:
bind_recursive_scroll(widget)
self
.root.after(
500
,
lambda
: bind_recursive_scroll(
self
.scrollable_frame)
if
self
.scrollable_frame
else
None
)
key_control_frame
=
ttk.Frame(
self
.key_list_frame)
key_control_frame.grid(row
=
2
, column
=
0
, columnspan
=
2
, sticky
=
"ew"
, pady
=
(
3
,
0
))
self
.add_key_button
=
ttk.Button(key_control_frame, text
=
"增加按键 (+)"
, command
=
self
.add_key_row)
self
.add_key_button.pack(side
=
tk.LEFT, padx
=
3
)
self
.remove_key_button
=
ttk.Button(key_control_frame, text
=
"移除末尾按键 (-)"
, command
=
self
.remove_key_row)
self
.remove_key_button.pack(side
=
tk.LEFT, padx
=
3
)
mode_frame
=
ttk.LabelFrame(tab, text
=
"执行方式 & 循环延迟"
, padding
=
"5"
)
mode_frame.grid(row
=
2
, column
=
0
, sticky
=
"ew"
, pady
=
3
)
mode_frame.columnconfigure(
1
, weight
=
0
)
self
.keyboard_mode_radios
=
[]
modes
=
[(
"依次按下"
,
"sequential"
), (
"同时按下"
,
"simultaneous"
)]
for
i, (txt, val)
in
enumerate
(modes):
rb
=
ttk.Radiobutton(mode_frame, text
=
txt, variable
=
self
.keyboard_mode, value
=
val, command
=
self
.update_ui_state)
rb.grid(row
=
0
, column
=
i, sticky
=
tk.W, padx
=
3
, pady
=
1
)
self
.keyboard_mode_radios.append(rb)
ttk.Label(mode_frame, text
=
"(同时按下: 如果设置间隔极短而且按键较多可能卡死或者导致无法识别到停止热键)"
, font
=
("",
8
)).grid(row
=
0
, column
=
len
(modes), sticky
=
tk.W, padx
=
5
)
self
.cycle_delay_label
=
ttk.Label(mode_frame, text
=
"循环间隔:"
)
self
.cycle_delay_label.grid(row
=
1
, column
=
0
, sticky
=
tk.W, padx
=
3
, pady
=
3
)
vcmd_delay
=
(
self
.root.register(
self
.validate_positive_float),
'%P'
)
self
.cycle_delay_entry
=
ttk.Entry(mode_frame, textvariable
=
self
.keyboard_cycle_delay, width
=
5
, validate
=
"key"
, validatecommand
=
vcmd_delay)
self
.cycle_delay_entry.grid(row
=
1
, column
=
1
, sticky
=
tk.W, padx
=
3
, pady
=
3
)
self
.cycle_delay_unit_label
=
ttk.Label(mode_frame, text
=
"秒 (≥0)"
)
self
.cycle_delay_unit_label.grid(row
=
1
, column
=
2
, sticky
=
tk.W, padx
=
3
, pady
=
3
, columnspan
=
len
(modes)
-
1
)
bottom_frame_kb
=
ttk.Frame(tab)
bottom_frame_kb.grid(row
=
3
, column
=
0
, sticky
=
"ew"
, pady
=
3
)
bottom_frame_kb.columnconfigure(
0
, weight
=
1
)
k_duration_frame
=
ttk.LabelFrame(bottom_frame_kb, text
=
"持续时间"
, padding
=
"3"
)
k_duration_frame.pack(side
=
tk.LEFT, padx
=
(
0
,
3
), fill
=
tk.X, expand
=
True
)
self
.keyboard_duration_label
=
ttk.Label(k_duration_frame, text
=
"持续:"
)
self
.keyboard_duration_label.grid(row
=
0
, column
=
0
, sticky
=
tk.W, padx
=
1
)
vcmd_pos_float_kb
=
(
self
.root.register(
self
.validate_positive_float),
'%P'
)
self
.keyboard_duration_entry
=
ttk.Entry(k_duration_frame, textvariable
=
self
.keyboard_duration, width
=
5
, validate
=
"key"
, validatecommand
=
vcmd_pos_float_kb)
self
.keyboard_duration_entry.grid(row
=
0
, column
=
1
, sticky
=
tk.W, padx
=
1
)
self
.keyboard_duration_unit_label
=
ttk.Label(k_duration_frame, text
=
"秒 (0=无限)"
)
self
.keyboard_duration_unit_label.grid(row
=
0
, column
=
2
, sticky
=
tk.W, padx
=
1
)
k_control_frame
=
ttk.Frame(bottom_frame_kb, padding
=
"3 0 0 0"
)
k_control_frame.pack(side
=
tk.LEFT, padx
=
(
3
,
0
))
self
.keyboard_start_button
=
ttk.Button(k_control_frame, text
=
"启动"
, command
=
self
.toggle_keyboard_automation)
self
.keyboard_start_button.pack(side
=
tk.LEFT, padx
=
(
0
,
3
))
self
.keyboard_stop_button
=
ttk.Button(k_control_frame, text
=
"停止"
, command
=
self
.stop_keyboard_automation, state
=
tk.DISABLED)
self
.keyboard_stop_button.pack(side
=
tk.LEFT)
def
_clear_key_rows(
self
):
print
(
"Clearing dynamic key rows..."
)
if
not
self
.scrollable_frame
or
not
hasattr
(
self
,
'key_row_frames'
):
return
widgets_to_destroy
=
list
(
self
.key_row_frames)
for
frame
in
widgets_to_destroy:
if
frame
and
frame.winfo_exists():
try
: frame.destroy()
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Unexpected error destroying frame: {e}"
)
self
.key_row_frames.clear()
self
.keyboard_target_keys.clear()
self
.keyboard_target_delays.clear()
self
.key_entries.clear()
self
.key_capture_buttons.clear()
self
.key_apply_buttons.clear()
self
.key_delay_entries.clear()
self
.key_delay_labels.clear()
self
.current_key_row_count
=
0
self
.valid_target_key_objects
=
[]
self
.update_add_remove_button_state()
self
.root.after(
20
,
self
.update_canvas_scrollregion)
def
add_key_row(
self
, key_value:
str
=
"", delay_value:
float
=
0.0
, update_scroll:
bool
=
True
):
if
self
.current_key_row_count >
=
MAX_AUTOMATION_KEYS:
messagebox.showwarning(
"达到上限"
, f
"最多只能添加 {MAX_AUTOMATION_KEYS} 个自动化按键。"
, parent
=
self
.root)
return
index
=
self
.current_key_row_count
if
not
self
.scrollable_frame:
print
(
"Error: scrollable_frame not initialized, cannot add key row."
)
return
row_frame
=
ttk.Frame(
self
.scrollable_frame)
row_frame.grid(row
=
index, column
=
0
, sticky
=
"ew"
, pady
=
1
, padx
=
1
)
row_frame.columnconfigure(
1
, weight
=
1
)
key_var
=
tk.StringVar(value
=
key_value)
delay_var
=
tk.DoubleVar(value
=
delay_value)
self
.keyboard_target_keys.append(key_var)
self
.keyboard_target_delays.append(delay_var)
ttk.Label(row_frame, text
=
f
"按键 {index + 1}:"
, width
=
7
).grid(row
=
0
, column
=
0
, padx
=
(
3
,
0
), sticky
=
tk.W)
entry
=
ttk.Entry(row_frame, textvariable
=
key_var, width
=
15
)
entry.grid(row
=
0
, column
=
1
, padx
=
3
, sticky
=
"ew"
)
self
.key_entries.append(entry)
btn_sub_frame
=
ttk.Frame(row_frame)
btn_sub_frame.grid(row
=
0
, column
=
2
, padx
=
0
, sticky
=
tk.W)
capture_btn
=
ttk.Button(btn_sub_frame, text
=
"采集"
, width
=
5
, command
=
lambda
idx
=
index:
self
.open_capture_dialog(
'target_key'
, index
=
idx))
capture_btn.pack(side
=
tk.LEFT, padx
=
(
0
,
2
))
self
.key_capture_buttons.append(capture_btn)
apply_btn
=
ttk.Button(btn_sub_frame, text
=
"应用"
, width
=
5
, command
=
lambda
idx
=
index:
self
._apply_target_key(idx))
apply_btn.pack(side
=
tk.LEFT)
self
.key_apply_buttons.append(apply_btn)
vcmd_pos_float
=
(
self
.root.register(
self
.validate_positive_float),
'%P'
)
delay_entry
=
ttk.Entry(row_frame, textvariable
=
delay_var, width
=
4
, validate
=
"key"
, validatecommand
=
vcmd_pos_float)
delay_entry.grid(row
=
0
, column
=
3
, padx
=
(
5
,
0
), sticky
=
tk.W)
self
.key_delay_entries.append(delay_entry)
delay_label
=
ttk.Label(row_frame, text
=
"秒后置"
)
delay_label.grid(row
=
0
, column
=
4
, padx
=
(
1
,
3
), sticky
=
tk.W)
self
.key_delay_labels.append(delay_label)
self
.key_row_frames.append(row_frame)
self
.current_key_row_count
+
=
1
self
.update_add_remove_button_state()
self
.update_ui_state()
if
update_scroll
and
self
.keys_canvas:
self
.keys_canvas.after(
20
,
self
.update_canvas_scrollregion)
self
.keys_canvas.after(
60
,
lambda
:
self
.keys_canvas.yview_moveto(
1.0
)
if
self
.keys_canvas
else
None
)
def
remove_key_row(
self
):
if
self
.current_key_row_count <
=
0
:
return
if
not
self
.scrollable_frame:
return
index_to_remove
=
self
.current_key_row_count
-
1
if
index_to_remove >
=
len
(
self
.key_row_frames):
print
(f
"Error: Row index {index_to_remove} out of sync. Adjusting."
)
self
.current_key_row_count
=
len
(
self
.key_row_frames)
if
self
.current_key_row_count <
=
0
:
return
index_to_remove
=
self
.current_key_row_count
-
1
try
:
if
index_to_remove <
len
(
self
.key_row_frames)
and
self
.key_row_frames[index_to_remove].winfo_exists():
self
.key_row_frames[index_to_remove].destroy()
except
Exception as e:
print
(f
"Warning: Error destroying key row frame {index_to_remove}: {e}"
)
def
safe_pop(lst, index, name):
if
index <
len
(lst):
return
lst.pop(index)
print
(f
"Error: Index {index} out of bounds for '{name}' during pop."
)
return
None
safe_pop(
self
.key_row_frames, index_to_remove,
"frames"
)
safe_pop(
self
.keyboard_target_keys, index_to_remove,
"keys"
)
safe_pop(
self
.keyboard_target_delays, index_to_remove,
"delays"
)
safe_pop(
self
.key_entries, index_to_remove,
"entries"
)
safe_pop(
self
.key_capture_buttons, index_to_remove,
"cap_btns"
)
safe_pop(
self
.key_apply_buttons, index_to_remove,
"app_btns"
)
safe_pop(
self
.key_delay_entries, index_to_remove,
"delay_entries"
)
safe_pop(
self
.key_delay_labels, index_to_remove,
"delay_labels"
)
self
.current_key_row_count
-
=
1
if
self
.current_key_row_count <
0
:
self
.current_key_row_count
=
0
self
.update_add_remove_button_state()
self
.update_ui_state()
if
self
.keys_canvas:
self
.keys_canvas.after(
20
,
self
.update_canvas_scrollregion)
def
update_canvas_scrollregion(
self
):
if
self
.keys_canvas
and
self
.scrollable_frame
and
self
.keys_canvas.winfo_exists()
and
self
.scrollable_frame.winfo_exists():
try
:
self
.scrollable_frame.update_idletasks()
self
.keys_canvas.update_idletasks()
bbox
=
self
.keys_canvas.bbox(
"all"
)
required_height
=
self
.scrollable_frame.winfo_reqheight()
required_width
=
self
.scrollable_frame.winfo_reqwidth()
canvas_width
=
self
.keys_canvas.winfo_width()
scroll_bbox
=
(
0
,
0
,
max
(required_width, canvas_width), required_height)
if
required_height >
1
or
required_width >
1
:
self
.keys_canvas.configure(scrollregion
=
scroll_bbox)
else
:
self
.keys_canvas.configure(scrollregion
=
(
0
,
0
,
1
,
1
))
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Unexpected error updating scrollregion: {e}"
)
def
update_add_remove_button_state(
self
):
is_busy
=
self
.is_running_mouse
or
self
.is_running_keyboard
or
self
.is_capturing
base_state
=
tk.DISABLED
if
is_busy
else
tk.NORMAL
def
safe_config(widget: Optional[ttk.Button], state:
str
):
if
widget
and
widget.winfo_exists():
try
: widget.config(state
=
state)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Error configuring button state: {e}"
)
add_state
=
tk.DISABLED
if
base_state
=
=
tk.DISABLED
or
self
.current_key_row_count >
=
MAX_AUTOMATION_KEYS
else
tk.NORMAL
safe_config(
self
.add_key_button, add_state)
remove_state
=
tk.DISABLED
if
base_state
=
=
tk.DISABLED
or
self
.current_key_row_count <
=
0
else
tk.NORMAL
safe_config(
self
.remove_key_button, remove_state)
def
update_ui_state(
self
):
is_mouse_running
=
self
.is_running_mouse
is_keyboard_running
=
self
.is_running_keyboard
is_either_running
=
is_mouse_running
or
is_keyboard_running
is_capturing_active
=
self
.is_capturing
general_state
=
tk.DISABLED
if
(is_either_running
or
is_capturing_active)
else
tk.NORMAL
start_button_state
=
tk.DISABLED
if
is_either_running
or
is_capturing_active
else
tk.NORMAL
entry_state
=
'readonly'
if
general_state
=
=
tk.DISABLED
else
tk.NORMAL
def
safe_config_state(widget: Optional[tk.Widget], state:
str
):
if
widget
and
widget.winfo_exists():
try
:
effective_state
=
state
if
isinstance
(widget, (ttk.Entry, tk.Entry))
and
state
=
=
tk.DISABLED:
effective_state
=
'readonly'
widget.config(state
=
effective_state)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Error in safe_config_state for {widget}: {e}"
)
safe_config_state(
self
.interval_entry, entry_state)
for
radio
in
self
.click_type_radios: safe_config_state(radio, general_state)
safe_config_state(
self
.mouse_hotkey_entry, entry_state)
safe_config_state(
self
.mouse_hotkey_capture_button, general_state)
safe_config_state(
self
.mouse_hotkey_apply_button, general_state)
safe_config_state(
self
.mouse_side_button_x1, general_state)
safe_config_state(
self
.mouse_side_button_x2, general_state)
safe_config_state(
self
.mouse_duration_label, general_state)
safe_config_state(
self
.mouse_duration_entry, entry_state)
safe_config_state(
self
.mouse_duration_unit_label, general_state)
safe_config_state(
self
.mouse_start_button, start_button_state
if
not
is_mouse_running
else
tk.DISABLED)
safe_config_state(
self
.mouse_stop_button, tk.NORMAL
if
is_mouse_running
else
tk.DISABLED)
safe_config_state(
self
.keyboard_hotkey_entry, entry_state)
safe_config_state(
self
.keyboard_hotkey_capture_button, general_state)
safe_config_state(
self
.keyboard_hotkey_apply_button, general_state)
safe_config_state(
self
.keyboard_side_button_x1, general_state)
safe_config_state(
self
.keyboard_side_button_x2, general_state)
for
entry
in
self
.key_entries: safe_config_state(entry, entry_state)
for
btn
in
self
.key_capture_buttons: safe_config_state(btn, general_state)
for
btn
in
self
.key_apply_buttons: safe_config_state(btn, general_state)
for
radio
in
self
.keyboard_mode_radios: safe_config_state(radio, general_state)
safe_config_state(
self
.cycle_delay_label, general_state)
safe_config_state(
self
.cycle_delay_entry, entry_state)
safe_config_state(
self
.cycle_delay_unit_label, general_state)
is_sequential
=
self
.keyboard_mode.get()
=
=
'sequential'
post_delay_state
=
tk.NORMAL
if
(general_state
=
=
tk.NORMAL
and
is_sequential)
else
tk.DISABLED
post_delay_entry_state
=
'readonly'
if
post_delay_state
=
=
tk.DISABLED
else
tk.NORMAL
for
entry
in
self
.key_delay_entries: safe_config_state(entry, post_delay_entry_state)
for
label
in
self
.key_delay_labels: safe_config_state(label, post_delay_state)
self
.update_add_remove_button_state()
safe_config_state(
self
.keyboard_duration_label, general_state)
safe_config_state(
self
.keyboard_duration_entry, entry_state)
safe_config_state(
self
.keyboard_duration_unit_label, general_state)
safe_config_state(
self
.keyboard_start_button, start_button_state
if
not
is_keyboard_running
else
tk.DISABLED)
safe_config_state(
self
.keyboard_stop_button, tk.NORMAL
if
is_keyboard_running
else
tk.DISABLED)
status_msg
=
"状态: 已停止"
if
is_mouse_running: status_msg
=
"状态: 鼠标连点运行中..."
elif
is_keyboard_running: status_msg
=
"状态: 键盘自动化运行中..."
elif
is_capturing_active: status_msg
=
"状态: 正在采集输入..."
if
self
.status_label
and
self
.status_label.winfo_exists():
try
:
if
self
.status_text.get() !
=
status_msg:
self
.status_text.
set
(status_msg)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Error setting status text: {e}"
)
def
validate_interval(
self
, P:
str
)
-
>
bool
:
if
P
=
=
"
" or P == "
.
" or P == "
-
":
return
True
if
P.startswith(
"0."
)
and
len
(P) <
=
3
:
return
True
if
P.startswith(
"-"
):
if
P
=
=
"-."
or
P
=
=
"-"
:
return
True
try
:
float
(P);
return
True
except
ValueError:
return
False
try
:
float
(P);
return
True
except
ValueError:
return
False
def
validate_positive_float(
self
, P:
str
)
-
>
bool
:
if
P
=
=
"
" or P == "
.
" or P == "
-
":
return
True
if
P
=
=
"0."
:
return
True
if
P.startswith(
"-"
):
if
P
=
=
"-."
or
P
=
=
"-"
:
return
True
try
:
float
(P);
return
True
except
ValueError:
return
False
try
:
float
(P);
return
True
except
ValueError:
return
False
def
toggle_clicking(
self
):
if
self
.is_running_keyboard:
messagebox.showwarning(
"操作冲突"
,
"请先停止键盘自动化。"
, parent
=
self
.root)
return
if
self
.is_running_mouse:
self
.stop_clicking()
else
:
if
self
._validate_interval_value()
and
self
._validate_duration_value(
'mouse'
):
self
.start_clicking()
def
_validate_interval_value(
self
)
-
>
bool
:
entry_widget
=
self
.interval_entry
error_bg, default_bg
=
'#FFCCCC'
,
'SystemWindow'
try
:
interval
=
self
.click_interval.get()
if
interval <
0.01
:
messagebox.showerror(
"输入错误"
,
"鼠标间隔时间不能低于 0.01 秒。"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
default_bg)
return
True
except
(tk.TclError, ValueError):
messagebox.showerror(
"输入错误"
,
"鼠标间隔时间必须是有效的数字 (≥ 0.01)。"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
except
Exception as e:
messagebox.showerror(
"验证错误"
, f
"验证鼠标间隔时出错: {e}"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
def
_validate_duration_value(
self
, op_type:
str
)
-
>
bool
:
duration_var
=
self
.mouse_duration
if
op_type
=
=
'mouse'
else
self
.keyboard_duration
entry_widget
=
self
.mouse_duration_entry
if
op_type
=
=
'mouse'
else
self
.keyboard_duration_entry
error_bg, default_bg
=
'#FFCCCC'
,
'SystemWindow'
try
:
duration
=
duration_var.get()
if
duration <
0.0
:
messagebox.showerror(
"输入错误"
,
"持续时间不能为负数。"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
default_bg)
return
True
except
(tk.TclError, ValueError):
messagebox.showerror(
"输入错误"
,
"持续时间必须是有效的非负数字。"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
except
Exception as e:
messagebox.showerror(
"验证错误"
, f
"验证持续时间时出错: {e}"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
def
start_clicking(
self
):
if
self
.is_running_mouse
or
self
.is_running_keyboard:
return
global
mouse_controller
if
mouse_controller
is
None
:
messagebox.showerror(
"错误"
,
"鼠标控制器未初始化。"
, parent
=
self
.root)
return
duration
=
self
.mouse_duration.get()
print
(
"Starting mouse clicking..."
)
self
.is_running_mouse
=
True
self
.mouse_stop_event.clear()
self
.window_hidden_by_automation
=
False
if
self
.tray_icon
and
self
.tray_thread
and
self
.tray_thread.is_alive():
print
(
"Mouse clicking started, hiding window to tray."
)
self
.hide_window()
self
.window_hidden_by_automation
=
True
if
self
.mouse_duration_after_id:
try
:
self
.root.after_cancel(
self
.mouse_duration_after_id)
except
(ValueError, tk.TclError):
pass
self
.mouse_duration_after_id
=
None
if
duration >
0
:
ms
=
int
(duration
*
1000
)
print
(f
"Scheduling mouse stop in {duration}s."
)
self
.mouse_duration_after_id
=
self
.root.after(ms,
self
.stop_clicking)
self
.update_ui_state()
self
.mouse_click_thread
=
threading.Thread(target
=
self
.click_worker, daemon
=
True
, name
=
"MouseClickThread"
)
self
.mouse_click_thread.start()
def
stop_clicking(
self
):
if
not
self
.is_running_mouse:
return
print
(
"Stopping mouse clicking..."
)
self
.is_running_mouse
=
False
if
self
.mouse_duration_after_id:
try
:
self
.root.after_cancel(
self
.mouse_duration_after_id)
except
(ValueError, tk.TclError):
pass
self
.mouse_duration_after_id
=
None
self
.mouse_stop_event.
set
()
if
self
.mouse_click_thread
is
not
None
:
self
.mouse_click_thread.join(timeout
=
0.5
)
if
self
.mouse_click_thread.is_alive():
print
(
"Warning: Mouse click thread join timed out."
)
self
.mouse_click_thread
=
None
should_restore
=
self
.window_hidden_by_automation
self
.window_hidden_by_automation
=
False
def
final_stop_actions_mouse():
if
self
.root.winfo_exists():
if
should_restore:
print
(
"Mouse clicking stopped, restoring window."
)
self
.show_window()
self
.update_ui_state()
try
:
if
self
.root.winfo_exists():
self
.root.after(
10
, final_stop_actions_mouse)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Error scheduling final mouse stop actions: {e}"
)
def
click_worker(
self
):
global
mouse_controller, Button
if
mouse_controller
is
None
or
'Button'
not
in
globals
()
or
not
hasattr
(Button,
'left'
):
print
(
"Error (click_worker): Mouse controller or Button class unavailable."
)
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
,
self
.stop_clicking)
except
tk.TclError:
pass
self
.is_running_mouse
=
False
return
try
:
interval
=
self
.click_interval.get()
click_type_str
=
self
.selected_click_type.get()
button_to_click
=
getattr
(Button, click_type_str,
None
)
if
button_to_click
is
None
:
raise
ValueError(f
"Invalid click type: {click_type_str}"
)
if
interval <
0.01
:
raise
ValueError(
"Interval too low"
)
print
(f
"Mouse Click worker started: Type={click_type_str}, Interval={interval:.4f}s"
)
while
not
self
.mouse_stop_event.is_set():
loop_start_time
=
time.perf_counter()
try
:
mouse_controller.click(button_to_click,
1
)
except
Exception as click_err:
print
(f
"Error during mouse click: {click_err}"
)
pass
elapsed
=
time.perf_counter()
-
loop_start_time
sleep_time
=
interval
-
elapsed
if
sleep_time >
0
:
granularity
=
0.005
end_time
=
time.perf_counter()
+
sleep_time
while
time.perf_counter() < end_time:
if
self
.mouse_stop_event.wait(timeout
=
min
(granularity, end_time
-
time.perf_counter())):
break
if
self
.mouse_stop_event.is_set():
break
print
(
"Mouse Click worker finished."
)
except
(tk.TclError, ValueError, AttributeError, RuntimeError) as e:
print
(f
"Error in mouse worker setup or value: {e}. Stopping."
)
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
,
self
.stop_clicking)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Unexpected error in click_worker: {e}"
); traceback.print_exc()
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
,
self
.stop_clicking)
except
tk.TclError:
pass
finally
:
self
.is_running_mouse
=
False
def
toggle_keyboard_automation(
self
):
if
self
.is_running_mouse:
messagebox.showwarning(
"操作冲突"
,
"请先停止鼠标连点。"
, parent
=
self
.root)
return
if
self
.is_running_keyboard:
self
.stop_keyboard_automation()
else
:
if
(
self
._validate_keyboard_cycle_delay()
and
self
._validate_duration_value(
'keyboard'
)
and
self
._validate_keyboard_targets()):
self
.start_keyboard_automation()
def
_validate_keyboard_cycle_delay(
self
)
-
>
bool
:
entry_widget
=
self
.cycle_delay_entry
error_bg, default_bg
=
'#FFCCCC'
,
'SystemWindow'
try
:
delay
=
self
.keyboard_cycle_delay.get()
if
delay <
0.0
:
raise
ValueError(
"Negative delay"
)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
default_bg)
return
True
except
(tk.TclError, ValueError):
messagebox.showerror(
"输入错误"
,
"键盘自动化循环间隔必须是有效的非负数字。"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
except
Exception as e:
messagebox.showerror(
"验证错误"
, f
"验证键盘循环间隔时出错: {e}"
, parent
=
self
.root)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
False
def
_apply_target_key(
self
, index:
int
):
if
index >
=
self
.current_key_row_count
or
index <
0
:
return
key_var
=
self
.keyboard_target_keys[index]
delay_var
=
self
.keyboard_target_delays[index]
key_entry
=
self
.key_entries[index]
delay_entry
=
self
.key_delay_entries[index]
is_seq
=
self
.keyboard_mode.get()
=
=
'sequential'
error_bg, default_bg
=
'#FFCCCC'
,
'SystemWindow'
key_ok, delay_ok
=
True
,
True
parsed_key
=
None
key_str
=
key_var.get().strip()
try
:
if
delay_var.get() <
0.0
:
raise
ValueError(
"Negative"
)
if
delay_entry.winfo_exists(): delay_entry.config(background
=
default_bg)
except
(tk.TclError, ValueError):
delay_ok
=
False
if
key_str
and
is_seq
and
delay_entry.winfo_exists(): delay_entry.config(background
=
error_bg)
elif
delay_entry.winfo_exists(): delay_entry.config(background
=
default_bg)
if
not
key_str:
if
key_entry.winfo_exists(): key_entry.config(background
=
default_bg)
if
not
delay_ok
and
delay_entry.winfo_exists(): delay_entry.config(background
=
default_bg)
else
:
try
:
parsed_set
=
self
.parse_hotkey_str(key_str)
if
len
(parsed_set) !
=
1
:
raise
ValueError(
"目标必须是单个按键/按钮"
)
parsed_key
=
list
(parsed_set)[
0
]
if
key_entry.winfo_exists(): key_entry.config(background
=
default_bg)
except
(ValueError, RuntimeError) as e:
key_ok
=
False
messagebox.showerror(
"格式错误"
, f
"按键 {index + 1} ('{key_str}') 格式无效:\n{e}"
, parent
=
self
.root)
if
key_entry.winfo_exists(): key_entry.config(background
=
error_bg)
except
Exception as e:
key_ok
=
False
;
print
(f
"Unexpected parse error: {e}"
)
if
key_entry.winfo_exists(): key_entry.config(background
=
error_bg)
if
key_ok
and
delay_ok
and
parsed_key:
try
:
key_var.
set
(
self
.format_pynput_key(parsed_key))
print
(f
"Target key {index + 1} format applied."
)
except
Exception:
pass
elif
key_ok
and
not
key_str:
print
(f
"Target key {index + 1} cleared."
)
def
_validate_keyboard_targets(
self
)
-
>
bool
:
self
.valid_target_key_objects
=
[]
has_valid_key
=
False
all_rows_ok
=
True
first_error
=
""
is_seq
=
self
.keyboard_mode.get()
=
=
'sequential'
error_bg, default_bg
=
'#FFCCCC'
,
'SystemWindow'
print
(
"Validating keyboard targets..."
)
for
i
in
range
(
self
.current_key_row_count):
key_str
=
self
.keyboard_target_keys[i].get().strip()
key_entry
=
self
.key_entries[i]
delay_entry
=
self
.key_delay_entries[i]
key_ok, delay_ok
=
True
,
True
parsed_key
=
None
row_error
=
""
try
:
if
self
.keyboard_target_delays[i].get() <
0.0
:
raise
ValueError(
"Negative"
)
if
delay_entry.winfo_exists(): delay_entry.config(background
=
default_bg)
except
(tk.TclError, ValueError):
delay_ok
=
False
if
key_str
and
is_seq:
row_error
=
f
"按键 {i + 1}: 后置延迟无效"
; all_rows_ok
=
False
if
delay_entry.winfo_exists(): delay_entry.config(background
=
error_bg)
elif
delay_entry.winfo_exists(): delay_entry.config(background
=
default_bg)
if
not
key_str:
if
key_entry.winfo_exists(): key_entry.config(background
=
default_bg)
if
not
delay_ok
and
delay_entry.winfo_exists(): delay_entry.config(background
=
default_bg)
else
:
try
:
parsed_set
=
self
.parse_hotkey_str(key_str)
if
len
(parsed_set) !
=
1
:
raise
ValueError(
"目标必须是单个按键/按钮"
)
parsed_key
=
list
(parsed_set)[
0
]
has_valid_key
=
True
if
key_entry.winfo_exists(): key_entry.config(background
=
default_bg)
except
(ValueError, RuntimeError) as e:
key_ok
=
False
; all_rows_ok
=
False
; row_error
=
f
"按键 {i + 1} ('{key_str}') 格式错误: {e}"
if
key_entry.winfo_exists(): key_entry.config(background
=
error_bg)
if
not
delay_ok
and
is_seq
and
delay_entry.winfo_exists(): delay_entry.config(background
=
error_bg)
except
Exception as e:
key_ok
=
False
; all_rows_ok
=
False
; row_error
=
f
"按键 {i + 1} 解析错误: {e}"
if
key_entry.winfo_exists(): key_entry.config(background
=
error_bg)
if
row_error
and
not
first_error: first_error
=
row_error
if
parsed_key
and
key_ok
and
(delay_ok
or
not
is_seq):
self
.valid_target_key_objects.append((parsed_key,
self
.keyboard_target_delays[i].get()))
if
not
all_rows_ok:
msg
=
first_error
or
"自动化按键列表中存在无效设置"
messagebox.showerror(
"验证错误"
, f
"{msg} (已标红)。\n请修正后重试。"
, parent
=
self
.root)
return
False
if
not
has_valid_key:
messagebox.showerror(
"错误"
,
"请至少设置并应用一个有效的键盘自动化目标按键。"
, parent
=
self
.root)
return
False
print
(f
"Keyboard target validation passed. {len(self.valid_target_key_objects)} valid keys found."
)
return
True
def
start_keyboard_automation(
self
):
if
self
.is_running_keyboard
or
self
.is_running_mouse:
return
global
keyboard_controller, mouse_controller
if
keyboard_controller
is
None
or
mouse_controller
is
None
:
messagebox.showerror(
"错误"
,
"输入控制器未初始化。"
, parent
=
self
.root)
return
duration
=
self
.keyboard_duration.get()
if
not
self
.valid_target_key_objects:
messagebox.showerror(
"错误"
,
"没有有效的自动化按键可执行。"
, parent
=
self
.root)
return
print
(
"Starting keyboard automation..."
)
self
.is_running_keyboard
=
True
self
.keyboard_stop_event.clear()
self
.window_hidden_by_automation
=
False
if
self
.tray_icon
and
self
.tray_thread
and
self
.tray_thread.is_alive():
print
(
"Keyboard automation started, hiding window to tray."
)
self
.hide_window()
self
.window_hidden_by_automation
=
True
if
self
.keyboard_duration_after_id:
try
:
self
.root.after_cancel(
self
.keyboard_duration_after_id)
except
(ValueError, tk.TclError):
pass
self
.keyboard_duration_after_id
=
None
if
duration >
0
:
ms
=
int
(duration
*
1000
)
print
(f
"Scheduling keyboard stop in {duration}s."
)
self
.keyboard_duration_after_id
=
self
.root.after(ms,
self
.stop_keyboard_automation)
self
.update_ui_state()
self
.keyboard_thread
=
threading.Thread(target
=
self
.keyboard_worker, daemon
=
True
, name
=
"KeyboardAutomateThread"
)
self
.keyboard_thread.start()
def
stop_keyboard_automation(
self
):
if
not
self
.is_running_keyboard:
return
print
(
"Stopping keyboard automation..."
)
self
.is_running_keyboard
=
False
if
self
.keyboard_duration_after_id:
try
:
self
.root.after_cancel(
self
.keyboard_duration_after_id)
except
(ValueError, tk.TclError):
pass
self
.keyboard_duration_after_id
=
None
self
.keyboard_stop_event.
set
()
if
self
.keyboard_thread
is
not
None
:
self
.keyboard_thread.join(timeout
=
0.6
)
if
self
.keyboard_thread.is_alive():
print
(
"Warning: Keyboard automation thread join timed out."
)
self
.keyboard_thread
=
None
should_restore
=
self
.window_hidden_by_automation
self
.window_hidden_by_automation
=
False
def
final_stop_actions_keyboard():
if
self
.root.winfo_exists():
if
should_restore:
print
(
"Keyboard automation stopped, restoring window."
)
self
.show_window()
self
.update_ui_state()
try
:
if
self
.root.winfo_exists():
self
.root.after(
10
, final_stop_actions_keyboard)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Error scheduling final keyboard stop actions: {e}"
)
def
keyboard_worker(
self
):
global
keyboard_controller, mouse_controller, Button, Key, KeyCode
req_globals
=
[
'keyboard_controller'
,
'mouse_controller'
,
'Button'
,
'Key'
,
'KeyCode'
]
if
not
all
(c
in
globals
()
and
globals
()[c]
is
not
None
for
c
in
req_globals):
print
(
"Error (keyboard_worker): Input controllers/pynput classes unavailable."
)
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
,
self
.stop_keyboard_automation)
except
tk.TclError:
pass
self
.is_running_keyboard
=
False
return
try
:
cycle_interval
=
self
.keyboard_cycle_delay.get()
mode
=
self
.keyboard_mode.get()
target_keys_with_delays
=
list
(
self
.valid_target_key_objects)
if
not
target_keys_with_delays:
raise
ValueError(
"Worker started with no valid keys."
)
if
cycle_interval <
0.0
:
raise
ValueError(
"Negative cycle interval."
)
if
mode
not
in
[
'sequential'
,
'simultaneous'
]:
raise
ValueError(f
"Invalid mode: {mode}"
)
print
(f
"Keyboard worker started: Mode={mode}, Interval={cycle_interval:.4f}s"
)
while
not
self
.keyboard_stop_event.is_set():
cycle_start_time
=
time.perf_counter()
if
mode
=
=
'simultaneous'
:
pressed_keys_sim
=
[]
try
:
for
key, _
in
target_keys_with_delays:
if
self
.keyboard_stop_event.is_set():
break
if
key
is
None
:
continue
if
isinstance
(key, (Key, KeyCode)): keyboard_controller.press(key)
elif
isinstance
(key, Button): mouse_controller.press(key)
pressed_keys_sim.append(key)
if
self
.keyboard_stop_event.is_set():
break
if
self
.keyboard_stop_event.wait(
0.02
):
break
for
key
in
reversed
(pressed_keys_sim):
if
isinstance
(key, (Key, KeyCode)): keyboard_controller.release(key)
elif
isinstance
(key, Button): mouse_controller.release(key)
except
Exception as sim_err:
print
(f
"Error during simultaneous press/release: {sim_err}"
)
for
key
in
reversed
(pressed_keys_sim):
try
:
if
isinstance
(key, (Key, KeyCode)): keyboard_controller.release(key)
elif
isinstance
(key, Button): mouse_controller.release(key)
except
Exception:
pass
self
.keyboard_stop_event.
set
();
break
elif
mode
=
=
'sequential'
:
for
i, (key, specific_delay)
in
enumerate
(target_keys_with_delays):
if
self
.keyboard_stop_event.is_set():
break
if
key
is
None
:
continue
action_start_time
=
time.perf_counter()
try
:
if
isinstance
(key, (Key, KeyCode)): keyboard_controller.tap(key)
elif
isinstance
(key, Button): mouse_controller.click(key,
1
)
except
Exception as seq_err:
print
(f
"Error during sequential action (key: {key}): {seq_err}"
)
self
.keyboard_stop_event.
set
();
break
if
self
.keyboard_stop_event.is_set():
break
if
specific_delay >
0
:
action_elapsed
=
time.perf_counter()
-
action_start_time
actual_sleep
=
specific_delay
-
action_elapsed
if
actual_sleep >
0
:
granularity
=
0.005
end_time
=
time.perf_counter()
+
actual_sleep
while
time.perf_counter() < end_time:
if
self
.keyboard_stop_event.wait(timeout
=
min
(granularity, end_time
-
time.perf_counter())):
break
if
self
.keyboard_stop_event.is_set():
break
if
self
.keyboard_stop_event.is_set():
break
if
self
.keyboard_stop_event.is_set():
break
if
self
.keyboard_stop_event.is_set():
break
cycle_elapsed
=
time.perf_counter()
-
cycle_start_time
sleep_time
=
cycle_interval
-
cycle_elapsed
if
sleep_time >
0
:
granularity
=
0.005
end_time
=
time.perf_counter()
+
sleep_time
while
time.perf_counter() < end_time:
if
self
.keyboard_stop_event.wait(timeout
=
min
(granularity, end_time
-
time.perf_counter())):
break
if
self
.keyboard_stop_event.is_set():
break
print
(
"Keyboard worker finished."
)
except
(tk.TclError, ValueError, RuntimeError) as e:
print
(f
"Error in keyboard worker setup/value: {e}. Stopping."
)
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
,
self
.stop_keyboard_automation)
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Unexpected error in keyboard_worker: {e}"
); traceback.print_exc()
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
,
self
.stop_keyboard_automation)
except
tk.TclError:
pass
finally
:
self
.is_running_keyboard
=
False
def
format_pynput_key(
self
, key: Optional[PynputKeyType])
-
>
str
:
if
key
is
None
:
return
""
global
Key, KeyCode, Button
if
'Key'
not
in
globals
():
return
str
(key)
try
:
if
isinstance
(key, KeyCode):
char
=
getattr
(key,
'char'
,
None
)
if
char:
if
char
=
=
if
len
(char)
=
=
1
and
char.isprintable()
and
char
not
in
"+'\""
:
return
f
"'{char}'"
vk
=
getattr
(key,
'vk'
,
None
)
return
f
"VK({vk})"
if
vk
else
str
(key)
elif
isinstance
(key, Key):
return
f
"Key.{key.name}"
elif
isinstance
(key, Button):
return
f
"Button.{key.name}"
else
:
return
str
(key)
except
Exception as e:
print
(f
"Error formatting key {key}: {e}"
);
return
str
(key)
def
parse_hotkey_str(
self
, hotkey_string:
str
)
-
>
Set
[PynputKeyType]:
global
Key, KeyCode, Button
if
'Key'
not
in
globals
():
raise
RuntimeError(
"Pynput classes unavailable for parsing."
)
if
not
isinstance
(hotkey_string,
str
):
raise
ValueError(
"Input must be string"
)
hotkey_string
=
hotkey_string.strip()
if
not
hotkey_string:
return
set
()
keys:
Set
[PynputKeyType]
=
set
()
processed_string
=
'+'
.join(part.strip()
for
part
in
hotkey_string.split(
'+'
))
parts
=
[p
for
p
in
processed_string.split(
'+'
)
if
p]
if
not
parts:
raise
ValueError(
"Invalid format (empty parts)"
)
for
part
in
parts:
part_lower
=
part.lower()
found
=
False
if
part_lower.startswith(
'button.'
):
name
=
part.split(
'.'
)[
-
1
].lower()
match
=
next
((m
for
n, m
in
Button.__members__.items()
if
n.lower()
=
=
name),
None
)
if
match: keys.add(match); found
=
True
else
:
raise
ValueError(f
"Unknown mouse button: '{part}'"
)
elif
part_lower.startswith(
'key.'
):
name
=
part.split(
'.'
)[
-
1
].lower()
match
=
next
((m
for
n, m
in
Key.__members__.items()
if
n.lower()
=
=
name),
None
)
if
match: keys.add(match); found
=
True
else
:
raise
ValueError(f
"Unknown special key: '{part}'"
)
elif
len
(part) >
=
2
and
part[
0
]
=
=
part[
-
1
]
and
part[
0
]
in
(
)); found
=
True
elif
part
=
=
"'\"'"
: keys.add(KeyCode.from_char(
'"'
)); found
=
True
else
:
raise
ValueError(f
"Quoted content must be single char: '{part}'"
)
elif
len
(part)
=
=
1
:
if
part
=
=
'+'
:
raise
ValueError(
"Use Key.add for '+' key"
)
try
: keys.add(KeyCode.from_char(part)); found
=
True
except
(ValueError, TypeError) as e:
raise
ValueError(f
"Cannot parse char '{part}': {e}"
)
if
not
found:
if
part_lower
in
[
'ctrl'
,
'alt'
,
'shift'
,
'cmd'
,
'win'
,
'command'
,
'option'
]:
raise
ValueError(f
"Use Key.{part_lower}_l/r for modifiers"
)
raise
ValueError(f
"Cannot parse hotkey part: '{part}'"
)
if
not
keys:
raise
ValueError(
"No valid keys parsed"
)
return
keys
def
format_hotkey_set(
self
, hotkey_set: Optional[
Set
[PynputKeyType]])
-
>
str
:
""
"Formats a set of pynput keys into a sorted, readable string."
""
if
not
hotkey_set:
return
""
keys_list
=
list
(hotkey_set)
def
sort_key(k: PynputKeyType)
-
>
Tuple
[
int
,
str
]:
""
"Sorts keys: Modifiers > Mouse > Numpad/Ops > Special > Chars."
""
sort_order
=
9
; name
=
str
(k).lower()
if
isinstance
(k, Key):
n
=
k.name.lower(); base
=
n.replace(
'_l'
, '
').replace('
_r
', '
')
name
=
n
if
base
in
[
'ctrl'
,
'shift'
,
'alt'
,
'cmd'
,
'win'
,
'option'
]: sort_order
=
0
elif
n.startswith(
"numpad"
)
or
n
in
[
"add"
,
"subtract"
,
"multiply"
,
"divide"
,
"decimal"
,
"separator"
]: sort_order
=
2
elif
n.startswith(
"f"
)
and
n[
1
:].isdigit(): sort_order
=
3
else
: sort_order
=
4
elif
isinstance
(k, Button): name
=
k.name.lower(); sort_order
=
1
elif
isinstance
(k, KeyCode):
char
=
getattr
(k,
'char'
,
None
)
if
char
and
len
(char)
=
=
1
and
char.isprintable(): name
=
char.lower(); sort_order
=
5
else
: vk
=
getattr
(k,
'vk'
,
None
); name
=
f
"vk_{vk}"
if
vk
else
name; sort_order
=
4
return
(sort_order, name)
try
:
sorted_keys
=
sorted
(keys_list, key
=
sort_key)
return
" + "
.join([
self
.format_pynput_key(k)
for
k
in
sorted_keys])
except
Exception as e:
print
(f
"Error formatting hotkey set {hotkey_set}: {e}"
)
return
" + "
.join([
self
.format_pynput_key(k)
for
k
in
keys_list])
def
_get_canonical_key(
self
, key: Optional[PynputKeyType])
-
> Optional[PynputKeyType]:
""
"Returns canonical key for matching (e.g., maps ctrl_l/r to generic ctrl)."
""
if
key
is
None
:
return
None
global
Key, KeyCode, Button
if
'Key'
not
in
globals
():
return
key
if
isinstance
(key, Key):
name
=
key.name.lower()
if
name
in
(
'ctrl_l'
,
'ctrl_r'
):
return
getattr
(Key,
'ctrl'
, key)
if
name
in
(
'shift_l'
,
'shift_r'
):
return
getattr
(Key,
'shift'
, key)
if
name
in
(
'alt_l'
,
'alt_r'
,
'alt_gr'
):
return
getattr
(Key,
'alt'
, key)
if
name
in
(
'cmd_l'
,
'cmd_r'
,
'win_l'
,
'win_r'
):
return
getattr
(Key,
'cmd'
, key)
return
key
def
apply_hotkey(
self
, hotkey_type:
str
):
""
"Parses hotkey string, validates for conflicts, updates state, restarts listener."
""
if
hotkey_type
not
in
(
'mouse'
,
'keyboard'
):
return
hotkey_var
=
self
.mouse_hotkey_str
if
hotkey_type
=
=
'mouse'
else
self
.keyboard_hotkey_str
entry_widget
=
self
.mouse_hotkey_entry
if
hotkey_type
=
=
'mouse'
else
self
.keyboard_hotkey_entry
error_bg, default_bg
=
'#FFCCCC'
,
'SystemWindow'
new_hotkey_str
=
hotkey_var.get().strip()
current_parsed
=
self
.parsed_mouse_hotkey
if
hotkey_type
=
=
'mouse'
else
self
.parsed_keyboard_hotkey
other_parsed
=
self
.parsed_keyboard_hotkey
if
hotkey_type
=
=
'mouse'
else
self
.parsed_mouse_hotkey
other_type_str
=
"键盘自动化"
if
hotkey_type
=
=
'mouse'
else
"鼠标连点"
parsed_set:
Set
[PynputKeyType]
=
set
()
try
:
parsed_set
=
self
.parse_hotkey_str(new_hotkey_str)
new_canon
=
{
self
._get_canonical_key(k)
for
k
in
parsed_set
if
k}
other_canon
=
{
self
._get_canonical_key(k)
for
k
in
(other_parsed
or
set
())
if
k}
if
new_canon
and
other_canon
and
new_canon
=
=
other_canon:
conflict_str
=
self
.format_hotkey_set(parsed_set)
messagebox.showerror(
"热键冲突"
, f
"热键 '{conflict_str}' 已被 '{other_type_str}' 使用。"
, parent
=
self
.root)
hotkey_var.
set
(
self
.format_hotkey_set(current_parsed))
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
return
formatted_str
=
self
.format_hotkey_set(parsed_set)
hotkey_var.
set
(formatted_str)
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
default_bg)
print
(
"Stopping listener to apply hotkey..."
)
self
.stop_hotkey_listener(wait
=
True
)
applied_str
=
formatted_str
or
'无'
if
hotkey_type
=
=
'mouse'
:
self
.parsed_mouse_hotkey
=
parsed_set;
print
(f
"Mouse Hotkey: {applied_str}"
)
else
:
self
.parsed_keyboard_hotkey
=
parsed_set;
print
(f
"Keyboard Hotkey: {applied_str}"
)
print
(
"Restarting listener..."
)
self
.root.after(
150
,
self
.start_hotkey_listener)
except
(ValueError, RuntimeError) as e:
messagebox.showerror(
"热键格式错误"
, f
"无法应用热键 '{new_hotkey_str}':\n{e}"
, parent
=
self
.root)
hotkey_var.
set
(
self
.format_hotkey_set(current_parsed))
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
self
.root.after(
150
,
self
.start_hotkey_listener)
except
Exception as e:
messagebox.showerror(
"应用错误"
, f
"应用热键时发生错误: {e}"
, parent
=
self
.root); traceback.print_exc()
hotkey_var.
set
(
self
.format_hotkey_set(current_parsed));
if
entry_widget
and
entry_widget.winfo_exists(): entry_widget.config(background
=
error_bg)
self
.root.after(
150
,
self
.start_hotkey_listener)
def
apply_specific_hotkey(
self
, hotkey_type:
str
, key_object: PynputKeyType):
""
"Applies a specific pynput key object (e.g., side button) as a hotkey."
""
if
key_object
is
None
:
return
try
:
hotkey_str
=
self
.format_pynput_key(key_object)
hotkey_var
=
self
.mouse_hotkey_str
if
hotkey_type
=
=
'mouse'
else
self
.keyboard_hotkey_str
hotkey_var.
set
(hotkey_str)
self
.apply_hotkey(hotkey_type)
except
Exception as e:
messagebox.showerror(
"应用错误"
, f
"应用侧键 ({key_object}) 时出错: {e}"
, parent
=
self
.root); traceback.print_exc()
def
start_hotkey_listener(
self
):
""
"Starts the single global hotkey listener thread if any hotkeys are set."
""
global
KeyboardListener, MouseListener
if
'KeyboardListener'
not
in
globals
()
or
'MouseListener'
not
in
globals
():
print
(
"Error: Pynput listeners unavailable."
);
return
has_mouse_hk
=
bool
(
self
.parsed_mouse_hotkey)
has_keyboard_hk
=
bool
(
self
.parsed_keyboard_hotkey)
if
not
(has_mouse_hk
or
has_keyboard_hk):
print
(
"Listener not started: No hotkeys configured."
)
if
self
.hotkey_listener_thread
and
self
.hotkey_listener_thread.is_alive():
self
.stop_hotkey_listener(wait
=
False
)
return
if
self
.hotkey_listener_thread
is
not
None
and
self
.hotkey_listener_thread.is_alive():
print
(
"Listener already running."
);
return
print
(
"Starting global hotkey listener thread..."
)
self
.hotkey_stop_event.clear()
self
.current_pressed_keys
=
set
()
self
._listener_keyboard_instance
=
None
self
._listener_mouse_instance
=
None
self
.hotkey_listener_thread
=
threading.Thread(target
=
self
.hotkey_listener_worker, daemon
=
True
, name
=
"HotkeyListenerThread"
)
self
.hotkey_listener_thread.start()
def
stop_hotkey_listener(
self
, wait:
bool
=
False
):
""
"Stops the global hotkey listener thread and its pynput listeners."
""
if
not
(
self
.hotkey_listener_thread
and
self
.hotkey_listener_thread.is_alive()):
self
.hotkey_listener_thread
=
None
;
self
._listener_keyboard_instance
=
None
;
self
._listener_mouse_instance
=
None
return
print
(
"Stopping global hotkey listener..."
)
self
.hotkey_stop_event.
set
()
kb_listener
=
self
._listener_keyboard_instance
m_listener
=
self
._listener_mouse_instance
self
._listener_keyboard_instance
=
None
self
._listener_mouse_instance
=
None
if
kb_listener
and
hasattr
(kb_listener,
'stop'
):
try
:
kb_listener.stop()
print
(
" pynput KB listener stop requested."
)
except
Exception as e:
print
(f
" Minor error requesting KB listener stop: {e}"
)
if
m_listener
and
hasattr
(m_listener,
'stop'
):
try
:
m_listener.stop()
print
(
" pynput Mouse listener stop requested."
)
except
Exception as e:
print
(f
" Minor error requesting Mouse listener stop: {e}"
)
if
wait:
join_timeout
=
1.0
if
self
.hotkey_listener_thread:
self
.hotkey_listener_thread.join(timeout
=
join_timeout)
if
self
.hotkey_listener_thread.is_alive():
print
(
" Warning: Listener thread join timed out."
)
else
:
print
(
" Listener thread joined."
)
else
:
print
(
" Listener thread already cleared."
)
self
.hotkey_listener_thread
=
None
self
.current_pressed_keys
=
set
()
print
(
"Listener cleanup sequence finished."
)
def
hotkey_listener_worker(
self
):
""
"Worker thread running pynput listeners to detect hotkeys."
""
global
KeyboardListener, MouseListener
if
'KeyboardListener'
not
in
globals
():
print
(
"Error(worker): Pynput unavailable."
);
return
listener_keyboard: Optional[
'KeyboardListener'
]
=
None
listener_mouse: Optional[
'MouseListener'
]
=
None
self
.current_pressed_keys
=
set
()
try
:
def
on_press(key_raw):
if
self
.hotkey_stop_event.is_set()
or
key_raw
is
None
:
return
key
=
self
._get_canonical_key(key_raw)
if
key:
is_new
=
key
not
in
self
.current_pressed_keys
if
is_new:
self
.current_pressed_keys.add(key)
check_hotkey_match()
def
on_release(key_raw):
if
self
.hotkey_stop_event.is_set()
or
key_raw
is
None
:
return
key
=
self
._get_canonical_key(key_raw)
if
key:
self
.current_pressed_keys.discard(key)
def
on_click(x, y, button_raw, pressed):
if
self
.hotkey_stop_event.is_set()
or
button_raw
is
None
:
return
button
=
self
._get_canonical_key(button_raw)
if
button
and
pressed:
is_new
=
button
not
in
self
.current_pressed_keys
if
is_new:
self
.current_pressed_keys.add(button)
check_hotkey_match()
elif
button:
self
.current_pressed_keys.discard(button)
def
check_hotkey_match():
target_mouse_set
=
self
.parsed_mouse_hotkey
or
set
()
target_kb_set
=
self
.parsed_keyboard_hotkey
or
set
()
if
not
target_mouse_set
and
not
target_kb_set:
return
pressed_canon
=
{k
for
k
in
self
.current_pressed_keys
if
k}
mouse_target_canon
=
{
self
._get_canonical_key(k)
for
k
in
target_mouse_set
if
k}
key_target_canon
=
{
self
._get_canonical_key(k)
for
k
in
target_kb_set
if
k}
matched
=
False
action_to_run
=
None
if
mouse_target_canon
and
pressed_canon
=
=
mouse_target_canon:
print
(f
"Mouse Hotkey triggered!"
)
action_to_run
=
self
.toggle_clicking
matched
=
True
elif
key_target_canon
and
pressed_canon
=
=
key_target_canon:
print
(f
"Keyboard Hotkey triggered!"
)
action_to_run
=
self
.toggle_keyboard_automation
matched
=
True
if
matched
and
action_to_run:
try
:
if
self
.root.winfo_exists():
self
.root.after(
0
, action_to_run)
time.sleep(
0.3
)
except
tk.TclError:
self
.hotkey_stop_event.
set
()
except
Exception as e:
print
(f
"Error scheduling hotkey action: {e}"
)
print
(
"Hotkey listener worker creating listeners."
)
listener_keyboard
=
KeyboardListener(on_press
=
on_press, on_release
=
on_release, suppress
=
False
)
listener_mouse
=
MouseListener(on_click
=
on_click, suppress
=
False
)
self
._listener_keyboard_instance
=
listener_keyboard
self
._listener_mouse_instance
=
listener_mouse
listener_keyboard.start()
listener_mouse.start()
print
(
"Hotkey listener worker running and waiting."
)
while
not
self
.hotkey_stop_event.is_set():
time.sleep(
0.5
)
print
(
"Hotkey listener worker stop signal received."
)
except
Exception as e:
print
(f
"Fatal error in hotkey worker: {e}"
); traceback.print_exc()
finally
:
print
(
"Hotkey listener worker cleanup..."
)
kb_listener_local
=
self
._listener_keyboard_instance
m_listener_local
=
self
._listener_mouse_instance
if
kb_listener_local
and
hasattr
(kb_listener_local,
'stop'
)
and
kb_listener_local.is_alive():
try
: kb_listener_local.stop()
except
Exception:
pass
if
m_listener_local
and
hasattr
(m_listener_local,
'stop'
)
and
m_listener_local.is_alive():
try
: m_listener_local.stop()
except
Exception:
pass
print
(
"Hotkey listener worker finished cleanup."
)
def
open_capture_dialog(
self
, capture_type:
str
, index: Optional[
int
]
=
None
):
""
"Opens the modal dialog to capture keys/hotkeys."
""
if
self
.is_running_mouse
or
self
.is_running_keyboard:
messagebox.showwarning(
"操作繁忙"
,
"请先停止运行中的任务再采集。"
, parent
=
self
.root);
return
if
self
.is_capturing:
messagebox.showwarning(
"操作繁忙"
,
"请先完成当前的采集操作。"
, parent
=
self
.root);
return
if
capture_type
=
=
'target_key'
and
(index
is
None
or
index <
0
or
index >
=
self
.current_key_row_count):
messagebox.showerror(
"内部错误"
, f
"无效的目标按键索引: {index}"
, parent
=
self
.root);
return
self
.is_capturing
=
True
;
self
.update_ui_state()
captured_value: Optional[
str
]
=
None
try
:
print
(
"Temporarily stopping global listener for capture..."
)
self
.stop_hotkey_listener(wait
=
True
)
dialog
=
HotkeyCaptureDialog(
self
.root,
self
, capture_type, index)
captured_value
=
dialog.show()
except
Exception as e:
print
(f
"Error opening capture dialog: {e}"
); traceback.print_exc()
finally
:
self
.is_capturing
=
False
if
self
.root.winfo_exists():
self
.root.after(
10
,
self
.update_ui_state)
print
(
"Restarting global listener after capture attempt..."
)
if
self
.root.winfo_exists():
self
.root.after(
150
,
self
.start_hotkey_listener)
if
captured_value
is
not
None
:
print
(f
"Capture successful: Type={capture_type}, Val='{captured_value}'"
)
if
capture_type
=
=
'mouse'
:
self
.mouse_hotkey_str.
set
(captured_value);
self
.apply_hotkey(
'mouse'
)
elif
capture_type
=
=
'keyboard'
:
self
.keyboard_hotkey_str.
set
(captured_value);
self
.apply_hotkey(
'keyboard'
)
elif
capture_type
=
=
'target_key'
and
index
is
not
None
:
if
index <
len
(
self
.keyboard_target_keys):
self
.keyboard_target_keys[index].
set
(captured_value);
self
._apply_target_key(index)
else
:
print
(f
"Error: Index {index} invalid after capture."
)
else
:
print
(f
"Capture cancelled or failed."
)
def
create_image(
self
, width:
int
, height:
int
, color1:
str
, color2:
str
)
-
> Optional[
'Image.Image'
]:
""
"Creates a simple image for the tray icon."
""
global
Image, ImageDraw
if
'Image'
not
in
globals
()
or
Image
is
None
:
print
(
"Error: Pillow (PIL) unavailable for tray image."
);
return
None
try
:
im
=
Image.new(
'RGB'
, (width, height), color1)
dc
=
ImageDraw.Draw(im)
dc.rectangle((width
/
/
2
,
0
, width, height
/
/
2
), fill
=
color2)
dc.rectangle((
0
, height
/
/
2
, width
/
/
2
, height), fill
=
color2)
return
im
except
Exception as e:
print
(f
"Error creating tray image: {e}"
);
return
None
def
setup_tray(
self
):
""
"Sets up the system tray icon and starts its thread."
""
global
pystray
if
'pystray'
not
in
globals
()
or
pystray
is
None
:
print
(
"Info: pystray unavailable, tray disabled."
);
return
try
:
image
=
self
.create_image(
64
,
64
,
'#1E90FF'
,
'#ADD8E6'
) # Blue colors
if
image
is
None
:
raise
RuntimeError(
"Icon image creation failed."
)
menu
=
(pystray.MenuItem(
'显示窗口'
,
self
.show_window, default
=
True
),
pystray.MenuItem(
'退出程序'
,
self
.quit_app))
self
.tray_icon
=
pystray.Icon(f
"{APP_NAME}_Tray"
, image, f
"高级连点器 V2.6.2"
, menu)
self
.tray_thread
=
threading.Thread(target
=
self
.run_tray, daemon
=
True
, name
=
"PystrayThread"
)
self
.tray_thread.start()
print
(
"Tray icon setup complete."
)
except
Exception as e:
print
(f
"Error setting up tray: {e}"
);
self
.tray_icon
=
None
;
self
.tray_thread
=
None
def
run_tray(
self
):
""
"Runs the pystray icon loop (target for tray_thread)."
""
if
self
.tray_icon:
try
:
print
(
"Pystray run loop starting."
)
self
.tray_icon.run()
except
Exception as e:
print
(f
"Error in tray loop: {e}"
)
finally
:
print
(
"Pystray run loop finished."
)
def
hide_window(
self
):
""
"Hides the main application window."
""
if
self
.root
and
self
.root.winfo_exists():
try
:
self
.root.withdraw();
print
(
"Window hidden."
)
except
tk.TclError as e:
print
(f
"Error hiding window: {e}"
)
def
show_window(
self
, icon
=
None
, item
=
None
):
""
"Schedules showing the main window via the main Tkinter thread."
""
if
self
.root
and
self
.root.winfo_exists():
self
.root.after(
0
,
self
._show_window_task)
def
_show_window_task(
self
):
""
"Actual window showing logic, executed in the main Tkinter thread."
""
if
self
.root
and
self
.root.winfo_exists():
try
:
self
.root.deiconify();
self
.root.lift();
self
.root.focus_force()
self
.root.attributes(
'-topmost'
,
True
)
self
.root.after(
200
,
lambda
:
self
.safe_set_topmost(
False
))
print
(
"Window shown via _show_window_task."
)
except
tk.TclError as e:
print
(f
"Error in _show_window_task: {e}"
)
def
safe_set_topmost(
self
, value:
bool
):
""
"Safely sets the topmost attribute."
""
try
:
if
self
.root
and
self
.root.winfo_exists():
self
.root.attributes(
'-topmost'
, value)
except
tk.TclError:
pass
def
on_close_requested(
self
):
""
"Handles the WM_DELETE_WINDOW event (clicking the 'X' button)."
""
if
self
._shutting_down:
return
tray_ok
=
self
.tray_icon
and
self
.tray_thread
and
self
.tray_thread.is_alive()
if
tray_ok:
result
=
messagebox.askyesnocancel(
"操作确认"
,
"最小化到系统托盘还是直接退出程序?\n\n [是] = 最小化\n [否] = 退出\n[取消] = 返回"
, icon
=
'question'
, parent
=
self
.root)
if
result
is
True
:
self
.hide_window()
elif
result
is
False
:
self
.quit_app()
else
:
if
messagebox.askokcancel(
"退出确认"
,
"系统托盘不可用。\n确定要退出程序吗?"
, icon
=
'warning'
, parent
=
self
.root):
self
.quit_app()
def
load_config(
self
):
""
"Loads configuration from the JSON file."
""
config_path
=
get_config_path()
print
(f
"Loading config from: {config_path}"
)
if
not
config_path.exists():
print
(
"Config file not found. Using defaults."
)
self
.status_text.
set
(
"状态: 已停止 | 使用默认"
)
if
self
.current_key_row_count
=
=
0
:
for
_
in
range
(DEFAULT_AUTOMATION_KEYS):
self
.add_key_row(update_scroll
=
False
)
if
self
.keys_canvas:
self
.keys_canvas.after(
50
,
self
.update_canvas_scrollregion)
return
try
:
with
open
(config_path,
'r'
, encoding
=
'utf-8'
) as f: config_data
=
json.load(f)
print
(
"Config loaded."
)
mouse_cfg
=
config_data.get(
"mouse"
, {})
self
.click_interval.
set
(mouse_cfg.get(
"interval"
,
0.01
))
self
.selected_click_type.
set
(mouse_cfg.get(
"click_type"
,
"left"
))
self
.mouse_hotkey_str.
set
(mouse_cfg.get(
"hotkey"
,
"Key.f8"
))
self
.mouse_duration.
set
(mouse_cfg.get(
"duration"
,
0.0
))
kb_cfg
=
config_data.get(
"keyboard"
, {})
self
.keyboard_hotkey_str.
set
(kb_cfg.get(
"hotkey"
,
"Key.f7"
))
self
.keyboard_mode.
set
(kb_cfg.get(
"mode"
,
"sequential"
))
self
.keyboard_cycle_delay.
set
(kb_cfg.get(
"cycle_delay"
,
0.1
))
self
.keyboard_duration.
set
(kb_cfg.get(
"duration"
,
0.0
))
loaded_keys
=
kb_cfg.get(
"target_keys"
, [])
loaded_delays
=
kb_cfg.get(
"target_delays"
, [])
num_to_load
=
min
(
len
(loaded_keys),
len
(loaded_delays), MAX_AUTOMATION_KEYS)
self
._clear_key_rows()
print
(f
"Loading {num_to_load} key rows from config..."
)
for
i
in
range
(num_to_load):
key_str
=
str
(loaded_keys[i])
if
loaded_keys[i]
is
not
None
else
""
delay_val
=
0.0
try
: delay_val
=
float
(loaded_delays[i])
if
loaded_delays[i]
is
not
None
else
0.0
except
(ValueError, TypeError):
pass
self
.add_key_row(key_value
=
key_str, delay_value
=
max
(
0.0
, delay_val), update_scroll
=
False
)
if
self
.keys_canvas:
self
.keys_canvas.after(
50
,
self
.update_canvas_scrollregion)
self
.status_text.
set
(
"状态: 已停止 | 配置已加载"
)
except
FileNotFoundError:
print
(
"Config file disappeared."
)
except
json.JSONDecodeError as e:
print
(f
"Error decoding config: {e}"
); messagebox.showerror(
"配置错误"
, f
"配置文件格式错误: {e}"
, parent
=
self
.root)
except
Exception as e:
print
(f
"Error loading config: {e}"
); traceback.print_exc(); messagebox.showerror(
"配置错误"
, f
"加载配置时出错: {e}"
, parent
=
self
.root)
def
save_config(
self
):
""
"Saves the current configuration to the JSON file."
""
config_path
=
get_config_path()
print
(f
"Saving config to: {config_path}"
)
try
: config_path.parent.mkdir(parents
=
True
, exist_ok
=
True
)
except
Exception as e:
print
(f
"Error ensuring config dir exists: {e}"
);
return
config_data
=
{
"app_version"
:
"2.6.2"
,
"mouse"
: {
"interval"
:
self
.click_interval.get(),
"click_type"
:
self
.selected_click_type.get(),
"hotkey"
:
self
.mouse_hotkey_str.get(),
"duration"
:
self
.mouse_duration.get() },
"keyboard"
: {
"hotkey"
:
self
.keyboard_hotkey_str.get(),
"mode"
:
self
.keyboard_mode.get(),
"cycle_delay"
:
self
.keyboard_cycle_delay.get(),
"duration"
:
self
.keyboard_duration.get(),
"target_keys"
: [var.get()
for
var
in
self
.keyboard_target_keys],
"target_delays"
: [var.get()
for
var
in
self
.keyboard_target_delays] }
}
try
:
with
open
(config_path,
'w'
, encoding
=
'utf-8'
) as f: json.dump(config_data, f, indent
=
4
, ensure_ascii
=
False
)
print
(
"Config saved successfully."
)
except
Exception as e:
print
(f
"Error saving config: {e}"
); traceback.print_exc(); messagebox.showerror(
"保存错误"
, f
"无法保存配置: {e}"
, parent
=
self
.root)
def
quit_app(
self
, icon
=
None
, item
=
None
):
""
"Stops processes, saves config, and exits cleanly."
""
if
self
._shutting_down:
return
print
(
"Quit requested. Shutting down..."
);
self
._shutting_down
=
True
if
self
.is_running_mouse:
self
.stop_clicking()
if
self
.is_running_keyboard:
self
.stop_keyboard_automation()
self
.stop_hotkey_listener(wait
=
True
)
if
self
.tray_icon
and
hasattr
(
self
.tray_icon,
'stop'
):
try
:
self
.tray_icon.stop()
except
Exception as e:
print
(f
"Minor error stopping tray icon: {e}"
)
if
self
.tray_thread
and
self
.tray_thread.is_alive():
print
(
"Waiting for tray thread to exit..."
)
self
.tray_thread.join(timeout
=
1.0
)
if
self
.tray_thread.is_alive():
print
(
" Warning: Tray thread join timed out."
)
else
:
print
(
" Tray thread exited."
)
self
.save_config()
if
self
.root
and
self
.root.winfo_exists():
try
:
self
.root.destroy()
except
tk.TclError:
pass
except
Exception as e:
print
(f
"Error destroying root: {e}"
)
print
(
"Shutdown sequence finished."
)
class
HotkeyCaptureDialog(Toplevel):
""
"Modal dialog for capturing keys/hotkeys using temporary listeners."
""
def
__init__(
self
, parent: tk.Tk, parent_app_ref: AutoClickerApp, capture_type:
str
, index: Optional[
int
]
=
None
):
super
().__init__(parent)
self
.transient(parent)
self
.parent_app
=
parent_app_ref
self
.capture_type
=
capture_type
self
.listener_kb: Optional[
'KeyboardListener'
]
=
None
self
.listener_mouse: Optional[
'MouseListener'
]
=
None
self
.captured_raw_keys:
Set
[PynputKeyType]
=
set
()
self
.result: Optional[
str
]
=
None
self
.confirmed
=
False
self
._stop_event
=
threading.Event()
global
KeyboardListener, MouseListener
if
'KeyboardListener'
not
in
globals
():
raise
RuntimeError(
"Pynput unavailable for capture"
)
title
=
"采集热键"
; info
=
"请按下并松开要设置的键盘/组合键..."
self
.allow_combinations
=
True
;
self
.listen_mouse
=
True
if
capture_type
=
=
'target_key'
:
title
=
f
"采集目标按键 {index + 1}"
if
index
is
not
None
else
"采集目标按键"
info
=
"请按下并松开 单个 键盘按键或鼠标按钮..."
self
.allow_combinations
=
False
self
.title(title);
self
.geometry(
"380x150"
);
self
.resizable(
False
,
False
)
self
.protocol(
"WM_DELETE_WINDOW"
,
self
.cancel);
self
.grab_set()
main_frame
=
ttk.Frame(
self
, padding
=
5
); main_frame.pack(expand
=
True
, fill
=
tk.BOTH)
ttk.Label(main_frame, text
=
info, justify
=
tk.CENTER, wraplength
=
360
).pack(pady
=
(
3
,
5
))
self
.status_var
=
tk.StringVar(value
=
"请按键..."
);
self
.status_label
=
ttk.Label(main_frame, textvariable
=
self
.status_var, justify
=
tk.CENTER, font
=
("
", 10, "
bold
"), foreground="
blue")
self
.status_label.pack(pady
=
3
)
ttk.Button(main_frame, text
=
"取消"
, command
=
self
.cancel).pack(pady
=
(
5
,
3
))
self
._start_listeners();
self
.focus_force();
self
._center_dialog()
def
_center_dialog(
self
):
""
"Centers the dialog relative to the parent window."
""
try
:
self
.update_idletasks()
parent
=
self
.master
if
not
parent:
return
parent_x, parent_y
=
parent.winfo_rootx(), parent.winfo_rooty()
parent_w, parent_h
=
parent.winfo_width(), parent.winfo_height()
dialog_w, dialog_h
=
self
.winfo_width(),
self
.winfo_height()
x
=
parent_x
+
(parent_w
-
dialog_w)
/
/
2
y
=
parent_y
+
(parent_h
-
dialog_h)
/
/
2
self
.geometry(f
"+{x}+{y}"
)
except
Exception as e:
print
(f
"Could not center dialog: {e}"
)
def
_start_listeners(
self
):
""
"Starts local listeners for this dialog."
""
global
KeyboardListener, MouseListener
self
._stop_event.clear()
try
:
self
.listener_kb
=
KeyboardListener(
on_press
=
self
.on_press, on_release
=
self
.on_release,
suppress
=
False
, stop_event
=
self
._stop_event
)
self
.listener_kb.start()
if
self
.listen_mouse:
self
.listener_mouse
=
MouseListener(
on_click
=
self
.on_click, suppress
=
False
,
stop_event
=
self
._stop_event
)
self
.listener_mouse.start()
except
TypeError:
print
(
"Warning: Using pynput version without stop_event. Capture stop might be less reliable."
)
self
.listener_kb
=
KeyboardListener(on_press
=
self
.on_press, on_release
=
self
.on_release, suppress
=
False
)
self
.listener_kb.start()
if
self
.listen_mouse:
self
.listener_mouse
=
MouseListener(on_click
=
self
.on_click, suppress
=
False
)
self
.listener_mouse.start()
except
Exception as e:
print
(f
"Error starting capture listeners: {e}"
);
self
.cancel()
def
_update_status(
self
, text:
str
, color:
str
=
"blue"
):
""
"Safely schedules status label update."
""
if
hasattr
(
self
,
'status_var'
)
and
self
.winfo_exists():
try
:
self
.parent_app.root.after(
0
,
lambda
:
self
._do_update(text, color)
if
self
.winfo_exists()
else
None
)
except
tk.TclError:
pass
def
_do_update(
self
, text, color):
""
"Actual status update in main thread."
""
if
self
.winfo_exists()
and
self
.status_label
and
self
.status_label.winfo_exists():
try
:
self
.status_var.
set
(text);
self
.status_label.config(foreground
=
color)
except
tk.TclError:
pass
def
on_press(
self
, key_raw):
if
self
.confirmed
or
self
._stop_event.is_set()
or
key_raw
is
None
:
return
if
not
self
.allow_combinations:
self
.captured_raw_keys.clear()
self
.captured_raw_keys.add(key_raw)
self
._update_status(f
"已按下: {self.parent_app.format_hotkey_set(self.captured_raw_keys)}"
)
def
on_release(
self
, key_raw):
if
self
.confirmed
or
self
._stop_event.is_set()
or
key_raw
is
None
or
key_raw
not
in
self
.captured_raw_keys:
return
is_valid
=
True
if
not
self
.allow_combinations
and
len
(
self
.captured_raw_keys) >
1
:
self
._update_status(
"错误: 只能采集单个按键!"
, color
=
"red"
);
self
.captured_raw_keys.clear(); is_valid
=
False
elif
not
self
.captured_raw_keys:
self
._update_status(
"未采集到按键"
, color
=
"red"
); is_valid
=
False
if
is_valid:
try
:
self
.result
=
self
.parent_app.format_hotkey_set(
self
.captured_raw_keys)
self
._update_status(f
"已捕获: {self.result}"
, color
=
"green"
)
self
.after(
50
,
self
.confirm)
except
Exception as e:
self
._update_status(f
"格式化错误: {e}"
, color
=
"red"
);
self
.captured_raw_keys.clear()
def
on_click(
self
, x, y, button_raw, pressed):
if
self
.confirmed
or
self
._stop_event.is_set()
or
not
self
.listen_mouse
or
button_raw
is
None
:
return
if
pressed:
if
not
self
.allow_combinations:
self
.captured_raw_keys.clear()
self
.captured_raw_keys.add(button_raw)
self
._update_status(f
"已按下: {self.parent_app.format_hotkey_set(self.captured_raw_keys)}"
)
elif
button_raw
in
self
.captured_raw_keys:
is_valid
=
True
if
not
self
.allow_combinations
and
len
(
self
.captured_raw_keys) >
1
:
self
._update_status(
"错误: 只能采集单个按钮!"
, color
=
"red"
);
self
.captured_raw_keys.clear(); is_valid
=
False
elif
not
self
.captured_raw_keys:
self
._update_status(
"未采集到按键"
, color
=
"red"
); is_valid
=
False
if
is_valid:
try
:
self
.result
=
self
.parent_app.format_hotkey_set(
self
.captured_raw_keys)
self
._update_status(f
"已捕获: {self.result}"
, color
=
"green"
)
self
.after(
50
,
self
.confirm)
except
Exception as e:
self
._update_status(f
"格式化错误: {e}"
, color
=
"red"
);
self
.captured_raw_keys.clear()
def
confirm(
self
):
if
self
.confirmed:
return
;
self
.confirmed
=
True
self
._stop_listeners();
self
.destroy()
def
cancel(
self
):
if
self
.confirmed:
return
;
self
.confirmed
=
True
self
.result
=
None
;
self
._stop_listeners();
self
.destroy()
def
_stop_listeners(
self
):
""
"Stops the local pynput listeners safely."
""
if
self
._stop_event.is_set():
return
print
(
"Stopping local capture listeners..."
)
self
._stop_event.
set
()
kb_listener
=
self
.listener_kb
m_listener
=
self
.listener_mouse
self
.listener_kb
=
None
self
.listener_mouse
=
None
if
kb_listener
and
hasattr
(kb_listener,
'stop'
):
try
: kb_listener.stop()
except
Exception as e:
print
(f
" Error stopping capture KB listener: {e}"
)
if
m_listener
and
hasattr
(m_listener,
'stop'
):
try
: m_listener.stop()
except
Exception as e:
print
(f
" Error stopping capture Mouse listener: {e}"
)
print
(
"Local capture listeners stop requested."
)
def
show(
self
)
-
> Optional[
str
]:
self
.wait_window(
self
);
return
self
.result
if
__name__
=
=
"__main__"
:
main_root: Optional[tk.Tk]
=
None
app: Optional[AutoClickerApp]
=
None
missing_lib
=
""
try
:
print
(
"Importing pynput..."
)
from
pynput
import
mouse, keyboard
from
pynput.mouse
import
Button as PynputMouseButton
from
pynput.mouse
import
Controller as MouseController
from
pynput.mouse
import
Listener as MouseListener
from
pynput.keyboard
import
Key as PynputKey
from
pynput.keyboard
import
KeyCode as PynputKeyCode
from
pynput.keyboard
import
Controller as KeyboardController
from
pynput.keyboard
import
Listener as KeyboardListener
Button, Key, KeyCode
=
PynputMouseButton, PynputKey, PynputKeyCode
PynputKeyType
=
Union[Key, KeyCode, Button]
print
(
"Importing optional: pystray, Pillow..."
)
try
:
import
pystray;
from
PIL
import
Image, ImageDraw
except
ImportError as opt_err:
missing_lib
=
str
(opt_err).split(
" "
)[
-
1
].strip(
"'"
)
print
(f
"Warning: Optional '{missing_lib}' not found. Tray features disabled."
)
if
'pystray'
in
missing_lib: pystray
=
None
if
'PIL'
in
missing_lib
or
'Image'
in
missing_lib: Image, ImageDraw
=
None
,
None
print
(
"Initializing controllers..."
); mouse_controller
=
MouseController(); keyboard_controller
=
KeyboardController();
print
(
"Controllers OK."
)
except
ImportError as core_err:
missing_lib
=
str
(core_err).split(
" "
)[
-
1
].strip(
"'"
); install_cmd
=
f
"pip install {missing_lib}"
print
(f
"错误: 核心库 {missing_lib} 未安装。\n请运行: {install_cmd}"
,
file
=
sys.stderr)
try
: temp_root
=
tk.Tk(); temp_root.withdraw(); messagebox.showerror(
"依赖错误"
, f
"缺少核心库 '{missing_lib}'.\n请安装: {install_cmd}"
); temp_root.destroy()
except
Exception:
pass
; sys.exit(
1
)
except
Exception as init_err:
print
(f
"启动错误: {init_err}"
,
file
=
sys.stderr); traceback.print_exc(
file
=
sys.stderr)
try
: temp_root
=
tk.Tk(); temp_root.withdraw(); messagebox.showerror(
"启动错误"
, f
"程序启动失败:\n{init_err}"
); temp_root.destroy()
except
Exception:
pass
; sys.exit(
1
)
try
:
main_root
=
tk.Tk()
try
:
style
=
ttk.Style(); themes
=
style.theme_names(); pref
=
[
'vista'
,
'clam'
,
'alt'
,
'default'
]
if
platform.system()
=
=
"Windows"
: pref.insert(
0
,
'xpnative'
)
if
platform.system()
=
=
"Darwin"
: pref.insert(
0
,
'aqua'
)
theme
=
next
((t
for
t
in
pref
if
t
in
themes), style.theme_use())
if
theme: style.theme_use(theme);
print
(f
"Using theme: {theme}"
)
except
Exception as e_style:
print
(f
"Theme error: {e_style}"
)
app
=
AutoClickerApp(main_root)
print
(
"Starting main loop..."
)
main_root.mainloop()
print
(
"Main loop finished."
)
except
KeyboardInterrupt:
print
(
"\nCtrl+C pressed. Exiting..."
);
if
app: app.quit_app()
else
: sys.exit(
1
)
except
Exception as main_e:
print
(f
"\n--- 主程序运行时发生错误 ---"
,
file
=
sys.stderr);
print
(f
"{type(main_e).__name__}: {main_e}"
,
file
=
sys.stderr); traceback.print_exc(
file
=
sys.stderr)
print
(
"--- 尝试关闭 ---"
,
file
=
sys.stderr)
try
:
if
app: app.quit_app()
else
: os._exit(
1
)
except
Exception: os._exit(
1
)
finally
:
print
(
"应用程序退出。"
)