UIAutomation
前言
UIAutomation(UI Automation) 是微软提供的一套 自动化测试框架,用于以编程方式访问和操作用户界面(UI)元素(如按钮、文本框、菜单等)。它主要服务于辅助功能(如屏幕阅读器)和自动化测试(如UI测试工具),支持 Win32、WPF、WinForms、UWP 等多种技术构建的应用程序。
核心功能
跨技术支持:统一操作 Win32、WPF、UWP、甚至浏览器(如 Chrome/Firefox 通过插件)。
树形结构访问:通过逻辑树(ControlView)和原始树(RawView)遍历UI元素。
模式(Patterns):提供标准化的交互方式(如点击、输入、滚动),例如:
- InvokePattern(按钮点击)
- ValuePattern(文本框输入)
- TogglePattern(复选框切换)。
属性访问:获取UI元素的名称、类型、状态等(如 Name、ClassName、IsEnabled)。
事件监听:监听UI变化(如窗口弹出、元素属性改变)。
关键信息
assemblies中勾選UIAutomationClient、UIAutomationTypes。
勾选后,需要引入命名空间:using System.Windows.Automation;
核心类:
AutomationElement:代表UI元素(如窗口、按钮等)。
AutomationElementCollection:
TreeScope:
Condition:用于查找元素(如按名称、控件类型)。
Patterns:定义交互行为(如 InvokePattern 触发按钮点击)。
AutomationElement类
作用:
查找元素:通过 FindFirst() / FindAll() 按条件(如 Name、ClassName)定位控件。
读取属性:获取控件的 Name、AutomationId、BoundingRectangle 等属性。
操作控件:使用 InvokePattern(点击)、ValuePattern(输入文本)自动化交互。
等待元素:配合 AutomationWatcher 监控 UI 变化(如动态加载的元素)。
常用属性
Current.Name:控件的显示名称(如按钮文本)。
Current.AutomationId:唯一标识符(类似 x:Name 或 win32控件的 ID)。
Current.ClassName:控件类名(如 \"Button\"、\"Edit\")。
Current.BoundingRectangle:控件的屏幕坐标(Rect 结构)。
Current.ControlType :控件类型(如 ControlType.Button)。
常用方法
FindFirst():查找匹配条件的第一个元素。
public AutomationElement FindFirst(
TreeScope scope, // 搜索范围(子级/后代/元素自身等)
Condition condition // 查找条件(如按名称、控件类型等)
);
例:查找名为 \"OK\" 的按钮
Condition condition = new PropertyCondition( AutomationElement.NameProperty, \"OK\");AutomationElement okButton = parent.FindFirst( TreeScope.Descendants, // 递归搜索所有后代 condition);
FindAll():查找匹配条件的所有元素。
public AutomationElementCollection FindAll(
TreeScope scope, // 搜索范围
Condition condition // 查找条件
);
例:查找所有文本框
Condition condition = new PropertyCondition( AutomationElement.ControlTypeProperty, ControlType.Edit);AutomationElementCollection textBoxes = parent.FindAll( TreeScope.Descendants, condition);
GetCurrentPattern():获取操作控件的模式.。
public object GetCurrentPattern(
AutomationPattern pattern // 模式标识符(如 InvokePattern.Pattern)
);
例:获取按钮的 InvokePattern 并点击
AutomationElement button = ...;InvokePattern invokePattern = button.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;invokePattern.Invoke();
注意:若控件不支持该模式,会抛出 InvalidOperationException。
TryGetCurrentPattern():安全获取模式(避免未支持该模式的控件抛出异常)。
public bool TryGetCurrentPattern(
AutomationPattern pattern, // 模式标识符
out object patternObject // 输出模式对象
);
例:安全获取 ValuePattern(适用于输入框)
AutomationElement textBox = ...;if (textBox.TryGetCurrentPattern( ValuePattern.Pattern, out object patternObj)){ ValuePattern valuePattern = patternObj as ValuePattern; valuePattern.SetValue(\"Text\");}
例:查找窗口
// 查找记事本窗口(按窗口标题)Condition condition = new PropertyCondition( AutomationElement.NameProperty, \"无标题 - 记事本\" // 窗口标题);AutomationElement notepadWindow = AutomationElement.RootElement.FindFirst( TreeScope.Children, condition);
例:查找子控件(如按钮)
// 在记事本窗口中查找\"文件\"菜单Condition menuCondition = new PropertyCondition( AutomationElement.NameProperty, \"文件\");AutomationElement fileMenu = notepadWindow.FindFirst( TreeScope.Descendants, menuCondition);
Condition类
Condition 类是一个基类,用于定义搜索 UI 元素的条件。它与 FindFirst() 和 FindAll() 一起使用,用于筛选符合特定条件的 AutomationElement 对象。
子类:
PropertyCondition:按 UI 元素的某个属性匹配。
按控件类型(ControlTypeProperty)
例:查找所有 Button 控件
Condition condition = new PropertyCondition( AutomationElement.ControlTypeProperty, ControlType.Button);AutomationElementCollection buttons = rootElement.FindAll( TreeScope.Children, condition);
按名称(NameProperty)
例:查找名称为Submit的按钮
Condition condition = new PropertyCondition( AutomationElement.NameProperty, \"Submit\");AutomationElement submitButton = rootElement.FindFirst( TreeScope.Descendants, condition);
按自动化 ID(AutomationIdProperty)
例:查找 AutomationId 为 \"txtUsername\" 的文本框(适用于唯一标识元素)
Condition condition = new PropertyCondition( AutomationElement.AutomationIdProperty, \"txtUsername\");AutomationElement usernameBox = rootElement.FindFirst( TreeScope.Descendants, condition);
按控件是否可交互(AutomationElement.IsEnabledProperty)
控件是否在屏幕上可见(AutomationElement.IsOffscreenProperty)
AndCondition:逻辑 AND,同时满足多个条件。
例:查找 所有 CheckBox 且 Name 包含 \"Agree\" 的元素
Condition controlTypeCondition = new PropertyCondition( AutomationElement.ControlTypeProperty, ControlType.CheckBox);Condition nameCondition = new PropertyCondition( AutomationElement.NameProperty, \"Agree\");AndCondition combinedCondition = new AndCondition(controlTypeCondition, nameCondition);AutomationElementCollection checkBoxes = rootElement.FindAll( TreeScope.Descendants, combinedCondition);
OrCondition:逻辑 OR,满足任意一个条件。
NotCondition:逻辑 NOT,排除满足条件的元素。
TrueCondition:匹配任何 UI 元素(相当于 * 通配符)。
例:查找当前窗口下的所有 UI 元素
AutomationElement rootElement = AutomationElement.RootElement;// 使用 TrueCondition(匹配所有元素)Condition condition = Condition.TrueCondition; // 或 new TrueCondition()// 查找所有子元素(包括嵌套元素)AutomationElementCollection allElements = rootElement.FindAll( TreeScope.Descendants, condition);// 遍历结果foreach (AutomationElement element in allElements){ Console.WriteLine($\"Element: {element.Current.Name} | ControlType: {element.Current.ControlType}\");}
FalseCondition:不匹配任何 UI 元素。
TreeScope
TreeScope 是一个枚举类型,用于指定在 UI 元素树(UI Tree)中的搜索范围,即在调用 FindFirst 或 FindAll 方法时,要查找哪些层级的元素。
搜索模式
Element:仅搜索当前元素本身(不包含子元素)。
Children:仅搜索当前元素的直接子元素(不递归)。
Descendants:递归搜索当前元素的所有后代元素(包括子元素的子元素)。
Subtree:当前元素 + 所有后代(相当于 `Element + Descendants`)。
Patterns
Patterns(模式) 代表 UI 元素的交互方式。不同的控件(如按钮、复选框、滑块、下拉菜单)支持不同的交互模式,例如 Invoke(执行操作)、Selection(选择项目)、或 Value(修改值)。通过 Patterns,自动化测试可以 模拟用户操作,如点击按钮、输入文本、选中选项等。
常见的 UIA Patterns
PATTERN 名称
功能描述
典型控件
Invoke
执行默认操作(如点击按钮)。
Button、Hyperlink
Selection
管理选中项(单/多选)。
ListBox、RadioButton
ExpandCollapse
展开/折叠(如树形菜单)。
TreeView、ComboBox
Value
设置/获取值(如文本框)。
TextBox、SpinBox
Toggle
开关状态(如复选框)。
CheckBox、Switch
Scroll
滚动容器(如滚动条)。
ScrollViewer、ListBox
Window
管理窗口(最小化/最大化)。
Window、Dialog
Text
获取或操作文本内容。
RichTextBox、Label
RangeValue
调节数值范围(如滑块)。
Slider、ProgressBar
例:点击按钮(InvokePattern)
// 查找按钮(示例:计算器中的“5”按钮)AutomationElement button = AutomationElement.RootElement .FindFirst(TreeScope.Descendants, new PropertyCondition( AutomationElement.NameProperty, \"5\" // 按钮名称 ));// 获取 InvokePattern(如按钮可点击)if (button.TryGetCurrentPattern(InvokePattern.Pattern, out object patternObj)){ InvokePattern invokePattern = (InvokePattern)patternObj; invokePattern.Invoke(); // 模拟点击 Console.WriteLine(\"Button clicked!\");}
例:操控复选框(TogglePattern)
// 查找复选框AutomationElement checkbox = AutomationElement.RootElement .FindFirst(TreeScope.Descendants, new PropertyCondition( AutomationElement.ControlTypeProperty, ControlType.CheckBox ));// 获取 TogglePatternif (checkbox.TryGetCurrentPattern(TogglePattern.Pattern, out object toggleObj)){ TogglePattern togglePattern = (TogglePattern)toggleObj; Console.WriteLine($\"Current state: {togglePattern.Current.ToggleState}\"); togglePattern.Toggle(); // 切换状态(On ↔ Off) Console.WriteLine($\"New state: {togglePattern.Current.ToggleState}\");}
例:修改文本框内容(ValuePattern)
// 查找文本框AutomationElement textBox = AutomationElement.RootElement .FindFirst(TreeScope.Descendants, new PropertyCondition( AutomationElement.ControlTypeProperty, ControlType.Edit ));// 获取 ValuePatternif (textBox.TryGetCurrentPattern(ValuePattern.Pattern, out object valueObj)){ ValuePattern valuePattern = (ValuePattern)valueObj; valuePattern.SetValue(\"Hello, UIA!\"); // 设置文本 Console.WriteLine($\"Text set to: {valuePattern.Current.Value}\");}
接口:
static AutomationElement GetElementByName(AutomationElement parent,string strName, int iAddTry = 0, bool bMsg = true) { AutomationElement child = null; child = parent.FindFirst( TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, strName)); int iRetry = 1; int iRetryEnd = RETRY + iAddTry; while (iRetry++ < iRetryEnd && child == null) { WriteLog(logFilePath, string.Format(\"retry found Name: \'{0}\' ,number: {1}.\", strName, iRetry)); child = parent.FindFirst( TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, strName)); if (child != null) { break; } Thread.Sleep(SLEEPTIME * iRetry); //The last time if (iRetry == iRetryEnd && bMsg) { var customMsgBox = new SiviewMessageBox(\"获取控件超时\", \"Warning\"); DialogResult result = customMsgBox.ShowDialog(); if (result == DialogResult.OK) { iRetry = 1; } } } return child; }
static AutomationElement GetElementByClassName(AutomationElement parent, string strClassName, int iAddTry = 0, bool bMsg = true) { AutomationElement child = null; child = parent.FindFirst( TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, strClassName)); int iRetry = 1; int iRetryEnd = RETRY + iAddTry; while (iRetry++ < iRetryEnd && child == null) { WriteLog(logFilePath, string.Format(\"retry found ClassName: \'{0}\' ,number: {1}.\", strClassName, iRetry)); child = parent.FindFirst( TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, strClassName)); if (child != null) { break; } Thread.Sleep(SLEEPTIME * iRetry); //The last time if (iRetry == iRetryEnd && bMsg) { var customMsgBox = new SiviewMessageBox(\"获取控件超时\", \"Warning\"); DialogResult result = customMsgBox.ShowDialog(); if (result == DialogResult.OK) { iRetry = 1; } } } return child; }
static async Task InvokeButtonAsync(AutomationElement btn) { await Task.Run(() => { InvokePattern invoke = btn.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern; if (invoke != null) { invoke.Invoke(); } }); }
static bool BtnAction(AutomationElement btn) { if (btn == null) { return false; } ControlType conType = btn.Current.ControlType; if (conType == ControlType.RadioButton || conType == ControlType.DataItem || conType==ControlType.TabItem) { SelectionItemPattern sele = btn.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern; if (sele != null) { sele.Select(); return true; } return false; } else if (conType == ControlType.Button || conType == ControlType.MenuItem) { InvokePattern invoke = btn.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern; if (invoke != null) { invoke.Invoke(); /*有些控件不能使用invoke.Invoke()即使他是InvokePattern Task.Run(() => InvokeButtonAsync(btn)); Thread.Sleep(iMS);*/ return true; } return false; } return false; }
static bool LineAction(AutomationElement line, string text) { if (line == null) { return false; } ControlType conType = line.Current.ControlType; if (conType == ControlType.Edit || conType == ControlType.ComboBox) { ValuePattern value = line.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern; if (value != null) { value.SetValue(text); return true; } return false; } else if (conType == ControlType.Document) { line.SetFocus(); if (text == \"\") return true; //SendKeys.SendWait(text); //会自动切换成中文输入法,导致输入不全 Clipboard.SetText(text); SendKeys.SendWait(\"^v\"); return true; } else if (conType == ControlType.Text) { if (line.Current.Name == \"Equipment ID\") { if (text == \"\") return true; //line.SetFocus(); //Call failure, 可能是父窗口下只有一个Line(可以放置焦点的元素),默认打开父窗口,焦点就会在这个元素上,此时调用SetFocus就会error //SendKeys.SendWait(text); //会自动切换成中文输入法,导致输入不全 Clipboard.SetText(text); SendKeys.SendWait(\"^v\"); } else { line.SetFocus(); if (text == \"\") return true; //SendKeys.SendWait(text); //会自动切换成中文输入法,导致输入不全 Clipboard.SetText(text); SendKeys.SendWait(\"^v\"); } return true; } return false; }