import time import math import logging import numpy as np from typing import Tuple, List, Dict, Optional, Any from queue import Queue, Empty import threading import ctypes from ctypes import wintypes import os import json # 尝试导入依赖库 try: from PIL import ImageGrab, Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False try: import win32gui import win32api import win32con import win32ui WIN32_AVAILABLE = True except ImportError: WIN32_AVAILABLE = False try: import cv2 CV2_AVAILABLE = True except ImportError: CV2_AVAILABLE = False try: from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QSpinBox, QSlider, QLineEdit, QFileDialog, QMessageBox, QGroupBox, QComboBox) from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QPixmap, QImage, QFont, QMouseEvent PYQT_AVAILABLE = True except ImportError: PYQT_AVAILABLE = False # -------------------------- 全局常量定义 -------------------------- WM_MOUSEMOVE = 0x0200 WM_LBUTTONDOWN = 0x0201 WM_LBUTTONUP = 0x0202 WM_RBUTTONDOWN = 0x0204 WM_RBUTTONUP = 0x0205 WM_LBUTTONDBLCLK = 0x0203 WM_RBUTTONDBLCLK = 0x0206 WM_KEYDOWN = 0x0100 WM_KEYUP = 0x0101 WM_MOUSEWHEEL = 0x020A KEY_MAP = { 'w': 0x57, 'a': 0x41, 's': 0x53, 'd': 0x44, 'q': 0x51, 'e': 0x45, 'b': 0x42, 'c': 0x43, 'f': 0x46, 'g': 0x47, 'h': 0x48, 'i': 0x49, 'j': 0x4A, 'k': 0x4B, 'l': 0x4C, 'm': 0x4D, 'n': 0x4E, 'o': 0x4F, 'p': 0x50, 'r': 0x52, 't': 0x54, 'u': 0x55, 'v': 0x56, 'x': 0x58, 'y': 0x59, 'z': 0x5A, '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39, 'num0': 0x60, 'num1': 0x61, 'num2': 0x62, 'num3': 0x63, 'num4': 0x64, 'num5': 0x65, 'num6': 0x66, 'num7': 0x67, 'num8': 0x68, 'num9': 0x69, 'enter': 0x0D, 'backspace': 0x08, 'tab': 0x09, 'esc': 0x1B, 'space': 0x20, 'shift': 0x10, 'ctrl': 0x11, 'alt': 0x12, 'capslock': 0x14, 'delete': 0x2E, 'insert': 0x2D, 'home': 0x24, 'end': 0x23, 'pgup': 0x21, 'pgdn': 0x22, 'f1': 0x70, 'f2': 0x71, 'f3': 0x72, 'f4': 0x73, 'f5': 0x74, 'f6': 0x75, 'f7': 0x76, 'f8': 0x77, 'f9': 0x78, 'f10': 0x79, 'f11': 0x7A, 'f12': 0x7B, 'up': 0x26, 'down': 0x28, 'left': 0x25, 'right': 0x27 } # 导航配置 CONTROL_PANEL_SIZE = (900, 700) MINIMAP_POPUP_SIZE = (600, 500) COORDINATE_MAX = 1000 GRID_SIZE = 20 ARRIVAL_THRESHOLD = 15 ICON_MATCH_THRESHOLD = 0.7 W_KEY_MIN_HOLD = 2.0 # W键最小长按时间(2秒) W_KEY_MAX_HOLD = 6.0 # W键最大长按时间(6秒) LONG_DISTANCE_THRESH = 300 # 长距离判定阈值 MEDIUM_DISTANCE_THRESH = 150 # 中距离判定阈值 DEBUG_MODE = True DEBUG_FOLDER = "debug_angles" if DEBUG_MODE and not os.path.exists(DEBUG_FOLDER): os.makedirs(DEBUG_FOLDER) # -------------------------- 依赖检查 -------------------------- def check_dependencies(): missing = [] if not PIL_AVAILABLE: missing.append("Pillow (pip install pillow)") if not WIN32_AVAILABLE: missing.append("pywin32 (pip install pywin32)") if not CV2_AVAILABLE: missing.append("opencv-python (pip install opencv-python)") if not PYQT_AVAILABLE: missing.append("PyQt5 (pip install pyqt5)") return not missing, "缺少必要依赖: " + " ".join(missing) if missing else "所有依赖已安装" # -------------------------- 日志配置 - 修复编码问题 -------------------------- class UTF8LoggingStreamHandler(logging.StreamHandler): def emit(self, record): try: msg = self.format(record) # 使用UTF-8编码写入 self.stream.buffer.write(msg.encode('utf-8') + self.terminator.encode('utf-8')) self.stream.buffer.flush() except Exception: self.handleError(record) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("navigation.log", encoding='utf-8'), # 文件日志使用UTF-8 UTF8LoggingStreamHandler() # 控制台日志使用自定义处理器 ] ) logger = logging.getLogger("Navigation") ui_queue = Queue() # -------------------------- 坐标校准器 -------------------------- class CoordinateCalibrator: def __init__(self): self.calibration_points = [] # 存储校准点: [(minimap_x, minimap_y, world_x, world_y), ...] self.calibration_matrix = None self.is_calibrated = False def add_calibration_point(self, minimap_x: int, minimap_y: int, world_x: float, world_y: float): """添加一个校准点""" self.calibration_points.append((minimap_x, minimap_y, world_x, world_y)) logger.info(f"添加校准点: 小地图({minimap_x},{minimap_y}) -> 世界({world_x},{world_y})") # 如果有至少2个点,计算转换矩阵 if len(self.calibration_points) >= 2: self._calculate_calibration_matrix() def clear_calibration_points(self): """清除所有校准点""" self.calibration_points = [] self.calibration_matrix = None self.is_calibrated = False logger.info("已清除所有校准点") def _calculate_calibration_matrix(self): """计算从小地图坐标到世界坐标的转换矩阵""" if len(self.calibration_points) < 2: return # 使用最小二乘法计算仿射变换矩阵 src_points = [] dst_points = [] for point in self.calibration_points: src_points.append([point[0], point[1], 1]) dst_points.append([point[2], point[3]]) src_matrix = np.array(src_points) dst_matrix = np.array(dst_points) # 计算变换矩阵: dst = src * M M, _, _, _ = np.linalg.lstsq(src_matrix, dst_matrix, rcond=None) self.calibration_matrix = M.T self.is_calibrated = True logger.info(f"计算校准矩阵成功: {self.calibration_matrix}") def minimap_to_world(self, minimap_x: int, minimap_y: int) -> Optional[Tuple[float, float]]: """将小地图坐标转换为世界坐标""" if not self.is_calibrated or self.calibration_matrix is None: return None src_point = np.array([minimap_x, minimap_y, 1]) world_coords = np.dot(src_point, self.calibration_matrix) # 确保坐标在有效范围内 world_x = max(0, min(world_coords[0], COORDINATE_MAX)) world_y = max(0, min(world_coords[1], COORDINATE_MAX)) return (world_x, world_y) def world_to_minimap(self, world_x: float, world_y: float) -> Optional[Tuple[int, int]]: """将世界坐标转换为小地图坐标(需要逆变换)""" if not self.is_calibrated or self.calibration_matrix is None: return None # 计算逆变换 try: inv_matrix = np.linalg.inv(np.vstack([self.calibration_matrix, [0, 0, 1]])) world_point = np.array([world_x, world_y, 1]) minimap_coords = np.dot(world_point, inv_matrix[:2, :]) return (int(minimap_coords[0]), int(minimap_coords[1])) except np.linalg.LinAlgError: return None def save_calibration(self, filepath: str) -> bool: """保存校准数据到文件""" try: data = { "calibration_points": self.calibration_points, "calibration_matrix": self.calibration_matrix.tolist() if self.calibration_matrix is not None else None, "is_calibrated": self.is_calibrated } with open(filepath, 'w') as f: json.dump(data, f) logger.info(f"校准数据已保存到: {filepath}") return True except Exception as e: logger.error(f"保存校准数据失败: {e}") return False def load_calibration(self, filepath: str) -> bool: """从文件加载校准数据""" try: with open(filepath, 'r') as f: data = json.load(f) self.calibration_points = data["calibration_points"] self.calibration_matrix = np.array(data["calibration_matrix"]) if data[ "calibration_matrix"] is not None else None self.is_calibrated = data["is_calibrated"] logger.info(f"从 {filepath} 加载校准数据成功") return True except Exception as e: logger.error(f"加载校准数据失败: {e}") return False # -------------------------- 窗口控制器(修复_is_window_valid隐患) -------------------------- class OptimizedWindowController: def __init__(self, window_title, timeout=5): self.hwnd = None # 初始化为None,避免后续调用报错 self.client_width = 0 self.client_height = 0 self.client_screen_x = 0 self.client_screen_y = 0 self._find_window_with_retry(window_title, timeout) if not self.hwnd: raise Exception(f"超时未找到窗口:{window_title}") self._update_client_area() logger.info(f"成功绑定窗口(句柄:{self.hwnd}),客户区大小:{self.client_width}x{self.client_height}") def _find_window_with_retry(self, title, timeout): start_time = time.time() while time.time() - start_time < timeout: self.hwnd = self._find_window(title) if self.hwnd: return time.sleep(0.5) def _find_window(self, title): hwnds = [] win32gui.EnumWindows(lambda h, ctx: ctx.append(h) if title in win32gui.GetWindowText(h) else None, hwnds) return hwnds[0] if hwnds else None def _update_client_area(self): try: rect = wintypes.RECT() ctypes.windll.user32.GetClientRect(self.hwnd, ctypes.byref(rect)) self.client_left = rect.left self.client_top = rect.top self.client_width = rect.right - rect.left self.client_height = rect.bottom - rect.top screen_pos = win32gui.ClientToScreen(self.hwnd, (self.client_left, self.client_top)) self.client_screen_x, self.client_screen_y = screen_pos except Exception as e: logger.error(f"更新客户区信息失败:{e},使用窗口整体区域") left, top, right, bottom = win32gui.GetWindowRect(self.hwnd) self.client_left, self.client_top = 0, 0 self.client_width = right - left self.client_height = bottom - top def ensure_window_active(self): if not self._is_window_valid(): logger.warning("窗口无效,无法激活") return try: win32gui.SetWindowPos( self.hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE ) win32gui.SetForegroundWindow(self.hwnd) time.sleep(0.2) logger.info("窗口已激活并置顶") except Exception as e: logger.warning(f"窗口激活失败(需管理员权限):{e}") def mouse_move(self, x, y, relative=False): if not self._is_window_valid(): return if relative: current_x, current_y = self._get_current_mouse_pos() x = current_x + x y = current_y + y x = max(0, min(x, self.client_width)) y = max(0, min(y, self.client_height)) lParam = y << 16 | x win32api.SendMessage(self.hwnd, WM_MOUSEMOVE, 0, lParam) logger.debug(f"鼠标移动到客户区({x},{y})") def mouse_click(self, x, y, button='left', double_click=False, delay=0.05): if not self._is_window_valid(): return self.mouse_move(x, y) time.sleep(0.05) down_msg = WM_LBUTTONDOWN if button == 'left' else WM_RBUTTONDOWN up_msg = WM_LBUTTONUP if button == 'left' else WM_RBUTTONUP dbl_msg = WM_LBUTTONDBLCLK if button == 'left' else WM_RBUTTONDBLCLK lParam = y << 16 | x if double_click: win32api.SendMessage(self.hwnd, dbl_msg, 0, lParam) logger.debug(f"鼠标{button}键双击客户区({x},{y})") else: win32api.SendMessage(self.hwnd, down_msg, 0, lParam) time.sleep(delay) win32api.SendMessage(self.hwnd, up_msg, 0, lParam) logger.debug(f"鼠标{button}键单击客户区({x},{y})") def mouse_scroll(self, delta=120): if not self._is_window_valid(): return x, y = self._get_current_mouse_pos() wParam = delta << 16 | 0 lParam = y << 16 | x win32api.SendMessage(self.hwnd, WM_MOUSEWHEEL, wParam, lParam) time.sleep(0.1) logger.debug(f"鼠标滚轮滚动:{'上滚' if delta > 0 else '下滚'}1格") def key_press(self, key, delay=0.05): if not self._is_window_valid(): logger.warning("窗口无效,无法发送按键") return key_lower = key.lower() vk_code = KEY_MAP.get(key_lower) if not vk_code: logger.warning(f"忽略不支持的按键:{key}") return # 长按逻辑:按下→等待→抬起 win32api.SendMessage(self.hwnd, WM_KEYDOWN, vk_code, 0) logger.debug(f"按下{key}键,开始长按({delay:.1f}秒)") # 长按期间检查窗口是否有效,避免卡死 start_hold = time.time() while time.time() - start_hold < delay: if not self._is_window_valid(): win32api.SendMessage(self.hwnd, WM_KEYUP, vk_code, 0) logger.warning(f"窗口无效,提前抬起{key}键") return time.sleep(0.1) win32api.SendMessage(self.hwnd, WM_KEYUP, vk_code, 0) logger.debug(f"抬起{key}键,长按结束(实际时长:{time.time() - start_hold:.1f}秒)") def key_combo(self, combo, delay=0.05): if not self._is_window_valid(): return keys = combo.split('+') if len(keys) < 2: logger.warning(f"组合键格式错误:{combo}") return modifiers = [] target_key = keys[-1] for mod in keys[:-1]: mod_lower = mod.lower() vk_mod = KEY_MAP.get(mod_lower) if not vk_mod: logger.warning(f"忽略无效修饰键:{mod}") continue modifiers.append((mod_lower, vk_mod)) if not modifiers: logger.warning(f"组合键无有效修饰键:{combo}") return for mod_name, vk_mod in modifiers: win32api.SendMessage(self.hwnd, WM_KEYDOWN, vk_mod, 0) logger.debug(f"组合键-按下修饰键:{mod_name}") time.sleep(0.03) self.key_press(target_key, delay=delay) for mod_name, vk_mod in reversed(modifiers): win32api.SendMessage(self.hwnd, WM_KEYUP, vk_mod, 0) logger.debug(f"组合键-抬起修饰键:{mod_name}") time.sleep(0.03) def type_text(self, text, interval=0.05): if not self._is_window_valid(): return for char in text: if char == ' ': self.key_press('space', delay=interval / 2) elif char == ' ': self.key_press('enter', delay=interval / 2) elif char.isupper(): self.key_combo(f'shift+{char.lower()}', delay=interval / 2) else: self.key_press(char.lower(), delay=interval / 2) time.sleep(interval) logger.debug(f"文本输入完成:{text}") def _is_window_valid(self): # 修复:先判断hwnd是否存在,再检查窗口状态 if self.hwnd is None: logger.error("窗口句柄未初始化") return False if not win32gui.IsWindow(self.hwnd) or not win32gui.IsWindowVisible(self.hwnd): logger.error("窗口已关闭或隐藏") return False return True def _get_current_mouse_pos(self): try: screen_x, screen_y = win32api.GetCursorPos() client_x, client_y = win32gui.ScreenToClient(self.hwnd, (screen_x, screen_y)) client_x = max(0, min(client_x, self.client_width)) client_y = max(0, min(client_y, self.client_height)) return (client_x, client_y) except Exception as e: logger.error(f"获取鼠标位置失败:{e}") return (0, 0) def capture_client_screenshot(self, region=None): if not self._is_window_valid(): return None try: if region: x1, y1, x2, y2 = region screen_x1 = self.client_screen_x + x1 screen_y1 = self.client_screen_y + y1 screen_x2 = self.client_screen_x + x2 screen_y2 = self.client_screen_y + y2 else: screen_x1 = self.client_screen_x screen_y1 = self.client_screen_y screen_x2 = self.client_screen_x + self.client_width screen_y2 = self.client_screen_y + self.client_height screenshot = ImageGrab.grab(bbox=(screen_x1, screen_y1, screen_x2, screen_y2)) return cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) except Exception as e: logger.error(f"客户区截图失败:{e}") return None # -------------------------- A星寻路算法 -------------------------- class AStarNode: def __init__(self, x: int, y: int): self.x = x self.y = y self.g = 0 self.h = 0 self.f = 0 self.parent = None def __eq__(self, other): return self.x == other.x and self.y == other.y def __lt__(self, other): return self.f < other.f class AStarPathfinder: def __init__(self, grid_size: int = GRID_SIZE, max_coord: int = COORDINATE_MAX): self.grid_size = grid_size self.grid_width = max_coord // grid_size self.grid_height = max_coord // grid_size self.obstacles = set() def add_obstacle(self, raw_x: float, raw_y: float): grid_x = int(raw_x // self.grid_size) grid_y = int(raw_y // self.grid_size) if 0 <= grid_x < self.grid_width and 0 <= grid_y < self.grid_height: self.obstacles.add((grid_x, grid_y)) logger.info(f"添加障碍物:原始({raw_x:.0f},{raw_y:.0f}) → 网格({grid_x},{grid_y})") def remove_obstacle(self, raw_x: float, raw_y: float): grid_x = int(raw_x // self.grid_size) grid_y = int(raw_y // self.grid_size) if (grid_x, grid_y) in self.obstacles: self.obstacles.remove((grid_x, grid_y)) logger.info(f"移除障碍物:原始({raw_x:.0f},{raw_y:.0f}) → 网格({grid_x},{grid_y})") def clear_obstacles(self): self.obstacles.clear() logger.info("已清空所有障碍物") def is_obstacle(self, grid_x: int, grid_y: int) -> bool: return (grid_x, grid_y) in self.obstacles or not (0 <= grid_x < self.grid_width and 0 <= grid_y < self.grid_height) def get_neighbors(self, node: AStarNode) -> List[AStarNode]: neighbors = [] directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] for dx, dy in directions: new_x = node.x + dx new_y = node.y + dy if not self.is_obstacle(new_x, new_y): neighbors.append(AStarNode(new_x, new_y)) return neighbors def calculate_distance(self, a: Tuple[int, int], b: Tuple[int, int]) -> float: return abs(a[0] - b[0]) + abs(a[1] - b[1]) def find_path(self, start_raw: Tuple[float, float], end_raw: Tuple[float, float]) -> List[Tuple[float, float]]: start_grid = (int(start_raw[0] // self.grid_size), int(start_raw[1] // self.grid_size)) end_grid = (int(end_raw[0] // self.grid_size), int(end_raw[1] // self.grid_size)) if self.is_obstacle(start_grid[0], start_grid[1]): logger.error(f"起点({start_raw[0]:.0f},{start_raw[1]:.0f})在障碍物上") return [] if self.is_obstacle(end_grid[0], end_grid[1]): logger.error(f"终点({end_raw[0]:.0f},{end_raw[1]:.0f})在障碍物上") return [] if start_grid == end_grid: logger.info("起点与终点重合,无需寻路") return [start_raw, end_raw] start_node = AStarNode(start_grid[0], start_grid[1]) end_node = AStarNode(end_grid[0], end_grid[1]) open_list = [start_node] closed_list = [] while open_list: current_node = min(open_list, key=lambda x: x.f) open_list.remove(current_node) closed_list.append(current_node) if current_node == end_node: path = [] while current_node: raw_x = current_node.x * self.grid_size + (self.grid_size // 2) raw_y = current_node.y * self.grid_size + (self.grid_size // 2) path.append((raw_x, raw_y)) current_node = current_node.parent return path[::-1] neighbors = self.get_neighbors(current_node) for neighbor in neighbors: if neighbor in closed_list: continue neighbor.g = current_node.g + self.calculate_distance( (current_node.x, current_node.y), (neighbor.x, neighbor.y) ) neighbor.h = self.calculate_distance( (neighbor.x, neighbor.y), (end_node.x, end_node.y) ) neighbor.f = neighbor.g + neighbor.h if neighbor not in open_list or neighbor.g < [n.g for n in open_list if n == neighbor][0]: neighbor.parent = current_node if neighbor not in open_list: open_list.append(neighbor) logger.error("A星寻路失败:未找到可行路径") return [] # -------------------------- 1.png图标模板匹配角度计算器(增加加载失败友好提示) -------------------------- class IconTemplateAngleCalculator: def __init__(self, template_path: str = "1.png"): self.template = None self.template_center = None self.template_size = None self.screenshot_center = None self.match_threshold = ICON_MATCH_THRESHOLD # 修复:加载1.png时捕获异常,避免程序直接崩溃 try: self._load_1png_template(template_path) except Exception as e: logger.error(f"1.png加载失败:{e}") raise Exception(f"1.png初始化错误:{e} 请确保1.png在代码同一文件夹,且为有效图片(如PNG/JPG格式)") def _load_1png_template(self, template_path: str): if not os.path.exists(template_path): raise FileNotFoundError(f"文件不存在:{os.path.abspath(template_path)}") template_img = cv2.imread(template_path) if template_img is None: raise ValueError(f"无法识别为图片文件(可能损坏或格式不支持)") self.template = cv2.cvtColor(template_img, cv2.COLOR_BGR2GRAY) self.template_size = (self.template.shape[1], self.template.shape[0]) self.template_center = (self.template_size[0] // 2, self.template_size[1] // 2) logger.info( f"1.png模板加载成功:尺寸{self.template_size[0]}x{self.template_size[1]},中心{self.template_center}") def set_screenshot_center(self, sc_width: int, sc_height: int): self.screenshot_center = (sc_width // 2, sc_height // 2) logger.info(f"截图中心(角度原点)设置:{self.screenshot_center}(截图尺寸{sc_width}x{sc_height})") def match_icon(self, screenshot: np.ndarray) -> Tuple[Optional[Tuple[int, int]], str]: if self.template is None: return None, "1.png模板未加载" if screenshot is None: return None, "截图为空" sc_gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) sc_h, sc_w = sc_gray.shape result = cv2.matchTemplate(sc_gray, self.template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) if max_val < self.match_threshold: return None, f"匹配度不足({max_val:.2f} < 阈值{self.match_threshold})" icon_tx = max_loc[0] + self.template_center[0] icon_ty = max_loc[1] + self.template_center[1] icon_tx = max(0, min(icon_tx, sc_w - 1)) icon_ty = max(0, min(icon_ty, sc_h - 1)) if DEBUG_MODE: icon_x1 = max_loc[0] icon_y1 = max_loc[1] icon_x2 = max_loc[0] + self.template_size[0] icon_y2 = max_loc[1] + self.template_size[1] cv2.rectangle(screenshot, (icon_x1, icon_y1), (icon_x2, icon_y2), (0, 255, 0), 2) cv2.imwrite(f"{DEBUG_FOLDER}/debug_icon_match_{time.time():.0f}.png", screenshot) return (icon_tx, icon_ty), f"匹配成功(匹配度:{max_val:.2f})" def calc_angle(self, icon_center: Tuple[int, int]) -> Tuple[Optional[float], str]: if self.screenshot_center is None: return None, "未设置截图中心" if icon_center is None: return None, "图标中心为空" sx, sy = self.screenshot_center tx, ty = icon_center dx = tx - sx dy = sy - ty if dx == 0 and dy == 0: return 0.0, "图标在截图中心,角度=0度" radian = math.atan2(dx, dy) angle = math.degrees(radian) angle = angle if angle >= 0 else angle + 360 return angle, f"偏移(dx={dx},dy={dy}) → 角度{angle:.1f}度" # -------------------------- 导航系统核心 -------------------------- class NavigationSystem: def __init__(self): self.running = False self.thread = None self.nav_queue = Queue() self.status_queue = Queue() self.start_pos = None self.dest_pos = None self.current_pos = (500, 500) self.current_angle = 0.0 self.path_points = [] self.navigating = False self.pathfinder = AStarPathfinder() self.window_controller = None self.calibrator = CoordinateCalibrator() # 新增:坐标校准器 # 修复:1.png加载失败时,让程序能捕获异常 try: self.icon_angle_calc = IconTemplateAngleCalculator(template_path="1.png") except Exception as e: logger.error(f"1.png计算器初始化失败:{e}") raise Exception(f"请先解决1.png问题!错误:{e}") self.TARGET_WINDOW_WIDTH = 1024 self.TARGET_WINDOW_HEIGHT = 768 self.icon_angle_calc.set_screenshot_center( sc_width=self.TARGET_WINDOW_WIDTH, sc_height=self.TARGET_WINDOW_HEIGHT ) self.angle_tolerance = 15.0 self.speed = 1.0 def bind_window(self, window_title: str) -> Tuple[bool, str]: try: self.window_controller = OptimizedWindowController(window_title) self.TARGET_WINDOW_WIDTH = self.window_controller.client_width self.TARGET_WINDOW_HEIGHT = self.window_controller.client_height self.icon_angle_calc.set_screenshot_center( sc_width=self.TARGET_WINDOW_WIDTH, sc_height=self.TARGET_WINDOW_HEIGHT ) self.window_controller.ensure_window_active() return True, f"窗口绑定成功(客户区:{self.TARGET_WINDOW_WIDTH}x{self.TARGET_WINDOW_HEIGHT})" except Exception as e: return False, f"窗口绑定失败:{str(e)}" def add_obstacle(self, x: float, y: float): self.pathfinder.add_obstacle(x, y) def clear_obstacles(self): self.pathfinder.clear_obstacles() def set_start_position(self, pos: Tuple[float, float]): self.start_pos = pos self.current_pos = pos logger.info(f"设置导航起点:{pos}") def set_destination(self, pos: Tuple[float, float]) -> Tuple[bool, str]: if not self.start_pos: return False, "请先设置导航起点" if not self.window_controller: return False, "请先绑定目标窗口" self.dest_pos = pos self.path_points = self.pathfinder.find_path(self.start_pos, pos) if not self.path_points: return False, "路径生成失败:无可行路径" self.navigating = True self.status_queue.put(("info", f"目标设置完成:{pos},路径点数量:{len(self.path_points)}")) return True, f"目标设置完成,路径点数量:{len(self.path_points)}" def start_navigation(self): if self.thread and self.thread.is_alive(): self.stop_navigation() if self.navigating and not self.running and self.window_controller: self.running = True self.thread = threading.Thread(target=self._nav_loop, daemon=True) self.thread.start() logger.info("新导航线程启动") def stop_navigation(self): self.running = False self.navigating = False if self.thread and self.thread.is_alive(): logger.info("等待导航线程退出...") self.thread.join(timeout=2.0) self.thread = None self.path_points = [] self.current_pos = self.start_pos if self.start_pos else (500, 500) self.status_queue.put(("info", "导航已停止(状态已重置)")) logger.info("导航停止完成(线程+状态全重置)") def set_speed(self, speed: float): self.speed = max(0.8, min(speed, 1.2)) self.status_queue.put(("info", f"导航速度调整为:{self.speed:.1f}x(W键长按时间不受影响)")) def get_status(self, timeout=0.1) -> Optional[Tuple[str, str]]: try: return self.status_queue.get(timeout=timeout) except Empty: return None def _update_angle_by_1png(self): if not self.window_controller: return screenshot = self.window_controller.capture_client_screenshot() if screenshot is None: logger.warning("截图失败,无法更新角度") return icon_center, match_msg = self.icon_angle_calc.match_icon(screenshot) if icon_center is None: logger.warning(f"1.png图标匹配失败:{match_msg}") return angle, calc_msg = self.icon_angle_calc.calc_angle(icon_center) if angle is not None: self.current_angle = angle logger.info(f"角度更新:{self.current_angle:.1f}度 | {match_msg} | {calc_msg}") def _calc_w_hold_time(self, distance_to_waypoint: float) -> float: if distance_to_waypoint >= LONG_DISTANCE_THRESH: hold_time = np.random.uniform(5.0, 6.0) elif distance_to_waypoint >= MEDIUM_DISTANCE_THRESH: hold_time = np.random.uniform(3.0, 5.0) else: hold_time = np.random.uniform(2.0, 3.0) hold_time = max(W_KEY_MIN_HOLD, min(hold_time, W_KEY_MAX_HOLD)) return round(hold_time, 1) def _nav_loop(self): waypoint_idx = 0 logger.info("导航线程启动(循环开始)") while True: if not self.running or not self.navigating or not self.path_points or not self.window_controller: logger.info("导航线程退出(满足停止条件)") break if waypoint_idx >= len(self.path_points): self.navigating = False self.status_queue.put(("arrived", f"已到达最终目标:{self.dest_pos}")) break target_waypoint = self.path_points[waypoint_idx] dx = target_waypoint[0] - self.current_pos[0] dy = target_waypoint[1] - self.current_pos[1] distance = math.hypot(dx, dy) if distance < ARRIVAL_THRESHOLD: waypoint_idx += 1 self.status_queue.put(("info", f"到达路径点 {waypoint_idx}/{len(self.path_points) - 1}")) self._update_angle_by_1png() time.sleep(0.5) continue target_angle = math.degrees(math.atan2(dx, -dy)) % 360 angle_diff = (target_angle - self.current_angle) % 360 angle_diff = angle_diff if angle_diff <= 180 else angle_diff - 360 if abs(angle_diff) > self.angle_tolerance: turn_key = 'a' if angle_diff < 0 else 'd' turn_time = min((abs(angle_diff) / 30) * 1.0, 2.0) self.window_controller.key_press(turn_key, delay=turn_time) logger.debug(f"转向调整:{turn_key}键({turn_time:.1f}秒),角度差{angle_diff:.1f}度") self._update_angle_by_1png() time.sleep(0.3) continue w_hold_time = self._calc_w_hold_time(distance) self.status_queue.put(("info", f"直线行走:长按W键{w_hold_time:.1f}秒(到目标距离:{distance:.0f})")) self.window_controller.key_press('w', delay=w_hold_time) move_distance = 10 * w_hold_time * self.speed self.current_pos = ( self.current_pos[0] + math.sin(math.radians(self.current_angle)) * move_distance, self.current_pos[1] - math.cos(math.radians(self.current_angle)) * move_distance ) self.current_pos = ( max(0, min(self.current_pos[0], COORDINATE_MAX)), max(0, min(self.current_pos[1], COORDINATE_MAX)) ) self._update_angle_by_1png() time.sleep(0.2) # -------------------------- 小地图跟踪器(增加校准支持) -------------------------- class MinimapTracker: def __init__(self, calibrator: CoordinateCalibrator): if not (PIL_AVAILABLE and CV2_AVAILABLE): raise Exception("缺少图像库:Pillow/opencv-python") self.running = False self.thread = None self.frame_queue = Queue(maxsize=1) self.current_pos = (500, 500) self.dest_pos = None self.path_points = [] self.background_image = None self.calibrator = calibrator # 新增:坐标校准器 def load_background(self, image_path: str) -> bool: try: with Image.open(image_path) as img: self.background_image = img.copy() logger.info(f"加载小地图背景图:{image_path}") return True except Exception as e: logger.error(f"加载小地图背景图失败:{e}") return False def set_destination(self, pos: Tuple[float, float]): self.dest_pos = pos logger.debug(f"小地图目标点更新:{pos}") def set_path(self, path: List[Tuple[float, float]]): self.path_points = path logger.debug(f"小地图路径更新:{len(path)}个路径点") def update_position(self, pos: Tuple[float, float]): self.current_pos = ( max(0, min(pos[0], COORDINATE_MAX)), max(0, min(pos[1], COORDINATE_MAX)) ) def start(self): if not self.running: self.running = True self.thread = threading.Thread(target=self._refresh_loop, daemon=True) self.thread.start() logger.info("小地图跟踪器启动") def stop(self): self.running = False if self.thread: self.thread.join() self.thread = None logger.info("小地图跟踪器停止") def get_frame(self, timeout=0.1) -> Optional[np.ndarray]: try: return self.frame_queue.get(timeout=timeout) except Empty: return None def _refresh_loop(self): while self.running: if self.background_image: frame = cv2.cvtColor(np.array(self.background_image), cv2.COLOR_RGB2BGR) h, w = frame.shape[:2] else: w, h = 600, 500 frame = np.ones((h, w, 3), dtype=np.uint8) * 240 # 绘制校准点(如果已校准) if self.calibrator.is_calibrated: for i, point in enumerate(self.calibrator.calibration_points): minimap_x, minimap_y, world_x, world_y = point # 将小地图坐标转换为显示坐标 display_x = int(minimap_x * w / COORDINATE_MAX) display_y = int(minimap_y * h / COORDINATE_MAX) cv2.circle(frame, (display_x, display_y), 5, (255, 255, 0), -1) cv2.putText(frame, f"C{i + 1}", (display_x + 10, display_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2) if self.path_points and len(self.path_points) > 1: for i in range(len(self.path_points) - 1): x1 = int(self.path_points[i][0] * w / COORDINATE_MAX) y1 = int(self.path_points[i][1] * h / COORDINATE_MAX) x2 = int(self.path_points[i + 1][0] * w / COORDINATE_MAX) y2 = int(self.path_points[i + 1][1] * h / COORDINATE_MAX) cv2.line(frame, (x1, y1), (x2, y2), (255, 0, 0), 2) pos_x = int(self.current_pos[0] * w / COORDINATE_MAX) pos_y = int(self.current_pos[1] * h / COORDINATE_MAX) cv2.circle(frame, (pos_x, pos_y), 5, (0, 0, 255), -1) cv2.putText(frame, "当前", (pos_x + 10, pos_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) if self.dest_pos: dest_x = int(self.dest_pos[0] * w / COORDINATE_MAX) dest_y = int(self.dest_pos[1] * h / COORDINATE_MAX) cv2.circle(frame, (dest_x, dest_y), 5, (0, 255, 0), -1) cv2.putText(frame, "目标", (dest_x + 10, dest_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) try: self.frame_queue.put(frame, block=False) except: if not self.frame_queue.empty(): self.frame_queue.get(block=False) self.frame_queue.put(frame, block=False) time.sleep(0.1) # -------------------------- 校准窗口 -------------------------- class CalibrationWindow(QWidget): calibration_complete = pyqtSignal() def __init__(self, parent, nav_system, minimap_tracker): super().__init__(parent) self.nav_system = nav_system self.minimap_tracker = minimap_tracker self.calibration_stage = 0 # 0: 未开始, 1: 选择小地图区域, 2-N: 设置校准点 self.calibration_points = [] self.minimap_region = None self.setWindowTitle("大地图-小地图校准") self.setGeometry(100, 100, 800, 600) self.init_ui() def init_ui(self): layout = QVBoxLayout(self) # 说明区域 info_label = QLabel( "校准步骤: " "1. 点击'选择小地图区域'按钮,然后在游戏窗口中框选小地图区域 " "2. 设置至少2个校准点: " " - 在游戏中移动到已知坐标点 " " - 点击'获取当前位置'获取世界坐标 " " - 在小地图预览中点击对应位置设置小地图坐标 " "3. 完成校准后点击'应用校准'" ) info_label.setWordWrap(True) layout.addWidget(info_label) # 小地图区域选择 region_group = QGroupBox("步骤1: 选择小地图区域") region_layout = QVBoxLayout() self.select_region_btn = QPushButton("选择小地图区域") self.select_region_btn.clicked.connect(self.start_region_selection) self.region_status_label = QLabel("未选择区域") region_layout.addWidget(self.select_region_btn) region_layout.addWidget(self.region_status_label) region_group.setLayout(region_layout) layout.addWidget(region_group) # 校准点设置 cal_group = QGroupBox("步骤2: 设置校准点 (至少需要2个点)") cal_layout = QVBoxLayout() # 当前世界坐标 pos_layout = QHBoxLayout() self.world_x_spin = QSpinBox() self.world_x_spin.setRange(0, COORDINATE_MAX) self.world_y_spin = QSpinBox() self.world_y_spin.setRange(0, COORDINATE_MAX) self.get_pos_btn = QPushButton("获取当前位置") self.get_pos_btn.clicked.connect(self.get_current_position) pos_layout.addWidget(QLabel("世界坐标 X:")) pos_layout.addWidget(self.world_x_spin) pos_layout.addWidget(QLabel("Y:")) pos_layout.addWidget(self.world_y_spin) pos_layout.addWidget(self.get_pos_btn) cal_layout.addLayout(pos_layout) # 校准点列表 self.cal_points_list = QTextEdit() self.cal_points_list.setReadOnly(True) self.cal_points_list.setMaximumHeight(100) cal_layout.addWidget(self.cal_points_list) # 校准点操作按钮 btn_layout = QHBoxLayout() self.add_point_btn = QPushButton("添加校准点") self.add_point_btn.clicked.connect(self.add_calibration_point) self.add_point_btn.setEnabled(False) self.clear_points_btn = QPushButton("清空校准点") self.clear_points_btn.clicked.connect(self.clear_calibration_points) btn_layout.addWidget(self.add_point_btn) btn_layout.addWidget(self.clear_points_btn) cal_layout.addLayout(btn_layout) cal_group.setLayout(cal_layout) layout.addWidget(cal_group) # 校准操作 action_layout = QHBoxLayout() self.apply_cal_btn = QPushButton("应用校准") self.apply_cal_btn.clicked.connect(self.apply_calibration) self.apply_cal_btn.setEnabled(False) self.save_cal_btn = QPushButton("保存校准数据") self.save_cal_btn.clicked.connect(self.save_calibration) self.load_cal_btn = QPushButton("加载校准数据") self.load_cal_btn.clicked.connect(self.load_calibration) action_layout.addWidget(self.apply_cal_btn) action_layout.addWidget(self.save_cal_btn) action_layout.addWidget(self.load_cal_btn) layout.addLayout(action_layout) # 小地图预览 self.minimap_preview = QLabel() self.minimap_preview.setMinimumSize(400, 300) self.minimap_preview.setStyleSheet("border: 1px solid black;") self.minimap_preview.mousePressEvent = self.on_minimap_click layout.addWidget(self.minimap_preview) self.update_cal_points_list() def start_region_selection(self): if not self.nav_system.window_controller: QMessageBox.warning(self, "错误", "请先绑定目标窗口") return self.calibration_stage = 1 self.select_region_btn.setEnabled(False) self.region_status_label.setText("请在游戏窗口中框选小地图区域...") # 在实际实现中,这里应该启动一个区域选择过程 # 简化实现:假设用户手动输入区域坐标 QMessageBox.information(self, "提示", "在实际实现中,这里应该有一个交互式的区域选择过程。 " "简化实现:请手动输入小地图区域的坐标。") # 假设我们设置一个默认的小地图区域 self.minimap_region = (100, 100, 200, 200) self.region_status_label.setText(f"已选择区域: {self.minimap_region}") self.add_point_btn.setEnabled(True) def get_current_position(self): if self.nav_system.current_pos: self.world_x_spin.setValue(int(self.nav_system.current_pos[0])) self.world_y_spin.setValue(int(self.nav_system.current_pos[1])) def add_calibration_point(self): if not self.minimap_region: QMessageBox.warning(self, "错误", "请先选择小地图区域") return world_x = self.world_x_spin.value() world_y = self.world_y_spin.value() # 在实际实现中,这里应该等待用户在小地图上点击 # 简化实现:假设用户已经点击了小地图,我们使用一个默认值 QMessageBox.information(self, "提示", "在实际实现中,这里应该等待用户在小地图预览上点击。 " "简化实现:使用默认的小地图坐标。") # 假设小地图坐标为区域中心 minimap_x = self.minimap_region[0] + (self.minimap_region[2] - self.minimap_region[0]) // 2 minimap_y = self.minimap_region[1] + (self.minimap_region[3] - self.minimap_region[1]) // 2 self.calibration_points.append((minimap_x, minimap_y, world_x, world_y)) self.update_cal_points_list() if len(self.calibration_points) >= 2: self.apply_cal_btn.setEnabled(True) def clear_calibration_points(self): self.calibration_points = [] self.update_cal_points_list() self.apply_cal_btn.setEnabled(False) def update_cal_points_list(self): text = "校准点列表: " for i, point in enumerate(self.calibration_points): text += f"点{i + 1}: 小地图({point[0]},{point[1]}) -> 世界({point[2]},{point[3]}) " self.cal_points_list.setPlainText(text) def apply_calibration(self): # 清除旧的校准点 self.nav_system.calibrator.clear_calibration_points() # 添加新的校准点 for point in self.calibration_points: self.nav_system.calibrator.add_calibration_point(point[0], point[1], point[2], point[3]) if self.nav_system.calibrator.is_calibrated: QMessageBox.information(self, "成功", "校准已应用!") self.calibration_complete.emit() else: QMessageBox.warning(self, "错误", "校准失败,请检查校准点") def save_calibration(self): filepath, _ = QFileDialog.getSaveFileName(self, "保存校准数据", "", "JSON文件 (*.json)") if filepath: if not filepath.endswith('.json'): filepath += '.json' self.nav_system.calibrator.save_calibration(filepath) def load_calibration(self): filepath, _ = QFileDialog.getOpenFileName(self, "加载校准数据", "", "JSON文件 (*.json)") if filepath and self.nav_system.calibrator.load_calibration(filepath): # 更新UI显示 self.calibration_points = self.nav_system.calibrator.calibration_points self.update_cal_points_list() if len(self.calibration_points) >= 2: self.apply_cal_btn.setEnabled(True) QMessageBox.information(self, "成功", "校准数据已加载!") def on_minimap_click(self, event: QMouseEvent): if self.calibration_stage >= 2 and self.minimap_region: # 计算点击位置在小地图上的坐标 x = event.pos().x() y = event.pos().y() # 将点击位置转换为小地图坐标 preview_width = self.minimap_preview.width() preview_height = self.minimap_preview.height() minimap_x = int(x * COORDINATE_MAX / preview_width) minimap_y = int(y * COORDINATE_MAX / preview_height) # 获取当前世界坐标 world_x = self.world_x_spin.value() world_y = self.world_y_spin.value() # 添加校准点 self.calibration_points.append((minimap_x, minimap_y, world_x, world_y)) self.update_cal_points_list() if len(self.calibration_points) >= 2: self.apply_cal_btn.setEnabled(True) # -------------------------- 主UI界面(增加校准功能) -------------------------- class NavigationUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("导航控制系统(W键2-6秒长按版)") self.setGeometry(100, 100, *CONTROL_PANEL_SIZE) # 修复:先初始化nav_system,避免后续调用报错 try: self.nav_system = NavigationSystem() except Exception as e: QMessageBox.critical(self, "初始化失败", f"导航系统启动不了:{e}") self.close() return self.minimap_tracker = MinimapTracker(self.nav_system.calibrator) # 传入校准器 self.minimap_popup = None self.calibration_window = None self.init_ui() self.minimap_tracker.start() self.ui_timer = QTimer(self) self.ui_timer.timeout.connect(self.update_ui) self.ui_timer.start(30) def init_ui(self): central = QWidget() self.setCentralWidget(central) main_layout = QVBoxLayout(central) # 1. 窗口绑定区域 win_group = QGroupBox("窗口绑定(绑定后自动获取尺寸)") win_layout = QVBoxLayout() win_group.setLayout(win_layout) main_layout.addWidget(win_group) win_title_layout = QHBoxLayout() self.win_title_edit = QLineEdit("魔兽世界") win_title_layout.addWidget(QLabel("窗口标题(关键词):")) win_title_layout.addWidget(self.win_title_edit) self.bind_win_btn = QPushButton("绑定窗口") self.bind_win_btn.clicked.connect(self.bind_window) win_title_layout.addWidget(self.bind_win_btn) self.win_status_label = QLabel("状态:未绑定 | 1.png已加载 | W键长按2-6秒") win_title_layout.addWidget(self.win_status_label) win_layout.addLayout(win_title_layout) test_layout = QHBoxLayout() # 修复:测试W键按钮调用正确的方法(用nav_system的window_controller) self.test_w_btn = QPushButton("测试W键(3秒长按)") self.test_w_btn.clicked.connect(lambda: self.test_w_key_hold(3.0)) self.test_click_btn = QPushButton("测试鼠标点击(100,100)") self.test_click_btn.clicked.connect(lambda: self.test_mouse_click(100, 100)) self.calibration_btn = QPushButton("大地图-小地图校准") # 新增:校准按钮 self.calibration_btn.clicked.connect(self.show_calibration_window) test_layout.addWidget(self.test_w_btn) test_layout.addWidget(self.test_click_btn) test_layout.addWidget(self.calibration_btn) win_layout.addLayout(test_layout) # 2. 导航设置 nav_group = QGroupBox("导航设置") nav_layout = QVBoxLayout() nav_group.setLayout(nav_layout) main_layout.addWidget(nav_group) start_layout = QHBoxLayout() self.start_x = QSpinBox() self.start_y = QSpinBox() self.start_x.setRange(0, COORDINATE_MAX) self.start_y.setRange(0, COORDINATE_MAX) self.start_x.setValue(200) self.start_y.setValue(200) start_layout.addWidget(QLabel("起点坐标(X,Y):")) start_layout.addWidget(self.start_x) start_layout.addWidget(self.start_y) self.set_start_btn = QPushButton("设置起点") self.set_start_btn.clicked.connect(self.set_start_position) start_layout.addWidget(self.set_start_btn) nav_layout.addLayout(start_layout) dest_layout = QHBoxLayout() self.dest_x = QSpinBox() self.dest_y = QSpinBox() self.dest_x.setRange(0, COORDINATE_MAX) self.dest_y.setRange(0, COORDINATE_MAX) self.dest_x.setValue(800) self.dest_y.setValue(800) dest_layout.addWidget(QLabel("终点坐标(X,Y):")) dest_layout.addWidget(self.dest_x) dest_layout.addWidget(self.dest_y) self.set_dest_btn = QPushButton("设置终点") self.set_dest_btn.clicked.connect(self.set_destination) self.show_minimap_btn = QPushButton("显示小地图") self.show_minimap_btn.clicked.connect(self.show_minimap) dest_layout.addWidget(self.set_dest_btn) dest_layout.addWidget(self.show_minimap_btn) nav_layout.addLayout(dest_layout) obstacle_layout = QHBoxLayout() self.add_obstacle_btn = QPushButton("添加障碍物(当前起点)") self.add_obstacle_btn.clicked.connect(self.add_obstacle) self.clear_obstacle_btn = QPushButton("清空障碍物") self.clear_obstacle_btn.clicked.connect(self.clear_obstacles) obstacle_layout.addWidget(self.add_obstacle_btn) obstacle_layout.addWidget(self.clear_obstacle_btn) nav_layout.addLayout(obstacle_layout) # 3. 导航控制 ctrl_group = QGroupBox("导航控制(W键长按2-6秒,长距离优先满时长)") ctrl_layout = QHBoxLayout() ctrl_group.setLayout(ctrl_layout) main_layout.addWidget(ctrl_group) self.start_nav_btn = QPushButton("开始导航") self.start_nav_btn.clicked.connect(self.start_navigation) self.start_nav_btn.setEnabled(False) self.stop_nav_btn = QPushButton("停止导航") self.stop_nav_btn.clicked.connect(self.stop_navigation) self.stop_nav_btn.setEnabled(False) ctrl_layout.addWidget(self.start_nav_btn) ctrl_layout.addWidget(self.stop_nav_btn) speed_layout = QHBoxLayout() speed_layout.addWidget(QLabel("导航速度(微调):")) self.speed_slider = QSlider(Qt.Horizontal) self.speed_slider.setRange(8, 12) self.speed_slider.setValue(10) self.speed_slider.valueChanged.connect(self.set_nav_speed) self.speed_label = QLabel("1.0x") speed_layout.addWidget(self.speed_slider) speed_layout.addWidget(self.speed_label) ctrl_layout.addLayout(speed_layout) self.nav_status_label = QLabel("导航状态:就绪 | W键长按2-6秒") ctrl_layout.addWidget(self.nav_status_label) # 4. 日志区域 self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setMaximumHeight(120) main_layout.addWidget(self.log_area) def log(self, content: str): timestamp = time.strftime("%H:%M:%S") self.log_area.append(f"[{timestamp}] {content}") self.log_area.moveCursor(self.log_area.textCursor().End) def bind_window(self): win_title = self.win_title_edit.text().strip() if not win_title: QMessageBox.warning(self, "提示", "请输入窗口标题关键词") return success, msg = self.nav_system.bind_window(win_title) if success: self.win_status_label.setText(f"状态:已绑定 | {msg} | W键长按2-6秒") self.log(f"窗口绑定成功:{msg}") self.start_nav_btn.setEnabled(True) self.test_w_btn.setEnabled(True) # 绑定后才启用测试按钮 else: self.win_status_label.setText(f"状态:绑定失败 | {msg} | W键长按2-6秒") self.log(f"窗口绑定失败:{msg}") QMessageBox.warning(self, "错误", msg) # 修复:单独的测试W键长按方法,确保调用nav_system的window_controller def test_w_key_hold(self, hold_time): if not self.nav_system.window_controller: QMessageBox.warning(self, "提示", "请先绑定目标窗口再测试!") return self.nav_system.window_controller.key_press('w', delay=hold_time) self.log(f"测试W键长按:{hold_time:.1f}秒") def set_start_position(self): x = self.start_x.value() y = self.start_y.value() self.nav_system.set_start_position((x, y)) self.minimap_tracker.update_position((x, y)) self.log(f"设置导航起点:({x},{y})") def set_destination(self): x = self.dest_x.value() y = self.dest_y.value() success, msg = self.nav_system.set_destination((x, y)) if success: self.minimap_tracker.set_destination((x, y)) self.minimap_tracker.set_path(self.nav_system.path_points) self.log(f"设置导航终点:({x},{y}),{msg}") else: self.log(f"设置终点失败:{msg}") QMessageBox.warning(self, "错误", msg) def add_obstacle(self): if not self.nav_system.start_pos: QMessageBox.warning(self, "提示", "请先设置导航起点") return x, y = self.nav_system.start_pos self.nav_system.add_obstacle(x, y) self.log(f"添加障碍物:({x},{y})") def clear_obstacles(self): self.nav_system.clear_obstacles() self.log("已清空所有障碍物") def set_nav_speed(self): speed = self.speed_slider.value() / 10 self.speed_label.setText(f"{speed:.1f}x") self.nav_system.set_speed(speed) self.log(f"导航速度调整为:{speed:.1f}x(W键长按时间不受影响)") def start_navigation(self): self.nav_system.start_navigation() self.start_nav_btn.setEnabled(False) self.stop_nav_btn.setEnabled(True) self.nav_status_label.setText("导航状态:导航中 | W键长按2-6秒") self.log("导航已启动(W键长按2-6秒,长距离优先满时长)") def stop_navigation(self): self.nav_system.stop_navigation() self.start_nav_btn.setEnabled(True) self.stop_nav_btn.setEnabled(False) self.nav_status_label.setText("导航状态:已停止 | W键长按2-6秒") self.log("导航已停止") def show_minimap(self): if self.minimap_popup and self.minimap_popup.isVisible(): self.minimap_popup.hide() return self.minimap_popup = QWidget(self) self.minimap_popup.setWindowTitle("小地图(路径可视化)") self.minimap_popup.setGeometry(200, 200, *MINIMAP_POPUP_SIZE) self.minimap_popup.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) self.minimap_label = QLabel(self.minimap_popup) self.minimap_label.setGeometry(0, 0, MINIMAP_POPUP_SIZE[0], MINIMAP_POPUP_SIZE[1]) self.minimap_popup.show() def test_mouse_click(self, x, y): if not self.nav_system.window_controller: QMessageBox.warning(self, "提示", "请先绑定目标窗口") return self.nav_system.window_controller.mouse_click(x, y, delay=0.1) self.log(f"测试鼠标左键点击:客户区({x},{y})(0.1秒)") def show_calibration_window(self): """显示校准窗口""" if not self.nav_system.window_controller: QMessageBox.warning(self, "错误", "请先绑定目标窗口") return if self.calibration_window is None: self.calibration_window = CalibrationWindow(self, self.nav_system, self.minimap_tracker) self.calibration_window.calibration_complete.connect(self.on_calibration_complete) self.calibration_window.show() def on_calibration_complete(self): """校准完成后的回调""" self.log("大地图-小地图校准已完成") if self.calibration_window: self.calibration_window.close() def update_ui(self): if self.minimap_popup and self.minimap_popup.isVisible(): frame = self.minimap_tracker.get_frame() if frame is not None: rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_frame.shape q_img = QImage(rgb_frame.data, w, h, ch * w, QImage.Format_RGB888) pixmap = QPixmap.fromImage(q_img).scaled( self.minimap_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.minimap_label.setPixmap(pixmap) status = self.nav_system.get_status(timeout=0.01) if status: stype, msg = status self.log(f"导航状态:{msg}") if stype == "arrived": self.nav_status_label.setText("导航状态:已到达 | W键长按2-6秒") self.start_nav_btn.setEnabled(True) self.stop_nav_btn.setEnabled(False) elif stype == "error": self.nav_status_label.setText(f"导航状态:错误 | {msg}") QMessageBox.warning(self, "导航错误", msg) if self.nav_system.current_pos: self.minimap_tracker.update_position(self.nav_system.current_pos) def closeEvent(self, event): if hasattr(self, 'nav_system'): self.nav_system.stop_navigation() if hasattr(self, 'minimap_tracker'): self.minimap_tracker.stop() if self.calibration_window: self.calibration_window.close() event.accept() # -------------------------- 程序入口(增加整体异常捕获) -------------------------- if __name__ == "__main__": import sys # 1. 先检查依赖 dependencies_ok, msg = check_dependencies() if not dependencies_ok: print(f"依赖错误:{msg}") print("请先执行以下命令安装依赖:") print("pip install pillow pywin32 opencv-python pyqt5") input("按回车键退出...") sys.exit(1) # 2. 启动UI,捕获所有异常 try: app = QApplication(sys.argv) window = NavigationUI() window.show() sys.exit(app.exec_()) except Exception as e: print(f"程序启动失败:{str(e)}") print(" 常见解决办法:") print("1. 确保1.png在代码同一文件夹,且是有效PNG/JPG图片;") print("2. 以管理员身份运行代码(解决窗口绑定权限问题);") print("3. 检查Python版本(推荐3.7-3.10,避免PyQt5不兼容)。") input("按回车键退出...") sys.exit(1)
© 版权声明
文章版权归作者所有,未经允许请勿转载。如内容涉嫌侵权,请在本页底部进入<联系我们>进行举报投诉!
THE END
暂无评论内容