博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

WPF: 使用TestApi模拟用户输入

Posted on 2011-05-26 10:47  linFen  阅读(502)  评论(0编辑  收藏  举报

参考资料:

(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,使用AutomationUtilitiesMouse 类.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//
// 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// 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
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/// <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:   }