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

[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()


↙↙↙阅读原文可查看相关链接,并与作者交流