gzsklsskszngc 发表于 2024-9-19 23:49

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

本帖最后由 gzsklsskszngc 于 2024-11-1 12:32 编辑

首先感谢原贴作者提供的代码:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1965479&highlight=%CD%BC%CF%F1
我在原代码的基础上做了以下小修改
上代码:
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

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.init_ui()
      self.init_logging()
      self.bind_arrow_keys()
      self.create_context_menu()
      atexit.register(self.cleanup_on_exit)

    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.right_frame = tk.Frame(self.main_frame)
      self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)

      # 使用 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, pady=5)
      self.tree.image_refs = []# 保持对图像的引用,防止被垃圾回收

      # 在创建 Treeview 后添加这些行
      self.tree.bind('<Up>', self.move_item_up)
      self.tree.bind('<Down>', self.move_item_down)

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

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

      # 为上下文菜单添加此绑定
      self.tree.bind('<Button-3>', self.show_context_menu)

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

    def prepare_capture_screenshot(self):
      # 在截图前计算新的步骤编号
      existing_steps = set()
      for item in self.image_list:
            step_name = item
            if step_name.startswith("步骤"):
                try:
                  num = int(step_name)# 提取"步骤"后面的数字
                  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)

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

      # 关闭全屏透明窗口
      self.top.destroy()
      self.root.deiconify()

    def update_image_listbox(self):
      try:
            # 保存当前选中的项目
            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
                  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

      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)
            if 0 <= selected_index < len(self.image_list):
                selected_image = self.image_list
                img_path = selected_image
               
                # 检查是否正在使用配置文件
                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
                  
                  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 == 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
                        # 检查是否有其他项目引用相同的图像文件
                        is_referenced = any(item == 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:
                current_step = self.image_list
                img_path, img_name, similarity_threshold, keyboard_input, mouse_click_coordinates, wait_time = current_step[:6]
                condition = current_step if len(current_step) > 6 else None
                jump_to = current_step if len(current_step) > 7 else None
               
                if self.follow_current_step.get():
                  self.tree.selection_set(self.tree.get_children())
                  self.tree.focus(self.tree.get_children())

                # 执行一次图像匹配
                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)
                else:
                  match_result = False

                # 处理条件跳转
                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 == jump_to:
                              index = i
                              break
                        continue
               
                # 如果没有跳转且匹配失败,则持续尝试匹配
                if not match_result and not (condition and jump_to):
                  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)
            if selected_index > 0:
               
                self.image_list, self.image_list = self.image_list, self.image_list
                self.update_image_listbox()

               
                item_id = self.tree.get_children()
                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)
            if selected_index < len(self.image_list) - 1:
               
                self.image_list, self.image_list = self.image_list, self.image_list
                self.update_image_listbox()

               
                item_id = self.tree.get_children()
                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 == template_path), None)
      if selected_index is not None:
            current_step = self.image_list
            mouse_coordinates = current_step# 获取鼠标坐标
            keyboard_input = current_step# 获取键盘输入
      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 "," in mouse_coordinates:
                try:
                  x, y = map(int, mouse_coordinates.split(','))
                  pyautogui.click(x, y)
                  print(f"匹配成功,点击指定坐标:({x}, {y})")
                  logging.info(f"匹配成功,点击指定坐标:({x}, {y})")
                except Exception as e:
                  print(f"点击指定坐标时出错: {e}")
                  logging.error(f"点击指定坐标时出错: {e}")
                  return False
            else:
                click_x = max_loc + template.shape // 2
                click_y = max_loc + template.shape // 2
                pyautogui.click(click_x, click_y)
                print(f"匹配成功,点击图像位置:({click_x}, {click_y})")
                logging.info(f"匹配成功,点击图像位置:({click_x}, {click_y})")

            # 再处理键盘输入
            if keyboard_input:
                try:
                  time.sleep(0.5)# 添加短暂延迟,确保点击后输入框已获得焦点
                  pyautogui.write(keyboard_input)
                  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 edit_keyboard_input(self):
      selected_items = self.tree.selection()
      if selected_items:
            selected_item = selected_items
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list

            # 创建置顶的对话框
            dialog = tk.Toplevel(self.root)
            dialog.title("修改键盘输入")
            dialog.transient(self.root)# 设置为主窗口的临时窗口
            dialog.grab_set()# 模态对话框
            dialog.attributes('-topmost', True)# 设置窗口置顶

            # 创建输入框和标签
            tk.Label(dialog, text="请输入键盘输入内容:").pack(pady=5)
            entry = tk.Entry(dialog, width=30)
            entry.insert(0, selected_image)
            entry.pack(pady=5)

            def save_input():
                new_keyboard_input = entry.get()
                self.image_list = (
                  selected_image, selected_image, selected_image,
                  new_keyboard_input, selected_image, selected_image,
                  selected_image, selected_image
                )
                self.update_image_listbox()
                dialog.destroy()

            # 添加保存按钮
            tk.Button(dialog, text="保存", command=save_input).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 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.root.unbind(self.hotkey)
                self.root.bind(new_hotkey, self.toggle_script)
                self.hotkey = new_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': )]
      }
      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):
                  missing_images.append(img_data)
            
            # 如果有任何图像文件不存在,直接返回,不做任何更改
            if missing_images:
                error_message = f"配置加载失败。以下图像文件不存在:\n" + "\n".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)
            
            # 清空并重新填充 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)
                  image.thumbnail((50, 50))
                  photo = ImageTk.PhotoImage(image)
                  
                  # 插入到 Treeview
                  tree_item = self.tree.insert("", tk.END, values=(
                        os.path.basename(img_data),# 图片名称
                        img_data,# 步骤名称
                        img_data,# 相似度
                        img_data,# 键盘输入
                        img_data,# 鼠标点击坐标
                        img_data,# 等待时间
                        img_data,# 条件
                        img_data   # 跳转步骤
                  ), image=photo)
                  self.tree.image_refs.append(photo)
                except Exception as e:
                  print(f"处理图像时出错 {img_data}: {e}")
                  logging.error(f"处理图像时出错 {img_data}: {e}")
            
            # 更新循环次数输入框
            self.loop_count_entry.delete(0, tk.END)
            self.loop_count_entry.insert(0, str(self.loop_count))
            
            # 重新绑定热键
            self.root.unbind(self.hotkey)
            self.root.bind(self.hotkey, self.toggle_script)
            
            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.update_image_listbox()
            self.root.bind(self.hotkey, self.toggle_script)
            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("重置", "程序已重置为初始状态")
      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)
            selected_image = self.image_list
            self.image_list = (selected_image, selected_image, selected_image, selected_image, f"{x},{y}", selected_image, selected_image, selected_image)
            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
                  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}")
      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)

      # 阻止事件传播
      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
      selected_index = self.tree.index(selected_item)
      img_data = self.image_list
      
      # 使用新的解包方式,适应可能存在的额外字段
      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 + w // 2
            center_y = max_loc + 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)
      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_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 start_from_step(self):
      selected_items = self.tree.selection()
      if not selected_items:
            messagebox.showwarning("警告", "请先选择一个项目进行操作")
            return

      # 获取当前选中的步骤索引
      selected_item = selected_items
      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_set(item)
            self.context_menu.post(event.x_root, event.y_root)

    def copy_item(self):
      selected_items = self.tree.selection()
      if selected_items:
            index = self.tree.index(selected_items)
            original_item = self.image_list
            
            # 创建像文件的副本
            new_image_path = self.create_image_copy(original_item)
            
            # 创建新的元组,使用新的图像路径
            self.copied_item = (new_image_path,) + tuple(original_item)

    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
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list

            new_image_name = simpledialog.askstring("修改步骤名称", "请输入新的步骤名称:", initialvalue=selected_image)
            if new_image_name is not None:
                self.image_list = (selected_image, new_image_name, selected_image, selected_image, selected_image, selected_image, selected_image, selected_image)
                self.update_image_listbox()

    def edit_similarity_threshold(self):
      selected_items = self.tree.selection()
      if selected_items:
            selected_item = selected_items
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list

            new_similarity_threshold = simpledialog.askfloat("修改相似度", "请输入新的相似度(0.1 - 1.0):", initialvalue=selected_image, minvalue=0.1, maxvalue=1.0)
            if new_similarity_threshold is not None:
                self.image_list = (selected_image, selected_image, new_similarity_threshold, selected_image, selected_image, selected_image, selected_image, selected_image)
                self.update_image_listbox()

    def edit_keyboard_input(self):
      selected_items = self.tree.selection()
      if selected_items:
            selected_item = selected_items
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list

            # 创建置顶的对话框
            dialog = tk.Toplevel(self.root)
            dialog.title("修改键盘输入")
            dialog.transient(self.root)# 设置为主窗口的临时窗口
            dialog.grab_set()# 模态对话框
            dialog.attributes('-topmost', True)# 设置窗口置顶

            # 创建输入框和标签
            tk.Label(dialog, text="请输入键盘输入内容:").pack(pady=5)
            entry = tk.Entry(dialog, width=30)
            entry.insert(0, selected_image)
            entry.pack(pady=5)

            def save_input():
                new_keyboard_input = entry.get()
                self.image_list = (
                  selected_image, selected_image, selected_image,
                  new_keyboard_input, selected_image, selected_image,
                  selected_image, selected_image
                )
                self.update_image_listbox()
                dialog.destroy()

            # 添加保存按钮
            tk.Button(dialog, text="保存", command=save_input).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 edit_wait_time(self):
      selected_items = self.tree.selection()
      if selected_items:
            selected_item = selected_items
            selected_index = self.tree.index(selected_item)
            selected_image = self.image_list

            new_wait_time = simpledialog.askinteger("修改等待时间", "请输入新的等待时间(毫秒):", initialvalue=selected_image)
            if new_wait_time is not None:
                self.image_list = (selected_image, selected_image, selected_image, selected_image, selected_image, new_wait_time, selected_image, selected_image)
                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
      selected_index = self.tree.index(selected_item)
      selected_image = list(self.image_list)# 转换为列表以便修改

      # 创建设置条件跳转的对话框
      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 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 if len(selected_image) > 7 else "")
      step_names = 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 = condition
                selected_image = jump_to
               
                # 更新 image_list 中的数据
                self.image_list = tuple(selected_image)
               
                # 立即更新显示
                self.update_image_listbox()
               
                # 选中更新后的项
                items = self.tree.get_children()
                if selected_index < len(items):
                  self.tree.selection_set(items)
                  self.tree.focus(items)
               
                # 打印日志确认更新
                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}")

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"
下载地址

zkpy 发表于 2024-9-20 04:13

本帖最后由 zkpy 于 2024-9-21 22:48 编辑

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
            img_path = self.tree.item(item, "values")
            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()
      selected_index = self.tree.index(selected_item)
      selected_image = self.image_list

      new_image_name = simpledialog.askstring("修改步骤名称", "请输入新的步骤名称:", initialvalue=selected_image)
      if new_image_name is not None:
            self.image_list = (selected_image, new_image_name, selected_image, selected_image)
            self.update_image_listbox()

    def edit_similarity_threshold(self, event):
      selected_item = self.tree.selection()
      selected_index = self.tree.index(selected_item)
      selected_image = self.image_list

      new_similarity_threshold = simpledialog.askfloat("修改相似度", "请输入新的相似度(0.1 - 1.0):", initialvalue=selected_image, minvalue=0.1, maxvalue=1.0)
      if new_similarity_threshold is not None:
            self.image_list = (selected_image, selected_image, new_similarity_threshold, selected_image)
            self.update_image_listbox()

    def edit_keyboard_input(self, event):
      selected_item = self.tree.selection()
      selected_index = self.tree.index(selected_item)
      selected_image = self.image_list

      new_keyboard_input = simpledialog.askstring("修改键盘输入", "请输入新的键盘输入:", initialvalue=selected_image)
      if new_keyboard_input is not None:
            self.image_list = (selected_image, selected_image, selected_image, 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)
            del self.image_list
            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 + template.shape // 2
            click_y = pt + template.shape // 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-11-1 12:29

18591406028 发表于 2024-11-1 10:15
想交流下关于关于图片识别那块:用的是什么方法?
我以前也写过一个类似于按键精灵的工具,自己玩游戏用, ...

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

gzsklsskszngc 发表于 2024-9-24 20:43

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

没有时限,就是以最快的速度去点击。程序是识别到下一步的图像出现时才去点,如果想要延迟,可以以别的图像作为参照,例如当你点击网页上的登录按钮之后要等到什么图像出现时再点击下一个地方。我修改的目的就是以最快的速度,不浪费一分一秒。目前我自己就用在工作的一些测试任务上,非常的爽。

ID08 发表于 2024-9-19 23:59

6666,大佬厉害&#128077;&#127995;,学习进步

kim945 发表于 2024-9-20 00:11

这我一直用按键精灵一点点试着做的...为了每天填写各种单位上报的网站..早要是有这个,起码我能省20个小时T_T

helh0275 发表于 2024-9-20 01:06

老大就不能顺手打个包?伪装成exe的样子{:301_1008:}

yueguangxiaxian 发表于 2024-9-20 06:45

希望有编译好的,谢谢楼主

yjn866y 发表于 2024-9-20 08:43

学习使人进步。。。

wasm2023 发表于 2024-9-20 08:49

学习了,感谢分享

Rx0 发表于 2024-9-20 09:05

这种软件,还是得 高速、大批量 处理才更实用。可以参考Duplicate Cleaner 来做。

vyuermeng 发表于 2024-9-20 09:41

我运行了一下框选截图   点运行鼠标会移到对应的图标上但没有点击操作,是我配置的不对吗
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 图像识别点击 [图像识别自动点击按键精灵] 修改版