折腾笔记[33]-使用uiautomation自动重复读取图片(被控程序为.net框架)
摘要
基于python使用uiautomation自动操作.net程序.读取目录中png以及查找与其对应的json数据输入软件和点击按钮.
实现
1. 简化版的被控程序(靶点窗口)
配置文件:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="TZ.HMI.Common.UI" Version="1.0.0.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="处理中.png">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup />
  <ItemGroup>
    <ApplicationDefinition Update="App.xaml">
      <SubType>Designer</SubType>
    </ApplicationDefinition>
  </ItemGroup>
  <ItemGroup>
    <Compile Update="JsonImportWindow.xaml.cs">
      <SubType>Code</SubType>
    </Compile>
  </ItemGroup>
  <ItemGroup>
    <Page Update="JsonImportWindow.xaml">
      <SubType>Designer</SubType>
    </Page>
    <Page Update="MainWindow.xaml">
      <SubType>Designer</SubType>
    </Page>
  </ItemGroup>
</Project>
界面:
<Window x:Class="MockWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="OCR自动核对" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <!-- 顶部按钮区域 -->
        <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="10" HorizontalAlignment="Left">
            <Button Name="btnLoadImage" Content="读取图片" Width="100" Height="30" Margin="5"/>
            <Button Name="btnOpenFolder" Content="打开文件夹" Width="100" Height="30" Margin="5"/>
            <Button Name="btnPrevImage" Content="上一张" Width="100" Height="30" Margin="5"/>
            <Button Name="btnNextImage" Content="下一张" Width="100" Height="30" Margin="5"/>
            <Button Name="btnDebug" Content="调试" Width="100" Height="30" Margin="5"/>
            <Button Name="btnMesImport" Content="Mes导入" Width="100" Height="30" Margin="5"/>
        </StackPanel>
        <!-- 图片显示区域 -->
        <Image Name="imgDisplay" Grid.Row="1" Margin="10" Stretch="Uniform"/>
    </Grid>
</Window>
<Window x:Class="MockWpf.JsonImportWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Json导入" Height="300" Width="500"
        WindowStartupLocation="CenterOwner">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBox x:Name="txtJson"
                 Grid.Row="0"
                 TextWrapping="Wrap"
                 AcceptsReturn="True"
                 ScrollViewer.HorizontalScrollBarVisibility="Auto"
                 ScrollViewer.VerticalScrollBarVisibility="Auto"
                 FontFamily="Consolas"/>
        <Button x:Name="btnOk"
                Grid.Row="1"
                Content="确认"
                Width="80" Height="28"
                HorizontalAlignment="Right"
                Margin="0,10,0,0"
                Click="BtnOk_Click"/>
    </Grid>
</Window>
代码:
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
namespace MockWpf
{
	public partial class MainWindow : Window
	{
		private List<string> imageFiles = new List<string>();
		private int currentIndex = -1;
		public MainWindow()
		{
			InitializeComponent();
			btnOpenFolder.Click += BtnOpenFolder_Click;
			btnLoadImage.Click += BtnLoadImage_Click;
			btnPrevImage.Click += BtnPrevImage_Click;
			btnNextImage.Click += BtnNextImage_Click;
			btnDebug.Click += BtnDebug_Click;
			btnMesImport.Click += BtnMesImport_Click;
		}
		private void BtnMesImport_Click(object sender, RoutedEventArgs e)
		{
			var w = new JsonImportWindow { Owner = this };
			if (w.ShowDialog() == true)          // 用户点了“确认”
			{
				string json = w.JsonText;
				// TODO: 在这里写真正的 Json 解析/导入逻辑
				// MessageBox.Show($"已获取到 Json 内容(长度 {json.Length})\n{json}");
			}
		}
		private void BtnDebug_Click(object sender, RoutedEventArgs e)
		{
			// 新窗口
			var win = new Window
			{
				Owner = this,
				Width = 600,
				Height = 400,
				WindowStartupLocation = WindowStartupLocation.CenterOwner,
				WindowStyle = WindowStyle.ToolWindow,
				ResizeMode = ResizeMode.NoResize,
				Title = "提示"
			};
			// 图片控件
			var img = new System.Windows.Controls.Image
			{
				Stretch = System.Windows.Media.Stretch.Uniform
			};
			try
			{
				img.Source = new BitmapImage(new Uri("./处理中.png", UriKind.Relative));
			}
			catch { /* 图片不存在则留空 */ }
			win.Content = img;
			win.Show();
			// 3 秒后自动关闭
			var timer = new System.Windows.Threading.DispatcherTimer
			{
				Interval = TimeSpan.FromSeconds(3)
			};
			timer.Tick += (_, __) =>
			{
				timer.Stop();
				win.Close();
			};
			timer.Start();
		}
		private void BtnOpenFolder_Click(object sender, RoutedEventArgs e)
		{
			var dialog = new System.Windows.Forms.FolderBrowserDialog();
			if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
			{
				var folderPath = dialog.SelectedPath;
				imageFiles = new List<string>(Directory.GetFiles(folderPath, "*.jpg"));
				imageFiles.AddRange(Directory.GetFiles(folderPath, "*.png"));
				imageFiles.AddRange(Directory.GetFiles(folderPath, "*.bmp"));
				if (imageFiles.Count > 0)
				{
					currentIndex = 0;
					LoadImage(imageFiles[currentIndex]);
				}
				else
				{
					MessageBox.Show("未找到图片文件");
				}
			}
		}
		private void BtnLoadImage_Click(object sender, RoutedEventArgs e)
		{
			OpenFileDialog openFileDialog = new OpenFileDialog
			{
				Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp",
				Title = "选择图片"
			};
			if (openFileDialog.ShowDialog() == true)
			{
				LoadImage(openFileDialog.FileName);
			}
		}
		private void BtnPrevImage_Click(object sender, RoutedEventArgs e)
		{
			if (imageFiles.Count == 0) return;
			currentIndex = (currentIndex - 1 + imageFiles.Count) % imageFiles.Count;
			LoadImage(imageFiles[currentIndex]);
		}
		private void BtnNextImage_Click(object sender, RoutedEventArgs e)
		{
			if (imageFiles.Count == 0) return;
			currentIndex = (currentIndex + 1) % imageFiles.Count;
			LoadImage(imageFiles[currentIndex]);
		}
		private void LoadImage(string path)
		{
			try
			{
				var bitmap = new BitmapImage();
				bitmap.BeginInit();
				bitmap.UriSource = new Uri(path);
				bitmap.CacheOption = BitmapCacheOption.OnLoad;
				bitmap.EndInit();
				imgDisplay.Source = bitmap;
			}
			catch (Exception ex)
			{
				MessageBox.Show($"加载图片失败: {ex.Message}");
			}
		}
	}
}
using System;
using System.Windows;
namespace MockWpf
{
	public partial class JsonImportWindow : Window
	{
		public string JsonText => txtJson.Text;
		public JsonImportWindow()
		{
			InitializeComponent();
		}
		// 点击“确认”后把对话框结果设为 OK,主窗口就能取到 JsonText
		private void BtnOk_Click(object sender, RoutedEventArgs e)
		{
			DialogResult = true;
			// 关闭窗口
			Close();
		}
	}
}
2. python脚本
pyproject.toml
[project]
name = "exp18-pyloid-pyautogui"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "comtypes>=1.4.12",
    "onnxruntime>=1.22.1",
    "pillow>=11.3.0",
    "psutil>=7.0.0",
    "pyautogui>=0.9.54",
    "pyinstaller>=6.15.0",
    "pyloid>=0.24.8",
    "pyperclip>=1.9.0",
    "pyscreeze>=1.0.1",
    "pywin32>=311",
    "rapidocr>=3.4.0",
    "uiautomation>=2.0.29",
]
脚本:
# -*- coding: utf-8 -*-
import uiautomation as auto
import win32api, win32con, win32gui, win32console, time, sys, os
import datetime
import pyperclip
CANDIDATE_TITLES = ['靶点窗口', 'OCR自动核对']
TASKS            = ['Mes导入']
DEFAULT_IMG_DIR  = r'D:\AppTest\测试工作空间'
# ---------------- 日志 ----------------
class _RealTimeTee:
    def __init__(self, log_path: str):
        # 行缓冲,保证实时落盘
        self.file = open(log_path, 'a', encoding='utf-8', buffering=1)
        # 防御:无控制台时退化为只写文件
        self.console = sys.__stdout__ if sys.__stdout__ is not None else None
        self._write_header(log_path)
    def _write_header(self, log_path: str):
        head = f'日志文件:{os.path.abspath(log_path)}\n'
        self.file.write(head)
        if self.console:
            self.console.write(head)
    def write(self, data: str):
        self.file.write(data)
        if self.console:
            self.console.write(data)
        self.flush()
    def flush(self):
        self.file.flush()
        if self.console:
            self.console.flush()
    def close(self):
        self.flush()
        self.file.close()
# --------------------------------------------------
# 日志:仅提供 printLog 函数,不替换 sys.stdout
# --------------------------------------------------
_log_file = None          # 全局日志句柄
def init_log(log_path: str):
    """初始化日志文件,写入头信息"""
    global _log_file
    _log_file = open(log_path, 'a', encoding='utf-8', buffering=1)
    header = f'日志文件:{os.path.abspath(log_path)}\n'
    printLog(header, end='')          # 屏幕
    _log_file.write(header)        # 文件
def printLog(*args, **kwargs):
    """同时打印到屏幕并写入日志"""
    if _log_file is None:
        raise RuntimeError('请先调用 init_log() 初始化日志')
    # 构造输出字符串
    sep  = kwargs.get('sep', ' ')
    end  = kwargs.get('end', '\n')
    msg  = sep.join(map(str, args)) + end
    print(msg, end='')             # 屏幕
    _log_file.write(msg)           # 文件
    _log_file.flush()
def close_log():
    """程序结束前关闭日志句柄"""
    global _log_file
    if _log_file:
        _log_file.close()
        _log_file = None
# ---------------- 控制台工具 ----------------
def alloc_console():
    if win32console.GetConsoleWindow() == 0:
        win32console.AllocConsole()
        sys.stdout = open('CONOUT$', 'w', encoding='utf-8')
        sys.stdin  = open('CONIN$',  'r', encoding='utf-8')
        sys.stderr = sys.stdout
def show_console(visible=True, topmost=False):
    hwnd = win32console.GetConsoleWindow()
    if not hwnd:
        return
    win32gui.ShowWindow(hwnd,
                        win32con.SW_SHOW if visible else win32con.SW_MINIMIZE)
    if topmost:
        win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST,
                              0, 0, 0, 0,
                              win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
# ---------------- 鼠标/高亮 ----------------
def highlight_rect(rect, sec=3):
    left, top, right, bottom = map(int, (rect.left, rect.top,
                                         rect.right, rect.bottom))
    hdc = win32gui.GetDC(0)
    pen = win32gui.CreatePen(win32con.PS_SOLID, 10, win32api.RGB(255, 0, 0))
    old_pen = win32gui.SelectObject(hdc, pen)
    win32gui.SelectObject(hdc, win32gui.GetStockObject(win32con.NULL_BRUSH))
    for _ in range(int(sec * 5)):
        win32gui.Rectangle(hdc, left, top, right, bottom)
        time.sleep(0.1)
    win32gui.SelectObject(hdc, old_pen)
    win32gui.ReleaseDC(0, hdc)
def real_click(rect):
    x = int((rect.left + rect.right) / 2)
    y = int((rect.top + rect.bottom) / 2)
    win32api.SetCursorPos((x, y))
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0)
    time.sleep(0.02)
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0)
def deep_click(ctrl):
    rect = ctrl.BoundingRectangle
    highlight_rect(rect, 1)
    sub = ctrl.GetFirstChildControl()
    while sub:
        if sub.ControlType in [auto.ButtonControl, auto.TextControl,
                               auto.CustomControl] \
           and abs(sub.BoundingRectangle.left - rect.left) <= 5 \
           and abs(sub.BoundingRectangle.top  - rect.top)  <= 5:
            printLog('  将点击子控件:', sub.Name)
            rect = sub.BoundingRectangle
            break
        sub = sub.GetNextSiblingControl()
    real_click(rect)
# ---------------- 业务逻辑 ----------------
def find_window():
    for ttl in CANDIDATE_TITLES:
        w = auto.WindowControl(searchDepth=1, Name=ttl)
        if w.Exists(0.5):
            printLog('已找到窗口:', ttl)
            return w
    return None
def click_button(win, name):
    for tp in ['ButtonControl', 'CustomControl', 'TextControl']:
        btn = getattr(win, tp)(Name=name)
        if btn.Exists():
            printLog('找到按钮:', name)
            deep_click(btn)
            return True
    printLog('未找到按钮:', name)
    return False
def wait_disappear(ctrl: auto.Control, timeout: int = 10):
    for _ in range(int(timeout * 10)):
        if not ctrl.Exists(0.05):
            return
        time.sleep(0.1)
    raise RuntimeError(f'等待 {ctrl.Name} 消失超时')
# ---------------- 单次完整流程 ----------------
def single_flow(win: auto.WindowControl, png_path: str, json_path: str) -> float:
    import win32gui as wg
    import win32con as wc
    import win32api as wa
    t0 = time.perf_counter()
    img_name = os.path.basename(png_path)
    # 1. 读取图片(键盘版,原逻辑不变)
    t1 = time.perf_counter()
    if not click_button(win, '读取图片'):
        raise RuntimeError('未找到【读取图片】按钮')
    def _find_open_dialog():
        lst = []
        def enum_cb(hwnd, _):
            if wg.GetClassName(hwnd) == '#32770' and wg.IsWindowVisible(hwnd):
                lst.append(hwnd)
            return True
        wg.EnumWindows(enum_cb, None)
        return lst[0] if lst else None
    for _ in range(30):
        h_dlg = _find_open_dialog()
        if h_dlg:
            break
        time.sleep(0.1)
    else:
        raise RuntimeError('等待文件选择器超时')
    if wg.IsIconic(h_dlg):
        wg.ShowWindow(h_dlg, wc.SW_RESTORE)
    wg.SetForegroundWindow(h_dlg)
    # Alt+N
    wa.keybd_event(wc.VK_MENU, 0, 0, 0); time.sleep(0.02)
    wa.keybd_event(0x4E, 0, 0, 0);       time.sleep(0.02)
    wa.keybd_event(0x4E, 0, wc.KEYEVENTF_KEYUP, 0); time.sleep(0.02)
    wa.keybd_event(wc.VK_MENU, 0, wc.KEYEVENTF_KEYUP, 0); time.sleep(0.2)
    # Ctrl+A 并输入路径
    wa.keybd_event(wc.VK_CONTROL, 0, 0, 0); time.sleep(0.02)
    wa.keybd_event(0x41, 0, 0, 0);          time.sleep(0.02)
    wa.keybd_event(0x41, 0, wc.KEYEVENTF_KEYUP, 0); time.sleep(0.02)
    wa.keybd_event(wc.VK_CONTROL, 0, wc.KEYEVENTF_KEYUP, 0); time.sleep(0.1)
    auto.SendKeys(png_path); time.sleep(0.1)
    # Alt+O
    wa.keybd_event(wc.VK_MENU, 0, 0, 0); time.sleep(0.02)
    wa.keybd_event(0x4F, 0, 0, 0);       time.sleep(0.02)
    wa.keybd_event(0x4F, 0, wc.KEYEVENTF_KEYUP, 0); time.sleep(0.02)
    wa.keybd_event(wc.VK_MENU, 0, wc.KEYEVENTF_KEYUP, 0)
    for _ in range(50):
        if not wg.IsWindow(h_dlg):
            break
        time.sleep(0.1)
    else:
        raise RuntimeError('文件对话框仍未关闭')
    t1 = time.perf_counter() - t1
    # 2. Mes 导入 —— 采用“参考代码”剪贴板方案
    t2 = time.perf_counter()
    if not click_button(win, 'Mes导入'):
        raise RuntimeError('未找到【Mes导入】按钮')
    # 等待 Json 导入窗口
    json_dlg = auto.WindowControl(searchDepth=2, Name='Json导入')
    if not json_dlg.Exists(3):
        raise RuntimeError('等待 Json导入 窗口超时')
    # 读取 json 内容
    try:
        with open(json_path, 'r', encoding='utf-8') as f:
            json_txt = f.read()
        json_ok = True
    except Exception as e:
        json_txt = ''
        json_ok = False
        printLog('[警告] 读取 json 失败:', e)
    # 找到编辑框并聚焦
    edit_j = json_dlg.EditControl(foundIndex=1)
    if not edit_j.Exists():
        # 某些版本用 Custom 包了一层
        edit_j = json_dlg.CustomControl(foundIndex=1).EditControl(foundIndex=1)
    edit_j.SetFocus()
    # 参考代码做法:全选 + 剪贴板写入 + Ctrl+V
    auto.SendKeys('{Ctrl}A'); time.sleep(0.05)
    pyperclip.copy(json_txt); time.sleep(0.05)
    auto.SendKeys('{Ctrl}V'); time.sleep(0.1)
    # 确定按钮
    ok_btn = json_dlg.ButtonControl(Name='确认')
    if ok_btn.Exists():
        deep_click(ok_btn)
    else:
        json_dlg.SendKeys('{Enter}')
    wait_disappear(json_dlg, 5)
    t2 = time.perf_counter() - t2
    # 3. 调试(原逻辑不变)
    t3 = time.perf_counter()
    if not click_button(win, '调试'):
        raise RuntimeError('未找到【调试】按钮')
    t3 = time.perf_counter() - t3
    # 4. 等待提示窗(原逻辑不变)
    t4 = time.perf_counter()
    tip = auto.WindowControl(searchDepth=2, SubName='提示')
    if tip.Exists(1):
        wait_disappear(tip, 15)
    t4 = time.perf_counter() - t4
    total = time.perf_counter() - t0
    printLog(f'[{img_name}]  读取图片:{t1:.2f}s  '
          f'Mes导入:{t2:.2f}s  调试:{t3:.2f}s  等待提示:{t4:.2f}s  '
          f'json成功:{json_ok}  总耗时:{total:.2f}s')
    return total
# ---------------- 主流程 ----------------
def main():
    alloc_console()
    show_console(visible=True, topmost=True)
    # ---- 生成唯一日志文件名 ----
    log_name = datetime.datetime.now().strftime(r'自动测试_%Y%m%d_%H%M%S.log')
    init_log(log_name)          # 初始化日志
    # 询问图片文件夹
    img_dir = input(f'请输入图片文件夹路径(直接回车使用默认值):\n{DEFAULT_IMG_DIR}\n> ').strip()
    if not img_dir:
        img_dir = DEFAULT_IMG_DIR
    printLog('使用文件夹:', img_dir)
    tasks = []
    for fn in sorted(os.listdir(img_dir)):
        if fn.lower().endswith('.png'):
            json_fn = fn[:-4] + '.json'
            json_path = os.path.join(img_dir, json_fn)
            if os.path.isfile(json_path):
                tasks.append((os.path.join(img_dir, fn), json_path))
            else:
                printLog(f'[跳过] {fn} 未找到同名 json')
    if not tasks:
        printLog('文件夹内没有可执行的 png/json 对!')
        show_console(visible=True, topmost=True)
        input('按回车键退出…')
        return
    total_cnt = len(tasks)
    printLog(f'\n共发现 {total_cnt} 组待执行任务,即将开始自动化…\n')
    show_console(visible=False)
    win = find_window()
    if not win:
        printLog('未找到任何候选窗口:', CANDIDATE_TITLES)
        show_console(visible=True, topmost=True)
        input('按回车键退出…')
        return
    done = 0
    sum_time = 0.0
    try:
        for png, json_ in tasks:
            done += 1
            cost = single_flow(win, png, json_)
            sum_time += cost
            printLog(f'[总进度] 已执行 {done}/{total_cnt} 张  累计耗时 {sum_time:.2f} s')
    except Exception as e:
        printLog('\n[异常] 流程中断:', e)
    finally:
        show_console(visible=True, topmost=True)
        printLog(f'\n全部任务执行完毕! 成功 {done}/{total_cnt} 张  总耗时 {sum_time:.2f} s')
        input('按回车键退出…')
if __name__ == '__main__':
    main()
打包:
uv run pyinstaller -F -w auto_test.py
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号