影刀RPA鼠标键盘精准控制:解决复杂桌面应用操作难题

0 阅读7分钟

影刀RPA鼠标键盘精准控制:解决复杂桌面应用操作难题

作者:林焱 | 发布平台:CSDN / 公众号 / 掘金


在这里插入图片描述

前言:当网页自动化遇到瓶颈

大多数RPA教程聚焦于网页自动化,但真实企业环境中存在大量桌面应用:

  • 老旧的Windows ERP客户端
  • 财务软件(用友、金蝶)
  • CAD/设计软件批处理
  • 医疗影像系统

这些系统没有Web界面,无法用CSS选择器定位,只能通过鼠标键盘模拟来实现自动化。本文深入讲解影刀RPA的桌面应用操作技术。

在这里插入图片描述


基础工具:pyautogui + win32api

[video(video-7WpPfjeG-1782319941974)(type-csdn)(url-live.csdn.net/v/embed/526…)]

[video(video-Ej9BjuKE-1782319948825)(type-csdn)(url-live.csdn.net/v/embed/526…)]

在这里插入图片描述

import shadowbot as sb
import pyautogui
import win32gui
import win32con
import win32api
import ctypes
import time

# 关键设置
pyautogui.PAUSE = 0.1           # 每次操作后暂停0.1秒
pyautogui.FAILSAFE = True       # 鼠标移到左上角紧急停止
pyautogui.MINIMUM_DURATION = 0.1  # 鼠标移动最短时间

def safe_click(x, y, button='left', clicks=1, interval=0.1):
    """安全点击(含验证)"""
    # 移动到目标位置
    pyautogui.moveTo(x, y, duration=0.3)
    time.sleep(0.1)
    
    # 执行点击
    pyautogui.click(x, y, button=button, clicks=clicks, interval=interval)
    time.sleep(0.1)
    
    sb.log.debug(f"点击操作:({x}, {y}), {button}键, {clicks}次")

技巧一:窗口精准定位

通过窗口标题找到目标程序

在这里插入图片描述

class WindowManager:
    """窗口管理工具"""
    
    @staticmethod
    def find_window(title_pattern, exact=False):
        """按标题查找窗口"""
        import re
        results = []
        
        def enum_callback(hwnd, results):
            if win32gui.IsWindowVisible(hwnd):
                title = win32gui.GetWindowText(hwnd)
                if exact:
                    if title == title_pattern:
                        results.append(hwnd)
                else:
                    if title_pattern.lower() in title.lower():
                        results.append(hwnd)
        
        win32gui.EnumWindows(enum_callback, results)
        return results
    
    @staticmethod
    def activate_window(hwnd):
        """激活并置前窗口"""
        # 如果窗口最小化,先恢复
        if win32gui.IsIconic(hwnd):
            win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
        
        # 置于前台
        win32gui.SetForegroundWindow(hwnd)
        time.sleep(0.3)
        
        return True
    
    @staticmethod
    def get_window_rect(hwnd):
        """获取窗口位置和大小"""
        rect = win32gui.GetWindowRect(hwnd)
        return {
            "left": rect[0],
            "top": rect[1],
            "right": rect[2],
            "bottom": rect[3],
            "width": rect[2] - rect[0],
            "height": rect[3] - rect[1],
        }
    
    @staticmethod
    def resize_window(hwnd, width, height, x=None, y=None):
        """调整窗口大小"""
        rect = win32gui.GetWindowRect(hwnd)
        new_x = x if x is not None else rect[0]
        new_y = y if y is not None else rect[1]
        
        win32gui.MoveWindow(hwnd, new_x, new_y, width, height, True)
        time.sleep(0.2)

wm = WindowManager()

# 找到用友财务软件
ufida_windows = wm.find_window("用友")
if ufida_windows:
    hwnd = ufida_windows[0]
    wm.activate_window(hwnd)
    win_rect = wm.get_window_rect(hwnd)
    sb.log.info(f"找到用友窗口:{win_rect}")

技巧二:基于坐标的相对定位

固定坐标会随屏幕分辨率变化而失效,应使用相对坐标

在这里插入图片描述

class RelativeCoordinator:
    """基于窗口的相对坐标系统"""
    
    def __init__(self, hwnd):
        self.hwnd = hwnd
        self._update_rect()
    
    def _update_rect(self):
        rect = win32gui.GetWindowRect(self.hwnd)
        self.left = rect[0]
        self.top = rect[1]
        self.width = rect[2] - rect[0]
        self.height = rect[3] - rect[1]
    
    def abs_pos(self, rel_x, rel_y):
        """相对坐标转绝对坐标"""
        self._update_rect()  # 每次都刷新,防止窗口移动
        
        if 0 <= rel_x <= 1:  # 比例形式
            abs_x = self.left + int(self.width * rel_x)
        else:  # 像素偏移形式
            abs_x = self.left + rel_x
        
        if 0 <= rel_y <= 1:
            abs_y = self.top + int(self.height * rel_y)
        else:
            abs_y = self.top + rel_y
        
        return abs_x, abs_y
    
    def click(self, rel_x, rel_y, **kwargs):
        """在相对位置点击"""
        abs_x, abs_y = self.abs_pos(rel_x, rel_y)
        safe_click(abs_x, abs_y, **kwargs)
    
    def input_at(self, rel_x, rel_y, text):
        """在相对位置输入文字"""
        self.click(rel_x, rel_y)
        time.sleep(0.2)
        
        # 清除原有内容
        pyautogui.hotkey('ctrl', 'a')
        time.sleep(0.1)
        
        # 使用剪贴板输入(避免中文输入法问题)
        import pyperclip
        pyperclip.copy(text)
        pyautogui.hotkey('ctrl', 'v')
        time.sleep(0.1)

# 使用示例:操作用友财务
hwnd_list = wm.find_window("用友T3")
if hwnd_list:
    coord = RelativeCoordinator(hwnd_list[0])
    
    # 点击"总账"菜单(位于窗口宽度20%,高度5%处)
    coord.click(0.20, 0.05)
    time.sleep(0.5)
    
    # 在凭证编号输入框输入
    coord.input_at(0.30, 0.35, "2024-001")

技巧三:图像识别定位(不依赖坐标)

当界面布局可能变化时,用截图模板匹配来定位元素:

在这里插入图片描述

import cv2
import numpy as np
from PIL import ImageGrab

class ImageBasedLocator:
    """基于图像识别的元素定位"""
    
    def __init__(self, screenshot_region=None):
        self.region = screenshot_region  # (left, top, right, bottom)
    
    def take_screenshot(self):
        """截取屏幕"""
        if self.region:
            screenshot = ImageGrab.grab(bbox=self.region)
        else:
            screenshot = ImageGrab.grab()
        
        return np.array(screenshot)
    
    def find_element(self, template_path, confidence=0.85):
        """在屏幕上找到模板图像的位置"""
        screenshot = self.take_screenshot()
        template = cv2.imread(template_path)
        
        if template is None:
            raise FileNotFoundError(f"模板文件不存在:{template_path}")
        
        # 转换颜色空间
        screenshot_gray = cv2.cvtColor(screenshot, cv2.COLOR_RGB2GRAY)
        template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
        
        # 模板匹配
        result = cv2.matchTemplate(screenshot_gray, template_gray, cv2.TM_CCOEFF_NORMED)
        
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
        
        if max_val >= confidence:
            # 找到元素,返回中心坐标
            h, w = template.shape[:2]
            center_x = max_loc[0] + w // 2
            center_y = max_loc[1] + h // 2
            
            # 如果指定了区域,需要加上偏移
            if self.region:
                center_x += self.region[0]
                center_y += self.region[1]
            
            return center_x, center_y, max_val
        else:
            return None, None, max_val
    
    def find_all_elements(self, template_path, confidence=0.85):
        """找到所有匹配的元素"""
        screenshot = self.take_screenshot()
        template = cv2.imread(template_path)
        
        screenshot_gray = cv2.cvtColor(screenshot, cv2.COLOR_RGB2GRAY)
        template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
        
        result = cv2.matchTemplate(screenshot_gray, template_gray, cv2.TM_CCOEFF_NORMED)
        
        h, w = template.shape[:2]
        locations = np.where(result >= confidence)
        
        points = []
        for pt in zip(*locations[::-1]):
            center_x = pt[0] + w // 2
            center_y = pt[1] + h // 2
            
            if self.region:
                center_x += self.region[0]
                center_y += self.region[1]
            
            points.append((center_x, center_y))
        
        # 去重(相近坐标合并)
        unique_points = []
        for pt in points:
            if not any(abs(pt[0] - u[0]) < 20 and abs(pt[1] - u[1]) < 20 for u in unique_points):
                unique_points.append(pt)
        
        return unique_points
    
    def click_element(self, template_path, confidence=0.85, max_wait=10):
        """找到并点击元素"""
        start_time = time.time()
        
        while time.time() - start_time < max_wait:
            x, y, score = self.find_element(template_path, confidence)
            
            if x is not None:
                safe_click(x, y)
                sb.log.info(f"点击元素成功(置信度:{score:.2f}):{template_path}")
                return True
            
            time.sleep(0.5)
        
        sb.log.warning(f"超时未找到元素:{template_path}")
        return False

# 使用示例
locator = ImageBasedLocator()

# 点击"保存"按钮(不依赖坐标)
locator.click_element("templates/save_button.png", confidence=0.9)

# 找到所有"删除"按钮
delete_buttons = locator.find_all_elements("templates/delete_icon.png")
sb.log.info(f"找到 {len(delete_buttons)} 个删除按钮")

技巧四:键盘快捷键自动化

常用软件快捷键封装

在这里插入图片描述 在这里插入图片描述

class KeyboardAutomation:
    """键盘操作封装"""
    
    @staticmethod
    def select_all():
        pyautogui.hotkey('ctrl', 'a')
        time.sleep(0.1)
    
    @staticmethod
    def copy():
        pyautogui.hotkey('ctrl', 'c')
        time.sleep(0.1)
    
    @staticmethod
    def paste():
        pyautogui.hotkey('ctrl', 'v')
        time.sleep(0.1)
    
    @staticmethod
    def undo():
        pyautogui.hotkey('ctrl', 'z')
        time.sleep(0.1)
    
    @staticmethod
    def save():
        pyautogui.hotkey('ctrl', 's')
        time.sleep(0.5)
    
    @staticmethod
    def tab_next(times=1):
        for _ in range(times):
            pyautogui.press('tab')
            time.sleep(0.1)
    
    @staticmethod
    def navigate_table(direction):
        """在表格中导航"""
        key_map = {
            'right': 'tab',
            'left': ('shift', 'tab'),
            'down': 'down',
            'up': 'up',
            'next_row': 'return',
        }
        key = key_map.get(direction, 'tab')
        if isinstance(key, tuple):
            pyautogui.hotkey(*key)
        else:
            pyautogui.press(key)
        time.sleep(0.1)
    
    @staticmethod
    def type_with_clipboard(text):
        """通过剪贴板输入文字(支持中文)"""
        import pyperclip
        pyperclip.copy(str(text))
        pyautogui.hotkey('ctrl', 'v')
        time.sleep(0.1)
    
    @staticmethod
    def press_function_key(n):
        """按功能键 F1-F12"""
        pyautogui.press(f'f{n}')
        time.sleep(0.2)

kbd = KeyboardAutomation()

def fill_form_with_keyboard(form_data):
    """用键盘Tab键逐字段填写表单"""
    fields = list(form_data.values())
    
    for i, value in enumerate(fields):
        # 输入当前字段
        kbd.type_with_clipboard(str(value))
        time.sleep(0.1)
        
        # Tab到下一个字段
        if i < len(fields) - 1:
            kbd.tab_next()
    
    # 最后按Enter提交
    pyautogui.press('enter')
    time.sleep(0.5)

实战:自动操作用友财务软件录入凭证

def input_accounting_voucher(voucher_data):
    """在用友T3中录入记账凭证"""
    
    # 查找并激活用友窗口
    hwnd_list = wm.find_window("用友T3")
    if not hwnd_list:
        sb.log.error("用友T3未启动")
        return False
    
    hwnd = hwnd_list[0]
    wm.activate_window(hwnd)
    
    coord = RelativeCoordinator(hwnd)
    locator = ImageBasedLocator(region=tuple(wm.get_window_rect(hwnd).values())[:4])
    
    # 1. 点击"总账" → "凭证" → "填制凭证"
    coord.click(0.05, 0.035)  # 总账菜单
    time.sleep(0.5)
    
    locator.click_element("templates/menu_voucher.png")
    time.sleep(0.3)
    locator.click_element("templates/menu_fill_voucher.png")
    time.sleep(1.0)
    
    # 2. 填写凭证头
    coord.input_at(0.12, 0.12, voucher_data["日期"])
    kbd.tab_next()
    kbd.type_with_clipboard(voucher_data["摘要"])
    kbd.tab_next()
    
    # 3. 填写凭证明细行
    for entry in voucher_data["明细"]:
        kbd.type_with_clipboard(entry["科目代码"])
        kbd.tab_next()
        
        if entry.get("借方金额"):
            kbd.type_with_clipboard(str(entry["借方金额"]))
        kbd.tab_next()
        
        if entry.get("贷方金额"):
            kbd.type_with_clipboard(str(entry["贷方金额"]))
        kbd.tab_next(2)
    
    # 4. 保存凭证
    kbd.save()
    
    # 5. 验证保存成功
    time.sleep(1.0)
    x, y, score = locator.find_element("templates/save_success.png")
    
    if x is not None:
        sb.log.info(f"凭证保存成功:{voucher_data['摘要']}")
        # 截图留档
        screenshot = locator.take_screenshot()
        cv2.imwrite(f"voucher_{datetime.now().strftime('%Y%m%d%H%M%S')}.png", 
                   cv2.cvtColor(screenshot, cv2.COLOR_RGB2BGR))
        return True
    else:
        sb.log.error("凭证保存状态未知,需人工确认")
        return False

# 批量录入凭证
vouchers = sb.excel.read_all("待录入凭证.xlsx")

for v in vouchers:
    result = input_accounting_voucher(v)
    if not result:
        sb.log.warning(f"凭证 {v.get('摘要')} 可能需要人工确认")
    time.sleep(2)  # 每条凭证后等待2秒

常见问题与解决方案

问题原因解决方案
点击偏移屏幕分辨率不同使用相对坐标或图像识别
中文输入乱码输入法干扰使用剪贴板粘贴方式
找不到窗口被遮挡/最小化使用win32api强制置前
操作太快软件响应慢增加wait/sleep时间
软件崩溃内存/兼容性添加崩溃检测与重启逻辑

总结

在这里插入图片描述

桌面应用自动化的核心技能:

  1. 窗口管理:win32gui定位、激活、调整窗口
  2. 坐标系统:使用相对坐标避免硬编码
  3. 图像识别:cv2模板匹配,不依赖固定坐标
  4. 键盘模拟:Tab键导航、快捷键操作
  5. 鼠标控制:平滑移动、随机间隔、防检测

作者:林焱 | 转载请注明出处 在这里插入图片描述