吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 24606|回复: 213
上一主题 下一主题
收起左侧

[Python 转载] 图像识别点击 [图像识别自动点击按键精灵] 修改版

     关闭 [复制链接]
跳转到指定楼层
楼主
gzsklsskszngc 发表于 2024-9-19 23:49 回帖奖励
本帖最后由 gzsklsskszngc 于 2024-12-30 19:37 编辑

首先感谢原贴作者提供的代码:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1965479&highlight=%CD%BC%CF%F1
我在原代码的基础上做了以下小修改
上代码:
[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import filedialog, simpledialog, ttk, messagebox
from PIL import Image, ImageTk, ImageGrab
import cv2
import numpy as np
import pyautogui
import time
import threading
import logging
from logging.handlers import RotatingFileHandler
import os
import json
import atexit
from datetime import datetime
import shutil
import keyboard

class ImageRecognitionApp:
    def __init__(self, root):
        self.root = root
        self.root.title("图像识别与点击")
        self.image_list = []  # 存储 (图像路径, 步骤名称, 相似度, 键盘输入, 鼠标点击坐标, 等待时间, 条件, 跳转步骤)
        self.screenshot_area = None  # 用于存储截图区域
        self.rect = None  # 用于存储 Canvas 上的矩形
        self.start_x = None
        self.start_y = None
        self.canvas = None
        self.running = False  # 控制脚本是否在运行
        self.thread = None  # 用于保存线程
        self.hotkey = '<F1>'  # 默认热键
        self.similarity_threshold = 0.8  # 默认相似度阈值
        self.delay_time = 0.1  # 默认延迟时间
        self.loop_count = 1  # 默认循环次数
        self.screenshot_folder = 'screenshots'  # 截图保存文件夹
        self.paused = False  # 控制脚本是否暂停
        self.copied_item = None
        self.config_filename = 'config.json'  # 默认配置文件名
        self.start_step_index = 0  # 初始化
        self.follow_current_step = tk.BooleanVar(value=False)  # 控制是否跟随当前步骤
        self.only_keyboard_var = tk.BooleanVar(value=False)  # 控制是否只进行键盘操作
        self.init_ui()
        self.tree.image_refs = []  # 初始化 image_refs 属性
        self.init_logging()
        self.bind_arrow_keys()
        self.create_context_menu()
        atexit.register(self.cleanup_on_exit)
        self.hotkey_id = None # 初始化热键id
        self.register_global_hotkey() # 注册全局热键

    def init_ui(self):
        # 主框架布局
        self.main_frame = tk.Frame(self.root)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 左侧布局:框选截图、运行脚本按钮
        self.left_frame = tk.Frame(self.main_frame)
        self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)

        # 框选截图按钮(微信风格截图)
        self.screenshot_button = tk.Button(self.left_frame, text="框选截图", command=self.prepare_capture_screenshot)
        self.screenshot_button.pack(pady=5)

        # 删除选中图片按钮
        self.delete_button = tk.Button(self.left_frame, text="删除图片", command=self.delete_selected_image)
        self.delete_button.pack(pady=5)

        # 运行/停止脚本按钮
        self.toggle_run_button = tk.Button(self.left_frame, text="开始运行", command=self.toggle_script)
        self.toggle_run_button.pack(pady=5)

        # 保存配置按钮
        self.save_config_button = tk.Button(self.left_frame, text="保存配置", command=self.save_config)
        self.save_config_button.pack(pady=5)

        # 设置热键按钮
        self.set_hotkey_button = tk.Button(self.left_frame, text="设置热键", command=self.set_hotkey)
        self.set_hotkey_button.pack(pady=5)

        # 手动加载配置按钮
        self.load_config_button = tk.Button(self.left_frame, text="加载配置", command=self.load_config_manually)
        self.load_config_button.pack(pady=5)

        # 循环次数输入框
        self.loop_count_label = tk.Label(self.left_frame, text="循环次数:")
        self.loop_count_label.pack(pady=5)
        self.loop_count_entry = tk.Entry(self.left_frame)
        self.loop_count_entry.insert(0, str(self.loop_count))
        self.loop_count_entry.pack(pady=5)

        # 添加测试匹配按钮
        self.test_match_button = tk.Button(self.left_frame, text="测试匹配", command=self.test_single_match)
        self.test_match_button.pack(pady=5)

        # 添加允许最小化和跟随当前步骤的勾选框
        self.allow_minimize_var = tk.BooleanVar(value=True)  # 默认允许最小化
        self.allow_minimize_checkbox = tk.Checkbutton(self.left_frame, text="允许最小化", variable=self.allow_minimize_var)
        self.allow_minimize_checkbox.pack(side=tk.LEFT, pady=5)

        self.follow_step_checkbox = tk.Checkbutton(self.left_frame, text="焦点跟随", variable=self.follow_current_step)
        self.follow_step_checkbox.pack(side=tk.LEFT, pady=5)  # 跟随当前步骤勾选框

        # 只键盘操作勾选框
        self.only_keyboard_checkbox = tk.Checkbutton(self.left_frame, text="只键盘操作", variable=self.only_keyboard_var)
        self.only_keyboard_checkbox.pack(side=tk.LEFT, pady=5)

        # 右侧布局:框架分为预览区和树形视图区
        self.right_frame = tk.Frame(self.main_frame)
        self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 创建右侧预览面板
        self.preview_panel = tk.Frame(self.right_frame, width=120, height=120,
                                    relief=tk.GROOVE, borderwidth=1)  # 添加边框效果
        self.preview_panel.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0))  # 调整右侧边距
        self.preview_panel.pack_propagate(False)
         
        # 使用更现代的标签样式
        self.preview_label = tk.Label(self.preview_panel, text="图像预览",
                                    font=('Arial', 9), fg='#666666')
        self.preview_label.pack(pady=3)
         
        # 创建预览图像容器
        self.preview_container = tk.Frame(self.preview_panel, 
                                        width=100, height=100,
                                        relief=tk.SUNKEN, borderwidth=1)
        self.preview_container.pack(pady=2)
        self.preview_container.pack_propagate(False)
         
        self.preview_image_label = tk.Label(self.preview_container, bg='#f5f5f5')
        self.preview_image_label.pack(expand=True)

        # 使用 Treeview 来显示图片和等待时间(调整包装方式)
        self.tree = ttk.Treeview(self.right_frame, columns=(
            "图片", "步骤名称", "相似度", "键盘输入", "鼠标点击坐标", "等待时间", "条件", "跳转步骤"
        ), show='headings')
        self.tree.heading("图片", text="图片")
        self.tree.heading("步骤名称", text="步骤名称")
        self.tree.heading("相似度", text="相似度")
        self.tree.heading("键盘输入", text="键盘输入")
        self.tree.heading("鼠标点击坐标", text="鼠标点击坐标(F2)")
        self.tree.heading("等待时间", text="等待时间 (毫秒)")
        self.tree.heading("条件", text="条件")
        self.tree.heading("跳转步骤", text="跳转步骤")

        # 设置列宽和对齐方式(居中)
        self.tree.column("图片", width=100, anchor='center')
        self.tree.column("步骤名称", width=100, anchor='center')
        self.tree.column("相似度", width=80, anchor='center')
        self.tree.column("键盘输入", width=100, anchor='center')
        self.tree.column("鼠标点击坐标", width=130, anchor='center')
        self.tree.column("等待时间", width=100, anchor='center')
        self.tree.column("条件", width=80, anchor='center')
        self.tree.column("跳转步骤", width=80, anchor='center')
          
        # 创建垂直滚动条
        self.scrollbar = ttk.Scrollbar(self.right_frame, orient="vertical", command=self.tree.yview)
        self.scrollbar.pack(side="right", fill="y")

        # 配置 Treeview 使用滚动条并调整其位置
        self.tree.configure(yscrollcommand=self.scrollbar.set)
        self.tree.pack(side="left", fill=tk.BOTH, expand=True)

        # 绑定 F2 键获取鼠标位置
        self.root.bind('<F2>', self.get_mouse_position)  

        # 初始化上下文菜单
        self.tree.unbind('<Double-1>')
        self.tree.unbind('<Double-3>')
        self.tree.unbind('<Double-2>')

        # 为上下文菜单添加此绑定
        self.tree.bind('<Button-3>', self.show_context_menu)  # 右键点击
        self.tree.bind('<Button-1>', self.on_treeview_select)  # 左键点击

        # 在 Treeview 创建后添加选择事件绑定
        self.tree.bind('<<TreeviewSelect>>', self.on_treeview_select)

    def init_logging(self): # 初始化日志
        handler = RotatingFileHandler('app.log', maxBytes=10*1024*1024, backupCount=5)  # 创建日志文件处理器
        logging.basicConfig(handlers=[handler], level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # 配置日志格式

    def register_global_hotkey(self):
        try:
            def hotkey_callback():
                self.root.after(0, self.toggle_script)  # 使用 after 在主线程中调用
            
            # 解析热键字符串
            hotkey_str = self.hotkey.replace('<', '').replace('>', '').lower()
            keyboard.add_hotkey(hotkey_str, hotkey_callback)
            
            print(f"全局热键已注册:{self.hotkey}")
            logging.info(f"全局热键已注册:{self.hotkey}")
        except Exception as e:
            print(f"注册热键失败: {e}")
            logging.error(f"注册热键失败: {e}")

    def unregister_global_hotkey(self):
        try:
           hotkey_str = self.hotkey.replace('<', '').replace('>', '').lower()
           keyboard.remove_hotkey(hotkey_str)
           print(f"全局热键已注销:{self.hotkey}")
           logging.info(f"全局热键已注销:{self.hotkey}")
        except Exception as e:
           print(f"注销全局热键出错:{e}")
           logging.error(f"注销全局热键出错:{e}")


    def prepare_capture_screenshot(self):
        # 在截图前计算新的步骤编号
        existing_steps = set()
        for item in self.image_list:
            step_name = item[1]
            if step_name.startswith("步骤"):
                try:
                    num = int(step_name[2:])  # 提取"步骤"后面的数字
                    existing_steps.add(num)
                except ValueError:
                    continue
          
        # 找到最小的未使用编号
        new_step_num = 1
        while new_step_num in existing_steps:
            new_step_num += 1
          
        # 保存新步骤编号,供 on_button_release 使用
        self._next_step_num = new_step_num
  
        # 隐藏主窗口
        self.root.withdraw()
        time.sleep(0.5)
  
        # 创建一个全屏幕的透明窗口,用于捕获框选区域
        self.top = tk.Toplevel(self.root)
        self.top.attributes('-fullscreen', True)
        self.top.attributes('-alpha', 0.3)  # 透明度设置
  
        # 在窗口上创建 Canvas
        self.canvas = tk.Canvas(self.top, cursor="cross", bg='grey')
        self.canvas.pack(fill=tk.BOTH, expand=True)
  
        # 绑定鼠标事件
        self.canvas.bind("<ButtonPress-1>", self.on_button_press)
        self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
  
    def on_button_press(self, event):
        # 记录起始点坐标
        self.start_x = event.x
        self.start_y = event.y
        # 如果有之前的矩形,删除
        if self.rect:
            self.canvas.delete(self.rect)
        # 创建新的矩形框
        self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y, outline='red', width=2)
  
    def on_mouse_drag(self, event):
        # 动态更新矩形框的大小
        cur_x, cur_y = (event.x, event.y)
        self.canvas.coords(self.rect, self.start_x, self.start_y, cur_x, cur_y)
  
    def on_button_release(self, event):
        # 记录终点坐标
        end_x = event.x
        end_y = event.y

        # 获取截图区域
        bbox = (min(self.start_x, end_x), min(self.start_y, end_y), max(self.start_x, end_x), max(self.start_y, end_y))

        # 使用规则 "截图(时间).png" 命名截图文件避免重复
        timestamp = datetime.now().strftime("%H%M%S")  # 使用当前时间作为文件名
        screenshot_path = os.path.join(self.screenshot_folder, f"JT_{timestamp}.png")

        # 确保截图文件夹存在
        os.makedirs(self.screenshot_folder, exist_ok=True)

        # 截图指定区域
        screenshot = ImageGrab.grab(bbox)
        screenshot.save(screenshot_path)

        # 计算截图区域的中心坐标
        center_x = (min(self.start_x, end_x) + max(self.start_x, end_x)) // 2
        center_y = (min(self.start_y, end_y) + max(self.start_y, end_y)) // 2
        mouse_click_coordinates = f"{center_x},{center_y}"  # 使用中心坐标

        # 更新图像列表
        selected_item = self.tree.selection()  # 获取当前选中的项
        step_name = f"步骤{self._next_step_num}"  # 生成递增的步骤名称
        if selected_item:   
            selected_index = self.tree.index(selected_item[0])  
            self.image_list.insert(selected_index, (screenshot_path, step_name, 0.8, "", mouse_click_coordinates, 100, "", ""))  # 在选中项之前插入新项
        else:
            self.image_list.append((screenshot_path, step_name, 0.8, "", mouse_click_coordinates, 100, "", ""))  # 在列表末尾添加新项
        self.update_image_listbox()  # 更新图像列表框

        # 关闭全屏透明窗口
        self.top.destroy()
        self.root.deiconify()
  
    def update_image_listbox(self):
        try:
            # 保存当前预览状态
            current_preview = None
            if hasattr(self.preview_image_label, 'image'):
                current_preview = self.preview_image_label.image
             
            # 保存当前选中的项目
            selected_items = self.tree.selection()
            focused_item = self.tree.focus()
  
            # 清空旧的列表项
            for row in self.tree.get_children():
                self.tree.delete(row)
  
            # 插入新项,显示图片名称和等待时间
            for index, item in enumerate(self.image_list):
                try:
                    if not item or len(item) < 1:  # 检查项目是否有效
                        continue
                      
                    img_path = item[0]
                    if not os.path.exists(img_path):
                        print(f"警告:图像文件不存在 {img_path}")
                        logging.warning(f"图像文件不存在 {img_path}")
                        continue
  
                    # 确保每个项目都有 8 个元素,如果没有,用空字符串填充
                    full_item = list(item)
                    while len(full_item) < 8:
                        full_item.append("")
                      
                    img_path, step_name, similarity_threshold, keyboard_input, mouse_click_coordinates, wait_time, condition, jump_to = full_item
  
                    # 加载图像并创建缩略图
                    try:
                        image = Image.open(img_path)
                        image.thumbnail((50, 50))  # 调整缩略图大小
                        photo = ImageTk.PhotoImage(image)
  
                        # 插入所有数据
                        tree_item = self.tree.insert("", tk.END, values=(
                            os.path.basename(img_path), 
                            step_name, 
                            similarity_threshold, 
                            keyboard_input, 
                            mouse_click_coordinates, 
                            wait_time,
                            condition,
                            jump_to
                        ), image=photo)
                        self.tree.image_refs.append(photo)  # 保持对图像的引用
  
                    except Exception as e:
                        print(f"处理图像时出错 {img_path}: {e}")
                        logging.error(f"处理图像时出错 {img_path}: {e}")
  
                except Exception as e:
                    print(f"处理列表项时出错: {e}")
                    logging.error(f"处理列表项时出错: {e}")
  
            # 恢复选择状态
            if selected_items:
                for item in selected_items:
                    try:
                        self.tree.selection_add(item)
                    except:
                        pass
            if focused_item:
                try:
                    self.tree.focus(focused_item)
                except:
                    pass
  
            # 恢复预览状态
            if current_preview:
                self.preview_image_label.config(image=current_preview)
                self.preview_image_label.image = current_preview
             
        except Exception as e:
            print(f"更新图像列表时出错: {e}")
            logging.error(f"更新图像列表时出错: {e}")
            self.reset_to_initial_state()
  
    def delete_selected_image(self):
        try:
            selected_item = self.tree.selection()
            if not selected_item:
                messagebox.showinfo("提示", "请先选择要删除的项目")
                return
  
            selected_index = self.tree.index(selected_item[0])
            if 0 <= selected_index < len(self.image_list):
                selected_image = self.image_list[selected_index]
                img_path = selected_image[0]
                  
                # 检查是否正在使用配置文件
                if hasattr(self, 'config_filename') and os.path.exists(self.config_filename):
                    # 显示确认对话框
                    result = messagebox.askyesnocancel(
                        "确认删除",
                        f"当正在使用配置文件\n{self.config_filename}\n\n是否删除该步骤并更新配置文件?\n\n选择:\n"
                        f"是 - 删除步骤并更新配置文件(同时删除图片文件)\n"
                        f"否 - 仅删除步骤(保留图片文件)\n"
                        f"取消 - 不执行任何操作"
                    )
                      
                    if result is None:  # 用户点击取消
                        return
                      
                    # 删除图像列表中的项目
                    del self.image_list[selected_index]
                      
                    if result:  # 用户点击是,更新配置文件并删除图片
                        try:
                            # 读取现有配置
                            with open(self.config_filename, 'r') as f:
                                config = json.load(f)
                              
                            # 更新配置中的图像列表
                            config['image_list'] = self.image_list
                              
                            # 保存更新后的配置
                            with open(self.config_filename, 'w') as f:
                                json.dump(config, f)
                                  
                            # 检查是否有其他项目引用相同的图像文件
                            is_referenced = any(item[0] == img_path for item in self.image_list)
                              
                            # 如果没有其他引用,则删除图像文件
                            if not is_referenced and os.path.exists(img_path):
                                try:
                                    os.remove(img_path)
                                    print(f"图像文件已删除: {img_path}")
                                    logging.info(f"图像文件已删除: {img_path}")
                                except Exception as e:
                                    print(f"删除图像文件时出错: {e}")
                                    logging.error(f"删除图像文件时出错: {e}")
                              
                            print(f"配置文件已更新: {self.config_filename}")
                            logging.info(f"配置文件已更新: {self.config_filename}")
                            messagebox.showinfo("成功", "步骤已删除,配置文件已更新,图片已删除")
                        except Exception as e:
                            error_msg = f"更新配置文件时出错: {str(e)}"
                            print(error_msg)
                            logging.error(error_msg)
                            messagebox.showerror("错误", error_msg)
                    else:  # 用户点击否,仅删除步骤,保留图片
                        messagebox.showinfo("成功", "步骤已删除(图片文件已保留)")
                else:
                    # 如果没有使用配置文件,直接删除步骤和图片
                    if messagebox.askyesno("确认删除", "是否删除该步骤和对应的图片文件?"):
                        del self.image_list[selected_index]
                        # 检查是否有其他项目引用相同的图像文件
                        is_referenced = any(item[0] == img_path for item in self.image_list)
                        if not is_referenced and os.path.exists(img_path):
                            try:
                                os.remove(img_path)
                                print(f"图像文件已删除: {img_path}")
                                logging.info(f"图像文件已删除: {img_path}")
                            except Exception as e:
                                print(f"删除图像文件时出错: {e}")
                                logging.error(f"删除图像文件时出错: {e}")
                  
                self.update_image_listbox()
            else:
                print("选中的索引超出范围")
                logging.warning("选中的索引超出范围")
        except Exception as e:
            print(f"删除图像时出错: {e}")
            logging.error(f"删除图像时出错: {e}")
            self.reset_to_initial_state()
  
    def toggle_script(self, event=None):
        if not self.running:
            self.start_step_index = 0  # 确保从第一步开始
            self.start_script_thread()
            self.toggle_run_button.config(text="停止运行")
            if self.allow_minimize_var.get():  # 检查勾选框状态
                self.root.iconify()  # 最小化主窗口
            else:
                self.root.lift()  # 确保主窗口在最上层
                self.root.attributes('-topmost', True)  # 设置为最上层窗口
        else:
            self.stop_script()
            self.toggle_run_button.config(text="开始运行")
            self.root.attributes('-topmost', False)  # 取消最上层设置
  
    def start_script_thread(self):
        if not self.running:
            self.running = True
            self.thread = threading.Thread(target=self.run_script, daemon=True)
            self.thread.start()
  
    def run_script(self):
        try:
            self.loop_count = int(self.loop_count_entry.get())
            if self.loop_count < 0:
                raise ValueError("循环次数不能为负数")
        except ValueError as e:
            messagebox.showerror("输入错误", f"请输入有效的非负整数作为循环次数: {str(e)}")
            self.running = False
            self.root.after(0, self.update_ui_after_stop)
            return
  
        print(f"开始执行脚本,从步骤 {self.start_step_index} 开始,循环次数:{self.loop_count}")
        logging.info(f"开始执行脚本,从步骤 {self.start_step_index} 开始,循环次数:{self.loop_count}")
  
        current_loop = 0
  
        while self.running and (current_loop < self.loop_count or self.loop_count == 0):
            if self.paused:
                time.sleep(0.1)
                continue
        
            index = self.start_step_index
            while index < len(self.image_list) and self.running:
                # 添加焦点跟随功能
                if self.follow_current_step.get():
                    self.root.after(0, lambda idx=index: self.focus_on_step(idx))
                
                current_step = self.image_list[index]
                img_path, img_name, similarity_threshold, keyboard_input, mouse_click_coordinates, wait_time = current_step[:6]
                condition = current_step[6] if len(current_step) > 6 else None
                jump_to = current_step[7] if len(current_step) > 7 else None
                
                # 执行一次图像匹配
                if mouse_click_coordinates and not self.only_keyboard_var.get():
                    match_result = self.match_and_click(img_path, similarity_threshold)
                elif os.path.exists(img_path):
                    match_result = self.match_and_click(img_path, similarity_threshold)
                else:
                     match_result = True if keyboard_input else False # 如果没有鼠标操作且有键盘操作,则默认视为匹配成功,否则为失败

                # 等待时间处理
                if wait_time > 0:
                    time.sleep(wait_time / 1000.0)  # 将毫秒转换为秒

                # 处理键盘输入中的延时
                if keyboard_input:
                    commands = self.parse_keyboard_input(keyboard_input)
                    for cmd in commands:
                        if isinstance(cmd, float):  # 延时
                            time.sleep(cmd)
                        else:
                            # 处理其他键盘输入
                            pyautogui.press(cmd)

                # 处理条件跳转
                if condition and jump_to:
                    should_jump = False
                    if condition == "True" and match_result:
                        should_jump = True
                        print(f"条件为True且匹配成功,从{img_name}跳转到{jump_to}")
                    elif condition == "False" and not match_result:
                        should_jump = True
                        print(f"条件为False且匹配失败,从{img_name}跳转到{jump_to}")
                    
                    if should_jump:
                        # 查找跳转目标步骤的索引
                        for i, step in enumerate(self.image_list):
                            if step[1] == jump_to:
                                index = i
                                break
                        continue
                
                # 如果没有跳转且匹配失败,则持续尝试匹配
                if not match_result and not (condition and jump_to) and not self.only_keyboard_var.get():
                    while not match_result and self.running:
                        time.sleep(wait_time / 1000.0)
                        if mouse_click_coordinates:
                            match_result = self.match_and_click(img_path, similarity_threshold)
                        elif os.path.exists(img_path):
                            match_result = self.match_and_click(img_path, similarity_threshold)

                index += 1

            current_loop += 1

        self.running = False
        self.root.after(0, self.update_ui_after_stop)
  
    def start_script_thread(self):
        if not self.running:
            self.running = True
            self.thread = threading.Thread(target=self.run_script, daemon=True)
            self.thread.start()
  
    def stop_script(self):
        self.running = False
        if self.thread is not None:
            self.thread.join(timeout=1)  # 等待线程结束,最多等待2秒
            if self.thread.is_alive():
                print("警告:脚本线程未能在1秒内停止")
                logging.warning("脚本线程未能在1秒内停运行")
        self.thread = None
        print("脚本已停止")
        logging.info("脚本已停止")
        self.root.after(0, self.update_ui_after_stop)
  
    def update_ui_after_stop(self):
        self.toggle_run_button.config(text="开始运行")
        self.root.deiconify()  # 恢复主窗口
  
    def move_item_up(self, event=None):
        selected_item = self.tree.selection()   
        if selected_item:
            selected_index = self.tree.index(selected_item[0])
            if selected_index > 0:
                  
                self.image_list[selected_index], self.image_list[selected_index - 1] = self.image_list[selected_index - 1], self.image_list[selected_index]
                self.update_image_listbox()
  
                  
                item_id = self.tree.get_children()[selected_index - 1]
                self.tree.selection_set(item_id)
                self.tree.focus(item_id)
  
    def move_item_down(self, event=None):
        selected_item = self.tree.selection()
        if selected_item:
            selected_index = self.tree.index(selected_item[0])
            if selected_index < len(self.image_list) - 1:
                  
                self.image_list[selected_index], self.image_list[selected_index + 1] = self.image_list[selected_index + 1], self.image_list[selected_index]
                self.update_image_listbox()
  
                  
                item_id = self.tree.get_children()[selected_index + 1]
                self.tree.selection_set(item_id)
                self.tree.focus(item_id)
  
    def match_and_click(self, template_path, similarity_threshold):
        # 获取当前步骤的完整信息
        selected_index = next((i for i, item in enumerate(self.image_list) if item[0] == template_path), None)
        if selected_index is not None:
            current_step = self.image_list[selected_index]
            mouse_coordinates = current_step[4]  # 获取鼠标坐标
            keyboard_input = current_step[3]  # 获取键盘输入
        else:
            mouse_coordinates = ""
            keyboard_input = ""

        # 获取屏幕截图
        screenshot = pyautogui.screenshot()
        screenshot = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

        # 读取模板图像
        template = cv2.imread(template_path)

        # 执行模板匹配
        result = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

        # 如果相似度超过阈值
        if max_val >= similarity_threshold:
            # 先处理鼠标点击
            if mouse_coordinates and not self.only_keyboard_var.get():
                try:
                   
                    if ":" in mouse_coordinates:
                        action, coords, is_dynamic = mouse_coordinates.split(":")
                        
                        # 如果是动态点击
                        if is_dynamic == "1":
                           x, y = max_loc[0] + template.shape[1] // 2, max_loc[1] + template.shape[0] // 2 # 计算动态点击中心
                        else:
                            x, y = map(int, coords.split(","))
                            
                        # 执行相应的鼠标操作
                        if action == "click":
                            pyautogui.click(x, y)
                        elif action == "rightClick":
                            # 使用 pyautogui 来模拟真实的右键点击
                            pyautogui.moveTo(x, y)
                            pyautogui.rightClick()
                        elif action == "doubleClick":
                            pyautogui.doubleClick(x, y)
                        elif action == "mouseDown":
                            pyautogui.mouseDown(x, y)
                        elif action == "mouseUp":
                            pyautogui.mouseUp(x, y)
                    else:
                        # 处理旧格式的坐标(向后兼容)
                        x, y = map(int, mouse_coordinates.split(','))
                        pyautogui.click(x, y)

                    print(f"执行鼠标操作:{mouse_coordinates}")
                    logging.info(f"执行鼠标操作:{mouse_coordinates}")
                    
                except Exception as e:
                    print(f"鼠标操作时出错: {e}")
                    logging.error(f"鼠标操作时出错: {e}")
                    return False

            # 再处理键盘输入
            if keyboard_input:
                try:
                    time.sleep(0.5)  # 等待点击后的界面响应
                    
                    # 解析并执行键盘输入
                    commands = self.parse_keyboard_input(keyboard_input)
                    for cmd in commands:
                        if isinstance(cmd, tuple) and cmd[0] == "hotkey":
                            keys = cmd[1]
                            pyautogui.hotkey(*keys)
                        elif isinstance(cmd, float):  # 延时
                            time.sleep(cmd)
                        else:  # 普通按键或特殊键
                            pyautogui.press(cmd)
                        time.sleep(0.1)  # 按键间短暂延时
                        
                    print(f"执行键盘输入:{keyboard_input}")
                    logging.info(f"执行键盘输入:{keyboard_input}")
                except Exception as e:
                    print(f"键盘输入时出错: {e}")
                    logging.error(f"键盘输入时出错: {e}")

            return True
        else:
            print(f"未找到匹配,最大相似度:{max_val}")
            logging.info(f"未找到匹配,最大相似度:{max_val}")
            return False
  
    def parse_keyboard_input(self, input_str):
        """解析键盘输入字符串,返回命令列表"""
        commands = []
        i = 0
        while i < len(input_str):
            if input_str[i] == '{':
                end = input_str.find('}', i)
                if end != -1:
                    cmd = input_str[i+1:end]
                    if cmd.startswith('delay:'):
                        # 处理延时命令
                        try:
                            delay_ms = int(cmd.split(':')[1])
                            commands.append(float(delay_ms / 1000))
                        except ValueError:
                            print(f"无效的延时值: {cmd}")
                    elif '+' in cmd:
                        # 处理组合键
                        keys = cmd.split('+')
                        commands.append(("hotkey", tuple(keys)))
                    else:
                        # 处理特殊键
                        commands.append(cmd)
                    i = end + 1
                    continue
            # 处理普通字符
            commands.append(input_str[i])
            i += 1
        return commands

    def add_special_key(self, key):
        current_entry = self.entry.get()
        self.entry.delete(0, tk.END)
        self.entry.insert(0, current_entry + key)
  
    def edit_keyboard_input(self):
        selected_items = self.tree.selection()
        if selected_items:
            selected_item = selected_items[0]
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list[selected_index]

            # 创建新窗口但不隐藏主窗口
            dialog = tk.Toplevel(self.root)
            dialog.title("修改键盘输入")
            dialog.transient(self.root)
            dialog.grab_set()
            
            # 创建输入框和标签
            input_frame = tk.Frame(dialog)
            input_frame.pack(fill=tk.X, pady=5)
            tk.Label(input_frame, text="键盘输入:").pack(side=tk.LEFT)
            entry = tk.Entry(input_frame, width=30)
            entry.insert(0, selected_image[3])
            entry.pack(side=tk.LEFT, padx=5)

            # 创建特殊键按钮框架
            special_keys_frame = tk.LabelFrame(dialog, text="特殊键", padx=5, pady=5)
            special_keys_frame.pack(fill=tk.X, pady=5)
  
            special_keys = [
                'enter', 'tab', 'space', 'backspace', 'delete',
                'esc', 'home', 'end', 'pageup', 'pagedown',
                'up', 'down', 'left', 'right'
            ]
  
            # 创建特殊键按钮
            for i, key in enumerate(special_keys):
                btn = tk.Button(special_keys_frame, text=key.upper(),
                              command=lambda k=key: add_special_key(f"{{{k}}}"))
                btn.grid(row=i//7, column=i%7, padx=2, pady=2, sticky='ew')
  
            # 创建组合键框架
            combo_keys_frame = tk.LabelFrame(dialog, text="组合键", padx=5, pady=5)
            combo_keys_frame.pack(fill=tk.X, pady=5)
  
            # 创建常用组合键按钮
            combo_keys = [
                ('复制', 'ctrl+c'), ('粘贴', 'ctrl+v'), ('剪切', 'ctrl+x'),
                ('全选', 'ctrl+a'), ('保存', 'ctrl+s'), ('撤销', 'ctrl+z')
            ]
  
            for i, (name, combo) in enumerate(combo_keys):
                btn = tk.Button(combo_keys_frame, text=name,
                              command=lambda c=combo: add_special_key(f"{{{c}}}"))
                btn.grid(row=i//3, column=i%3, padx=2, pady=2, sticky='ew')
  
            # 创建修饰键框架
            modifier_keys_frame = tk.LabelFrame(dialog, text="修饰键", padx=5, pady=5)
            modifier_keys_frame.pack(fill=tk.X, pady=5)
  
            modifier_keys = ['ctrl', 'alt', 'shift', 'win']
            for i, key in enumerate(modifier_keys):
                btn = tk.Button(modifier_keys_frame, text=key.upper(),
                              command=lambda k=key: add_special_key(f"{{{k}}}"))
                btn.grid(row=0, column=i, padx=2, pady=2, sticky='ew')
  
            # 创建功能键框架
            function_keys_frame = tk.LabelFrame(dialog, text="功能键", padx=5, pady=5)
            function_keys_frame.pack(fill=tk.X, pady=5)
  
            for i in range(12):
                btn = tk.Button(function_keys_frame, text=f"F{i+1}",
                              command=lambda k=i+1: add_special_key(f"{{f{k}}}"))
                btn.grid(row=i//6, column=i%6, padx=2, pady=2, sticky='ew')
  
            def add_special_key(key):
                current_pos = entry.index(tk.INSERT)
                entry.insert(current_pos, key)
                entry.focus_set()
  
            def save_input():
                new_keyboard_input = entry.get()
                self.image_list[selected_index] = (
                    selected_image[0], selected_image[1], selected_image[2],
                    new_keyboard_input, selected_image[4], selected_image[5],
                    selected_image[6], selected_image[7]
                )
                self.update_image_listbox()
                dialog.destroy()
  
            # 添加说明文本
            help_text = """
支持的输入格式:
1. 普通文本:直接输入
2. 特殊键:使用花括号,如 {enter}, {tab}
3. 组合键:使用加号连接,如 {ctrl+c}, {alt+tab}
4. 按键序列:直接连接,如 {ctrl+c}{enter}
5. 延时:{delay:1000} 表示延时1秒
            """
            tk.Label(dialog, text=help_text, justify=tk.LEFT,
                    font=('Arial', 9)).pack(pady=5)
  
            # 添加保存和取消按钮
            button_frame = tk.Frame(dialog)
            button_frame.pack(fill=tk.X, pady=10)
            tk.Button(button_frame, text="保存", command=save_input).pack(side=tk.RIGHT, padx=5)
            tk.Button(button_frame, text="取消", command=lambda: [dialog.destroy(), self.root.deiconify(), self.root.lift()]).pack(side=tk.RIGHT)
  
            # 居中显示对话框
            dialog.update_idletasks()
            x = self.root.winfo_x() + (self.root.winfo_width() - dialog.winfo_width()) // 2
            y = self.root.winfo_y() + (self.root.winfo_height() - dialog.winfo_height()) // 2
            dialog.geometry(f"+{x}+{y}")
  
    def set_hotkey(self):
        # 创建一个置顶的对话框
        dialog = tk.Toplevel(self.root)
        dialog.title("设置热键")
        dialog.transient(self.root)  # 将对话框设置为主窗口的临时窗口
        dialog.grab_set()  # 模态对话框
        dialog.attributes('-topmost', True)  # 设置窗口置顶
          
        # 创建输入框和标签
        tk.Label(dialog, text="请输入新的热键(例如:<F1>):").pack(pady=5)
        entry = tk.Entry(dialog)
        entry.insert(0, self.hotkey)
        entry.pack(pady=5)
          
        def save_hotkey():
            new_hotkey = entry.get()
            try:
                self.unregister_global_hotkey() #取消之前的全局热键
                self.hotkey = new_hotkey
                self.register_global_hotkey()
                print(f"热键已更改为 {self.hotkey}")
                logging.info(f"热键已更改为 {self.hotkey}")
                messagebox.showinfo("设置热键", f"热键已更改为 {self.hotkey}")
                dialog.destroy()
            except tk.TclError as e:
                print(f"绑定热键失败: {e}")
                logging.error(f"绑定热键失败: {e}")
                messagebox.showerror("设置热键失败", f"无法绑定热键: {new_hotkey}")
          
        # 添加确定按钮
        tk.Button(dialog, text="确定", command=save_hotkey).pack(pady=10)
          
        # 居中显示对话框
        dialog.geometry("+%d+%d" % (
            self.root.winfo_rootx() + self.root.winfo_width()//2 - dialog.winfo_reqwidth()//2,
            self.root.winfo_rooty() + self.root.winfo_height()//2 - dialog.winfo_reqheight()//2
        ))
  
    def save_config(self):
        config = {
            'hotkey': self.hotkey,
            'similarity_threshold': self.similarity_threshold,
            'delay_time': self.delay_time,
            'loop_count': self.loop_count,
            'image_list': [img for img in self.image_list if os.path.exists(img[0])],
            'only_keyboard': self.only_keyboard_var.get()
        }
        filename = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON files", "*.json")])
        if filename:
            try:
                with open(filename, 'w') as f:
                    json.dump(config, f)
                print(f"配置已保存到 {filename}")
                logging.info(f"配置已保存到 {filename}")
                self.config_filename = filename
                # 添加成功提示
                messagebox.showinfo("保存成功", f"配置已成功保存到:\n{filename}")
            except Exception as e:
                error_msg = f"保存配置时出错: {str(e)}"
                print(error_msg)
                logging.error(error_msg)
                messagebox.showerror("保存失败", error_msg)
  
    def load_config(self):
        try:
            if not os.path.exists(self.config_filename):
                raise FileNotFoundError(f"配置文件不存在: {self.config_filename}")
  
            # 先读取配置文件
            with open(self.config_filename, 'r') as f:
                config = json.load(f)
              
            # 在应用任何更改之前,先验证所有图像文件
            missing_images = []
            for img_data in config.get('image_list', []):
                if not os.path.exists(img_data[0]):
                    missing_images.append(img_data[0])
              
            # 如果有任何图像文件不存在,直接返回,不做任何更改
            if missing_images:
                error_message = f"配置文件中缺少以下图像文件: {', '.join(missing_images)}"
                messagebox.showerror("错误", error_message)
                logging.error(error_message)
                return False
              
            # 只有当所有图像文件都存在时,才应用配置
            self.image_list = config.get('image_list', [])
            self.hotkey = config.get('hotkey', '<F1>')
            self.similarity_threshold = config.get('similarity_threshold', 0.8)
            self.delay_time = config.get('delay_time', 0.1)
            self.loop_count = config.get('loop_count', 1)
            self.only_keyboard_var.set(config.get('only_keyboard', False))

            # 清空并重新填充 Treeview
            for item in self.tree.get_children():
                self.tree.delete(item)
              
            for img_data in self.image_list:
                # 确保每个项目都有 8 个元素
                while len(img_data) < 8:
                    img_data = img_data + ("",)
                      
                # 加载图像并创建缩略图
                try:
                    image = Image.open(img_data[0])
                    image.thumbnail((50, 50))
                    photo = ImageTk.PhotoImage(image)
                      
                    # 插入到 Treeview
                    tree_item = self.tree.insert("", tk.END, values=(
                        os.path.basename(img_data[0]),  # 图片名称
                        img_data[1],  # 步骤名称
                        img_data[2],  # 相似度
                        img_data[3],  # 键盘输入
                        img_data[4],  # 鼠标点击坐标
                        img_data[5],  # 等待时间
                        img_data[6],  # 条件
                        img_data[7]   # 跳转步骤
                    ), image=photo)
                    self.tree.image_refs.append(photo)
                except Exception as e:
                    print(f"处理图像时出错 {img_data[0]}: {e}")
                    logging.error(f"处理图像时出错 {img_data[0]}: {e}")
              
            # 更新循环次数输入框
            self.loop_count_entry.delete(0, tk.END)
            self.loop_count_entry.insert(0, str(self.loop_count))
              
             # 取消之前的全局热键, 注册新的全局热键
            self.unregister_global_hotkey()
            self.register_global_hotkey()
              
            print(f"配置已从 {self.config_filename} 加载")
            logging.info(f"配置已从 {self.config_filename} 加载")
              
            # 显示成功消息
            messagebox.showinfo("成功", f"配置已成功加载:\n{self.config_filename}")
            return True
              
        except Exception as e:
            error_message = f"加载配置时出错: {str(e)}"
            print(error_message)
            logging.error(error_message)
            messagebox.showerror("错误", error_message)
            return False
  
    def reset_to_initial_state(self):
        """重置程序到初始状态"""
        try:
            # 重置所有变量到初始值
            self.hotkey = '<F1>'
            self.similarity_threshold = 0.8
            self.delay_time = 0.1
            self.loop_count = 1
            self.image_list = []
            self.running = False
            self.paused = False
            self.start_step_index = 0
            self.only_keyboard_var.set(False)
              
            # 重置界面元素
            self.update_image_listbox()
            self.loop_count_entry.delete(0, tk.END)
            self.loop_count_entry.insert(0, str(self.loop_count))
            self.toggle_run_button.config(text="开始运行")
            self.follow_current_step.set(False)
            self.allow_minimize_var.set(True)
            
            # 清空临时文件
            if os.path.exists(self.screenshot_folder):
                try:
                    shutil.rmtree(self.screenshot_folder)
                    os.makedirs(self.screenshot_folder)
                except Exception as e:
                    print(f"清理截图文件夹时出错: {e}")
                    logging.error(f"清理截图文件夹时出错: {e}")
              
            print("程序已重置为初始状态")
            logging.info("程序已重置为初始状态")
            messagebox.showinfo("重置", "程序已重置为初始状态")
            
            #  取消之前的全局热键, 注册新的全局热键
            self.unregister_global_hotkey()
            self.register_global_hotkey()
        except Exception as e:
            print(f"重置程序状态时出错: {e}")
            logging.error(f"重置程序状态时出错: {e}")
  
    def load_config_manually(self):
        """手动加载配置文件"""
        file_path = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")])
        if file_path:
            # 保存当前配置文件路径
            old_config_filename = self.config_filename
            self.config_filename = file_path
              
            # 尝试加载新配置
            if not self.load_config():
                # 如果加载失败,恢复原来的配置文件路径
                self.config_filename = old_config_filename
                # 重置到初始状态
                self.reset_to_initial_state()
  
    def get_mouse_position(self, event=None):
        # 获取当前鼠标位置
        x, y = pyautogui.position()
        messagebox.showinfo("鼠标位置", f"当前鼠标位置: ({x}, {y})")
  
        # 将鼠标位置存储到当前选中的图像中
        selected_item = self.tree.selection()
        if selected_item:
            selected_index = self.tree.index(selected_item[0])
            selected_image = self.image_list[selected_index]
            self.image_list[selected_index] = (selected_image[0], selected_image[1], selected_image[2], selected_image[3], f"{x},{y}", selected_image[5], selected_image[6], selected_image[7])
            self.update_image_listbox()
  
    def cleanup_on_exit(self):
        try:
            # 退出程序时删除未保存的图像
            if not os.path.exists(self.config_filename):
                for item in self.image_list:
                    img_path = item[0]
                    if os.path.exists(img_path):
                        try:
                            os.remove(img_path)
                            print(f"图像文件已删除: {img_path}")
                            logging.info(f"图像文件已删除: {img_path}")
                        except Exception as e:
                            print(f"删除图像文件时出错: {e}")
                            logging.error(f"删除图像文件时出错: {e}")
            #  取消全局热键
            self.unregister_global_hotkey()
        except Exception as e:
            print(f"清理时出错: {e}")
            logging.error(f"清理时出错: {e}")
            self.reset_to_initial_state()
  
    def bind_arrow_keys(self):
        self.tree.bind('<Up>', self.move_item_up)
        self.tree.bind('<Down>', self.move_item_down)
  
    def move_item_up(self, event):
        selected_items = self.tree.selection()
        if not selected_items:
            return
  
        for item in selected_items:
            index = self.tree.index(item)
            if index > 0:
                self.tree.move(item, '', index - 1)
                self.image_list.insert(index - 1, self.image_list.pop(index))
  
        # 确保第一个选定项目可见
        self.tree.see(selected_items[0])
  
        # 阻止事件传播
        return "break"
  
    def move_item_down(self, event):
        selected_items = self.tree.selection()
        if not selected_items:
            return
  
        for item in reversed(selected_items):
            index = self.tree.index(item)
            if index < len(self.image_list) - 1:
                self.tree.move(item, '', index + 1)
                self.image_list.insert(index + 1, self.image_list.pop(index))
  
        # 确保最后一项可见
        self.tree.see(selected_items[-1])
  
        # 阻止事件传播
        return "break"
  
    def test_single_match(self):
        selected_items = self.tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个项目进行测试")
            return
  
        selected_item = selected_items[0]
        selected_index = self.tree.index(selected_item)
        img_data = self.image_list[selected_index]
          
        # 使用新的解包方式,适应可能存在的额外字段
        img_path, img_name, similarity_threshold = img_data[:3]
  
        # 执行匹配测试
        matched, location = self.match_template(img_path, float(similarity_threshold))
  
        if matched:
            messagebox.showinfo("匹配结果", f"成功匹配到图像 '{img_name}'\n位置: {location}")
        else:
            messagebox.showinfo("匹配结果", f"未能匹配到图像 '{img_name}'")
  
    def match_template(self, template_path, similarity_threshold):
        # 截取当前屏幕
        screenshot = pyautogui.screenshot()
        screenshot = np.array(screenshot)
        screenshot = cv2.cvtColor(screenshot, cv2.COLOR_RGB2BGR)
  
        # 读取模板图像
        template = cv2.imread(template_path, 0)
        if template is None:
            print(f"无法读取图像: {template_path}")
            logging.error(f"无法读取图像: {template_path}")
            return False, None
  
        # 转换截图为灰度图像
        gray_screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
  
        # 进行模板匹配
        result = cv2.matchTemplate(gray_screenshot, template, cv2.TM_CCOEFF_NORMED)
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
  
        if max_val >= similarity_threshold:
            # 计算匹配位置的中心点
            h, w = template.shape
            center_x = max_loc[0] + w // 2
            center_y = max_loc[1] + h // 2
            return True, (center_x, center_y)
        else:
            return False, None
  
    def create_context_menu(self):
        self.context_menu = tk.Menu(self.root, tearoff=0, postcommand=self.update_context_menu)
        self.context_menu.add_command(label="编辑步骤名称", command=self.edit_image_name)
        self.context_menu.add_command(label="编辑相似度", command=self.edit_similarity_threshold)
        self.context_menu.add_command(label="编辑键盘输入", command=self.edit_keyboard_input)
        self.context_menu.add_command(label="编辑鼠标操作", command=self.edit_mouse_action)  # 添加这一行
        self.context_menu.add_command(label="编辑等待时间", command=self.edit_wait_time)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="复制", command=self.copy_item)
        self.context_menu.add_command(label="粘贴", command=self.paste_item)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="从此步骤开始运行", command=self.start_from_step)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="设置条件跳转", command=self.set_condition_jump)
  
    def update_context_menu(self):
        # 在显示菜单前更新菜单状态
        selected = self.tree.selection()
        # 可以在这里根据选择状态启用/禁用某些菜单项
  
    def start_from_step(self):
        selected_items = self.tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个项目进行操作")
            return
  
        # 获取当前选中的步骤索引
        selected_item = selected_items[0]
        self.start_step_index = self.tree.index(selected_item)  # 使用当前选中的步骤索引
        self.start_script_thread()  # 启动脚本
  
        # 更新按钮文本以指示脚本正在运行
        self.toggle_run_button.config(text="停止运行")
  
    def show_context_menu(self, event):
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_clear()
            self.tree.selection_set(item)
            self.tree.focus(item)
            # 使用after方法延迟显示菜单
            self.root.after(1, lambda: self.context_menu.post(event.x_root, event.y_root))
        return "break"
  
    def copy_item(self):
        selected_items = self.tree.selection()
        if selected_items:
            index = self.tree.index(selected_items[0])
            original_item = self.image_list[index]
              
            # 创建像文件的副本
            new_image_path = self.create_image_copy(original_item[0])
              
            # 创建新的元组,使用新的图像路径
            self.copied_item = (new_image_path,) + tuple(original_item[1:])
  
    def create_image_copy(self, original_path):
        # 创建图像文件的副本
        base_name = os.path.basename(original_path)
        name, ext = os.path.splitext(base_name)
        new_name = f"{name}_copy{ext}"
        new_path = os.path.join(self.screenshot_folder, new_name)
        shutil.copy2(original_path, new_path)
        return new_path
      
    def paste_item(self):
        if self.copied_item:
            target = self.tree.focus()
            if target:
                target_index = self.tree.index(target)
                # 使用与复制的项相同的数据创新元组
                new_item = tuple(self.copied_item)
                self.image_list.insert(target_index + 1, new_item)  # 插入到下方
                self.update_image_listbox()
  
    def edit_image_name(self):
        selected_items = self.tree.selection()
        if selected_items:
            selected_item = selected_items[0]
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list[selected_index]
  
            new_image_name = simpledialog.askstring("修改步骤名称", "请输入新的步骤名称:", initialvalue=selected_image[1])
            if new_image_name is not None:
                self.image_list[selected_index] = (selected_image[0], new_image_name, selected_image[2], selected_image[3], selected_image[4], selected_image[5], selected_image[6], selected_image[7])
                self.update_image_listbox()
  
    def edit_similarity_threshold(self):
        selected_items = self.tree.selection()
        if selected_items:
            selected_item = selected_items[0]
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list[selected_index]
  
            new_similarity_threshold = simpledialog.askfloat("修改相似度", "请输入新的相似度(0.1 - 1.0):", initialvalue=selected_image[2], minvalue=0.1, maxvalue=1.0)
            if new_similarity_threshold is not None:
                self.image_list[selected_index] = (selected_image[0], selected_image[1], new_similarity_threshold, selected_image[3], selected_image[4], selected_image[5], selected_image[6], selected_image[7])
                self.update_image_listbox()
  
    def edit_wait_time(self):
        selected_items = self.tree.selection()
        if selected_items:
            selected_item = selected_items[0]
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list[selected_index]
  
            new_wait_time = simpledialog.askinteger("修改等待时间", "请输入新的等待时间(毫秒):", initialvalue=selected_image[5])
            if new_wait_time is not None:
                self.image_list[selected_index] = (selected_image[0], selected_image[1], selected_image[2], selected_image[3], selected_image[4], new_wait_time, selected_image[6], selected_image[7])
                self.update_image_listbox()
  
    def set_condition_jump(self):
        selected_items = self.tree.selection()
        if not selected_items:
            messagebox.showwarning("警告", "请先选择一个项目")
            return
  
        selected_item = selected_items[0]
        selected_index = self.tree.index(selected_item)
        selected_image = list(self.image_list[selected_index])  # 转换为列表以便修改
  
        # 创建设置条件跳转的对话框
        dialog = tk.Toplevel(self.root)
        dialog.title("设置条件跳转")
        dialog.geometry("300x150")
        dialog.transient(self.root)
        dialog.grab_set()
        dialog.attributes('-topmost', True)  # 设置窗口置顶
  
        # 条件选择
        condition_frame = tk.Frame(dialog)
        condition_frame.pack(pady=5)
        tk.Label(condition_frame, text="条件:").pack(side=tk.LEFT)
        condition_var = tk.StringVar(value=selected_image[6] if len(selected_image) > 6 else "")
        condition_combo = ttk.Combobox(condition_frame, textvariable=condition_var, values=["True", "False"], width=10)
        condition_combo.pack(side=tk.LEFT)
  
        # 跳转步骤选择
        jump_frame = tk.Frame(dialog)
        jump_frame.pack(pady=5)
        tk.Label(jump_frame, text="跳转到步骤:").pack(side=tk.LEFT)
        jump_var = tk.StringVar(value=selected_image[7] if len(selected_image) > 7 else "")
        step_names = [img[1] for img in self.image_list]  # 获取所有步骤名称
        jump_combo = ttk.Combobox(jump_frame, textvariable=jump_var, values=step_names, width=15)
        jump_combo.pack(side=tk.LEFT)
  
        def save_condition():
            condition = condition_var.get()
            jump_to = jump_var.get()
              
            # 确保选择了有效的条件和跳转步骤
            if not condition or not jump_to:
                messagebox.showwarning("警告", "请选择条件和跳转步骤")
                return
                  
            try:
                # 确保列表长度至少为8
                while len(selected_image) < 8:
                    selected_image.append("")
                  
                # 更新条件和跳转步骤
                selected_image[6] = condition
                selected_image[7] = jump_to
                  
                # 更新 image_list 中的数据
                self.image_list[selected_index] = tuple(selected_image)
                  
                # 立即更新显示
                self.update_image_listbox()
                  
                # 选中更新后的项
                items = self.tree.get_children()
                if selected_index < len(items):
                    self.tree.selection_set(items[selected_index])
                    self.tree.focus(items[selected_index])
                  
                # 打印日志确认更新
                print(f"已更新条件跳转设置:条件={condition}, 跳转到={jump_to}")
                logging.info(f"已更新条件跳转设置:条件={condition}, 跳转到={jump_to}")
                  
                dialog.destroy()
            except Exception as e:
                error_msg = f"保存条件跳转设置时出错: {str(e)}"
                print(error_msg)
                logging.error(error_msg)
                messagebox.showerror("错误", error_msg)
  
        # 保存按钮
        tk.Button(dialog, text="保存", command=save_condition).pack(pady=10)
  
        # 等待窗口绘制完成
        dialog.update_idletasks()
          
        # 计算居中位置
        x = self.root.winfo_x() + (self.root.winfo_width() - dialog.winfo_width()) // 2
        y = self.root.winfo_y() + (self.root.winfo_height() - dialog.winfo_height()) // 2
          
        # 设置窗口位置
        dialog.geometry(f"+{x}+{y}")
  
    def on_treeview_select(self, event):
        """当 Treeview 选择改变时更新预览图像"""
        selected_items = self.tree.selection()
        if not selected_items:
            # 清空预览
            self.preview_image_label.config(image='')
            return
         
        selected_item = selected_items[0]
        selected_index = self.tree.index(selected_item)
         
        try:
            # 获取选中项的图像路径
            img_path = self.image_list[selected_index][0]
             
            # 检查文件是否存在
            if not os.path.exists(img_path):
                self.preview_image_label.config(image='')
                return
             
            # 加载原始图像
            original_image = Image.open(img_path)
             
            # 获取原始尺寸
            original_width, original_height = original_image.size
             
            # 设置最大预览尺寸
            max_width = 90
            max_height = 90
             
            # 计算缩放比例
            width_ratio = max_width / original_width
            height_ratio = max_height / original_height
            scale_ratio = min(width_ratio, height_ratio)
             
            # 计算新的尺寸
            new_width = int(original_width * scale_ratio)
            new_height = int(original_height * scale_ratio)
             
            # 调整图像大小
            resized_image = original_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
            # 创建 PhotoImage 对象
            photo = ImageTk.PhotoImage(resized_image)
             
            # 更新预览标签
            self.preview_image_label.config(image=photo)
            self.preview_image_label.image = photo  # 保持引用以防止垃圾回收
             
            # 添加尺寸信息到预览标签
            self.preview_label.config(text=f"图像预览 ({original_width}x{original_height})")
             
        except Exception as e:
            print(f"预览图像时出错: {e}")
            logging.error(f"预览图像时出错: {e}")
            self.preview_image_label.config(image='')
  
    def edit_mouse_action(self):
        selected_items = self.tree.selection()
        if selected_items:
            selected_item = selected_items[0]
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list[selected_index]

            # 创建新窗口但不隐藏主窗口
            dialog = tk.Toplevel(self.root)
            dialog.title("设置鼠标操作")
            dialog.transient(self.root)
            dialog.grab_set()

            
            # 创建坐标输入框
            coord_frame = tk.Frame(dialog)
            coord_frame.pack(fill=tk.X, pady=5)
            tk.Label(coord_frame, text="坐标 (x,y):").pack(side=tk.LEFT)
            coord_entry = tk.Entry(coord_frame, width=20)
            coord_entry.pack(side=tk.LEFT, padx=5)

            # 解析现有的鼠标操作数据
            current_action = "click"
            current_coords = ""
            current_dynamic = False
             
            if selected_image[4]:  # 如果有现有的鼠标操作数据
                try:
                    if ":" in selected_image[4]:
                        action, coords, dynamic = selected_image[4].split(":")
                        current_action = action
                        current_coords = coords
                        current_dynamic = dynamic == "1"
                    else:
                        current_coords = selected_image[4]
                except:
                    pass
             
            coord_entry.insert(0, current_coords)

            # 创建鼠标动作选择框架
            action_frame = tk.LabelFrame(dialog, text="鼠标动作", padx=5, pady=5)
            action_frame.pack(fill=tk.X, pady=5)

            # 鼠标动作类型
            action_var = tk.StringVar(value=current_action)
            actions = [
                ("左键单机", "click"),
                ("右键单击", "rightClick"),
                ("双击", "doubleClick"),
                ("按住", "mouseDown"),
                ("释放", "mouseUp")
            ]

            for text, value in actions:
                tk.Radiobutton(action_frame, text=text, value=value, 
                              variable=action_var).pack(anchor=tk.W)
                
            # 添加动态点击勾选框
            dynamic_var = tk.BooleanVar(value=current_dynamic)
            tk.Checkbutton(action_frame, text="动态点击", variable=dynamic_var).pack(anchor=tk.W)

            def save_mouse_action():
                try:
                    # 获取坐标
                    coords = coord_entry.get().strip()
                    if not coords or "," not in coords:
                        messagebox.showerror("错误", "请输入有效的坐标 (x,y)")
                        return
 
                    # 验证坐标格式
                    try:
                        x, y = map(int, coords.split(","))
                    except ValueError:
                        messagebox.showerror("错误", "坐标必须是整数")
                        return
 
                    # 获取动作类型
                    action = action_var.get()
                    dynamic = "1" if dynamic_var.get() else "0"

                    # 构建鼠标操作字符串
                    mouse_action = f"{action}:{coords}:{dynamic}"

                    # 更新图像列表
                    self.image_list[selected_index] = (
                        selected_image[0], selected_image[1], selected_image[2],
                        selected_image[3], mouse_action, selected_image[5],
                        selected_image[6], selected_image[7]
                    )
                    self.update_image_listbox()
                    dialog.destroy()
                except Exception as e:
                    messagebox.showerror("错误", f"保存鼠标操作时出错: {str(e)}")

            # 添加说明文本
            help_text = """
支持的鼠标操作:
1. 左键单击:在指定位置单击左键
2. 右键单击:在指定位置单击右键
3. 双击:在指定位置双击左键
4. 按住:按住鼠标左键
5. 释放:释放鼠标左键
 
注意:
- 可以手动修改鼠标位置
- 坐标格式为"x,y"(例如:100,200)
- 勾选“动态点击”后,鼠标点击位置将根据匹配到的图像位置动态确定
        """
            tk.Label(dialog, text=help_text, justify=tk.LEFT,
                    font=('Arial', 9)).pack(pady=5)

            # 添加保存和取消按钮
            button_frame = tk.Frame(dialog)
            button_frame.pack(fill=tk.X, pady=10)
            tk.Button(button_frame, text="保存", command=save_mouse_action).pack(side=tk.RIGHT, padx=5)
            tk.Button(button_frame, text="取消", command=lambda: [dialog.destroy(), self.root.deiconify(), self.root.lift()]).pack(side=tk.RIGHT)

            # 居中显示对话框
            dialog.update_idletasks()
            x = self.root.winfo_x() + (self.root.winfo_width() - dialog.winfo_width()) // 2
            y = self.root.winfo_y() + (self.root.winfo_height() - dialog.winfo_height()) // 2
            dialog.geometry(f"+{x}+{y}")
 
    def focus_on_step(self, index):
        """实现焦点跟随功能"""
        try:
            items = self.tree.get_children()
            if 0 <= index < len(items):
                self.tree.selection_set(items[index])
                self.tree.focus(items[index])
                self.tree.see(items[index])
        except Exception as e:
            print(f"焦点跟随出错: {e}")
            logging.error(f"焦点跟随出错: {e}")
  
if __name__ == "__main__":
    root = tk.Tk()
    app = ImageRecognitionApp(root)
    root.mainloop()

本来不想打包的,毕竟这玩意打包后有点大(python通病)

2024/09/30更新
1.增加鼠标按坐标位置点击
使用方法:选中列表的相应图像,按下F2获取鼠标位置的坐标,点击确认后自动将坐标插入到图片中,当脚本运行到该行时,只点击鼠标坐标。
2.修复删除图像后相应的图像文件还存在的问题

2024/10/08更新
1.去掉上传图像功能, 感觉这个功能的存在没有必要
2.修复手动点击停止运行时程序偶尔卡死的问题
3.保存配置增加自定义配置文件名
4.增加列表上下移动功能,按上下键盘可移动指定的行,可单行或多行同时移动
5.修改循环次数,如果为0则无限循环运行
6.如果没有保存配置,退出程序后将不保留本次框选的所有的图像
7.现在程序在运行时会自动最小化到任务栏,避免遮住匹配的项目

2024/10/09更新
1.增加测试匹配功能,用来测试框选的图像是否成功匹配到,如果匹配失败可适当调整相似度
2.为了节省资源,增加复制粘贴脚本行功能(上下两条相同的脚本行可实现双击打开效果)
3.现在将所有的编辑功能都集成到右键菜单去了


2024/10/11更新
延迟功能回归


2024/10/18紧急更新
修复一些列错误问题,新版本链接同上

2024/10/23更新
1.增加程序在运行时是否允许最小化的选择
2.增加焦点跟随,当运行脚本时会直观的看到当前运行到哪一步骤,例如在运行时卡住的情况下可以看到是卡在哪一步。
3.增加从某一步骤开始运行脚本的功能,已集成到右键菜单。
4.去掉图片路径显示
5.修复当鼠标坐标存在时无论是否匹配到图像都去点击的问题
6.修正键盘输入时区分大小写
其它一些细节优化和小BUG修复

已重新上传下载地址
目前任在维护软件,如有建议欢迎提出,脱离本软件主题的无视。
后期功能预告:
实现更复杂的条件逻辑,如IF-ELSE语句和OCR功能,允许基于屏幕文字内容进行操作,但这是大工程,有空再慢慢去完善了。
反正源码我也放出来了,有能力的自己也可以去做。



2024/10/28 更新
1.增加按条件(真/假)跳转到指定步骤
2.修改删除图片的逻辑
3.修改加载配置文件失败的逻辑
其它优化和BUG修复


2024/11/01 更新
增加局部循环功能,允许指定步骤范围进行重复执行。
使用方法:
例如,如果你想让步骤2到步骤4循环执行3次:
选中步骤2
右键 -> 设置循环区间
在弹出窗口中:
选择"步骤4"作为结束步骤
输入循环次数"3"

最近很忙,没有时间关注,简单的做了下更新。
2024.12/4
增加图像预览
扩展鼠标和键盘的操作
源码已跟新
下载地址:https://wwts.lanzoub.com/iYVD82h7u5gd密码:cknr


2024.12/29更新
最近真的太忙没时间关注,周末依然加班中
随便做了下更新并强化了注册热键
可选择只键盘操作,还有大家期待的动态点击图像位置等功能(在编辑鼠标操作里)
现在程序越大越容易出BUG,有问题了评论区反馈
后续有空再调整,有能力的也可根据上面的源码做修改或增加
在这里提前祝大家元旦节快乐!
新板下载地址:https://www.vyuyun.com/s/j1p7H2


发现还有很多小伙伴不会使用这个程序,其实很简单.
以下是程序使用教程,后续如果有新功能也同步更新吧!

1. 软件界面
软件界面主要分为左右两个部分:
左侧区域:
框选截图按钮: 用于截取屏幕指定区域的图像,作为识别模板。
删除图片按钮: 删除列表中选中的图像及相关步骤。
开始运行/停止运行按钮: 启动或停止脚本的执行。
保存配置按钮: 将当前配置(包括图像列表、热键等)保存到 JSON 文件。
设置热键按钮: 设置启动/停止脚本的全局热键。
加载配置按钮: 手动加载之前保存的 JSON 配置文件。
循环次数输入框: 设置脚本循环执行的次数,输入 0 表示无限循环。
测试匹配按钮: 测试当前选中步骤的图像是否能在屏幕上找到。
允许最小化复选框: 勾选后,运行脚本时可以最小化主窗口。
焦点跟随复选框: 勾选后,脚本运行时会自动选中并滚动到当前执行的步骤。
只键盘操作复选框: 勾选后,脚本运行时将只执行键盘输入操作,忽略鼠标点击操作。

右侧区域:
顶部:图像预览区 显示当前选中步骤的图像缩略图及其原始尺寸。
底部:图像列表(Treeview) 以表格形式展示所有添加的识别步骤,包含以下列:
图片: 识别图像的文件名。
步骤名称: 当前步骤的自定义名称。
相似度: 图像匹配的相似度阈值(0.1 - 1.0)。
键盘输入: 匹配成功后需要执行的键盘输入操作。
鼠标点击坐标(F2): 匹配成功后需要点击的鼠标坐标,这里也可以按F2获取当前鼠标位置作为点击坐标。
等待时间 (毫秒): 执行当前步骤后等待的时间。
条件: 控制是否执行跳转步骤的条件(True 或 False)。
跳转步骤: 满足条件时需要跳转到的步骤名称。

2. 基本操作流程
添加识别图像 (框选截图)
点击左侧的 “框选截图” 按钮。
跟微信截图一样出现一个十字光标。
按住鼠标左键,拖动光标框选需要识别的屏幕区域。
松开鼠标左键,截图会自动保存,并在右侧的图像列表中添加一个新的步骤。
新步骤的默认名称为“步骤 + 数字”,鼠标点击坐标为截图区域的中心点。

编辑步骤信息:

在右侧的图像列表中,右键菜单选择需要编辑的单元格,可以修改以下信息:
步骤名称: 自定义步骤的名称,方便识别和管理。
相似度: 调整图像匹配的灵敏度。值越高,匹配要求越严格。
等待时间: 设置执行该步骤后等待的时间,单位为毫秒。
条件: 选择 True 或 False 作为条件判断。
跳转步骤: 输入要跳转到的目标步骤的名称。
编辑键盘输入:
选中需要编辑的步骤。
右键点击该步骤,选择 “编辑键盘输入”。
在弹出的窗口中输入需要模拟的键盘操作。

支持的输入格式:
普通文本: 直接输入需要键入的文本。
特殊键: 使用花括号 {} 包裹,例如 {enter},{tab},{space},{backspace},{delete},{esc},{home},{end},{pageup},{pagedown},{up},{down},{left},{right}。
组合键: 使用加号 + 连接,例如 {ctrl+c},{alt+tab}。
按键序列: 连续输入多个按键,例如 {ctrl+c}{enter}。
延时: 使用 {delay:毫秒},例如 {delay:1000} 表示延时 1 秒。

编辑鼠标操作:
选中需要编辑的步骤。
右键点击该步骤,选择 “编辑鼠标操作”。
在弹出的窗口中设置鼠标操作类型和坐标。
坐标 (x,y): 输入鼠标点击的屏幕坐标。
获取鼠标坐标: 选中步骤后,按下 F2 键,软件会弹窗显示当前鼠标位置,并自动填入选中步骤的鼠标点击坐标。
鼠标动作: 选择需要的鼠标操作类型,例如“左键单击”、“右键单击”、“双击”、“按住”、“释放”。
动态点击: 勾选后,鼠标点击位置将根据实际匹配到的图像位置动态计算中心点进行点击。

调整步骤顺序
选中需要调整顺序的步骤。
按住 向上箭头键 (↑) 或 向下箭头键 (↓) 可以上下移动选中的步骤。

删除步骤
选中需要删除的步骤。
点击左侧的 “删除图片” 按钮。
如果正在使用配置文件,会弹出提示,询问是否同时更新配置文件并删除图片文件。

运行脚本
点击左侧的 “开始运行” 按钮。
脚本会按照图像列表中的顺序逐个执行步骤。

图像匹配: 软件会截取当前屏幕,并与列表中的图像进行匹配。

鼠标点击: 如果匹配成功且设置了鼠标点击坐标,软件会在指定位置进行点击。

键盘输入: 如果匹配成功且设置了键盘输入,软件会模拟键盘输入。

等待时间: 执行完当前步骤后,会等待设置的时间。

循环执行: 脚本会循环执行,直到达到设置的循环次数或点击 “停止运行” 按钮。

全局热键: 在任何时候按下设置的全局热键(默认为 F1)都可以启动或停止脚本。

停止脚本
点击左侧的 “停止运行” 按钮。
或按下设置的全局热键。

3. 高级功能

条件跳转
可以为每个步骤设置条件和跳转目标。
在 “条件” 列选择 True 或 False。
在 “跳转步骤” 列输入要跳转到的目标步骤的名称。
当脚本执行到该步骤时:
如果 “条件” 为 True 且图像匹配成功,则会跳转到指定的 “跳转步骤”。
如果 “条件” 为 False 且图像匹配失败,则会跳转到指定的 “跳转步骤”。
如果不满足跳转条件,则会继续执行下一个步骤。

设置条件跳转:
选中需要设置条件跳转的步骤。
右键点击该步骤,选择 “设置条件跳转”。
在弹出的窗口中选择条件(True/False)和要跳转到的步骤。

保存与加载配置
点击左侧的 “保存配置” 按钮,选择保存路径,可以将当前的步骤列表、热键、循环次数等配置保存到 JSON 文件中。
点击左侧的 “加载配置” 按钮,选择之前保存的 JSON 文件,可以恢复之前的配置。
手动加载配置:点击 “加载配置” 按钮,选择要加载的配置文件。

测试匹配
选中一个步骤。
点击左侧的 “测试匹配” 按钮。
软件会尝试在当前屏幕上查找该步骤的图像,并弹出提示显示是否匹配成功以及匹配到的位置。

从此步骤开始运行:
选中需要开始运行的步骤。
右键点击该步骤,选择 “从此步骤开始运行”。
脚本将从选中的步骤开始执行。

复制与粘贴步骤
选中需要复制的步骤。
右键点击该步骤,选择 “复制”。
选中要粘贴的位置上方的步骤。
右键点击该步骤,选择 “粘贴”。
会在选中的步骤下方插入一个副本,图像文件也会被复制一份。

4. 菜单栏功能(通过右键菜单实现)
在右侧的图像列表上右键单击,可以弹出上下文菜单,包含以下功能:
编辑步骤名称: 修改当前选中步骤的名称。
编辑相似度: 修改当前选中步骤的相似度阈值。
编辑键盘输入: 修改当前选中步骤的键盘输入操作。
编辑鼠标操作: 修改当前选中步骤的鼠标操作。
编辑等待时间: 修改当前选中步骤的等待时间。
复制: 复制当前选中的步骤。
粘贴: 将复制的步骤粘贴到当前选中步骤的下方。
从此步骤开始运行: 从当前选中的步骤开始执行脚本。
设置条件跳转: 为当前选中的步骤设置条件和跳转目标。

5. 其他功能

获取鼠标位置 (F2): 在图像列表选中步骤后,按下 F2 键,可以获取当前鼠标的屏幕坐标,并将其设置为该步骤的鼠标点击坐标。
允许最小化: 勾选后,点击“开始运行”,主窗口可以最小化到任务栏,不影响脚本执行。
焦点跟随: 勾选后,脚本运行时会自动选中并滚动到当前执行的步骤,方便追踪执行进度。
只键盘操作: 勾选后,脚本运行时将只执行键盘输入,忽略鼠标点击操作,适用于只需要键盘操作的场景。

6. 注意事项

图像质量: 确保用于识别的图像具有良好的质量和清晰度,避免模糊或遮挡,当然也可以自己设置相似度来测试匹配.程序默认是0.8.
屏幕分辨率和缩放: 脚本的运行依赖于屏幕的当前状态。如果屏幕分辨率或缩放比例发生变化,可能导致图像匹配失败。
权限问题: 某些需要管理员权限的操作可能无法通过脚本自动化。
配置文件管理: 建议定期保存配置,以便在需要时恢复。

免费评分

参与人数 69吾爱币 +73 热心值 +61 收起 理由
axiaowei + 1 + 1 我很赞同!
wwwcchao + 1 + 1 谢谢@Thanks!
para + 1 我很赞同!
TinPandora + 1 + 1 谢谢@Thanks!
小郎君心痒难耐 + 3 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
莫奇 + 1 + 1 谢谢@Thanks!
秦11012 + 1 + 1 我很赞同!
mfl666 + 1 + 1 我很赞同!
sfy39 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
huainianjs + 1 + 1 我很赞同!
debiduoman + 1 + 1 谢谢@Thanks!
s1eep + 1 + 1 谢谢@Thanks!
hanweibo20 + 1 谢谢@Thanks!
sw99227 + 1 + 1 谢谢@Thanks!
XMax + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
jyxctg + 1 + 1 上传图像功能建议保留
m3362168 + 1 + 1 谢谢@Thanks!
海风不停息 + 1 + 1 谢谢@Thanks!
Tencent516 + 1 用心讨论,共获提升!
Mc东哥_在线秒单 + 1 + 1 大佬 恳请加一个键盘按下 和键盘弹起的设置 和鼠标按住 释放一样的
私はあなたが好 + 1 + 1 谢谢@Thanks!
灰烬鬼舞 + 2 + 1 谢谢@Thanks!
墨羽临 + 1 + 1 谢谢@Thanks!
lwx25933 + 1 谢谢@Thanks!
superliuy + 1 感谢您的宝贵建议,我们会努力争取做得更好!
kingtod + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
ShiCan + 1 + 1 原创精品、大爱无私、很受用、支持下!
alastone + 1 + 1 我很赞同!
zzxlxx + 1 + 1 我很赞同!
华科A + 1 + 1 谢谢@Thanks!
小船遮天 + 1 我很赞同!
yjyy123 + 1 + 1 666
booja + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
唐三没藏 + 1 + 1 谢谢@Thanks!
csnf2008 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
e9mok1 + 1 + 1 我很赞同!
tianbowen0 + 1 + 1 高分屏下,更改高DPI设置后,图像预览和框选截图的区域完全不一致,需要楼 ...
zxc237283 + 1 + 1 赞!!!!
etz2008 + 1 + 1 12/4号更新的能打包吗?谢谢
xiaocao + 1 + 1 如果把循环次数延时秒数就好
寂寞才说爱 + 1 + 1 谢谢@Thanks!
tdjsmzzz + 1 + 1 谢谢@Thanks!
碧波江影 + 1 + 1 谢谢@Thanks!
mikelang + 1 + 1 我很赞同!
笨蛋笨蛋笨蛋! + 1 + 1 大佬 能添加个识别到图片后自动按键盘的某个键的功能吗
23923235 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
初雪樱 + 1 + 1 谢谢@Thanks!
wazyl + 1 + 1 我很赞同!
Msy丿平 + 1 + 1 谢谢@Thanks!
zc55100 + 1 + 1 我很赞同!
alohayan + 1 + 1 谢谢@Thanks!
Livemy + 1 用心讨论,共获提升!
irfox + 1 我很赞同!
治不好的拖延症 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
soar550 + 1 + 1 谢谢@Thanks!
ruanxiaoqi + 1 + 1 我很赞同!
fcml45 + 1 + 1 谢谢@Thanks!
yueguangxiaxian + 1 + 1 鼓励转贴优秀软件安全工具和文档!
wapjsx + 1 + 1 谢谢@Thanks!
kingzswang + 1 + 1 我很赞同!
小色虫 + 1 大佬,来做个quicker动作
MiluChild + 1 + 1 用心讨论,共获提升!
lkl2425572 + 1 期待成品,感谢楼主
yjn866y + 1 + 1 热心回复!
chesterche + 1 可以后台吗?
helh0275 + 1 + 1 谢谢@Thanks!
zhufengwan + 1 + 1 谢谢@Thanks!
lgc81034 + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

来自 2#
zkpy 发表于 2024-9-20 04:13
本帖最后由 zkpy 于 2024-9-21 22:48 编辑

[Asm] 纯文本查看 复制代码
import tkinter as tk
from tkinter import filedialog, simpledialog, ttk, messagebox
from PIL import Image, ImageTk, ImageGrab, ImageEnhance
import cv2
import numpy as np
import pyautogui
import time
import random
import threading
import logging
import os
import json
import tkinter.scrolledtext as scrolledtext
import queue

class QueueHandler(logging.Handler):
    def __init__(self, log_queue):
        super().__init__()
        self.log_queue = log_queue

    def emit(self, record):
        self.log_queue.put(self.format(record))

class ImageRecognitionApp:
    def __init__(self, root):
        self.root = root
        self.root.title("图像识别与点击")
        self.image_list = []
        self.init_variables()
        self.init_ui()
        self.log_queue = queue.Queue()
        self.init_logging()
        self.load_config()
        self.root.after(100, self.check_log_queue)
 
    def init_variables(self):
        self.screenshot_area = None
        self.rect = None
        self.start_x = self.start_y = None
        self.canvas = None
        self.running = False
        self.thread = None
        self.hotkey = '<F1>'
        self.similarity_threshold = 0.8
        self.delay_time = 0.1
        self.loop_count = 1
        self.selected_image = None
        self.preview_photo = None  # Added to store the reference to the preview image
 
    def init_ui(self):
        self.main_frame = tk.Frame(self.root)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        self.left_right_frame = tk.Frame(self.main_frame)
        self.left_right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.create_left_frame()
        self.create_right_frame()
        self.create_log_area()  # Moved to the right

        self.root.bind(self.hotkey, self.toggle_script)
 
    def create_left_frame(self):
        self.left_frame = tk.Frame(self.left_right_frame)
        self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)
 
        buttons = [
            ("上传图像", self.batch_upload_images),
            ("框选截图", self.prepare_capture_screenshot),
            ("删除图片", self.delete_selected_image),
            ("保存配置", self.save_config),
            ("设置热键", self.set_hotkey),
            ("加载配置", self.load_config_manually)
        ]
 
        for text, command in buttons:
            tk.Button(self.left_frame, text=text, command=command).pack(pady=5)
 
        self.toggle_run_button = tk.Button(self.left_frame, text="开始运行", command=self.toggle_script)
        self.toggle_run_button.pack(pady=5)
 
        self.create_loop_count_input()
 
    def create_loop_count_input(self):
        tk.Label(self.left_frame, text="循环次数:").pack(pady=5)
        self.loop_count_entry = tk.Entry(self.left_frame)
        self.loop_count_entry.insert(0, str(self.loop_count))
        self.loop_count_entry.pack(pady=5)
 
    def create_right_frame(self):
        self.right_frame = tk.Frame(self.left_right_frame)
        self.right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
 
        self.create_tree_view()
        self.create_preview_area()
 
    def create_tree_view(self):
        self.tree_frame = tk.Frame(self.right_frame)
        self.tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
 
        columns = ("图片", "步骤名称", "相似度", "键盘输入")
        self.tree = ttk.Treeview(self.tree_frame, columns=columns, show='headings')
         
        for col in columns:
            self.tree.heading(col, text=col)
            self.tree.column(col, width=100)
 
        self.tree.column("图片", width=200)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, pady=5)
        self.tree.bind('<<TreeviewSelect>>', self.on_treeview_select)
        self.tree.bind('<Double-1>', self.edit_image_name)
        self.tree.bind('<Double-3>', self.edit_similarity_threshold)
        self.tree.bind('<Double-2>', self.edit_keyboard_input)
 
        self.scrollbar = ttk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview)
        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.configure(yscrollcommand=self.scrollbar.set)
 
    def create_preview_area(self):
        self.preview_frame = tk.Frame(self.right_frame)
        self.preview_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, pady=10)
 
        tk.Label(self.preview_frame, text="图片预览").pack()
        self.preview_image = tk.Label(self.preview_frame)
        self.preview_image.pack()
 
    def create_log_area(self):
        self.log_frame = tk.Frame(self.main_frame)
        self.log_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=10, pady=10)

        tk.Label(self.log_frame, text="日志输出").pack()

        self.log_area = scrolledtext.ScrolledText(self.log_frame, wrap=tk.WORD, width=30, height=20)
        self.log_area.pack(fill=tk.Y, expand=True)
        self.log_area.config(state=tk.DISABLED)  # Set to read-only

    def check_log_queue(self):
        while not self.log_queue.empty():
            message = self.log_queue.get()
            self.log_message(message)
        self.root.after(100, self.check_log_queue)

    def log_message(self, message):
        self.log_area.config(state=tk.NORMAL)
        self.log_area.insert(tk.END, message + "\n")
        self.log_area.see(tk.END)
        self.log_area.config(state=tk.DISABLED)

    def init_logging(self):
        logging.getLogger().setLevel(logging.INFO)
        
        # 设置文件处理器
        file_handler = logging.FileHandler('app.log')
        file_handler.setLevel(logging.INFO)
        file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
        file_handler.setFormatter(file_formatter)
        
        # 设置队列处理器(用于GUI显示)
        queue_handler = QueueHandler(self.log_queue)
        queue_handler.setLevel(logging.INFO)
        queue_formatter = logging.Formatter('%(message)s')  # 简化的格式,只显示消息
        queue_handler.setFormatter(queue_formatter)
        
        # 获取 root logger 并添加处理器
        logger = logging.getLogger()
        logger.addHandler(file_handler)
        logger.addHandler(queue_handler)

    def batch_upload_images(self):
        file_paths = filedialog.askopenfilenames(filetypes=[("Image files", "*.jpg *.png")])
        if file_paths:
            for file_path in file_paths:
                self.image_list.append((file_path, os.path.basename(file_path), 0.8, ""))
            self.update_image_listbox()
 
    def prepare_capture_screenshot(self):
        self.root.withdraw()
        time.sleep(0.5)
        self.create_screenshot_window()
 
    def create_screenshot_window(self):
        self.top = tk.Toplevel(self.root)
        self.top.attributes('-fullscreen', True, '-alpha', 0.3)
        self.canvas = tk.Canvas(self.top, cursor="cross", bg='grey')
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas.bind("<ButtonPress-1>", self.on_button_press)
        self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
 
    def on_button_press(self, event):
        self.start_x = event.x
        self.start_y = event.y
        if self.rect:
            self.canvas.delete(self.rect)
        self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y, outline='red', width=2)
 
    def on_mouse_drag(self, event):
        cur_x, cur_y = (event.x, event.y)
        self.canvas.coords(self.rect, self.start_x, self.start_y, cur_x, cur_y)
 
    def on_button_release(self, event):
        end_x = event.x
        end_y = event.y
 
        bbox = (min(self.start_x, end_x), min(self.start_y, end_y), max(self.start_x, end_x), max(self.start_y, end_y))
 
        timestamp = f"JT{random.randint(100000, 999999)}.png"
        screenshot_path = timestamp
 
        screenshot = ImageGrab.grab(bbox)
        screenshot.save(screenshot_path)
 
        self.image_list.append((screenshot_path, timestamp, 0.8, ""))
        self.update_image_listbox()
 
        self.top.destroy()
        self.root.deiconify()
 
    def update_image_listbox(self):
        for row in self.tree.get_children():
            self.tree.delete(row)
 
        self.tree.image_refs = []  # Reset the image reference list
 
        for img_path, img_name, similarity_threshold, keyboard_input in self.image_list:
            image = Image.open(img_path)
            image.thumbnail((50, 50))
            photo = ImageTk.PhotoImage(image)
            self.tree.insert("", tk.END, values=(img_path, img_name, similarity_threshold, keyboard_input), image=photo)
            self.tree.image_refs.append(photo)  # Save the image reference
 
        if self.tree.selection():
            self.on_treeview_select(None)
 
    def on_treeview_select(self, event):
        selected_items = self.tree.selection()
        if selected_items:
            item = selected_items[0]
            img_path = self.tree.item(item, "values")[0]
            self.display_preview(img_path)
 
    def display_preview(self, img_path):
        try:
            image = Image.open(img_path)
            image.thumbnail((300, 300))
            self.preview_photo = ImageTk.PhotoImage(image)  # Use instance variable to store reference
            self.preview_image.config(image=self.preview_photo)
        except Exception as e:
            logging.error(f"无法加载图片: {e}")
            self.preview_image.config(image=None)
            self.preview_photo = None
 
    def edit_image_name(self, event):
        selected_item = self.tree.selection()[0]
        selected_index = self.tree.index(selected_item)
        selected_image = self.image_list[selected_index]
 
        new_image_name = simpledialog.askstring("修改步骤名称", "请输入新的步骤名称:", initialvalue=selected_image[1])
        if new_image_name is not None:
            self.image_list[selected_index] = (selected_image[0], new_image_name, selected_image[2], selected_image[3])
            self.update_image_listbox()
 
    def edit_similarity_threshold(self, event):
        selected_item = self.tree.selection()[0]
        selected_index = self.tree.index(selected_item)
        selected_image = self.image_list[selected_index]
 
        new_similarity_threshold = simpledialog.askfloat("修改相似度", "请输入新的相似度(0.1 - 1.0):", initialvalue=selected_image[2], minvalue=0.1, maxvalue=1.0)
        if new_similarity_threshold is not None:
            self.image_list[selected_index] = (selected_image[0], selected_image[1], new_similarity_threshold, selected_image[3])
            self.update_image_listbox()
 
    def edit_keyboard_input(self, event):
        selected_item = self.tree.selection()[0]
        selected_index = self.tree.index(selected_item)
        selected_image = self.image_list[selected_index]
 
        new_keyboard_input = simpledialog.askstring("修改键盘输入", "请输入新的键盘输入:", initialvalue=selected_image[3])
        if new_keyboard_input is not None:
            self.image_list[selected_index] = (selected_image[0], selected_image[1], selected_image[2], new_keyboard_input)
            self.update_image_listbox()
 
    def delete_selected_image(self):
        selected_item = self.tree.selection()
        if selected_item:
            selected_index = self.tree.index(selected_item[0])
            del self.image_list[selected_index]
            self.update_image_listbox()
 
    def toggle_script(self, event=None):
        if not self.running:
            self.start_script_thread()
            self.toggle_run_button.config(text="停止运行")
            self.root.after(100, self.check_script_status)
        else:
            self.stop_script()
        
        # Added to update UI
        self.root.update()
 
    def start_script_thread(self):
        if not self.running:
            self.running = True
            self.thread = threading.Thread(target=self.run_script, daemon=True)
            self.thread.start()
 
    def check_script_status(self):
        if self.running:
            self.root.after(100, self.check_script_status)
        else:
            self.toggle_run_button.config(text="开始运行")
 
    def run_script(self):
        self.loop_count = int(self.loop_count_entry.get())
        current_loop = 0
 
        while self.running and current_loop < self.loop_count:
            logging.info(f"开始第 {current_loop + 1} 次循环")
            for img_path, img_name, similarity_threshold, keyboard_input in self.image_list:
                if not self.running:
                    return
                # logging.info(f"正在识别图像: {img_name}")
                if self.match_and_click(img_path, similarity_threshold):
                    if keyboard_input:
                        pyautogui.write(keyboard_input)
                        logging.info(f"输入键盘内容: {keyboard_input}")
                time.sleep(self.delay_time)
            current_loop += 1
        
        self.running = False
        logging.info("脚本运行完成")
 
    def stop_script(self):
        self.running = False
        if self.thread is not None:
            self.thread.join(timeout=2)  # Wait for thread to finish up to 2 seconds
            if self.thread.is_alive():
                logging.warning("警告:线程未能在2秒内停止")
        logging.info("脚本已停止")
        self.toggle_run_button.config(text="开始运行")
 
    def match_and_click(self, template_path, similarity_threshold):
        screenshot = pyautogui.screenshot()
        screenshot = np.array(screenshot)
        screenshot = cv2.cvtColor(screenshot, cv2.COLOR_RGB2BGR)
 
        template = cv2.imread(template_path, 0)
        if template is None:
            logging.error(f"无法读取图像: {template_path}")
            return False
 
        gray_screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
 
        result = cv2.matchTemplate(gray_screenshot, template, cv2.TM_CCOEFF_NORMED)
        loc = np.where(result >= similarity_threshold)
 
        found = False
        for pt in zip(*loc[::-1]):
            click_x = pt[0] + template.shape[1] // 2
            click_y = pt[1] + template.shape[0] // 2
            pyautogui.click(click_x, click_y)
            found = True
            logging.info(f"找到并点击了图像: {template_path} 在位置 ({click_x}, {click_y})")
            break
 
        if not found:
            # ogging.warning(f"未找到匹配区域: {template_path}")
            return False
 
        return True
 
    def set_hotkey(self):
        new_hotkey = simpledialog.askstring("设置热键", "请输入新的热键(例如:<F1>):", initialvalue=self.hotkey)
        if new_hotkey is not None:
            self.hotkey = new_hotkey
            self.root.bind(self.hotkey, self.toggle_script)
            logging.info(f"热键已设置为: {self.hotkey}")
 
    def save_config(self):
        config = {
            "image_list": self.image_list,
            "hotkey": self.hotkey,
            "screenshot_area": self.screenshot_area,
            "similarity_threshold": self.similarity_threshold,
            "delay_time": self.delay_time,
            "loop_count": self.loop_count
        }
        with open('config.json', 'w') as f:
            json.dump(config, f)
        logging.info("配置已保存")
 
    def load_config(self):
        if os.path.exists('config.json'):
            with open('config.json', 'r') as f:
                config = json.load(f)
                self.image_list = config.get("image_list", [])
                self.hotkey = config.get("hotkey", '<F1>')
                self.screenshot_area = config.get("screenshot_area", None)
                self.similarity_threshold = config.get("similarity_threshold", 0.8)
                self.delay_time = config.get("delay_time", 0.1)
                self.loop_count = config.get("loop_count", 1)
                self.loop_count_entry.delete(0, tk.END)
                self.loop_count_entry.insert(0, str(self.loop_count))
                self.root.bind(self.hotkey, self.toggle_script)
                self.update_image_listbox()
            logging.info("配置已加载")
 
    def load_config_manually(self):
        file_path = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")])
        if file_path:
            with open(file_path, 'r') as f:
                config = json.load(f)
                self.image_list = config.get("image_list", [])
                self.hotkey = config.get("hotkey", '<F1>')
                self.screenshot_area = config.get("screenshot_area", None)
                self.similarity_threshold = config.get("similarity_threshold", 0.8)
                self.delay_time = config.get("delay_time", 0.1)
                self.loop_count = config.get("loop_count", 1)
                self.loop_count_entry.delete(0, tk.END)
                self.loop_count_entry.insert(0, str(self.loop_count))
                self.root.bind(self.hotkey, self.toggle_script)
                self.update_image_listbox()
            logging.info("配置已加载")
 
if __name__ == "__main__":
    root = tk.Tk()
    app = ImageRecognitionApp(root)
    root.mainloop()

已添加图片预览

吧友提出无法更改停止运行问题,看了下,确实存在.
这里简单说明下,由于python线程的特殊性,已做优化改进!
顺手填上了日志输出!
有的地方可能未注意到,遇到了在优化!
推荐
 楼主| gzsklsskszngc 发表于 2024-12-5 20:35 |楼主
yjohjot 发表于 2024-12-5 13:38
楼主  你这个是顺序识别的 有没有办法同步识别?  如果步骤1图片一直没识别到 永远不会识别步骤2的图片     ...

程序默认是一直等到图像出现为止,但你可以设置条件啊!比如我先设置上一个延迟5秒钟,然后条件设置如果步骤1为"False",那就跳转到步骤2。很好理解吧?就是当点击完上一步,五秒钟后如果没有识别到下一步的图像出现,那就直接跳过它,执行下一步骤。
推荐
 楼主| gzsklsskszngc 发表于 2025-1-6 11:18 |楼主
superliuy 发表于 2025-1-2 12:05
楼主新年快乐,这个软件可以做到后台点击吗?就是在后台运行点,还能开网页做自己的事情互不影响?

好的,目前正在写,待测试完成后再发布.

免费评分

参与人数 2吾爱币 +2 热心值 +1 收起 理由
zhengzhenhui945 + 1 + 1 弃坑了吗。。。。
young19627 + 1 做好了吗?等着后台点击呢

查看全部评分

推荐
helh0275 发表于 2024-9-23 23:40
感谢大佬进行增补,但你这没有每个图片每个动作的时限设定,完全就是一通狂点
推荐
vyuermeng 发表于 2024-9-20 09:41
我运行了一下  框选截图   点运行  鼠标会移到对应的图标上  但没有点击操作,是我配置的不对吗

免费评分

参与人数 1热心值 +1 收起 理由
舌尖上的幻想乡 + 1 我也是这个问题,请问解决了吗

查看全部评分

推荐
君功梦梦梦 发表于 2025-1-17 18:33
感觉还有些bug。1.一点“条件跳转”后就必须选一个true or false了,不能为空。还有鼠标也不能为空等等,有些步骤不想点鼠标,只点键盘都不行。2.还有步骤无法调跳转的问题,单独点三和四步骤是可以的,但是两个连起来就不行了。。。还有保存的文件1和文件2不能连起来调用,在过多步骤复杂化时不好编辑
推荐
 楼主| gzsklsskszngc 发表于 2024-11-1 12:29 |楼主
18591406028 发表于 2024-11-1 10:15
想交流下关于关于图片识别那块:用的是什么方法?
我以前也写过一个类似于按键精灵的工具,自己玩游戏用, ...

主要使用了OpenCV的模板匹配方法,归一化相关系数匹配,这样能够提高计算速度。
再将图像转换为灰度图进行匹配,并使用RGB到BGR的颜色空间转换,适应OpenCV的格式要求,也能减少计算量。
这样的优点是:
实现简单,计算速度快
对目标的完整匹配效果好
不需要训练过程

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
白鹿原露白 + 1 + 1 用心讨论,共获提升!

查看全部评分

推荐
 楼主| gzsklsskszngc 发表于 2024-9-24 20:43 |楼主
helh0275 发表于 2024-9-23 23:40
感谢大佬进行增补,但你这没有每个图片每个动作的时限设定,完全就是一通狂点

没有时限,就是以最快的速度去点击。程序是识别到下一步的图像出现时才去点,如果想要延迟,可以以别的图像作为参照,例如当你点击网页上的登录按钮之后要等到什么图像出现时再点击下一个地方。我修改的目的就是以最快的速度,不浪费一分一秒。目前我自己就用在工作的一些测试任务上,非常的爽。
5#
ID08 发表于 2024-9-19 23:59
6666,大佬厉害&#128077;&#127995;,学习进步
6#
kim945 发表于 2024-9-20 00:11
这我一直用按键精灵一点点试着做的...为了每天填写各种单位上报的网站..早要是有这个,起码我能省20个小时T_T
7#
helh0275 发表于 2024-9-20 01:06
老大就不能顺手打个包?伪装成exe的样子
8#
yjn866y 发表于 2024-9-20 08:43
学习使人进步。。。
9#
Rx0 发表于 2024-9-20 09:05
这种软件,还是得 高速、大批量 处理才更实用。可以参考Duplicate Cleaner 来做。
10#
jinos 发表于 2024-9-20 10:04

学习使人进步。。。感谢分想
11#
just4p 发表于 2024-9-20 10:22
楼主可以将打包好的发出来就最好了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-10 20:11

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表