[Asm] 纯文本查看 复制代码
import sys
import cv2
import numpy as np
import json
import os
import time
import random
import ctypes
import win32gui
import win32con
import win32api
import shutil
from PIL import ImageGrab
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from pynput.mouse import Controller as MouseController, Listener as MouseListener, Button
from pynput.keyboard import Controller as KeyboardController, Listener as KeyboardListener, Key
def get_app_base_dir():
"""获取应用程序基础目录,兼容脚本运行和PyInstaller打包"""
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
return os.path.dirname(os.path.abspath(__file__))
# 设置 DPI 感知
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except:
try:
ctypes.windll.user32.SetProcessDPIAware()
except:
pass
class Step:
def __init__(self, step_type, image_path=None, x=None, y=None,
click_type='left', wait_time=1000, similarity=0.8,
random_delay_enabled=False, random_delay_min=100, random_delay_max=1000,
accept_offset=True, accept_random_delay=True,
jump_on_found=None, jump_on_not_found=None,
key_sequence=None, text_content=None,
drag_start_x=None, drag_start_y=None, drag_end_x=None, drag_end_y=None,
# 键盘操作相关字段
key_action_type=0, # 0=按键一次, 1=长按指定时间, 2=按住直到图像出现
key_hold_time=0, # 按键按住时间(毫秒)
key_hold_image_path=None, # 按住直到出现的图像路径
# 拖动缓冲相关
drag_start_delay=0, # 拖动开始前的延迟(毫秒)
drag_duration=1000): # 拖动持续时间(毫秒),默认1秒
self.step_type = step_type
self.image_path = image_path
self.x = x
self.y = y
self.click_type = click_type
self.wait_time = wait_time
self.similarity = similarity
self.random_delay_enabled = random_delay_enabled
self.random_delay_min = random_delay_min
self.random_delay_max = random_delay_max
self.accept_offset = accept_offset
self.accept_random_delay = accept_random_delay
self.jump_on_found = jump_on_found
self.jump_on_not_found = jump_on_not_found
self.key_sequence = key_sequence
self.text_content = text_content
# 鼠标拖动相关
self.drag_start_x = drag_start_x
self.drag_start_y = drag_start_y
self.drag_end_x = drag_end_x
self.drag_end_y = drag_end_y
# 键盘操作相关字段初始化
self.key_action_type = key_action_type
self.key_hold_time = key_hold_time
self.key_hold_image_path = key_hold_image_path
# 拖动缓冲相关字段初始化
self.drag_start_delay = drag_start_delay
self.drag_duration = drag_duration
class AutoClickerApp(QMainWindow):
coordinate_captured = pyqtSignal(int, int)
stop_coordinate_mode = pyqtSignal()
stop_select_window_mode_signal = pyqtSignal()
screenshot_closed = pyqtSignal()
drag_coordinate_captured = pyqtSignal(int, int)
hotkey_recorded = pyqtSignal(str)
# 新增信号:用于键盘对话框中的图像选择
keyboard_image_selected = pyqtSignal(str)
# 参考分辨率和默认尺寸
REFERENCE_WIDTH = 1920
REFERENCE_HEIGHT = 1080
DEFAULT_WIDTH = 1000
DEFAULT_HEIGHT = 785
# 最小尺寸限制
MIN_WIDTH = 800
MIN_HEIGHT = 600
def __init__(self):
super().__init__()
# 获取应用程序基础目录
self.app_base_dir = get_app_base_dir()
# 定义配置文件路径(始终在EXE/脚本所在目录)
self.config_file = os.path.join(self.app_base_dir, "autoclicker_config.json")
self.settings_file = os.path.join(self.app_base_dir, "autoclicker_settings.json")
self.screenshot_dir = os.path.join(self.app_base_dir, "screenshots")
self.first_run_flag_file = os.path.join(self.app_base_dir, ".first_run_flag")
# 加载窗口尺寸
self.window_width, self.window_height = self.load_window_size()
self.setWindowTitle("自动点击工具(图片识别与坐标点击)V1.3--吾爱破解论坛")
# 获取屏幕信息
self.screen = QApplication.primaryScreen()
self.screen_geometry = self.screen.availableGeometry()
self.screen_width = self.screen_geometry.width()
self.screen_height = self.screen_geometry.height()
# 计算缩放比例
self.scale_factor = self.calculate_scale_factor()
# 应用缩放
self.scaled_width = int(self.window_width * self.scale_factor)
self.scaled_height = int(self.window_height * self.scale_factor)
# 确保窗口不会超出屏幕
self.scaled_width = min(self.scaled_width, int(self.screen_width * 0.95))
self.scaled_height = min(self.scaled_height, int(self.screen_height * 0.95))
# 设置窗口尺寸
self.resize(self.scaled_width, self.scaled_height)
self.setMinimumSize(
int(self.MIN_WIDTH * self.scale_factor),
int(self.MIN_HEIGHT * self.scale_factor)
)
# 居中显示
self.center_on_screen()
self.mouse_listener = None
self.keyboard_listener = None
self.global_hotkey_listener = None
self.target_hwnd = None
self.steps = [] # 初始化步骤列表
# 定时执行相关
self.timed_execution_enabled = False
self.start_datetime = None
self.end_datetime = None
# 运行状态
self.is_running = False
self.is_paused = False
self.current_step_index = 0
# 默认设置值
self.loop_type = 0 # 0=无限循环, 1=次数循环
self.loop_count = 1000
self.offset_enabled = False
self.offset_x = 5
self.offset_y = 5
self.random_delay_enabled = False
self.random_delay_min = 100
self.random_delay_max = 1000
# 创建必要的目录
os.makedirs(self.screenshot_dir, exist_ok=True)
self.mouse = MouseController()
self.keyboard = KeyboardController()
self.hotkey = "F12"
# 加载全局设置
self.load_global_settings()
self.init_ui()
self.setup_global_hotkey()
self.execution_thread = ExecutionThread(self)
self.execution_thread.finished.connect(self.on_execution_finished)
self.execution_thread.error.connect(self.on_execution_error)
self.execution_thread.step_completed.connect(self.on_step_completed)
self.coordinate_captured.connect(self.on_coordinate_captured)
self.stop_coordinate_mode.connect(self.stop_add_coordinate_mode)
self.stop_select_window_mode_signal.connect(self.stop_select_window_mode)
self.screenshot_closed.connect(self.on_screenshot_closed)
self.drag_coordinate_captured.connect(self.on_drag_coordinate_captured)
self.hotkey_recorded.connect(self.on_hotkey_recorded)
self.keyboard_image_selected.connect(self.on_keyboard_image_selected)
# 检查是否首次运行,如果是则显示声明弹窗
self.check_first_run()
def check_first_run(self):
"""检查是否首次运行,如果是则显示声明弹窗"""
if not os.path.exists(self.first_run_flag_file):
# 显示声明弹窗
self.show_first_run_dialog()
# 创建标志文件,表示已经运行过
try:
with open(self.first_run_flag_file, 'w', encoding='utf-8') as f:
f.write("First run completed.")
except Exception as e:
print(f"创建首次运行标志文件失败: {e}")
def show_first_run_dialog(self):
"""显示首次运行声明弹窗"""
dialog = QDialog(self)
dialog.setWindowTitle("自动点击工具声明")
dialog.setModal(True)
dialog.setFixedSize(500, 300)
# 设置样式
dialog.setStyleSheet("""
QDialog {
background-color: #f8f9fa;
}
QLabel#titleLabel {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
padding: 10px;
}
QLabel#contentLabel {
font-size: 16px;
color: #34495e;
padding: 5px;
line-height: 1.5;
}
QPushButton {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
font-size: 14px;
border-radius: 5px;
min-width: 100px;
}
QPushButton:hover {
background-color: #2980b9;
}
QPushButton:pressed {
background-color: #21618c;
}
""")
layout = QVBoxLayout(dialog)
layout.setSpacing(20)
layout.setContentsMargins(30, 30, 30, 30)
# 标题
title_label = QLabel("🎉 欢迎使用自动点击工具")
title_label.setObjectName("titleLabel")
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# 内容
content_label = QLabel()
content_label.setObjectName("contentLabel")
content_label.setText("""
<div style='text-align: center;'>
<p style='margin: 10px 0;'>本工具出自吾爱破解论坛</p>
<p style='margin: 10px 0;'>本软件完全免费,仅供学习和技术交流使用</p>
<p style='margin: 10px 0; color: #e74c3c; font-weight: bold;'>
请勿将其用于任何违反服务条款或法律法规的活动
</p>
</div>
""")
content_label.setWordWrap(True)
content_label.setAlignment(Qt.AlignCenter)
layout.addWidget(content_label)
# 按钮
button_layout = QHBoxLayout()
button_layout.addStretch()
ok_button = QPushButton("我知道了")
ok_button.clicked.connect(dialog.accept)
button_layout.addWidget(ok_button)
button_layout.addStretch()
layout.addLayout(button_layout)
# 显示弹窗
dialog.exec_()
def calculate_scale_factor(self):
"""计算基于参考分辨率的缩放比例"""
# 使用宽度和高度中较小的缩放比例,确保窗口完全可见
width_scale = self.screen_width / self.REFERENCE_WIDTH
height_scale = self.screen_height / self.REFERENCE_HEIGHT
return min(width_scale, height_scale)
def center_on_screen(self):
"""将窗口居中显示在屏幕上"""
x = (self.screen_width - self.width()) // 2
y = (self.screen_height - self.height()) // 2
# 确保不会超出屏幕顶部
y = max(0, y)
self.move(x, y)
def load_window_size(self):
"""加载保存的窗口尺寸"""
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r', encoding='utf-8') as f:
settings = json.load(f)
width = settings.get("window_width", self.DEFAULT_WIDTH)
height = settings.get("window_height", self.DEFAULT_HEIGHT)
return width, height
except Exception as e:
print(f"加载窗口尺寸失败: {e}")
return self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT
def save_window_size(self):
"""保存当前窗口尺寸"""
# 保存原始尺寸(未缩放的)
unscaled_width = int(self.width() / self.scale_factor)
unscaled_height = int(self.height() / self.scale_factor)
try:
settings = {}
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r', encoding='utf-8') as f:
settings = json.load(f)
settings["window_width"] = unscaled_width
settings["window_height"] = unscaled_height
with open(self.settings_file, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
print(f"设置已保存到: {self.settings_file}")
except Exception as e:
print(f"保存窗口尺寸失败: {e}")
def scaled_value(self, value):
"""返回缩放后的值"""
return int(value * self.scale_factor)
def init_ui(self):
# 根据缩放比例调整字体
base_font_size = 10
scaled_font_size = max(8, int(base_font_size * self.scale_factor))
font = QFont("Microsoft YaHei", scaled_font_size)
self.setFont(font)
main_widget = QWidget()
main_layout = QVBoxLayout()
main_layout.setSpacing(self.scaled_value(10))
main_layout.setContentsMargins(
self.scaled_value(10),
self.scaled_value(10),
self.scaled_value(10),
self.scaled_value(10)
)
main_splitter = QSplitter(Qt.Horizontal)
left_widget = QWidget()
left_layout = QVBoxLayout()
left_layout.setSpacing(self.scaled_value(10))
left_widget.setMinimumWidth(self.scaled_value(320))
left_widget.setMaximumWidth(self.scaled_value(380))
# 1. 流程控制模块
control_group = QGroupBox("流程控制")
control_layout = QVBoxLayout()
control_layout.setSpacing(self.scaled_value(5))
self.start_btn = QPushButton("▶ 开始执行")
self.start_btn.setMinimumHeight(self.scaled_value(35))
self.start_btn.clicked.connect(self.start_execution)
self.stop_btn = QPushButton("⏹ 停止执行")
self.stop_btn.setMinimumHeight(self.scaled_value(35))
self.stop_btn.clicked.connect(self.stop_execution)
self.stop_btn.setEnabled(False)
self.pause_btn = QPushButton("⏸ 暂停执行")
self.pause_btn.setMinimumHeight(self.scaled_value(35))
self.pause_btn.clicked.connect(self.pause_execution)
self.pause_btn.setEnabled(False)
control_layout.addWidget(self.start_btn)
stop_pause_layout = QHBoxLayout()
stop_pause_layout.addWidget(self.pause_btn)
stop_pause_layout.addWidget(self.stop_btn)
control_layout.addLayout(stop_pause_layout)
control_group.setLayout(control_layout)
left_layout.addWidget(control_group)
# 2. 文件操作模块
file_group = QGroupBox("文件操作")
file_layout = QHBoxLayout()
file_layout.setSpacing(self.scaled_value(5))
self.save_btn = QPushButton("💾 保存配置")
self.save_btn.setMinimumHeight(self.scaled_value(35))
self.save_btn.clicked.connect(self.save_config)
self.load_btn = QPushButton("📂 加载配置")
self.load_btn.setMinimumHeight(self.scaled_value(35))
self.load_btn.clicked.connect(self.load_config)
file_layout.addWidget(self.save_btn)
file_layout.addWidget(self.load_btn)
file_group.setLayout(file_layout)
left_layout.addWidget(file_group)
# 3. 定时执行模块
timed_group = QGroupBox("定时执行")
timed_layout = QVBoxLayout()
timed_layout.setSpacing(self.scaled_value(5))
self.timed_execution_checkbox = QCheckBox("启用定时执行")
self.timed_execution_checkbox.setChecked(self.timed_execution_enabled)
self.timed_execution_checkbox.stateChanged.connect(self.toggle_timed_execution)
timed_layout.addWidget(self.timed_execution_checkbox)
datetime_layout1 = QHBoxLayout()
datetime_layout1.addWidget(QLabel("开始时间:"))
self.start_datetime_edit = QDateTimeEdit()
self.start_datetime_edit.setCalendarPopup(True)
self.start_datetime_edit.setDisplayFormat("yyyy-MM-dd HH:mm")
if self.start_datetime:
self.start_datetime_edit.setDateTime(self.start_datetime)
else:
self.start_datetime_edit.setDateTime(QDateTime.currentDateTime())
datetime_layout1.addWidget(self.start_datetime_edit)
timed_layout.addLayout(datetime_layout1)
datetime_layout2 = QHBoxLayout()
datetime_layout2.addWidget(QLabel("结束时间:"))
self.end_datetime_edit = QDateTimeEdit()
self.end_datetime_edit.setCalendarPopup(True)
self.end_datetime_edit.setDisplayFormat("yyyy-MM-dd HH:mm")
if self.end_datetime:
self.end_datetime_edit.setDateTime(self.end_datetime)
else:
self.end_datetime_edit.setDateTime(QDateTime.currentDateTime().addSecs(3600))
datetime_layout2.addWidget(self.end_datetime_edit)
timed_layout.addLayout(datetime_layout2)
timed_group.setLayout(timed_layout)
left_layout.addWidget(timed_group)
# 4. 目标窗口设置模块
window_group = QGroupBox("目标窗口设置 (后台执行关键)")
window_layout = QVBoxLayout()
window_layout.setSpacing(self.scaled_value(5))
self.window_title_edit = QLineEdit()
self.window_title_edit.setPlaceholderText("点击下方按钮选择目标窗口")
self.window_title_edit.setReadOnly(True)
self.window_title_edit.setMinimumHeight(self.scaled_value(30))
self.select_window_btn = QPushButton("🎯 选取窗口")
self.select_window_btn.setMinimumHeight(self.scaled_value(35))
self.select_window_btn.clicked.connect(self.start_select_window_mode)
self.clear_window_btn = QPushButton("❌ 清除窗口")
self.clear_window_btn.setMinimumHeight(self.scaled_value(35))
self.clear_window_btn.clicked.connect(self.clear_target_window)
window_layout.addWidget(self.window_title_edit)
window_layout.addWidget(self.select_window_btn)
window_layout.addWidget(self.clear_window_btn)
window_group.setLayout(window_layout)
left_layout.addWidget(window_group)
# 5. 热键设置模块
hotkey_group = QGroupBox("热键设置")
hotkey_layout = QVBoxLayout()
hotkey_layout.setSpacing(self.scaled_value(5))
hotkey_layout.addWidget(QLabel("启动/停止热键:"))
self.hotkey_display = QLineEdit()
self.hotkey_display.setReadOnly(True)
self.hotkey_display.setText(self.hotkey)
self.hotkey_display.setMinimumHeight(self.scaled_value(35))
self.hotkey_display.setStyleSheet(f"""
QLineEdit {{
padding: {self.scaled_value(8)}px;
font-size: {self.scaled_value(14)}px;
font-weight: bold;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: {self.scaled_value(3)}px;
}}
""")
hotkey_layout.addWidget(self.hotkey_display)
self.record_hotkey_btn = QPushButton("🎤 录制热键")
self.record_hotkey_btn.setMinimumHeight(self.scaled_value(35))
self.record_hotkey_btn.clicked.connect(self.start_record_hotkey)
self.record_hotkey_btn.setStyleSheet(f"""
QPushButton {{
padding: {self.scaled_value(8)}px;
font-size: {self.scaled_value(12)}px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: {self.scaled_value(3)}px;
}}
QPushButton:hover {{
background-color: #45a049;
}}
QPushButton:pressed {{
background-color: #3d8b40;
}}
""")
hotkey_layout.addWidget(self.record_hotkey_btn)
hotkey_group.setLayout(hotkey_layout)
left_layout.addWidget(hotkey_group)
# 6. 其他设置按钮
other_settings_group = QGroupBox("高级设置")
other_settings_layout = QVBoxLayout()
self.other_settings_btn = QPushButton("⚙️ 其他设置")
self.other_settings_btn.setMinimumHeight(self.scaled_value(40))
self.other_settings_btn.clicked.connect(self.open_other_settings)
self.other_settings_btn.setStyleSheet(f"""
QPushButton {{
padding: {self.scaled_value(10)}px;
font-size: {self.scaled_value(12)}px;
background-color: #2196F3;
color: white;
border: none;
border-radius: {self.scaled_value(3)}px;
}}
QPushButton:hover {{
background-color: #1976D2;
}}
QPushButton:pressed {{
background-color: #1565C0;
}}
""")
other_settings_layout.addWidget(self.other_settings_btn)
other_settings_group.setLayout(other_settings_layout)
left_layout.addWidget(other_settings_group)
left_layout.addStretch()
left_widget.setLayout(left_layout)
right_widget = QWidget()
right_layout = QVBoxLayout()
right_layout.setSpacing(self.scaled_value(10))
top_widget = QWidget()
top_layout = QHBoxLayout()
top_layout.setSpacing(self.scaled_value(10))
step_ops_group = QGroupBox("步骤操作")
step_ops_layout = QGridLayout()
step_ops_layout.setSpacing(self.scaled_value(5))
self.add_image_btn = QPushButton("📷 添加图像")
self.add_image_btn.setMinimumHeight(self.scaled_value(40))
self.add_image_btn.clicked.connect(self.add_image_step)
step_ops_layout.addWidget(self.add_image_btn, 0, 0)
self.add_coordinate_btn = QPushButton("🎯 添加坐标")
self.add_coordinate_btn.setMinimumHeight(self.scaled_value(40))
self.add_coordinate_btn.clicked.connect(self.start_add_coordinate_mode)
step_ops_layout.addWidget(self.add_coordinate_btn, 0, 1)
self.add_keyboard_btn = QPushButton("⌨️ 添加键盘")
self.add_keyboard_btn.setMinimumHeight(self.scaled_value(40))
self.add_keyboard_btn.clicked.connect(self.add_keyboard_step)
step_ops_layout.addWidget(self.add_keyboard_btn, 1, 0)
self.add_text_btn = QPushButton("📝 添加文字")
self.add_text_btn.setMinimumHeight(self.scaled_value(40))
self.add_text_btn.clicked.connect(self.add_text_step)
step_ops_layout.addWidget(self.add_text_btn, 1, 1)
self.add_drag_btn = QPushButton("🖱️ 鼠标拖动")
self.add_drag_btn.setMinimumHeight(self.scaled_value(40))
self.add_drag_btn.clicked.connect(self.start_add_drag_mode)
step_ops_layout.addWidget(self.add_drag_btn, 2, 0)
self.delete_btn = QPushButton("🗑️ 删除")
self.delete_btn.setMinimumHeight(self.scaled_value(40))
self.delete_btn.clicked.connect(self.delete_step)
step_ops_layout.addWidget(self.delete_btn, 2, 1)
self.move_up_btn = QPushButton("⬆️ 上移")
self.move_up_btn.setMinimumHeight(self.scaled_value(40))
self.move_up_btn.clicked.connect(self.move_step_up)
step_ops_layout.addWidget(self.move_up_btn, 3, 0)
self.move_down_btn = QPushButton("⬇️ 下移")
self.move_down_btn.setMinimumHeight(self.scaled_value(40))
self.move_down_btn.clicked.connect(self.move_step_down)
step_ops_layout.addWidget(self.move_down_btn, 3, 1)
step_ops_group.setLayout(step_ops_layout)
top_layout.addWidget(step_ops_group)
preview_group = QGroupBox("预览区域")
preview_layout = QVBoxLayout()
self.preview_label = QLabel("预览区域")
self.preview_label.setAlignment(Qt.AlignCenter)
self.preview_label.setMinimumHeight(self.scaled_value(150))
self.preview_label.setMaximumHeight(self.scaled_value(150))
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; border-radius: {self.scaled_value(3)}px; "
f"background-color: #f9f9f9; font-size: {self.scaled_value(11)}px; padding: {self.scaled_value(5)}px;"
)
preview_layout.addWidget(self.preview_label)
preview_group.setLayout(preview_layout)
top_layout.addWidget(preview_group)
top_widget.setLayout(top_layout)
right_layout.addWidget(top_widget)
steps_group = QGroupBox("步骤列表")
steps_layout = QVBoxLayout()
self.steps_table = QTableWidget()
self.steps_table.setColumnCount(9)
self.steps_table.setHorizontalHeaderLabels([
"步骤", "类型", "点击类型", "等待(ms)",
"相似度", "接受偏移", "接受随机延迟",
"找到跳转", "未找到跳转"
])
header = self.steps_table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
header.setSectionsClickable(False)
header.setSectionsMovable(False)
self.steps_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.steps_table.setAlternatingRowColors(True)
self.steps_table.setStyleSheet(f"""
QTableWidget {{
gridline-color: #ddd;
font-size: {self.scaled_value(11)}px;
border: 1px solid #ccc;
border-radius: {self.scaled_value(3)}px;
}}
QTableWidget::item {{
padding: {self.scaled_value(6)}px {self.scaled_value(4)}px;
min-height: {self.scaled_value(20)}px;
text-align: center;
}}
QTableWidget::item:selected {{
background-color: #e3f2fd;
}}
QHeaderView::section {{
background-color: #f0f0f0;
padding: {self.scaled_value(6)}px;
border: 1px solid #ddd;
font-weight: bold;
text-align: center;
}}
""")
self.steps_table.verticalHeader().setDefaultSectionSize(self.scaled_value(35))
self.steps_table.verticalHeader().setVisible(False)
self.steps_table.cellClicked.connect(self.on_step_selected_from_table)
steps_layout.addWidget(self.steps_table)
steps_group.setLayout(steps_layout)
right_layout.addWidget(steps_group)
right_widget.setLayout(right_layout)
main_splitter.addWidget(left_widget)
main_splitter.addWidget(right_widget)
main_splitter.setSizes([self.scaled_value(350), self.scaled_value(850)])
main_layout.addWidget(main_splitter)
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
def resizeEvent(self, event):
"""窗口大小改变时保存尺寸"""
super().resizeEvent(event)
# 只在非最大化状态下保存尺寸
if not self.isMaximized():
self.save_window_size()
def open_other_settings(self):
"""打开其他设置对话框"""
dialog = OtherSettingsDialog(self)
dialog.exec_()
def start_record_hotkey(self):
"""开始录制热键"""
self.is_recording_hotkey = True
self.record_hotkey_btn.setText("🎤 正在录制...按ESC取消")
self.record_hotkey_btn.setStyleSheet(f"""
QPushButton {{
padding: {self.scaled_value(8)}px;
font-size: {self.scaled_value(12)}px;
background-color: #f44336;
color: white;
border: none;
border-radius: {self.scaled_value(3)}px;
}}
QPushButton:hover {{
background-color: #da190b;
}}
""")
self.hotkey_display.setText("请按下组合键...")
# 启动键盘监听
self.hotkey_listener = KeyboardListener(on_press=self.on_hotkey_record_press)
self.hotkey_listener.start()
def on_hotkey_record_press(self, key):
"""录制热键时的按键回调"""
if not self.is_recording_hotkey:
return False
# 按ESC取消录制
if key == Key.esc:
self.stop_record_hotkey()
return False
# 记录热键
try:
keys = []
if self.keyboard.ctrl_pressed:
keys.append("Ctrl")
if self.keyboard.alt_pressed:
keys.append("Alt")
if self.keyboard.shift_pressed:
keys.append("Shift")
# 添加主键
if hasattr(key, 'char') and key.char:
keys.append(key.char.upper())
elif hasattr(key, 'name'):
keys.append(key.name.capitalize())
if keys:
hotkey_str = "+".join(keys)
self.hotkey_recorded.emit(hotkey_str)
return False
except:
pass
return True
def on_hotkey_recorded(self, hotkey_str):
"""热键录制完成"""
self.hotkey = hotkey_str
self.hotkey_display.setText(hotkey_str)
self.stop_record_hotkey()
self.setup_global_hotkey()
def stop_record_hotkey(self):
"""停止录制热键"""
self.is_recording_hotkey = False
if hasattr(self, 'hotkey_listener'):
self.hotkey_listener.stop()
self.record_hotkey_btn.setText("🎤 录制热键")
self.record_hotkey_btn.setStyleSheet(f"""
QPushButton {{
padding: {self.scaled_value(8)}px;
font-size: {self.scaled_value(12)}px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: {self.scaled_value(3)}px;
}}
QPushButton:hover {{
background-color: #45a049;
}}
QPushButton:pressed {{
background-color: #3d8b40;
}}
""")
def toggle_timed_execution(self, state):
self.timed_execution_enabled = (state == Qt.Checked)
def load_global_settings(self):
"""加载全局设置(热键等)"""
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r', encoding='utf-8') as f:
settings = json.load(f)
self.hotkey = settings.get("hotkey", "F12")
# 加载定时设置
self.timed_execution_enabled = settings.get("timed_execution_enabled", False)
start_time_str = settings.get("start_datetime", "")
end_time_str = settings.get("end_datetime", "")
# 尝试解析时间字符串,支持新旧两种格式
if start_time_str:
# 先尝试新格式(精确到分钟)
self.start_datetime = QDateTime.fromString(start_time_str, "yyyy-MM-dd HH:mm")
if not self.start_datetime.isValid():
# 尝试旧格式(精确到秒)
self.start_datetime = QDateTime.fromString(start_time_str, "yyyy-MM-dd HH:mm:ss")
if self.start_datetime.isValid():
# 如果是旧格式,将秒设为0
self.start_datetime.setTime(QTime(self.start_datetime.time().hour(),
self.start_datetime.time().minute(), 0))
if end_time_str:
# 先尝试新格式(精确到分钟)
self.end_datetime = QDateTime.fromString(end_time_str, "yyyy-MM-dd HH:mm")
if not self.end_datetime.isValid():
# 尝试旧格式(精确到秒)
self.end_datetime = QDateTime.fromString(end_time_str, "yyyy-MM-dd HH:mm:ss")
if self.end_datetime.isValid():
# 如果是旧格式,将秒设为0
self.end_datetime.setTime(QTime(self.end_datetime.time().hour(),
self.end_datetime.time().minute(), 0))
# 加载其他设置
self.loop_type = settings.get("loop_type", 0)
self.loop_count = settings.get("loop_count", 1000)
self.offset_enabled = settings.get("offset_enabled", False)
self.offset_x = settings.get("offset_x", 5)
self.offset_y = settings.get("offset_y", 5)
self.random_delay_enabled = settings.get("random_delay_enabled", False)
self.random_delay_min = settings.get("random_delay_min", 100)
self.random_delay_max = settings.get("random_delay_max", 1000)
print(f"已从 {self.settings_file} 加载设置")
except Exception as e:
print(f"加载全局设置失败: {e}")
self.hotkey = "F12"
self.timed_execution_enabled = False
def save_global_settings(self):
"""保存全局设置"""
settings = {
"hotkey": self.hotkey,
"loop_type": self.loop_type,
"loop_count": self.loop_count,
"offset_enabled": self.offset_enabled,
"offset_x": self.offset_x,
"offset_y": self.offset_y,
"random_delay_enabled": self.random_delay_enabled,
"random_delay_min": self.random_delay_min,
"random_delay_max": self.random_delay_max,
"window_width": self.window_width,
"window_height": self.window_height
}
try:
with open(self.settings_file, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
print(f"全局设置已保存到: {self.settings_file}")
except Exception as e:
print(f"保存全局设置失败: {e}")
def set_controls_enabled(self, enabled):
controls = [
self.start_btn, self.stop_btn, self.pause_btn,
self.save_btn, self.load_btn,
self.timed_execution_checkbox,
self.start_datetime_edit, self.end_datetime_edit,
self.select_window_btn, self.clear_window_btn,
self.record_hotkey_btn, self.other_settings_btn,
self.steps_table, self.add_image_btn,
self.add_coordinate_btn, self.add_keyboard_btn, self.add_text_btn,
self.add_drag_btn, self.delete_btn, self.move_up_btn,
self.move_down_btn
]
for control in controls:
control.setEnabled(enabled)
def setup_global_hotkey(self):
try:
if self.global_hotkey_listener:
self.global_hotkey_listener.stop()
keys = self.parse_hotkey_string(self.hotkey)
# 修复拼写错误:global_hotkey_listner -> global_hotkey_listener
self.global_hotkey_listener = KeyboardListener(
on_press=lambda key: self.on_global_key_press(key, keys)
)
self.global_hotkey_listener.start()
print(f"全局热键已设置: {self.hotkey}")
except Exception as e:
print(f"全局热键设置失败: {str(e)}")
def parse_hotkey_string(self, hotkey_str):
keys = []
parts = hotkey_str.lower().split('+')
for part in parts:
part = part.strip()
if part == 'ctrl':
keys.append(Key.ctrl)
elif part == 'alt':
keys.append(Key.alt)
elif part == 'shift':
keys.append(Key.shift)
elif part.startswith('f') and part[1:].isdigit():
keys.append(getattr(Key, f'f{part[1:]}'))
elif len(part) == 1:
keys.append(part)
return keys
def on_global_key_press(self, key, target_keys):
try:
if isinstance(target_keys[-1], str):
if hasattr(key, 'char') and key.char and key.char.lower() == target_keys[-1].lower():
if all(
self.keyboard.ctrl_pressed if m == Key.ctrl else
self.keyboard.alt_pressed if m == Key.alt else
self.keyboard.shift_pressed
for m in target_keys[:-1]
):
QTimer.singleShot(0, self.toggle_execution)
return True
else:
if key == target_keys[-1]:
if all(
self.keyboard.ctrl_pressed if m == Key.ctrl else
self.keyboard.alt_pressed if m == Key.alt else
self.keyboard.shift_pressed
for m in target_keys[:-1]
):
QTimer.singleShot(0, self.toggle_execution)
return True
except:
pass
return True
def start_select_window_mode(self):
self.is_selecting_window = True
self.setWindowState(Qt.WindowMinimized)
self.preview_label.setText("请将鼠标移动到目标窗口上并点击左键...\n按ESC取消")
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; background-color: #fff3cd; color: #856404; font-size: {self.scaled_value(11)}px;"
)
self.mouse_listener = MouseListener(on_click=self.on_select_window_click)
self.mouse_listener.start()
self.keyboard_listener = KeyboardListener(on_press=self.on_select_window_cancel)
self.keyboard_listener.start()
def on_select_window_click(self, x, y, button, pressed):
if pressed and button == Button.left:
hwnd = win32gui.WindowFromPoint((x, y))
while win32gui.GetParent(hwnd) != 0:
hwnd = win32gui.GetParent(hwnd)
self.target_hwnd = hwnd
window_title = win32gui.GetWindowText(hwnd)
self.window_title_edit.setText(f"{window_title} (HWND: {hwnd})")
self.stop_select_window_mode_signal.emit()
return False
return True
def on_select_window_cancel(self, key):
if key == Key.esc:
self.stop_select_window_mode_signal.emit()
return False
return True
def stop_select_window_mode(self):
self.is_selecting_window = False
if self.mouse_listener:
self.mouse_listener.stop()
self.mouse_listener = None
if self.keyboard_listener:
self.keyboard_listener.stop()
self.keyboard_listener = None
self.showNormal()
self.activateWindow()
self.preview_label.setText("预览区域")
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; background-color: #f9f9f9; font-size: {self.scaled_value(11)}px; padding: {self.scaled_value(5)}px;"
)
def start_add_coordinate_mode(self):
self.is_adding_coordinate = True
self.setWindowState(Qt.WindowMinimized)
self.preview_label.setText("请在屏幕任意位置点击要添加的坐标...\n按ESC或右键取消")
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; background-color: #fff3cd; color: #856404; font-size: {self.scaled_value(11)}px;"
)
self.mouse_listener = MouseListener(on_click=self.on_global_mouse_click)
self.mouse_listener.start()
self.keyboard_listener = KeyboardListener(on_press=self.on_global_key_press_for_cancel)
self.keyboard_listener.start()
def on_global_mouse_click(self, x, y, button, pressed):
if pressed:
if button == Button.left:
self.coordinate_captured.emit(x, y)
return False
elif button == Button.right:
self.stop_coordinate_mode.emit()
return False
return True
def on_global_key_press_for_cancel(self, key):
if key == Key.esc:
self.stop_coordinate_mode.emit()
return False
return True
def stop_add_coordinate_mode(self):
self.is_adding_coordinate = False
if self.mouse_listener:
self.mouse_listener.stop()
self.mouse_listener = None
if self.keyboard_listener:
self.keyboard_listener.stop()
self.keyboard_listener = None
self.showNormal()
self.activateWindow()
self.preview_label.setText("预览区域")
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; background-color: #f9f9f9; font-size: {self.scaled_value(11)}px; padding: {self.scaled_value(5)}px;"
)
def start_add_drag_mode(self):
"""开始添加鼠标拖动步骤"""
self.is_adding_drag = True
self.drag_points = []
self.setWindowState(Qt.WindowMinimized)
self.preview_label.setText("请点击第一个坐标点(起点)...\n按ESC或右键取消")
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; background-color: #fff3cd; color: #856404; font-size: {self.scaled_value(11)}px;"
)
self.mouse_listener = MouseListener(on_click=self.on_drag_mouse_click)
self.mouse_listener.start()
self.keyboard_listener = KeyboardListener(on_press=self.on_global_key_press_for_cancel)
self.keyboard_listener.start()
def on_drag_mouse_click(self, x, y, button, pressed):
if pressed:
if button == Button.left:
self.drag_points.append((x, y))
if len(self.drag_points) == 1:
self.preview_label.setText("请点击第二个坐标点(终点)...")
elif len(self.drag_points) == 2:
self.drag_coordinate_captured.emit(x, y)
return False
elif button == Button.right:
self.stop_add_drag_mode()
return False
return True
def on_drag_coordinate_captured(self, x, y):
"""处理鼠标拖动坐标采集完成"""
start_x, start_y = self.drag_points[0]
end_x, end_y = self.drag_points[1]
# 转换为相对于目标窗口的坐标
if self.target_hwnd:
left, top, _, _ = win32gui.GetWindowRect(self.target_hwnd)
start_x -= left
start_y -= top
end_x -= left
end_y -= top
# 显示拖动设置对话框
dialog = DragSettingsDialog(self, (start_x, start_y), (end_x, end_y))
if dialog.exec_() == QDialog.Accepted:
step = Step(
step_type='drag',
drag_start_x=start_x,
drag_start_y=start_y,
drag_end_x=end_x,
drag_end_y=end_y,
click_type=dialog.get_drag_type(),
wait_time=1000,
random_delay_enabled=self.random_delay_enabled,
drag_start_delay=dialog.get_start_delay(),
drag_duration=dialog.get_duration()
)
self.steps.append(step)
self.update_steps_table()
self.stop_add_drag_mode()
def stop_add_drag_mode(self):
"""停止添加鼠标拖动步骤"""
self.is_adding_drag = False
self.drag_points = []
if self.mouse_listener:
self.mouse_listener.stop()
self.mouse_listener = None
if self.keyboard_listener:
self.keyboard_listener.stop()
self.keyboard_listener = None
self.showNormal()
self.activateWindow()
self.preview_label.setText("预览区域")
self.preview_label.setStyleSheet(
f"border: 1px solid #ccc; background-color: #f9f9f9; font-size: {self.scaled_value(11)}px; padding: {self.scaled_value(5)}px;"
)
def add_image_step(self):
self.is_adding_image = True
self.setWindowState(Qt.WindowMinimized)
self.screenshot_window = ScreenshotWindow(self)
self.screenshot_window.image_captured.connect(self.on_image_captured)
self.screenshot_window.closed.connect(self.on_screenshot_closed)
self.screenshot_window.setWindowFlags(
self.screenshot_window.windowFlags()
| Qt.WindowStaysOnTopHint
| Qt.Tool
)
self.screenshot_window.show()
self.screenshot_window.raise_()
self.screenshot_window.activateWindow()
def add_text_step(self):
"""添加文字输入步骤"""
dialog = TextInputDialog(self)
if dialog.exec_() == QDialog.Accepted:
text = dialog.get_text()
if text:
step = Step(
step_type='text',
text_content=text,
wait_time=1000,
click_type='press',
random_delay_enabled=self.random_delay_enabled
)
self.steps.append(step)
self.update_steps_table()
def add_keyboard_step(self):
"""添加键盘步骤"""
dialog = KeyboardInputDialog(self)
if dialog.exec_() == QDialog.Accepted:
key_sequence = dialog.get_key_sequence()
if key_sequence:
step = Step(
step_type='keyboard',
key_sequence=key_sequence,
wait_time=1000,
click_type='press',
random_delay_enabled=self.random_delay_enabled,
key_action_type=dialog.get_action_type(),
key_hold_time=dialog.get_hold_time(),
key_hold_image_path=dialog.get_hold_image_path()
)
self.steps.append(step)
self.update_steps_table()
def on_keyboard_image_selected(self, image_path):
"""处理键盘对话框中的图像选择"""
# 发送信号给键盘对话框
if hasattr(self, 'current_keyboard_dialog'):
self.current_keyboard_dialog.set_captured_image(image_path)
def on_screenshot_closed(self):
self.is_adding_image = False
self.showNormal()
self.activateWindow()
def on_coordinate_captured(self, x, y):
rel_x, rel_y = x, y
if self.target_hwnd:
left, top, _, _ = win32gui.GetWindowRect(self.target_hwnd)
rel_x -= left
rel_y -= top
step = Step(
step_type='coordinate',
x=rel_x,
y=rel_y,
wait_time=1000,
click_type='left',
random_delay_enabled=self.random_delay_enabled
)
self.steps.append(step)
self.update_steps_table()
self.stop_add_coordinate_mode()
def toggle_offset(self, state):
enabled = state == Qt.Checked
self.offset_x_spin.setEnabled(enabled)
self.offset_y_spin.setEnabled(enabled)
def toggle_random_delay(self, state):
enabled = state == Qt.Checked
self.random_delay_min_spin.setEnabled(enabled)
self.random_delay_max_spin.setEnabled(enabled)
def toggle_execution(self):
self.update_all_settings()
# 热键启动同步定时时间
if self.timed_execution_checkbox.isChecked():
self.start_datetime = self.start_datetime_edit.dateTime()
self.end_datetime = self.end_datetime_edit.dateTime()
if self.is_running:
self.stop_execution()
else:
self.start_execution()
def get_loop_count(self):
return 999999 if self.loop_type == 0 else self.loop_count
def start_execution(self):
if not self.steps:
QMessageBox.warning(self, "错误", "没有可执行的步骤")
return
# 每次启动强制同步界面最新定时时间
if self.timed_execution_checkbox.isChecked():
start_dt = self.start_datetime_edit.dateTime()
end_dt = self.end_datetime_edit.dateTime()
if start_dt >= end_dt:
QMessageBox.warning(self, "错误", "开始时间必须早于结束时间")
return
current_dt = QDateTime.currentDateTime()
if current_dt > end_dt:
QMessageBox.warning(self, "错误", "结束时间已过,请重新设置")
return
# 强制刷新全局定时变量,线程实时读取
self.start_datetime = start_dt
self.end_datetime = end_dt
else:
self.start_datetime = None
self.end_datetime = None
if self.target_hwnd and not ctypes.windll.shell32.IsUserAnAdmin():
reply = QMessageBox.question(
self, '权限提示',
'检测到您选择了指定窗口,为保证后台点击生效,建议以管理员身份运行此程序。\n是否继续?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.No:
return
self.update_all_settings()
if self.is_paused:
self.is_paused = False
self.is_running = True
self.execution_thread.resume()
self.pause_btn.setText("⏸ 暂停执行")
else:
self.current_step_index = 0
self.is_running = True
self.is_paused = False
loop_count = self.get_loop_count()
self.execution_thread.set_params(loop_count, self.current_step_index)
self.execution_thread.start()
self.set_controls_enabled(False)
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.pause_btn.setEnabled(True)
def stop_execution(self):
self.is_running = False
self.is_paused = False
self.current_step_index = 0
self.execution_thread.stop()
self.set_controls_enabled(True)
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.pause_btn.setEnabled(False)
self.pause_btn.setText("⏸ 暂停执行")
def pause_execution(self):
if self.is_running and not self.is_paused:
self.is_paused = True
self.is_running = False
self.execution_thread.pause()
self.start_btn.setEnabled(True)
self.pause_btn.setText("▶ 继续执行")
elif self.is_paused:
self.update_all_settings()
self.is_paused = False
self.is_running = True
self.execution_thread.resume()
self.start_btn.setEnabled(False)
self.pause_btn.setText("⏸ 暂停执行")
def update_all_settings(self):
for step in self.steps:
step.random_delay_enabled = self.random_delay_enabled
step.random_delay_min = self.random_delay_min
step.random_delay_max = self.random_delay_max
def clear_target_window(self):
self.target_hwnd = None
self.window_title_edit.clear()
QMessageBox.information(self, "提示", "已清除目标窗口")
def on_execution_finished(self):
self.is_running = False
self.is_paused = False
self.current_step_index = 0
self.set_controls_enabled(True)
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.pause_btn.setEnabled(False)
self.pause_btn.setText("⏸ 暂停执行")
def on_execution_error(self, error_msg):
self.is_running = False
self.is_paused = False
self.current_step_index = 0
self.set_controls_enabled(True)
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.pause_btn.setEnabled(False)
self.pause_btn.setText("⏸ 暂停执行")
QMessageBox.critical(self, "错误", f"执行出错: {error_msg}")
def on_step_completed(self, step_index):
self.current_step_index = step_index
self.steps_table.selectRow(step_index)
def on_image_captured(self, image_path):
# 将截图保存到screenshots目录
filename = os.path.basename(image_path)
new_path = os.path.join(self.screenshot_dir, filename)
shutil.move(image_path, new_path)
image_path = new_path
if self.target_hwnd:
left, top, right, bottom = win32gui.GetWindowRect(self.target_hwnd)
bbox = (left, top, right, bottom)
screen_image = ImageGrab.grab(bbox)
template_path = os.path.join(self.screenshot_dir, f"template_{int(time.time())}.png")
screen_image.save(template_path)
image_path = template_path
step = Step(
step_type='image',
image_path=image_path,
wait_time=1000,
similarity=0.8,
click_type='left',
random_delay_enabled=self.random_delay_enabled
)
self.steps.append(step)
self.update_steps_table()
def delete_step(self):
current_row = self.steps_table.currentRow()
if current_row >= 0:
del self.steps[current_row]
self.update_steps_table()
def move_step_up(self):
current_row = self.steps_table.currentRow()
if current_row > 0:
self.steps[current_row], self.steps[current_row - 1] = \
self.steps[current_row - 1], self.steps[current_row]
self.update_steps_table()
self.steps_table.selectRow(current_row - 1)
def move_step_down(self):
current_row = self.steps_table.currentRow()
if current_row < len(self.steps) - 1:
self.steps[current_row], self.steps[current_row + 1] = \
self.steps[current_row + 1], self.steps[current_row]
self.update_steps_table()
self.steps_table.selectRow(current_row + 1)
def update_steps_table(self):
self.steps_table.setRowCount(len(self.steps))
for i, step in enumerate(self.steps):
step_item = QTableWidgetItem(f"{i + 1}")
step_item.setTextAlignment(Qt.AlignCenter)
self.steps_table.setItem(i, 0, step_item)
# 类型列
if step.step_type == 'image':
type_text = "图像识别"
elif step.step_type == 'coordinate':
type_text = "坐标点击"
elif step.step_type == 'keyboard':
action_types = ["按键一次", "长按指定时间", "按住直到图像出现"]
action_text = action_types[step.key_action_type]
if step.key_action_type == 1: # 长按指定时间
action_text += f"({step.key_hold_time}ms)"
elif step.key_action_type == 2: # 按住直到图像出现
if step.key_hold_image_path and os.path.exists(step.key_hold_image_path):
img_name = os.path.basename(step.key_hold_image_path)
action_text += f"(等待:{img_name})"
else:
action_text += "(等待:未设置图像)"
type_text = f"按键: {step.key_sequence} [{action_text}]"
elif step.step_type == 'text':
type_text = "文字输入"
elif step.step_type == 'drag':
delay_info = ""
if step.drag_start_delay > 0:
delay_info += f"延迟{step.drag_start_delay}ms"
if step.drag_duration > 0:
delay_info += f" 持续{step.drag_duration}ms"
type_text = f"鼠标拖动{delay_info}"
else:
type_text = "未知"
type_item = QTableWidgetItem(type_text)
type_item.setTextAlignment(Qt.AlignCenter)
self.steps_table.setItem(i, 1, type_item)
# 点击类型列
click_combo = QComboBox()
if step.step_type == 'keyboard':
click_combo.addItem("按键一次")
click_combo.setEnabled(False)
elif step.step_type == 'text':
click_combo.addItem("输入文字")
click_combo.setEnabled(False)
elif step.step_type == 'drag':
click_combo.addItems(["左键拖动", "右键拖动"])
if step.click_type == 'left':
click_combo.setCurrentIndex(0)
else:
click_combo.setCurrentIndex(1)
else:
click_combo.addItems(["左键单击", "右键单击", "双击"])
if step.click_type == 'left':
click_combo.setCurrentIndex(0)
elif step.click_type == 'right':
click_combo.setCurrentIndex(1)
elif step.click_type == 'double':
click_combo.setCurrentIndex(2)
click_combo.currentTextChanged.connect(
lambda text, row=i: self.update_step_click_type(row, text)
)
self.steps_table.setCellWidget(i, 2, click_combo)
# 等待时间列
wait_spin = QSpinBox()
wait_spin.setRange(0, 3600000)
wait_spin.setValue(step.wait_time)
wait_spin.valueChanged.connect(
lambda value, row=i: self.update_step_wait_time(row, value)
)
self.steps_table.setCellWidget(i, 3, wait_spin)
# 相似度列(仅图像步骤)
if step.step_type == 'image':
similarity_spin = QDoubleSpinBox()
similarity_spin.setRange(0.1, 1.0)
similarity_spin.setSingleStep(0.05)
similarity_spin.setValue(step.similarity)
similarity_spin.setDecimals(2)
similarity_spin.valueChanged.connect(
lambda value, row=i: self.update_step_similarity(row, value)
)
self.steps_table.setCellWidget(i, 4, similarity_spin)
else:
similarity_item = QTableWidgetItem("-")
similarity_item.setTextAlignment(Qt.AlignCenter)
self.steps_table.setItem(i, 4, similarity_item)
# 接受偏移列
offset_checkbox = QCheckBox()
offset_checkbox.setStyleSheet("QCheckBox { margin-left: auto; margin-right: auto; }")
offset_checkbox.setChecked(step.accept_offset)
offset_checkbox.setEnabled(step.step_type not in ['keyboard', 'text', 'drag'])
offset_checkbox.stateChanged.connect(
lambda state, row=i: self.update_step_accept_offset(row, state)
)
self.steps_table.setCellWidget(i, 5, offset_checkbox)
# 接受随机延迟列
random_delay_checkbox = QCheckBox()
random_delay_checkbox.setStyleSheet("QCheckBox { margin-left: auto; margin-right: auto; }")
random_delay_checkbox.setChecked(step.accept_random_delay)
random_delay_checkbox.stateChanged.connect(
lambda state, row=i: self.update_step_accept_random_delay(row, state)
)
self.steps_table.setCellWidget(i, 6, random_delay_checkbox)
# 找到跳转列(仅图像步骤)
found_combo = QComboBox()
found_combo.addItem("无跳转", -1)
for j in range(len(self.steps)):
found_combo.addItem(f"步骤 {j + 1}", j)
if step.jump_on_found is not None and step.jump_on_found < len(self.steps):
found_combo.setCurrentIndex(step.jump_on_found + 1)
found_combo.setEnabled(step.step_type == 'image')
found_combo.currentIndexChanged.connect(
lambda index, row=i: self.update_step_jump_on_found(row, index)
)
self.steps_table.setCellWidget(i, 7, found_combo)
# 未找到跳转列(仅图像步骤)
not_found_combo = QComboBox()
not_found_combo.addItem("无跳转", -1)
for j in range(len(self.steps)):
not_found_combo.addItem(f"步骤 {j + 1}", j)
if step.jump_on_not_found is not None and step.jump_on_not_found < len(self.steps):
not_found_combo.setCurrentIndex(step.jump_on_not_found + 1)
not_found_combo.setEnabled(step.step_type == 'image')
not_found_combo.currentIndexChanged.connect(
lambda index, row=i: self.update_step_jump_on_not_found(row, index)
)
self.steps_table.setCellWidget(i, 8, not_found_combo)
def update_step_jump_on_found(self, row, index):
if row < len(self.steps):
combo = self.steps_table.cellWidget(row, 7)
if combo:
jump_to = combo.itemData(index)
self.steps[row].jump_on_found = jump_to if jump_to != -1 else None
def update_step_jump_on_not_found(self, row, index):
if row < len(self.steps):
combo = self.steps_table.cellWidget(row, 8)
if combo:
jump_to = combo.itemData(index)
self.steps[row].jump_on_not_found = jump_to if jump_to != -1 else None
def update_step_accept_offset(self, row, state):
if row < len(self.steps):
self.steps[row].accept_offset = (state == Qt.Checked)
def update_step_accept_random_delay(self, row, state):
if row < len(self.steps):
self.steps[row].accept_random_delay = (state == Qt.Checked)
def update_step_click_type(self, row, text):
if row < len(self.steps):
if text == "左键单击":
self.steps[row].click_type = 'left'
elif text == "右键单击":
self.steps[row].click_type = 'right'
elif text == "双击":
self.steps[row].click_type = 'double'
elif text == "左键拖动":
self.steps[row].click_type = 'left'
elif text == "右键拖动":
self.steps[row].click_type = 'right'
def update_step_wait_time(self, row, value):
if row < len(self.steps):
self.steps[row].wait_time = value
def update_step_similarity(self, row, value):
if row < len(self.steps):
self.steps[row].similarity = value
def on_step_selected_from_table(self, row, column):
if row < len(self.steps):
step = self.steps[row]
if step.step_type == 'image' and step.image_path and os.path.exists(step.image_path):
pixmap = QPixmap(step.image_path)
self.preview_label.setPixmap(pixmap.scaled(
self.scaled_value(300),
self.scaled_value(150),
Qt.KeepAspectRatio
))
elif step.step_type == 'coordinate':
self.preview_label.setText(f"坐标: ({step.x}, {step.y})")
elif step.step_type == 'keyboard':
action_types = ["按键一次", "长按指定时间", "按住直到图像出现"]
action_text = action_types[step.key_action_type]
detail_text = f"按键: {step.key_sequence}\n操作类型: {action_text}"
if step.key_action_type == 1: # 长按指定时间
detail_text += f"\n按住时间: {step.key_hold_time}ms"
elif step.key_action_type == 2: # 按住直到图像出现
if step.key_hold_image_path and os.path.exists(step.key_hold_image_path):
img_name = os.path.basename(step.key_hold_image_path)
detail_text += f"\n等待图像: {img_name}"
else:
detail_text += "\n等待图像: 未设置"
self.preview_label.setText(detail_text)
elif step.step_type == 'text':
preview_text = step.text_content[:50] + "..." if len(step.text_content) > 50 else step.text_content
self.preview_label.setText(f"文字: {preview_text}")
elif step.step_type == 'drag':
delay_info = ""
if step.drag_start_delay > 0:
delay_info += f"\n开始延迟: {step.drag_start_delay}ms"
if step.drag_duration > 0:
delay_info += f"\n拖动持续: {step.drag_duration}ms"
self.preview_label.setText(f"拖动: ({step.drag_start_x},{step.drag_start_y}) → ({step.drag_end_x},{step.drag_end_y}){delay_info}")
def save_config(self):
file_path, _ = QFileDialog.getSaveFileName(self, "保存配置", "", "JSON文件 (*.json)")
if not file_path:
return
try:
config = {
"steps": [],
"loop_type": self.loop_type,
"loop_count": self.loop_count,
"offset_enabled": self.offset_enabled,
"offset_x": self.offset_x,
"offset_y": self.offset_y,
"random_delay_enabled": self.random_delay_enabled,
"random_delay_min": self.random_delay_min,
"random_delay_max": self.random_delay_max,
}
config_dir = os.path.dirname(file_path)
screenshots_dir = os.path.join(config_dir, "screenshots")
# 确保截图目录存在
os.makedirs(screenshots_dir, exist_ok=True)
for i, step in enumerate(self.steps):
step_data = {
"step_type": step.step_type,
"image_path": step.image_path,
"x": step.x,
"y": step.y,
"click_type": step.click_type,
"wait_time": step.wait_time,
"similarity": step.similarity,
"random_delay_enabled": step.random_delay_enabled,
"random_delay_min": step.random_delay_min,
"random_delay_max": step.random_delay_max,
"accept_offset": step.accept_offset,
"accept_random_delay": step.accept_random_delay,
"jump_on_found": step.jump_on_found,
"jump_on_not_found": step.jump_on_not_found,
"key_sequence": step.key_sequence,
"text_content": step.text_content,
"drag_start_x": step.drag_start_x,
"drag_start_y": step.drag_start_y,
"drag_end_x": step.drag_end_x,
"drag_end_y": step.drag_end_y,
# 键盘操作相关字段
"key_action_type": step.key_action_type,
"key_hold_time": step.key_hold_time,
"key_hold_image_path": step.key_hold_image_path,
# 拖动缓冲相关字段
"drag_start_delay": step.drag_start_delay,
"drag_duration": step.drag_duration
}
if step.step_type == "image" and step.image_path:
src_path = step.image_path
if os.path.isabs(src_path):
# 使用安全的文件名
safe_filename = f"step_{i+1}_{int(time.time())}.png"
dst_path = os.path.join(screenshots_dir, safe_filename)
# 确保源文件存在
if os.path.exists(src_path):
try:
# 使用copy2而不是move,避免源文件丢失
shutil.copy2(src_path, dst_path)
step_data["image_path"] = os.path.join("screenshots", safe_filename)
except Exception as e:
print(f"复制图像文件失败: {e}")
# 如果复制失败,使用相对路径
step_data["image_path"] = os.path.relpath(src_path, config_dir)
else:
print(f"警告: 图像文件不存在: {src_path}")
step_data["image_path"] = src_path
# 处理键盘操作中使用的图像
if step.step_type == "keyboard" and step.key_action_type == 2 and step.key_hold_image_path:
src_path = step.key_hold_image_path
if os.path.isabs(src_path):
safe_filename = f"keyboard_wait_{i+1}_{int(time.time())}.png"
dst_path = os.path.join(screenshots_dir, safe_filename)
if os.path.exists(src_path):
try:
shutil.copy2(src_path, dst_path)
step_data["key_hold_image_path"] = os.path.join("screenshots", safe_filename)
except Exception as e:
print(f"复制键盘等待图像失败: {e}")
step_data["key_hold_image_path"] = src_path
config["steps"].append(step_data)
# 使用with语句确保文件正确关闭
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
QMessageBox.information(self, "成功", f"配置已保存到:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存配置时发生错误:\n{str(e)}")
import traceback
traceback.print_exc()
def load_config(self):
file_path, _ = QFileDialog.getOpenFileName(self, "加载配置", "", "JSON文件 (*.json)")
if not file_path:
return
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
self.steps.clear()
self.current_step_index = 0
self.is_running = False
self.is_paused = False
self.target_hwnd = None
self.window_title_edit.clear()
config_dir = os.path.dirname(file_path)
for step_data in config["steps"]:
if step_data["step_type"] == "image" and step_data["image_path"]:
path = step_data["image_path"]
if os.path.isabs(path):
final_path = path
else:
config_path = os.path.join(config_dir, path)
base_path = os.path.join(self.app_base_dir, path)
base_screenshots_path = os.path.join(self.app_base_dir, "screenshots", os.path.basename(path))
config_screenshots_path = os.path.join(config_dir, "screenshots", os.path.basename(path))
if os.path.exists(config_path):
final_path = config_path
elif os.path.exists(base_path):
final_path = base_path
elif os.path.exists(base_screenshots_path):
final_path = base_screenshots_path
elif os.path.exists(config_screenshots_path):
final_path = config_screenshots_path
else:
final_path = config_path
print(f"警告: 找不到图像文件: {path}")
step_data["image_path"] = final_path
# 处理键盘操作中使用的图像
if step_data["step_type"] == "keyboard" and step_data.get("key_hold_image_path"):
path = step_data["key_hold_image_path"]
if os.path.isabs(path):
final_path = path
else:
config_path = os.path.join(config_dir, path)
base_path = os.path.join(self.app_base_dir, path)
base_screenshots_path = os.path.join(self.app_base_dir, "screenshots", os.path.basename(path))
config_screenshots_path = os.path.join(config_dir, "screenshots", os.path.basename(path))
if os.path.exists(config_path):
final_path = config_path
elif os.path.exists(base_path):
final_path = base_path
elif os.path.exists(base_screenshots_path):
final_path = base_screenshots_path
elif os.path.exists(config_screenshots_path):
final_path = config_screenshots_path
else:
final_path = config_path
print(f"警告: 找不到键盘等待图像文件: {path}")
step_data["key_hold_image_path"] = final_path
# 兼容新增字段
step = Step(
step_type=step_data["step_type"],
image_path=step_data.get("image_path"),
x=step_data.get("x"),
y=step_data.get("y"),
click_type=step_data.get("click_type", "left"),
wait_time=step_data.get("wait_time", 1000),
similarity=step_data.get("similarity", 0.8),
random_delay_enabled=step_data.get("random_delay_enabled", False),
random_delay_min=step_data.get("random_delay_min", 100),
random_delay_max=step_data.get("random_delay_max", 1000),
accept_offset=step_data.get("accept_offset", True),
accept_random_delay=step_data.get("accept_random_delay", True),
jump_on_found=step_data.get("jump_on_found"),
jump_on_not_found=step_data.get("jump_on_not_found"),
key_sequence=step_data.get("key_sequence"),
text_content=step_data.get("text_content"),
drag_start_x=step_data.get("drag_start_x"),
drag_start_y=step_data.get("drag_start_y"),
drag_end_x=step_data.get("drag_end_x"),
drag_end_y=step_data.get("drag_end_y"),
# 新增字段的兼容处理
key_action_type=step_data.get("key_action_type", 0),
key_hold_time=step_data.get("key_hold_time", 0),
key_hold_image_path=step_data.get("key_hold_image_path"),
drag_start_delay=step_data.get("drag_start_delay", 0),
drag_duration=step_data.get("drag_duration", 1000)
)
self.steps.append(step)
# 加载设置
self.loop_type = config.get("loop_type", 0)
self.loop_count = config.get("loop_count", 1000)
self.offset_enabled = config.get("offset_enabled", False)
self.offset_x = config.get("offset_x", 5)
self.offset_y = config.get("offset_y", 5)
self.random_delay_enabled = config.get("random_delay_enabled", False)
self.random_delay_min = config.get("random_delay_min", 100)
self.random_delay_max = config.get("random_delay_max", 1000)
# 加载定时设置
timed_enabled = config.get("timed_execution_enabled", False)
self.timed_execution_checkbox.setChecked(timed_enabled)
start_datetime_str = config.get("start_datetime", "")
end_datetime_str = config.get("end_datetime", "")
# 尝试解析时间字符串,支持新旧两种格式
if start_datetime_str:
self.start_datetime = QDateTime.fromString(start_datetime_str, "yyyy-MM-dd HH:mm")
if not self.start_datetime.isValid():
# 尝试旧格式
self.start_datetime = QDateTime.fromString(start_datetime_str, "yyyy-MM-dd HH:mm:ss")
if self.start_datetime.isValid():
self.start_datetime.setTime(QTime(self.start_datetime.time().hour(),
self.start_datetime.time().minute(), 0))
if self.start_datetime.isValid():
self.start_datetime_edit.setDateTime(self.start_datetime)
if end_datetime_str:
self.end_datetime = QDateTime.fromString(end_datetime_str, "yyyy-MM-dd HH:mm")
if not self.end_datetime.isValid():
# 尝试旧格式
self.end_datetime = QDateTime.fromString(end_datetime_str, "yyyy-MM-dd HH:mm:ss")
if self.end_datetime.isValid():
self.end_datetime.setTime(QTime(self.end_datetime.time().hour(),
self.end_datetime.time().minute(), 0))
if self.end_datetime.isValid():
self.end_datetime_edit.setDateTime(self.end_datetime)
self.update_steps_table()
QMessageBox.information(self, "成功", f"配置已从以下位置加载:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "加载失败", f"加载配置时发生错误:\n{str(e)}")
import traceback
traceback.print_exc()
def closeEvent(self, event):
self.stop_execution()
if self.global_hotkey_listener:
self.global_hotkey_listener.stop()
if self.mouse_listener:
self.mouse_listener.stop()
if self.keyboard_listener:
self.keyboard_listener.stop()
if hasattr(self, 'hotkey_listener'):
self.hotkey_listener.stop()
self.save_global_settings()
self.save_window_size()
event.accept()
class OtherSettingsDialog(QDialog):
"""其他设置对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.setWindowTitle("其他设置")
self.setModal(True)
# 根据父窗口的缩放比例调整对话框尺寸
self.setFixedSize(
parent.scaled_value(400),
parent.scaled_value(450)
)
layout = QVBoxLayout()
layout.setSpacing(parent.scaled_value(10))
layout.setContentsMargins(
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15)
)
# 循环设置组
loop_group = QGroupBox("循环设置")
loop_layout = QVBoxLayout()
loop_layout.setSpacing(parent.scaled_value(5))
self.loop_type_group = QButtonGroup()
self.radio_infinite = QRadioButton("无限循环")
self.radio_count = QRadioButton("次数循环")
self.radio_infinite.setChecked(parent.loop_type == 0)
self.radio_count.setChecked(parent.loop_type == 1)
self.loop_type_group.addButton(self.radio_infinite, 0)
self.loop_type_group.addButton(self.radio_count, 1)
loop_layout.addWidget(self.radio_infinite)
loop_layout.addWidget(self.radio_count)
count_layout = QHBoxLayout()
count_layout.addWidget(QLabel("次数:"))
self.loop_spin = QSpinBox()
self.loop_spin.setRange(1, 99999)
self.loop_spin.setValue(parent.loop_count)
self.loop_spin.setMinimumHeight(parent.scaled_value(30))
count_layout.addWidget(self.loop_spin)
loop_layout.addLayout(count_layout)
loop_group.setLayout(loop_layout)
layout.addWidget(loop_group)
# 偏移设置组
offset_group = QGroupBox("偏移设置(X轴和Y轴点击偏移量)")
offset_layout = QVBoxLayout()
offset_layout.setSpacing(parent.scaled_value(5))
self.offset_checkbox = QCheckBox("启用偏移")
self.offset_checkbox.setChecked(parent.offset_enabled)
self.offset_checkbox.stateChanged.connect(self.toggle_offset)
x_layout = QHBoxLayout()
x_layout.addWidget(QLabel("X轴偏移:"))
self.offset_x_spin = QSpinBox()
self.offset_x_spin.setRange(0, 50)
self.offset_x_spin.setValue(parent.offset_x)
self.offset_x_spin.setEnabled(parent.offset_enabled)
self.offset_x_spin.setMinimumHeight(parent.scaled_value(30))
x_layout.addWidget(self.offset_x_spin)
y_layout = QHBoxLayout()
y_layout.addWidget(QLabel("Y轴偏移:"))
self.offset_y_spin = QSpinBox()
self.offset_y_spin.setRange(0, 50)
self.offset_y_spin.setValue(parent.offset_y)
self.offset_y_spin.setEnabled(parent.offset_enabled)
self.offset_y_spin.setMinimumHeight(parent.scaled_value(30))
y_layout.addWidget(self.offset_y_spin)
offset_layout.addWidget(self.offset_checkbox)
offset_layout.addLayout(x_layout)
offset_layout.addLayout(y_layout)
offset_group.setLayout(offset_layout)
layout.addWidget(offset_group)
# 随机延迟设置组
random_delay_group = QGroupBox("启动随机延迟(1秒等于1000毫秒)")
random_delay_layout = QVBoxLayout()
random_delay_layout.setSpacing(parent.scaled_value(5))
self.random_delay_checkbox = QCheckBox("启动随机延迟")
self.random_delay_checkbox.setChecked(parent.random_delay_enabled)
self.random_delay_checkbox.stateChanged.connect(self.toggle_random_delay)
min_layout = QHBoxLayout()
min_layout.addWidget(QLabel("最低值(毫秒):"))
self.random_delay_min_spin = QSpinBox()
self.random_delay_min_spin.setRange(0, 10000)
self.random_delay_min_spin.setValue(parent.random_delay_min)
self.random_delay_min_spin.setEnabled(parent.random_delay_enabled)
self.random_delay_min_spin.setMinimumHeight(parent.scaled_value(30))
min_layout.addWidget(self.random_delay_min_spin)
max_layout = QHBoxLayout()
max_layout.addWidget(QLabel("最高值(毫秒):"))
self.random_delay_max_spin = QSpinBox()
self.random_delay_max_spin.setRange(0, 10000)
self.random_delay_max_spin.setValue(parent.random_delay_max)
self.random_delay_max_spin.setEnabled(parent.random_delay_enabled)
self.random_delay_max_spin.setMinimumHeight(parent.scaled_value(30))
max_layout.addWidget(self.random_delay_max_spin)
random_delay_layout.addWidget(self.random_delay_checkbox)
random_delay_layout.addLayout(min_layout)
random_delay_layout.addLayout(max_layout)
random_delay_group.setLayout(random_delay_layout)
layout.addWidget(random_delay_group)
# 按钮
button_layout = QHBoxLayout()
button_layout.setSpacing(parent.scaled_value(10))
ok_button = QPushButton("确定")
ok_button.setMinimumHeight(parent.scaled_value(35))
ok_button.clicked.connect(self.apply_settings)
cancel_button = QPushButton("取消")
cancel_button.setMinimumHeight(parent.scaled_value(35))
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def toggle_offset(self, state):
enabled = state == Qt.Checked
self.offset_x_spin.setEnabled(enabled)
self.offset_y_spin.setEnabled(enabled)
def toggle_random_delay(self, state):
enabled = state == Qt.Checked
self.random_delay_min_spin.setEnabled(enabled)
self.random_delay_max_spin.setEnabled(enabled)
def apply_settings(self):
"""应用设置到主窗口"""
self.parent.loop_type = 0 if self.radio_infinite.isChecked() else 1
self.parent.loop_count = self.loop_spin.value()
self.parent.offset_enabled = self.offset_checkbox.isChecked()
self.parent.offset_x = self.offset_x_spin.value()
self.parent.offset_y = self.offset_y_spin.value()
self.parent.random_delay_enabled = self.random_delay_checkbox.isChecked()
self.parent.random_delay_min = self.random_delay_min_spin.value()
self.parent.random_delay_max = self.random_delay_max_spin.value()
self.accept()
QMessageBox.information(self.parent, "成功", "设置已保存!")
class ScreenshotWindow(QWidget):
image_captured = pyqtSignal(str)
closed = pyqtSignal()
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setWindowTitle("截图")
self.setWindowState(Qt.WindowFullScreen)
self.setWindowOpacity(0.3)
self.setMouseTracking(True)
self.setWindowFlags(
Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint |
Qt.Tool
)
self.start_pos = None
self.end_pos = None
self.is_selecting = False
self.shortcut_esc = QShortcut(QKeySequence(Qt.Key_Escape), self)
self.shortcut_esc.activated.connect(self.cancel_screenshot)
def cancel_screenshot(self):
self.close()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.start_pos = event.pos()
self.is_selecting = True
elif event.button() == Qt.RightButton:
self.cancel_screenshot()
def mouseMoveEvent(self, event):
if self.is_selecting:
self.end_pos = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton and self.is_selecting:
self.is_selecting = False
self.end_pos = event.pos()
x = min(self.start_pos.x(), self.end_pos.x())
y = min(self.start_pos.y(), self.end_pos.y())
width = abs(self.start_pos.x() - self.end_pos.x())
height = abs(self.start_pos.y() - self.end_pos.y())
screen = QApplication.primaryScreen()
pixmap = screen.grabWindow(0, x, y, width, height)
image_path = os.path.join(self.parent.screenshot_dir, f"screenshot_{int(time.time())}.png")
pixmap.save(image_path)
self.image_captured.emit(image_path)
self.close()
def paintEvent(self, event):
if self.is_selecting and self.start_pos and self.end_pos:
painter = QPainter(self)
painter.setPen(QPen(Qt.red, 2, Qt.DashLine))
x = min(self.start_pos.x(), self.end_pos.x())
y = min(self.start_pos.y(), self.end_pos.y())
width = abs(self.start_pos.x() - self.end_pos.x())
height = abs(self.start_pos.y() - self.end_pos.y())
painter.drawRect(x, y, width, height)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.cancel_screenshot()
def closeEvent(self, event):
self.closed.emit()
event.accept()
class TextInputDialog(QDialog):
"""文字输入对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.setWindowTitle("添加文字输入")
self.setModal(True)
self.setFixedSize(
parent.scaled_value(400),
parent.scaled_value(220)
)
layout = QVBoxLayout()
layout.setSpacing(parent.scaled_value(10))
layout.setContentsMargins(
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15)
)
label = QLabel("请输入要输出的文字:")
layout.addWidget(label)
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText("在此输入文字...")
self.text_edit.setStyleSheet(f"padding: {parent.scaled_value(5)}px; font-size: {parent.scaled_value(12)}px;")
self.text_edit.setMinimumHeight(parent.scaled_value(100))
layout.addWidget(self.text_edit)
button_layout = QHBoxLayout()
button_layout.setSpacing(parent.scaled_value(10))
ok_button = QPushButton("确定")
ok_button.setMinimumHeight(parent.scaled_value(35))
ok_button.clicked.connect(self.validate_and_accept)
cancel_button = QPushButton("取消")
cancel_button.setMinimumHeight(parent.scaled_value(35))
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.text_edit.setFocus()
def validate_and_accept(self):
"""验证输入并确认"""
text = self.text_edit.toPlainText().strip()
if not text:
QMessageBox.warning(self, "警告", "请写入需要输出的文字。")
return
self.accept()
def get_text(self):
return self.text_edit.toPlainText().strip()
class KeyboardInputDialog(QDialog):
"""键盘输入对话框(支持三种操作模式)"""
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.setWindowTitle("添加键盘操作")
self.setModal(True)
self.setFixedSize(
parent.scaled_value(550),
parent.scaled_value(450)
)
self.key_sequence = ""
self.modifiers = set()
self.captured_image_path = None
layout = QVBoxLayout()
layout.setSpacing(parent.scaled_value(10))
layout.setContentsMargins(
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15)
)
# 按键输入部分
label = QLabel("请在下方输入框中按下要添加的按键(支持组合键):")
layout.addWidget(label)
self.key_edit = QLineEdit()
self.key_edit.setReadOnly(True)
self.key_edit.setPlaceholderText("点击此处后按下按键...")
self.key_edit.setStyleSheet(f"padding: {parent.scaled_value(10)}px; font-size: {parent.scaled_value(14)}px;")
self.key_edit.setMinimumHeight(parent.scaled_value(40))
layout.addWidget(self.key_edit)
# 操作模式选择组
mode_group = QGroupBox("操作模式(只能选择一项)")
mode_layout = QVBoxLayout()
self.mode_group = QButtonGroup()
# 模式1:按键一次
self.radio_single = QRadioButton("按键一次(执行操作时按下添加的按键一次)")
self.radio_single.setChecked(True)
self.mode_group.addButton(self.radio_single, 0)
mode_layout.addWidget(self.radio_single)
# 模式2:长按指定时间
self.radio_hold_time = QRadioButton("长按键盘(按住添加的按键不放,直到设置的时长到了则松开)")
self.mode_group.addButton(self.radio_hold_time, 1)
mode_layout.addWidget(self.radio_hold_time)
# 长按时长设置
hold_time_layout = QHBoxLayout()
hold_time_layout.addWidget(QLabel(" 按住时间(毫秒):"))
self.hold_time_spin = QSpinBox()
self.hold_time_spin.setRange(100, 60000) # 100ms到60秒
self.hold_time_spin.setValue(1000) # 默认1秒
self.hold_time_spin.setSuffix(" ms")
self.hold_time_spin.setMinimumHeight(parent.scaled_value(30))
self.hold_time_spin.setEnabled(False)
hold_time_layout.addWidget(self.hold_time_spin)
mode_layout.addLayout(hold_time_layout)
# 模式3:按住直到图像出现
self.radio_hold_until_image = QRadioButton("按住直到图像出现松开按键(按住添加的按键不放,直到指定图像出现后松开)")
self.mode_group.addButton(self.radio_hold_until_image, 2)
mode_layout.addWidget(self.radio_hold_until_image)
# 图像选择部分
image_layout = QHBoxLayout()
image_layout.addWidget(QLabel(" 等待图像:"))
self.image_path_label = QLabel("未选择图像")
self.image_path_label.setStyleSheet("padding: 5px; border: 1px solid #ccc; background-color: #f9f9f9;")
self.image_path_label.setMinimumHeight(parent.scaled_value(30))
image_layout.addWidget(self.image_path_label)
self.select_image_btn = QPushButton("选择图像")
self.select_image_btn.setMinimumHeight(parent.scaled_value(30))
self.select_image_btn.clicked.connect(self.select_wait_image)
self.select_image_btn.setEnabled(False)
image_layout.addWidget(self.select_image_btn)
mode_layout.addLayout(image_layout)
mode_group.setLayout(mode_layout)
layout.addWidget(mode_group)
# 连接信号
self.mode_group.buttonClicked.connect(self.on_mode_changed)
self.radio_hold_time.toggled.connect(lambda checked: self.hold_time_spin.setEnabled(checked))
self.radio_hold_until_image.toggled.connect(lambda checked: self.select_image_btn.setEnabled(checked))
# 按钮
button_layout = QHBoxLayout()
button_layout.setSpacing(parent.scaled_value(10))
ok_button = QPushButton("确定")
ok_button.setMinimumHeight(parent.scaled_value(35))
ok_button.clicked.connect(self.validate_and_accept)
cancel_button = QPushButton("取消")
cancel_button.setMinimumHeight(parent.scaled_value(35))
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.key_edit.focusInEvent = self.on_focus_in
self.key_edit.installEventFilter(self)
# 保存当前对话框引用,以便主窗口可以访问
self.parent.current_keyboard_dialog = self
def on_focus_in(self, event):
self.key_edit.clear()
self.key_sequence = ""
self.modifiers.clear()
def on_mode_changed(self, button):
"""当操作模式改变时更新UI状态"""
if button == self.radio_single:
self.hold_time_spin.setEnabled(False)
self.select_image_btn.setEnabled(False)
elif button == self.radio_hold_time:
self.hold_time_spin.setEnabled(True)
self.select_image_btn.setEnabled(False)
elif button == self.radio_hold_until_image:
self.hold_time_spin.setEnabled(False)
self.select_image_btn.setEnabled(True)
def select_wait_image(self):
"""选择等待图像(从screenshots目录中选择)"""
# 打开文件对话框,默认打开screenshots目录
image_path, _ = QFileDialog.getOpenFileName(
self,
"选择等待图像",
self.parent.screenshot_dir, # 默认打开screenshots目录
"图像文件 (*.png *.jpg *.jpeg *.bmp *.gif)"
)
if image_path:
self.set_captured_image(image_path)
def set_captured_image(self, image_path):
"""设置选择的图像路径"""
self.captured_image_path = image_path
self.image_path_label.setText(os.path.basename(image_path))
self.image_path_label.setToolTip(image_path)
print(f"键盘等待图像已设置: {image_path}")
def get_action_type(self):
"""获取操作模式"""
return self.mode_group.checkedId()
def get_hold_time(self):
"""获取按住时间"""
return self.hold_time_spin.value()
def get_hold_image_path(self):
"""获取等待图像路径"""
return self.captured_image_path
def validate_and_accept(self):
"""验证输入并确认"""
# 检查是否设置了按键
if not self.key_sequence:
QMessageBox.warning(self, "警告", "请先按下要添加的按键。")
return
# 检查模式3是否设置了图像
if self.get_action_type() == 2 and not self.captured_image_path:
QMessageBox.warning(self, "警告", "请为'按住直到图像出现'模式选择等待图像。")
return
self.accept()
def eventFilter(self, obj, event):
if obj == self.key_edit and event.type() == QEvent.KeyPress:
self.handle_key_press(event)
return True
return super().eventFilter(obj, event)
def handle_key_press(self, event):
key = event.key()
modifiers = event.modifiers()
self.modifiers.clear()
if modifiers & Qt.ControlModifier:
self.modifiers.add("Ctrl")
if modifiers & Qt.AltModifier:
self.modifiers.add("Alt")
if modifiers & Qt.ShiftModifier:
self.modifiers.add("Shift")
if key == Qt.Key_Space:
key_name = "Space"
elif key == Qt.Key_Return or key == Qt.Key_Enter:
key_name = "Enter"
elif key == Qt.Key_Escape:
key_name = "Esc"
elif key == Qt.Key_Tab:
key_name = "Tab"
elif key == Qt.Key_Backspace:
key_name = "Backspace"
elif key == Qt.Key_Delete:
key_name = "Delete"
elif key == Qt.Key_Up:
key_name = "Up"
elif key == Qt.Key_Down:
key_name = "Down"
elif key == Qt.Key_Left:
key_name = "Left"
elif key == Qt.Key_Right:
key_name = "Right"
else:
key_name = QKeySequence(key).toString()
parts = list(self.modifiers)
parts.append(key_name)
self.key_sequence = "+".join(parts)
self.key_edit.setText(self.key_sequence)
def get_key_sequence(self):
return self.key_sequence
class DragSettingsDialog(QDialog):
"""鼠标拖动设置对话框"""
def __init__(self, parent=None, start_point=None, end_point=None):
super().__init__(parent)
self.parent = parent
self.start_point = start_point
self.end_point = end_point
self.setWindowTitle("设置鼠标拖动")
self.setModal(True)
self.setFixedSize(
parent.scaled_value(400),
parent.scaled_value(250)
)
layout = QVBoxLayout()
layout.setSpacing(parent.scaled_value(10))
layout.setContentsMargins(
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15),
parent.scaled_value(15)
)
# 坐标信息
info_label = QLabel(f"起点: {start_point}\n终点: {end_point}")
layout.addWidget(info_label)
# 拖动类型
type_layout = QHBoxLayout()
type_layout.addWidget(QLabel("拖动类型:"))
self.drag_type_combo = QComboBox()
self.drag_type_combo.addItems(["左键拖动", "右键拖动"])
self.drag_type_combo.setMinimumHeight(parent.scaled_value(30))
type_layout.addWidget(self.drag_type_combo)
layout.addLayout(type_layout)
# 开始延迟
start_delay_layout = QHBoxLayout()
start_delay_layout.addWidget(QLabel("开始延迟(毫秒):"))
self.start_delay_spin = QSpinBox()
self.start_delay_spin.setRange(0, 10000)
self.start_delay_spin.setValue(1000) # 默认1秒
self.start_delay_spin.setSuffix(" ms")
self.start_delay_spin.setMinimumHeight(parent.scaled_value(30))
start_delay_layout.addWidget(self.start_delay_spin)
layout.addLayout(start_delay_layout)
# 拖动持续时间
duration_layout = QHBoxLayout()
duration_layout.addWidget(QLabel("拖动持续时间(毫秒):"))
self.duration_spin = QSpinBox()
self.duration_spin.setRange(0, 10000)
self.duration_spin.setValue(1000) # 默认1秒
self.duration_spin.setSuffix(" ms")
self.duration_spin.setMinimumHeight(parent.scaled_value(30))
duration_layout.addWidget(self.duration_spin)
layout.addLayout(duration_layout)
# 按钮
button_layout = QHBoxLayout()
ok_button = QPushButton("确定")
ok_button.setMinimumHeight(parent.scaled_value(35))
ok_button.clicked.connect(self.accept)
cancel_button = QPushButton("取消")
cancel_button.setMinimumHeight(parent.scaled_value(35))
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def get_drag_type(self):
return 'left' if self.drag_type_combo.currentIndex() == 0 else 'right'
def get_start_delay(self):
return self.start_delay_spin.value()
def get_duration(self):
return self.duration_spin.value()
class ExecutionThread(QThread):
finished = pyqtSignal()
error = pyqtSignal(str)
step_completed = pyqtSignal(int)
def __init__(self, parent):
super().__init__()
self.parent = parent
self.loop_count = 1
self.current_step_index = 0
self.is_stopped = False
self.is_paused = False
self.current_loop = 0
def set_params(self, loop_count, start_index=0):
self.loop_count = loop_count
self.current_step_index = start_index
self.current_loop = 0
self.is_stopped = False
self.is_paused = False
def stop(self):
self.is_stopped = True
def pause(self):
self.is_paused = True
def resume(self):
self.is_paused = False
def run(self):
try:
self.is_stopped = False
self.is_paused = False
self.current_loop = 0
self.current_step_index = 0 # 初始重置指针
loop_count = self.loop_count
# 外层定时总循环:直到停止/超时
while not self.is_stopped:
# 每次循环重新读取界面最新定时配置
if self.parent.timed_execution_enabled:
start_str = self.parent.start_datetime_edit.text()
end_str = self.parent.end_datetime_edit.text()
start_dt = QDateTime.fromString(start_str, "yyyy-MM-dd HH:mm")
if not start_dt.isValid():
start_dt = QDateTime.fromString(start_str, "yyyy-MM-dd HH:mm:ss")
end_dt = QDateTime.fromString(end_str, "yyyy-MM-dd HH:mm")
if not end_dt.isValid():
end_dt = QDateTime.fromString(end_str, "yyyy-MM-dd HH:mm:ss")
current_dt = QDateTime.currentDateTime()
print(f"定时校验|当前:{current_dt.toString('yyyy-MM-dd HH:mm')} 目标启动:{start_str} 结束:{end_str}")
# 超过结束时间直接退出
if current_dt > end_dt:
print("已超过定时结束时间,停止执行")
break
# 未到开始时间,每秒刷新时间等待,支持暂停/停止
while current_dt < start_dt and not self.is_stopped:
# 暂停状态卡住等待
while self.is_paused and not self.is_stopped:
time.sleep(0.1)
if self.is_stopped:
break
# 1秒刷新一次系统时间,适配修改电脑时间场景
time.sleep(1)
# 重新读取最新时间字符串
start_str = self.parent.start_datetime_edit.text()
end_str = self.parent.end_datetime_edit.text()
start_dt = QDateTime.fromString(start_str, "yyyy-MM-dd HH:mm")
if not start_dt.isValid():
start_dt = QDateTime.fromString(start_str, "yyyy-MM-dd HH:mm:ss")
end_dt = QDateTime.fromString(end_str, "yyyy-MM-dd HH:mm")
if not end_dt.isValid():
end_dt = QDateTime.fromString(end_str, "yyyy-MM-dd HH:mm:ss")
current_dt = QDateTime.currentDateTime()
print(f"等待中|当前:{current_dt.toString('yyyy-MM-dd HH:mm')} 等待启动:{start_str}")
# 等待结束后再次判断是否停止/超时
if self.is_stopped or current_dt > end_dt:
break
# 【每轮执行前重置步骤指针】
self.current_step_index = 0
self.current_loop += 1
run_one_loop = True
# 执行一轮完整步骤
while run_one_loop and not self.is_stopped:
# 暂停阻塞
while self.is_paused and not self.is_stopped:
time.sleep(0.1)
if self.is_stopped:
break
# 单步前重新校验定时
if self.parent.timed_execution_enabled:
end_str = self.parent.end_datetime_edit.text()
end_dt = QDateTime.fromString(end_str, "yyyy-MM-dd HH:mm")
if not end_dt.isValid():
end_dt = QDateTime.fromString(end_str, "yyyy-MM-dd HH:mm:ss")
if QDateTime.currentDateTime() > end_dt:
print("执行中单步检测已超时,终止流程")
self.is_stopped = True
break
if self.current_step_index >= len(self.parent.steps):
run_one_loop = False
break
step = self.parent.steps[self.current_step_index]
# 键盘步骤
if step.step_type == 'keyboard':
self.execute_keyboard_step(step)
time.sleep(step.wait_time / 1000.0)
self.current_step_index += 1
self.step_completed.emit(self.current_step_index)
continue
# 文字输入步骤
if step.step_type == 'text':
self.execute_text_step(step)
time.sleep(step.wait_time / 1000.0)
self.current_step_index += 1
self.step_completed.emit(self.current_step_index)
continue
# 拖动步骤
if step.step_type == 'drag':
self.execute_drag_step(step)
time.sleep(step.wait_time / 1000.0)
self.current_step_index += 1
self.step_completed.emit(self.current_step_index)
continue
# 图像/坐标点击逻辑(原代码完全保留无修改)
target_pos = None
if step.step_type == 'image':
target_pos = self.find_image(step.image_path, step.similarity)
if target_pos and self.parent.target_hwnd:
left, top, _, _ = win32gui.GetWindowRect(self.parent.target_hwnd)
target_pos = (target_pos[0] + left, target_pos[1] + top)
else:
target_pos = (step.x, step.y)
if self.parent.target_hwnd:
left, top, _, _ = win32gui.GetWindowRect(self.parent.target_hwnd)
target_pos = (target_pos[0] + left, target_pos[1] + top)
total_wait = step.wait_time
if step.accept_random_delay and step.random_delay_enabled:
total_wait += random.randint(step.random_delay_min, step.random_delay_max)
# 图像跳转逻辑不变
if step.step_type == 'image':
if target_pos is not None:
if step.jump_on_found is not None:
self.current_step_index = step.jump_on_found
self.step_completed.emit(self.current_step_index)
time.sleep(total_wait / 1000.0)
continue
else:
if step.jump_on_not_found is not None:
self.current_step_index = step.jump_on_not_found
self.step_completed.emit(self.current_step_index)
time.sleep(total_wait / 1000.0)
continue
if target_pos is not None:
self.click_target(target_pos, step)
else:
print(f"步骤 {self.current_step_index + 1}: 未找到目标,跳过")
time.sleep(total_wait / 1000.0)
self.current_step_index += 1
self.step_completed.emit(self.current_step_index)
# 判断是否达到循环次数上限
if loop_count != 999999 and self.current_loop >= loop_count:
print("达到循环次数上限,执行结束")
break
time.sleep(0.5)
self.finished.emit()
except Exception as e:
self.error.emit(f"线程异常:{str(e)}")
def execute_text_step(self, step):
"""执行文字输入步骤"""
try:
if self.parent.target_hwnd:
# 激活目标窗口
win32gui.SetForegroundWindow(self.parent.target_hwnd)
time.sleep(0.1)
# 输入文字
self.parent.keyboard.type(step.text_content)
print(f"输入文字: {step.text_content[:20]}...")
except Exception as e:
print(f"文字输入失败: {e}")
def execute_drag_step(self, step):
"""执行鼠标拖动步骤(支持缓冲时间)"""
try:
start_x, start_y = step.drag_start_x, step.drag_start_y
end_x, end_y = step.drag_end_x, step.drag_end_y
if self.parent.target_hwnd:
left, top, _, _ = win32gui.GetWindowRect(self.parent.target_hwnd)
start_x += left
start_y += top
end_x += left
end_y += top
# 后台拖动(支持延迟)
self.drag_backend_with_delay(
self.parent.target_hwnd,
start_x, start_y,
end_x, end_y,
step.click_type,
step.drag_start_delay,
step.drag_duration
)
else:
# 前台拖动(支持延迟)
self.drag_foreground_with_delay(
start_x, start_y,
end_x, end_y,
step.click_type,
step.drag_start_delay,
step.drag_duration
)
print(f"鼠标拖动: ({start_x},{start_y}) -> ({end_x},{end_y}), "
f"按钮: {step.click_type}, "
f"开始延迟: {step.drag_start_delay}ms, "
f"持续时间: {step.drag_duration}ms")
except Exception as e:
print(f"鼠标拖动失败: {e}")
def drag_foreground_with_delay(self, start_x, start_y, end_x, end_y, button_type, start_delay, duration):
"""前台鼠标拖动(支持延迟)"""
# 移动鼠标到起点
self.parent.mouse.position = (start_x, start_y)
time.sleep(0.05)
# 按下鼠标键
button = Button.left if button_type == 'left' else Button.right
self.parent.mouse.press(button)
# 开始延迟(在起点等待)
if start_delay > 0:
time.sleep(start_delay / 1000.0)
# 移动到终点(支持平滑移动)
if duration > 0:
# 平滑移动
steps = max(10, int(duration / 10)) # 至少10步
step_delay = duration / steps / 1000.0
for i in range(1, steps + 1):
curr_x = start_x + (end_x - start_x) * i / steps
curr_y = start_y + (end_y - start_y) * i / steps
self.parent.mouse.position = (curr_x, curr_y)
time.sleep(step_delay)
else:
# 立即移动
self.parent.mouse.position = (end_x, end_y)
time.sleep(0.05)
# 释放鼠标键
self.parent.mouse.release(button)
def drag_backend_with_delay(self, hwnd, start_x, start_y, end_x, end_y, button_type, start_delay, duration):
"""后台鼠标拖动(支持延迟)"""
# 转换到窗口相对坐标
left, top, _, _ = win32gui.GetWindowRect(hwnd)
rel_start_x = start_x - left
rel_start_y = start_y - top
rel_end_x = end_x - left
rel_end_y = end_y - top
# 确定消息类型
if button_type == 'left':
down_msg, up_msg = win32con.WM_LBUTTONDOWN, win32con.WM_LBUTTONUP
mk_flag = win32con.MK_LBUTTON
else:
down_msg, up_msg = win32con.WM_RBUTTONDOWN, win32con.WM_RBUTTONUP
mk_flag = win32con.MK_RBUTTON
# 按下起点
lparam_down = win32api.MAKELONG(rel_start_x, rel_start_y)
win32gui.SendMessage(hwnd, down_msg, mk_flag, lparam_down)
# 开始延迟(在起点等待)
if start_delay > 0:
time.sleep(start_delay / 1000.0)
# 移动到终点(支持平滑移动)
if duration > 0:
steps = max(10, int(duration / 10)) # 至少10步
step_delay = duration / steps / 1000.0
for i in range(1, steps + 1):
curr_x = rel_start_x + (rel_end_x - rel_start_x) * i / steps
curr_y = rel_start_y + (rel_end_y - rel_start_y) * i / steps
lparam_move = win32api.MAKELONG(int(curr_x), int(curr_y))
win32gui.SendMessage(hwnd, win32con.WM_MOUSEMOVE, mk_flag, lparam_move)
time.sleep(step_delay)
else:
# 立即移动
lparam_move = win32api.MAKELONG(rel_end_x, rel_end_y)
win32gui.SendMessage(hwnd, win32con.WM_MOUSEMOVE, mk_flag, lparam_move)
time.sleep(0.01)
# 释放终点
lparam_up = win32api.MAKELONG(rel_end_x, rel_end_y)
win32gui.SendMessage(hwnd, up_msg, 0, lparam_up)
def execute_keyboard_step(self, step):
"""执行键盘步骤(支持三种操作模式)"""
try:
if self.parent.target_hwnd:
# 激活目标窗口
win32gui.SetForegroundWindow(self.parent.target_hwnd)
time.sleep(0.1)
keys = step.key_sequence.split('+')
# 按下修饰键
for key in keys[:-1]:
if key == 'Ctrl':
self.parent.keyboard.press(Key.ctrl)
elif key == 'Alt':
self.parent.keyboard.press(Key.alt)
elif key == 'Shift':
self.parent.keyboard.press(Key.shift)
# 按下主键
main_key = keys[-1]
vk_code = None
# 映射特殊键
special_keys = {
'Space': Key.space,
'Enter': Key.enter,
'Esc': Key.esc,
'Tab': Key.tab,
'Backspace': Key.backspace,
'Delete': Key.delete,
'Up': Key.up,
'Down': Key.down,
'Left': Key.left,
'Right': Key.right
}
if main_key in special_keys:
self.parent.keyboard.press(special_keys[main_key])
vk_code = special_keys[main_key]
elif len(main_key) == 1:
self.parent.keyboard.press(main_key.lower())
vk_code = main_key.lower()
# 根据操作模式执行不同逻辑
if step.key_action_type == 0: # 按键一次
# 立即释放
if vk_code:
if isinstance(vk_code, Key):
self.parent.keyboard.release(vk_code)
else:
self.parent.keyboard.release(vk_code)
print(f"执行键盘操作(按键一次): {step.key_sequence}")
elif step.key_action_type == 1: # 长按指定时间
if step.key_hold_time > 0:
# 保持按键按下状态,直到时间到达
start_time = time.time()
elapsed = 0
while elapsed < (step.key_hold_time / 1000.0) and not self.is_stopped:
time.sleep(0.001) # 短暂休眠,避免CPU占用过高
elapsed = time.time() - start_time
# 释放按键
if vk_code:
if isinstance(vk_code, Key):
self.parent.keyboard.release(vk_code)
else:
self.parent.keyboard.release(vk_code)
print(f"执行键盘操作(长按{step.key_hold_time}ms): {step.key_sequence}")
elif step.key_action_type == 2: # 按住直到图像出现
if step.key_hold_image_path and os.path.exists(step.key_hold_image_path):
print(f"开始等待图像出现: {step.key_hold_image_path}")
# 无限等待直到图像出现或线程被停止
image_found = False
while not self.is_stopped and not image_found:
# 检查图像是否出现
if self.find_image(step.key_hold_image_path, 0.8):
print(f"检测到目标图像,释放按键: {step.key_sequence}")
image_found = True
break
time.sleep(0.1) # 短暂休眠,避免CPU占用过高
# 释放按键
if vk_code:
if isinstance(vk_code, Key):
self.parent.keyboard.release(vk_code)
else:
self.parent.keyboard.release(vk_code)
if self.is_stopped:
print("等待图像过程中出现停止信号,释放按键并退出")
elif image_found:
print(f"执行键盘操作(按住直到图像出现): {step.key_sequence}")
# 只有找到图像后才继续到下一步
# 注意:这里不增加current_step_index,由主循环控制
else:
print(f"警告: 未设置有效的等待图像,执行普通按键: {step.key_sequence}")
# 如果没有有效图像,当作普通按键处理
if vk_code:
if isinstance(vk_code, Key):
self.parent.keyboard.release(vk_code)
else:
self.parent.keyboard.release(vk_code)
# 释放修饰键
for key in reversed(keys[:-1]):
if key == 'Ctrl':
self.parent.keyboard.release(Key.ctrl)
elif key == 'Alt':
self.parent.keyboard.release(Key.alt)
elif key == 'Shift':
self.parent.keyboard.release(Key.shift)
except Exception as e:
print(f"键盘操作执行失败: {e}")
def find_image(self, image_path, similarity=0.8):
print(f"尝试查找图像: {image_path}")
print(f"文件是否存在: {os.path.exists(image_path)}")
if not os.path.exists(image_path):
print(f"警告: 图像文件不存在: {image_path}")
return None
# 支持中文路径的读取方式
try:
img_array = np.fromfile(image_path, dtype=np.uint8)
template = cv2.imdecode(img_array, cv2.IMREAD_GRAYSCALE)
except Exception as e:
print(f"读取图像文件失败: {e}")
return None
if template is None:
print(f"警告: 无法读取图像文件: {image_path}")
return None
# 获取截图区域
if self.parent.target_hwnd:
left, top, right, bottom = win32gui.GetWindowRect(self.parent.target_hwnd)
if right - left <= 0 or bottom - top <= 0:
return None
bbox = (left, top, right, bottom)
screen_image = ImageGrab.grab(bbox)
else:
screen = QApplication.primaryScreen()
screen_image = screen.grabWindow(0).toImage()
if isinstance(screen_image, QImage):
width = screen_image.width()
height = screen_image.height()
ptr = screen_image.bits()
ptr.setsize(height * width * 4)
arr = np.array(ptr).reshape(height, width, 4)
screenshot = cv2.cvtColor(arr, cv2.COLOR_BGRA2GRAY)
else:
screenshot = cv2.cvtColor(np.array(screen_image), cv2.COLOR_RGB2GRAY)
if screenshot.shape[0] < template.shape[0] or screenshot.shape[1] < template.shape[1]:
return None
result = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(result)
print(f"匹配相似度: {max_val}")
if max_val >= similarity:
h, w = template.shape[:2]
center_x = max_loc[0] + w // 2
center_y = max_loc[1] + h // 2
return (center_x, center_y)
return None
def click_target(self, target_pos, step):
x, y = target_pos
if step.accept_offset and self.parent.offset_enabled:
x += random.randint(
-self.parent.offset_x,
self.parent.offset_x
)
y += random.randint(
-self.parent.offset_y,
self.parent.offset_y
)
if self.parent.target_hwnd:
left, top, _, _ = win32gui.GetWindowRect(self.parent.target_hwnd)
rel_x, rel_y = x - left, y - top
if step.click_type == 'left':
down_msg, up_msg = win32con.WM_LBUTTONDOWN, win32con.WM_LBUTTONUP
elif step.click_type == 'right':
down_msg, up_msg = win32con.WM_RBUTTONDOWN, win32con.WM_RBUTTONUP
elif step.click_type == 'double':
self.click_target_backend(self.parent.target_hwnd, rel_x, rel_y,
win32con.WM_LBUTTONDOWN, win32con.WM_LBUTTONUP)
time.sleep(0.05)
self.click_target_backend(self.parent.target_hwnd, rel_x, rel_y,
win32con.WM_LBUTTONDOWN, win32con.WM_LBUTTONUP)
return
else:
return
self.click_target_backend(self.parent.target_hwnd, rel_x, rel_y, down_msg, up_msg)
else:
self.parent.mouse.position = (x, y)
time.sleep(0.05)
if step.click_type == 'left':
self.parent.mouse.click(Button.left)
elif step.click_type == 'right':
self.parent.mouse.click(Button.right)
elif step.click_type == 'double':
self.parent.mouse.click(Button.left)
time.sleep(0.05)
self.parent.mouse.click(Button.left)
def click_target_backend(self, hwnd, x, y, down_msg, up_msg):
lparam = win32api.MAKELONG(x, y)
win32gui.SendMessage(hwnd, down_msg, win32con.MK_LBUTTON, lparam)
time.sleep(0.01)
win32gui.SendMessage(hwnd, up_msg, 0, lparam)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = AutoClickerApp()
window.show()
sys.exit(app.exec_())