WPF: 使用TestApi模拟用户输入
参考资料:
(1). WPF:自动点击某个FrameworkElement
(2). TestApi - a library of Test APIs
(3). Introduction to TestApi – Part 1: Input Injection APIs
1. 模拟用户输入的五种方式:
(A)直接调用UI element的方法,例如:Button.IsPressed
(B)利用可用的接口(UIA, MSAA, etc.),例如: AutomationElement
(C)使用底层输入模拟,与操作系统相关,例如:Windows中的 SendInput Win32 API 和 Raw Input Win32 API
(D)使用设备驱动模拟
(E)使用机器人模拟人类操作,例如:敲击键盘
方法A是framework级别的,只对WPF有效,而对Winform无效;
方法B比framework级别低一些,但是任然有许多限制,因为一些framework需要的可用接的口实现方式是不同的;
方法C和D是操作系统级别的,其中D比C要难以实现;
方法E是一种普遍使用的方法(我想只是在美国吧,汗),虽然它代价昂贵而且速度很慢。
TestApi提供了最常用的B和C两种方式,其中B方式由AutomationUtilities类实现,C方式由Mouse 和 Keyboard两个类实现。
2. 使用TestApi模拟的例子
例1:在WPF Window中查找并按下一个WPF Button,使用AutomationUtilities 和 Mouse 类.
//
// EXAMPLE #1
// This code below discovers and clicks the Close button in an About
//dialog box, thus dismissing the About dialog box.
//
string aboutDialogName = "About";
string closeButtonName = "Close";
AutomationElementCollection aboutDialogs = AutomationUtilities.FindElementsByName(
AutomationElement.RootElement,
aboutDialogName);
AutomationElementCollection closeButtons = AutomationUtilities.FindElementsByName(
aboutDialogs[0],
closeButtonName);
//
// You can either invoke the discovered control, through its invoke
// pattern...
//
InvokePattern p =
closeButtons[0].GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
p.Invoke();
//
// ... or you can handle the Mouse directly and click on the control.
//
Mouse.MoveTo(closeButton.GetClickablePoint());
Mouse.Click(MouseButton.Left);
例2:自动查找一个 TextBox 并在其中打字,使用Mouse 和 Keyboard类
//
// EXAMPLE #2
// Discover the location of a TextBox with a given name.
//
string textboxName = "ssnInputField";
AutomationElement textBox = AutomationUtilities.FindElementsByName(
AutomationElement.RootElement,
textboxName)[0];
Point textboxLocation = textbox.GetClickablePoint();
//
// Move the mouse to the textbox, click, then type something
//
Mouse.MoveTo(textboxLocation);
Mouse.Click(MouseButton.Left);
Keyboard.Type("Hello world.");
Keyboard.Press(Key.Shift);
Keyboard.Type("hello, capitalized world.");
Keyboard.Release(Key.Shift);
3. 后记
TestApi中的Mouse 和 Keyboard类可以在任何窗体应用程序中使用,与测试框架和测试流程无关,并且提供了源代码和文档,你可以集成到自己的project中,也可以直接引用Dll
需要注意的是,虽然TestApi提供如此简单的方法实现UI测试,但是UI测试是一件棘手而复杂的事情,在任何时候都应该尽量避免。一般来说,宁可在应用程序中采用多层设计模式(multi-tier),而设计一个浅\薄(thin)的UI,以尽量规避UI测试。
4. 附:如何计算控件位置
如需单独计算控件元素位置,而不是使用TestApi中的GetClickablePoint()方法,可采用以下方法:
1: /// <summary>
2: /// Get mouse move to location
3: /// </summary>
4: /// <param name="element">element</param>
5: /// <param name="logicalOffset">wpf logical pixel offset</param>
6: /// <returns>screen physical pixel location</returns>
7: public static System.Drawing.Point GetMoveToLocation(FrameworkElement element, Point logicalOffset)
8: {
9: Point mouseLocation = default(Point);
10: FlowDirection flowDirection = Window.GetWindow(element).FlowDirection;
11:
12: // We don't need to convert element location to physical screen pixel because wpf takes care of it.
13: Point elementLocation = element.PointToScreen(new Point());
14:
15: // We need to convert offset to physical screen pixel since we're pass in wpf logical pixel
16: double physicalXOffset = ConvertToPhysicalPixel(logicalOffset.X);
17: double physicalYOffset = ConvertToPhysicalPixel(logicalOffset.Y);
18:
19: switch (flowDirection)
20: {
21: case FlowDirection.LeftToRight:
22: mouseLocation = new Point(elementLocation.X + physicalXOffset, elementLocation.Y + physicalYOffset);
23: break;
24: case FlowDirection.RightToLeft:
25: // We need to subtract physical offsetX because the element location starting point is in right most
26: mouseLocation = new Point(elementLocation.X - physicalXOffset, elementLocation.Y + physicalYOffset);
27: break;
28: }
29:
30: return new System.Drawing.Point(Convert.ToInt32(mouseLocation.X), Convert.ToInt32(mouseLocation.Y));
31: }
32:
33: /// <summary>
34: /// WPF has its own pixel system in double value type, and screen pixel includes different DPIs is in int value type.
35: /// In 96 dpi, wpf and screen pixels are the same, but other dpi, we need to convert wpf logical pixel to screen physical
36: /// pixel by using formula (wpf pixel value * dpi / 96.0).
37: /// </summary>
38: /// <param name="logicalPixel">Logical(WPF) pixel value</param>
39: /// <returns>Physical(Screen) pixel value</returns>
40: public static int ConvertToPhysicalPixel(double logicalPixel)
41: {
42: return Convert.ToInt32(logicalPixel * GetDpi() / 96.0);
43: }
44:
45: /// <summary>
46: /// Get DPI of the system
47: /// </summary>
48: /// <returns></returns>
49: public static float GetDpi()
50: {
51: using (System.Drawing.Graphics graph = System.Drawing.Graphics.FromHwnd(IntPtr.Zero))
52: {
53: if (graph == null)
54: {
55: throw new NullReferenceException("Graphics not found");
56: }
57:
58: if (!graph.DpiX.Equals(graph.DpiY))
59: {
60: throw new ArithmeticException("DpiX != DpiY");
61: }
62:
63: return graph.DpiX;
64: }
65: }