自动寻路简单代码后续自己开发

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
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
叮咚麻麻的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容