近期玩了下 UIAutomation。C# 中有 UI Automation 庫,C++可以看msdn的 Accessibility。這兩個東西網上能找到的東西太少了,只能自己看微軟的官方文檔。我把我的一些代碼段貼到下面,希望能幫助需要的人。Python有個庫 UIAutomation,就是封裝微軟提供的 UIAutomation,網上有一些 資料。但是這個庫的作者說沒有準備文檔,所以需要的人自己看Demo去猜函數該怎麼用吧。我用的時候結合了另一個庫 pyautogui,用於操作鍵盤、鼠標。
Python 的 UI Automation 這個庫怎麼用可以直接看 uiautomation.py 這個文件,其中 class Control() 這個在5156行。在尋找控件的時候可先用命令行工具 automation.py -t3 去查找,具體參考 這個網頁;關於找到元素的代碼參考 這個。在遇到問題時可以參考 C# 的代碼。比如從 EditControl 獲取內容:
var clickPattern = (TextPattern)textedit.GetCurrentPattern(TextPattern.Pattern);
Console.WriteLine(clickPattern.DocumentRange.GetText(100));
Console.WriteLine(clickPattern.DocumentRange.GetText(100));
相應的 python 代碼:
EditControl.GetPattern(PatternId.TextPattern).DocumentRange.GetText(100)
傳統的Win32程序有句柄這個東西,每個控件的句柄很容易獲取。WPF 程序的句柄就一個,是主窗口的,裏邊的控件是框架渲染的,所以 spy++ 無能爲力了。UIAutomation是向 WPF 窗體發消息,如果窗口不處理這個消息,那也是獲取不到控件的,比如QQ輕聊版。處理消息的是 LresultFromObject 函數。引用 大牛 的話:
UIAutomation的工作原理是: 當你用UIAutomation操作程序時,UIAutomation會給程序發送WM_GETOBJECT消息, 如果程序處理WM_GETOBJECT消息,實現UI Automation Provider,並調用函數 UiaReturnRawElementProvider(HWND hwnd,WPARAM wparam,LPARAM lparam,IRawElementProviderSimple *el), 此程序就支持UIAutomation。
IRawElementProviderSimple就是UI Automation Provider,包含了控件的各種信息,如Name,ClassName,ContorlType,座標... UIAutomation根據程序返回的IRawElementProviderSimple,就能遍歷程序的控件,得到控件各種屬性,進行自動化操作。
所以如果你發現UIAutomation不能識別一些程序內的控件或部分不支持,這並不是UIAutomation的問題, 是程序作者沒有處理WM_GETOBJECT或沒有實現UIAutomation Provider,或者故意不想支持UIAutomation。 很多DirectUI程序都沒有實現UIAutomation Provider,所以不支持自動化,要想支持自動化,必須程序作者修改源碼支持。
和 spy++ 類似的有個 snoop,在 GitHub 上開源的,可以看 WPF 窗體的控件樹。自動化測試的商業軟件有 Ranorex,可以試用30天。他錄製的過程生成了一個解決方案,這個解決方案可以直接在 VS 中打開進行更改。還有些 FlaUI,UISpy.exe,QAliber,White.NUnit IL ,Inspector,ildasm,ilspy 我就沒嘗試了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Threading;
using System.Windows.Automation.Provider;
using System.Windows.Automation.Text;
using System.Windows.Automation;
using System.Windows.Forms;
using System.Runtime.InteropServices; // DllImport() 需要它。
namespace UITest
{
/// <summary>
/// MainWindow.xaml 的交互邏輯
/// </summary>
public partial class MainWindow : Window
{
[DllImport("USER32.DLL", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName,string lpWindowName);
// Activate an application window.
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
public MainWindow()
{
InitializeComponent();
try
{
Console.WriteLine("\nBegin WinForm UIAutomation test run\n");
// launch Form1 application
// get refernce to main Form control
// get references to user controls
// manipulate application
// check resulting state and determine pass/fail
Console.WriteLine("\nBegin WinForm UIAutomation test run\n");
Console.WriteLine("Launching WinFormTest application");
//啓動被測試的程序
Process p = Process.Start(@"D:\Program Files (x86)\{exe文件路徑}");
//自動化根元素.桌面的根。
AutomationElement aeDeskTop = AutomationElement.RootElement;
Thread.Sleep(2000);
// AutomationElement aeForm = AutomationElement.FromHandle(p.MainWindowHandle);
AutomationElement aeForm = null;
//獲得對主窗體對象的引用,該對象實際上就是 Form1 應用程序(方法一)
//if (null == aeForm)
//{
// Console.WriteLine("Can not find the WinFormTest from.");
//}
//獲得對主窗體對象的引用,該對象實際上就是 Form1 應用程序(方法二)
//有些exe啓動另外一個exe自己就退出了,所以要自己再找一遍。
int numWaits = 0;
do
{
Console.WriteLine("Looking for WinFormTest……");
//查找第一個自動化元素
aeForm = aeDeskTop.FindFirst(TreeScope.Children, new PropertyCondition(
AutomationElement.NameProperty, "{窗口名稱}"));
++numWaits;
Thread.Sleep(100);
} while (null == aeForm && numWaits < 50);
if (null == aeForm)
throw new NullReferenceException("Failed to find WinFormTest.");
else
Console.WriteLine("Found it!");
Console.WriteLine("Finding all user controls");
//找到第一次出現的Button控件
// AutomationElement aeButton = aeForm.FindFirst(TreeScope.Children,
// new PropertyCondition(AutomationElement.NameProperty, "Button"));
// 根據Button控件Content屬性精確查找。
//找到所有的TextBox控件
//AutomationElementCollection aeAllTextBoxes = aeForm.FindAll(TreeScope.Children,
// new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
// 控件初始化的順序是先初始化後添加到控件
// this.Controls.Add(this.textBox3);
// this.Controls.Add(this.textBox2);
// this.Controls.Add(this.textBox1);
//AutomationElement aeTextBox1 = aeAllTextBoxes[2];
//AutomationElement aeTextBox2 = aeAllTextBoxes[0];
//AutomationElement aeTextBox3 = aeAllTextBoxes[1];
// 這個順序與你編程的時候往面板上拖控件的順序有關。
Thread.Sleep(3000);
AutomationElementCollection aeRadioButtons = aeForm.FindAll(TreeScope.Descendants,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.RadioButton));
Console.WriteLine(aeRadioButtons[10].Current.Name); // 這一句必須要有,否則aeRadioButtons就是個空的,奇怪!
IntPtr hWnd =FindWindow(null,"{窗口名}");
//for (int i = 0; i < 100; i++) {
// Console.WriteLine(aeWhats[i].GetCurrentPropertyValue(AutomationElement.CultureProperty) as string);
// Console.WriteLine(i.ToString()); // 只發現了一個。
//}
for (int i = 0; i < aeRadioButtons.Count; i++)
{
Console.WriteLine("按鈕名字:");
Console.WriteLine(aeRadioButtons[i].Current.Name);
if (aeRadioButtons[i].Current.Name == "{按鈕名字}")
{
Console.WriteLine("找到RadioButton");
// 這裏的RadioButton不是用來選的,是用來點擊的。文檔說RadioButon僅支持SelectionItemPattern,不支持點擊。
// SelectionItemPattern selectionItemPattern = aeRadioButtons[i].GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern;
// selectionItemPattern.Select();
Thread.Sleep(100);
SetForegroundWindow(hWnd);
aeRadioButtons[i].SetFocus();
Console.WriteLine("已設置焦點");
// 這個不行,原因 https://stackoverflow.com/questions/2958561/when-using-sendkeys-invalidoperationexception-undo-operation-encountered
//System.Windows.Forms.SendKeys.Send("{ENTER}");
// System.Windows.Forms.Control.Invoke(Delegate method);
Console.WriteLine("Select後");
}
}
// 此處需要刷新控件樹,界面已刷新。
//refresh TreeScope
// 窗口刷新以後應該按新窗口對待。本來最開始的啓動程序後獲得的句柄也沒什麼用。
Console.WriteLine("開始轉換界面");
Thread.Sleep(30*1000);
AutomationElementCollection aeTextBlocks = aeForm.FindAll(TreeScope.Descendants,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Document));
Console.WriteLine("嘗試輸出一個Label");
Console.WriteLine(aeTextBlocks[0].Current.LabeledBy); // 這一句必須要有,否則aeRadioButtons就是個空的,奇怪!
for (int i = 0; i < aeTextBlocks.Count; i++)
{
Console.WriteLine("文本框內容:");
Console.WriteLine(aeRadioButtons[i].Current.Name);
}
// TreeWalker walker = new TreeWalker(condition3);
//AutomationElement elementNode = walker.GetFirstChild(aeWhats[0]);
//AutomationElementCollection aeRadioButtons = aeForm.FindAll(TreeScope.Children,
// new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.RadioButton));
//for(int i=0;i<100;i++)
// Console.WriteLine(aeRadioButtons[0].GetCurrentPropertyValue(AutomationElement.CultureProperty) as string);
//-----------------------
//Console.WriteLine("Settiing input to '30'");
////通過ValuePattern設置TextBox1的值
//ValuePattern vpTextBox1 = (ValuePattern)aeTextBox1.GetCurrentPattern(ValuePattern.Pattern);
//vpTextBox1.SetValue("30");
//Console.WriteLine("Settiing input to '50'");
////通過ValuePattern設置TextBox2的值
//ValuePattern vpTextBox2 = (ValuePattern)aeTextBox2.GetCurrentPattern(ValuePattern.Pattern);
//vpTextBox2.SetValue("50");
//Thread.Sleep(1500);
//Console.WriteLine("Clickinig on button1 Button.");
////通過InvokePattern模擬點擊按鈕
//InvokePattern ipClickButton1 = (InvokePattern)aeButton.GetCurrentPattern(InvokePattern.Pattern);
//ipClickButton1.Invoke();
//Thread.Sleep(1500);
////驗證計算的結果與預期的結果是否相符合
//Console.WriteLine("Checking textBox3 for '80'");
//TextPattern tpTextBox3 = (TextPattern)aeTextBox3.GetCurrentPattern(TextPattern.Pattern);
//string result = tpTextBox3.DocumentRange.GetText(-1);//獲取textbox3中的值
// //獲取textbox3中的值
// //string result = (string)aeTextBox2.GetCurrentPropertyValue(ValuePattern.ValueProperty);
//if ("80" == result)
//{
// Console.WriteLine("Found it.");
// Console.WriteLine("TTest scenario: *PASS*");
//}
//else
//{
// Console.WriteLine("Did not find it.");
// Console.WriteLine("Test scenario: *FAIL*");
//}
//Console.WriteLine("Close application in 5 seconds.");
//Thread.Sleep(5000);
////實現關閉被測試程序
//WindowPattern wpCloseForm = (WindowPattern)aeForm.GetCurrentPattern(WindowPattern.Pattern);
//wpCloseForm.Close();
//Console.WriteLine("\nEnd test run\n");
}
catch (Exception ex)
{
Console.WriteLine("Fatal error: " + ex.Message);
}
}
// 遍歷控件樹,把類型名放入TreeNode(Windows.Forms中的類)裏
private void WalkEnabledElements(AutomationElement rootElement, TreeNode treeNode)
{
System.Windows.Automation.Condition condition1 = new PropertyCondition(AutomationElement.IsControlElementProperty, true);
System.Windows.Automation.Condition condition2 = new PropertyCondition(AutomationElement.IsEnabledProperty, true);
System.Windows.Automation.Condition condition3 = new PropertyCondition(AutomationElement.IsContentElementProperty, true);
TreeWalker walker = new TreeWalker(new AndCondition(condition1, condition2));
AutomationElement elementNode = walker.GetFirstChild(rootElement);
while (elementNode != null)
{
TreeNode childTreeNode = treeNode.Nodes.Add(elementNode.Current.ControlType.LocalizedControlType);
WalkEnabledElements(elementNode, childTreeNode);
elementNode = walker.GetNextSibling(elementNode);
}
}
// 操作ListItem所需的Pattern
AutomationElement GetListItemParent(AutomationElement listItem)
{
if (listItem == null) throw new ArgumentException();
SelectionItemPattern pattern = listItem.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern;
if (pattern == null)
{
return null;
}
else
{
SelectionItemPattern.SelectionItemPatternInformation properties = pattern.Current;
return properties.SelectionContainer;
}
}
}
}