在 Windows 上,使用 C# + UI Automation + WinAPI 进行 UI 测试的方法

本帖已被设为精华帖!,

一、获取待测程序主窗体的句柄

[DllImport("user32.dll ", EntryPoint = "FindWindow")]
static extern IntPtr FindWindow_AppFormHandle(string lpClassName, string lpWindowName);

/// <summary>
/// 通过WinAPI,获取待测程序主窗体的句柄
/// </summary>
/// <param name="AppFormClassName">待测程序主窗体类名</param>
/// <param name="AppFormWindowName">待测程序主窗体名</param>
/// <returns></returns>
public IntPtr AppFormHandle(string AppFormClassName, string AppFormWindowName)
{
IntPtr AppFormHandle = IntPtr.Zero;
bool FormFound = false;
int Attempts = 0;
do
{
if (AppFormHandle == IntPtr.Zero)
{
Thread.Sleep(100);
Attempts = Attempts + 1;
AppFormHandle = FindWindow_AppFormHandle(AppFormClassName, AppFormWindowName);
}
else
{
FormFound = true;
}
} while (!FormFound && Attempts < 99);//为防止因FindWindow无法遍历到待测主窗体,而陷入无限循环之中,因此增加一个遍历次数Attempts < 99的条件
if (AppFormHandle == IntPtr.Zero)
{
new Exception("获取待测程序主窗体 失败");
}
return AppFormHandle;
}//AppFormHandle()

获取待测程序主窗体句柄的方法有两种:

第一种方法是使用Process.Start()启动待测程序后,再用Process.MainWindowHandle来获取,但是这种方法有一定的局限性,因为如果Process.Start()所启动的exe程序可能会去启动另一个exe程序,并关闭自身进程,这样的话该方法就不再适用了

第二种方法是我们先手动将待测程序的主窗体打开,然后通过调用WinAPI中的FindWindow()函数去遍历窗口,从而来获取获取待测程序主窗体句柄,其中FindWindow()函数中需要2个变量,一个是窗体类名,一个是窗体名,这些变量我们可以使用VS自带的工具Spy++来获取

二、获取待测程序主窗体的AutomationElement

/// <summary>
/// 通过主窗体句柄,获取待测程序主窗体的AutomationElement
/// </summary>
/// <param name="AppFormHandle"></param>
/// <returns></returns>
public AutomationElement AppAE(IntPtr AppFormHandle)
{
//获取待测程序主窗体的AutomationElement
AutomationElement AppAE = AutomationElement.FromHandle(AppFormHandle);
if (AppAE == null)
{
new Exception("获取待测程序窗体的AutomationElement 失败");
}
return AppAE;
}//AppAE()

当我们获取到待测程序主窗体的句柄后,我们就可以通过UI Automation中的AutomationElement.FromHandle()函数获取到待测程序主窗体的AutomationElement(下面就简称AE)。在UI Automation中,所有的窗体或控件都表现为一个AE,AE中包含了此窗体或控件的相关属性,我们可以通过这些属性从而来实现对窗体或控件的自动化操作。

三、获取待测程序子窗体或子控件的AutomationElement

/// <summary>
/// 通过Name,获取DescendantsAE
/// </summary>
/// <param name="ScopeAE"></param>
/// <param name="TargetName"></param>
/// <returns></returns>
public AutomationElement DescendantsAE_Name(AutomationElement ScopeAE, string TargetName)
{
PropertyCondition Condition = new PropertyCondition(AutomationElement.NameProperty, TargetName);
AutomationElement DescendantsAE = ScopeAE.FindFirst(TreeScope.Descendants, Condition);
if (DescendantsAE == null)
{
new Exception("获取DescendantsAE 失败");
}
return DescendantsAE;
}//DescendantsAE_Name()

/// <summary>
/// 通过AutomationID,获取DescendantsAE
/// </summary>
/// <param name="ScopeAE"></param>
/// <param name="AutomationID"></param>
/// <returns></returns>
public AutomationElement DescendantsAE_AutomationID(AutomationElement ScopeAE, string AutomationID)
{
PropertyCondition Condition = new PropertyCondition(AutomationElement.AutomationIdProperty, AutomationID);
AutomationElement DescendantsAE = ScopeAE.FindFirst(TreeScope.Descendants, Condition);
if (DescendantsAE == null)
{
new Exception("获取DescendantsAE 失败");
}
return DescendantsAE;
}//DescendantsAE_AutomationID()

/// <summary>
/// 通过ControlType类中的字段,获取所有ChildAE的集合ChildAEC,再通过索引值Index,从中获取相应的ChildAE
/// </summary>
/// <param name="ScopeAE"></param>
/// <param name="ControlType"></param>
/// <param name="Index"></param>
/// <returns></returns>
public AutomationElement ChildAE_ControlType(AutomationElement ScopeAE, object ControlType, int Index)
{
PropertyCondition Condition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType);
AutomationElementCollection ChildAEC = ScopeAE.FindAll(TreeScope.Children, Condition);
AutomationElement ChildAE = ChildAEC[Index];
if (ChildAE == null)
{
new Exception("获取ChildAE 失败");
}
return ChildAE;
}//ChildAE_ControlType()

我们可以通过窗体或控件的Name属性或AutomationID属性,并使用ScopeAE.FindFirst(TreeScope.Descendants, Condition)函数来获取窗体或控件AE,其中ScopeAE是指父AE,TreeScope是指搜索的范围,Descendants是TreeScope枚举的一个成员,表示的是父AE下的所有子AE,PropertyCondition Condition表示的是查找条件,我们可以通过new PropertyCondition(AutomationProperty, Object)来设置该查找条件,其中AutomationProperty表示的是需要搜索的属性,Object表示的是需要搜索的属性值

ScopeAE.FindAll(TreeScope.Children, Condition);是另一种获取获取窗体或控件AE的方法,使用该方法,其返回的是所有符合条件的窗体或控件AE的集合AutomationElementCollection(AutomationElementCollection简称AEC,其就相当于一个AutomationElement[ ]),因此我们可以像数组那样使用Index从AEC中取出我们所需的窗体或控件AE。当然由于FindAll这样的返回方式,因此我建议使用另一个TreeScope枚举成员Children,Children是TreeScope枚举的另一个成员,其表示的是父AE下的直接子AE(即其不包含子AE的子AE)

备注:对于我们查找所需要知道的Name、AutomationID、ControlType等属性,我们可以通过使用UISpy或Inspect工具来查看,当然这两个工具还能查看其它窗口或控件的属性

四、使用WinAPI中的PostMessage()函数,向主窗体发送消息,从而模拟鼠标左键点击控件

Windows的消息机制:当我们按下鼠标或键盘的按钮时,其动作会被系统捕捉到,然后系统会根据捕捉到的动作产生出一个对应的消息,并将该消息发送到消息队列中,然后应用程序会根据消息中所包含的句柄从消息队列中获取消息,从而完成相应的处理。而对于DirectUI这类的窗体,虽然其控件是没有句柄的,但是其不可能不遵守这Windows底层的消息机制,DirectUI这类窗体的做法实际上是通过主窗体句柄获取消息队列中的消息,然后主窗体在根据该消息的内容,将消息发给相应的控件,因此我们在进行UI测试时,完全可以利用此机制,直接对应用程序的主窗体句柄发送消息,从而直接进行对窗体或控件的相关自动化操作。

[DllImport("user32.dll ", EntryPoint = "PostMessage")]
static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);

PostMessage()是发送消息的方式之一,该函数是指将一个消息放入消息队列后,不等待线程处理消息就返回。由于在C#中PostMessage()会随着消息内容Msg的变化,其所需的wParam和lParam的变量类型也会随之发生变化,因此我们可以在导入user32.dll时,使用EntryPoint =来指明所需调用的WinAPI的函数名,这样就可以将该WinAPI函数名定义为一个C#的方法别名,例如我在上面利用FindWindow()来获取主窗体句柄时,我将FindWindow()这个WinAPI函数名定义为了本程序中C#的方法别名FindWindow_AppFormHandle() 。

PostMessage()中有4个参数,第一个参数代表的是我们发送消息的对象的窗体或控件句柄,在这里就是指主窗体句柄,第二个参数代表的是我们发给窗体或控件的Windows消息码,第三个和第四个参数为一般参数,其含义及数据类型取决于第二个参数,即Windows消息码。而现在我们是要通过PostMessage()函数,向主窗体发送消息,来模拟鼠标左键点击控件,因此根据我们即将使用到的Windows消息码,wParam的数据类型设为int型,其含义是指当鼠标按键被点击时,其他功能键是否被按下,在这里我们只需将其设为0即可。我们将lParam的数据类型也设为int型,其含义是指鼠标单击的位置,其中X坐标为低字节,Y坐标为高字节,由于int型为32位,因此我们只需将Y坐标左移16位,然后再加上X坐标即可,即X + (Y << 16) 。

const uint WM_LBUTTONDOWN = 0x0201;//表示按下鼠标左键
const uint WM_LBUTTONUP = 0x0202;//表示鼠标左键抬起

上面就是我们模拟鼠标左键点击控件时所需要用到的Windows消息码,这些消息码是微软给定的,我们只需直接使用即可。

/// <summary>
/// 通过WinAPI,模拟鼠标左键点击TargetAE
/// </summary>
/// <param name="AppHandle">待测程序主窗体句柄</param>
/// <param name="TargetAE">目标控件AE</param>
/// <param name="ClickCount">点击次数</param>
public void MouseLeftClick(IntPtr AppFormHandle, AutomationElement TargetAE, int ClickCount)
{
//由于WM_LBUTTONDOWN和WM_LBUTTONUP消息中的lParam是指控件相对于主窗体的坐标
//因此我们需要将 控件中心的相对于桌面的坐标 减去 主窗体左上角的相对桌面的坐标

//重新利用传入主窗体句柄来获取主窗体的AE
AutomationElement AppAE = AppAE(AppFormHandle);

//通过AutomationElement.Current.BoundingRectangle来获取该对象的尺寸及位置
Rect AppRect = AppAE.Current.BoundingRectangle; //获取待测主窗体的宽度、高度、X坐标、Y坐标
Rect TargetRect = TargetAE.Current.BoundingRectangle; //获取目标控件的宽度、高度、X坐标、Y坐标

int X = (int)(TargetRect.Left + TargetRect.Width / 2 - AppRect.Left); //目标控件的X坐标+目标控件的宽度/2-待测主窗体的X坐标
int Y = (int)(TargetRect.Top + TargetRect.Height / 2 - AppRect.Top); //目标控件的Y坐标+目标控件的高度/2-待测主窗体的Y坐标

int I = 0;
do
{
Thread.Sleep(250);
PostMessage(AppFormHandle, WM_LBUTTONDOWN, 0, X + (Y << 16));//发送鼠标左键按下的消息
PostMessage(AppFormHandle, WM_LBUTTONUP, 0, X + (Y << 16));//发送鼠标左键抬起的消息
I = I + 1;
} while (I < ClickCount); //ClickCount是指点击次数,我们可以通过控制循环次数来控制点击次数
}//MouseLeftClick()

五、使用WinAPI中的SendMessage()函数,向主窗体发送消息,从而模拟键盘输入

[DllImport("user32.dll ", EntryPoint = "SendMessage")]
static extern void SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);

const uint WM_CHAR = 0x0102;

SendMessage()是发送消息的方式之一,SendMessage()与PostMessage()不同的是SendMessage()函数是指把消息放入消息队列后,并等待消息处理完后才返回。SendMessage()有4个参数,这4个参数与PostMessage()的4个参数一样,因此不在多说。在这里我们需要发送的是WM_CHAR消息,因此我们将wParam定义为int型,其代表的是按键扫描码,其范围在0x00-0xFFFF之间(0x00-0x7f为ASCII码,0x80-0xFFFF为拓展ASCII码),而对于一个字符串string来说,其汉字的编码是以Unicode码进行存储的,因此我们需要将其转换成ANSI码。我们也将lParam设为int型,其代表的是按键状态的掩码,在这里我们只需将其设为0即可。

/// <summary>
/// 通过WinAPI,模拟键盘在TargetAE上输入信息
/// </summary>
/// <param name="AppFormHandle"></param>
/// <param name="Message"></param>
public void SendMessage(IntPtr AppFormHandle, string Message)
{
//将字符串string Message的Unicode码转换成ANSI码
byte[] B = Encoding.Default.GetBytes(Message);
foreach (char C in B)
{
SendMessage(AppFormHandle, WM_CHAR, C, 0);
}
}//SendMessage()

六、使用WinAPI中的mouse_event()函数,操纵鼠标来点击控件

[DllImport("user32.dll",EntryPoint= "mouse_event")]
extern static void mouse_event(uint dwFlags, int dX, int dY, uint dwData, IntPtr dwExtralnfo);

这是我们所使用到的mouse_event()函数,第一个参数是我们所需要使用的消息码,备注:该消息码与之前我用PostMessage()模拟鼠标单击时的的消息码的定义是不同的,我们需要使用的是另一个消息码,下面将会写到;第二个和第三个参数是控件相对于桌面的X、Y坐标,也是我们操纵鼠标所需点击的位置;第四个参数与鼠标滚轮有关,在这里我们只需要设为0即可;第五个参数是与鼠标事件相关的一个附加值,在这里我们只需要设为IntPtr.Zero即可。

mouse_event()函数与我们之前使用PostMessage()函数模拟鼠标单击的方法有着本质的区别,前者是通过操纵鼠标来完成点击动作,因此其还需要使用另一个WinAPI函数SetCursorPos()来移动鼠标从而完成点击,而后者则是发消息直接告诉应用程序 鼠标点击控件的事件,无需操纵鼠标。

[DllImport("user32.dll",EntryPoint = "SetCursorPos")]
extern static bool SetCursorPos(int x, int y);

SetCursorPos()函数是将鼠标移至坐标(X,Y)点上,其中两个参数表示的是控件相对于桌面的(X,Y)坐标。

const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
const uint MOUSEEVENTF_LEFTUP = 0x0004;

这是用于mouse_event()函数的消息码的定义。

public void MouseLeftClick(AutomationElement TargetAE, int ClickCount)
{
Rect TargetRect = TargetAE.Current.BoundingRectangle;
int dX = (int)(TargetRect.Left + TargetRect.Width / 2);//目标控件的X坐标+目标控件的宽度/2
int dY = (int)(TargetRect.Top + TargetRect.Height / 2);//目标控件的Y坐标+目标控件的高度/2

//将鼠标移至控件中心(dx,dY)
SetCursorPos(dX, dY);

int I = 0;
do
{
Thread.Sleep(250);
mouse_event(MOUSEEVENTF_LEFTDOWN, dX, dY, 0, IntPtr.Zero);//鼠标左键按下
mouse_event(MOUSEEVENTF_LEFTDOWN, dX, dY, 0, IntPtr.Zero);//鼠标左键抬起
I = I + 1;
} while (I < ClickCount);//ClickCount是指点击次数,我们可以通过控制循环次数来控制点击次数
}

七、利用UI Automation中的各种Pattern模式来实现对控件的相关自动化操作

由于现在应用程序中的控件基本上都不支持接受键盘焦点,而同时微软并未对其当初所推出的UI Automation框架中的各种Pattern模式进行优化,导致其中有许多Pattern模式无法使用,因此在这里我就只说一些目前仍可以使用的Pattern模式。

7.1、ValuePattern

/// <summary>
/// 获取TargetAE的Value值
/// </summary>
/// <param name="TargetAE"></param>
/// <returns></returns>
public string Value(AutomationElement TargetAE)
{
try
{
//通过AutomationElement.GetCurrentPropertyValue()的方法可以直接获得控件的属性值
string TargetVP = (string)TargetAE.GetCurrentPropertyValue(ValuePattern.ValueProperty);
return TargetVP;
}
catch
{
Console.WriteLine("获取TargetAE的Text值 失败");
return null;
}
}//Value()

7.2、TextPattern

/// <summary>
/// 获取TargetAE的Text值
/// </summary>
/// <param name="TargetAE"></param>
/// <returns></returns>
public string Text(AutomationElement TargetAE)
{
try
{
//通过AutomationElement.GetCurrentPattern()的方法获得控件的Pattern模式对象
TextPattern TargetTP = (TextPattern)TargetAE.GetCurrentPattern(TextPattern.Pattern);
//通过TextPattern.DocumentRange来获取该控件中的文本对象TextPatternRange
TextPatternRange TargetTPR = TargetTP.DocumentRange;
//使用TextPatternRange.GetText(int maxLength)获取该控件的文本内容,maxLength表示要返回的字符串的最大长度, -1表示全文本
string Text = TargetTPR.GetText(-1);
return Text;
}
catch
{
Console.WriteLine("获取TargetAE的Text值 失败");
return null;
}
}//Text()

* 注:本文来自网络投稿,不代表本站立场,如若侵犯版权,请及时知会删除