使用 Microsoft Active Accessibility (MSAA) 进行 UI 自动化






4.92/5 (42投票s)
本文介绍了一种自动控制窗口应用程序的新方法,该方法基于 MSAA,而其他技术无法实现这一点。
引言
Microsoft Active Accessibility 是 Microsoft 的用户界面技术。所有基于此技术实现的窗口控件都可以向外部世界公开有关 UI 元素的信息。它是一项基于 COM 的技术,并提供了 IAccessible
接口。实现此接口的控件公开了某些方法,这些方法提供有关 UI 元素的信息,例如位置、类型、状态、名称、值、角色以及可以调用的默认操作。我们使用这项技术为 MS Word 2007 编写自动化库,而其 UI 完全基于 MSAA。
我想在这个论坛上分享同样的经验,因为它给了我们非常有希望的结果。
背景
我们开始基于 Microsoft UIA 开发了一个 Windows UI 自动化库。我们将这个库作为平台来编写 Windows 客户端的自动化功能测试用例。由于这个库基于 Microsoft UIA,它可以很好地用于 Win32 和 WinForms 应用程序,但不能用于基于 MSAA 的 Windows 应用程序(例如,MS Office 2007)。因此,我们扩展了 UI 自动化库以支持 MSAA,并成功完成了我们的任务。
方法
我们遵循了下面图示的方法
- Win32 API 从 Windows OS 消耗 MSAA 服务。
- MSAA 层 是一个核心自动化模块。它从 Win32 API 消耗 MSAA 服务,并提供在屏幕上搜索 UI 元素的服务,以及为该 UI 元素获取
IAccessible
接口(作为 COM 对象)。此层将每个 UI 元素视为一个AccessibleUIItem
。它可以是窗口、按钮、文本框或实现IAccessible
接口的任何控件。它提供了一种高效的搜索机制来遍历可访问树。 - UI 管理器 定义了我们要自动化的应用程序 UI 的对象模型。它扩展了 MSAA 层提供的基类
AssessibleUIItem
。稍后,我们将在本文中看到一个为 MS Office 2007 设计的 UI 对象模型的示例。
现在,让我们详细看看每一层。这里我将重点介绍每一层的代码演练。
Win32 API
我们在 .NET 中开发了自动化库,因此我们使用 PInvoke 来获取 Win32 API 的服务。以下是我们使用的 Win32 API 集合
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", EntryPoint = "FindWindow",
SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr FindWindowByClass(string lpClassName, IntPtr zero);
[DllImport("user32.dll", EntryPoint = "FindWindow",
SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr FindWindowByCaption(IntPtr zero, string lpWindowName);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool EnumChildWindows(IntPtr hWndParent,
EnumWindowsProc lpEnumFunc, int lParam);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hwnd,
StringBuilder lpString, int cch);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetWindowTextLength(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool BringWindowToTop(IntPtr hwnd);
[DllImport("oleacc.dll")]
public static extern uint GetRoleText(uint dwRole,
[Out] StringBuilder lpszRole, uint cchRoleMax);
[DllImport("oleacc.dll")]
public static extern uint GetStateText(uint dwStateBit,
[Out] StringBuilder lpszStateBit, uint cchStateBitMax);
[DllImport("oleacc.dll")]
public static extern uint WindowFromAccessibleObject(IAccessible pacc,
ref IntPtr phwnd);
[DllImport("oleacc.dll", PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern object AccessibleObjectFromWindow(int hwnd,
int dwId, ref Guid riid);
[DllImport("oleacc.dll")]
public static extern int AccessibleObjectFromWindow(
IntPtr hwnd,
uint id,
ref Guid iid,
[In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object ppvObject);
[DllImport("oleacc.dll")]
public static extern int AccessibleChildren(IAccessible paccContainer,
int iChildStart, int cChildren,
[Out()] [MarshalAs(UnmanagedType.LPArray,
SizeParamIndex = 4)] object[] rgvarChildren,
ref int pcObtained);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
MSAA 层
此层提供了 Win32 API 之上的抽象。其主要工作是从整个可访问树(根节点代表桌面)中搜索可访问的 UI 项。找到项后,它会获取其所有属性并提供 MSAAUIItem
对象。搜索机制非常优化。我们在搜索时可以忽略不可见的 UI 项。
“Microsoft 提供了一个名为 Accessibility Explorer (AccExplorer32.exe) 的工具。使用此工具,我们可以轻松地遍历所有可访问树。”
MSAAUIItme
公开以下 UI 项属性
名称
角色
状态
Location
值
句柄
IsEnabled
默认操作
如何搜索 UI 项
这是一个两步过程。
MSAA.cs
- 步骤 1:获取顶级应用程序窗口的可访问对象。例如,“MS Word 2007”窗口。
public static IAccessible GetTopWindowAccessibleObject(Regex windowName)
{
foreach (IAccessible accWindowObject in GetTopWindowAccessibleList())
{
try
{
string accWindowName = accWindowObject.get_accName(0);
if (!string.IsNullOrEmpty(accWindowName))
{
if (windowName.Match(accWindowObject.get_accName(0)).Success)
{
return accWindowObject;
}
}
}
catch (Exception ex)
{
}
}
return default(IAccessible);
}
public static IAccessible GetObjectByName(IAccessible objParent,
Regex objName, bool ignoreInvisible)
{
IAccessible objToReturn = default(IAccessible);
if (objParent != null)
{
IAccessible[] children = GetAccessibleChildren(objParent);
foreach (IAccessible child in children)
{
string childName = null;
string childState = string.Empty;
try
{
childName = child.get_accName(0);
childState =
GetStateText(Convert.ToUInt32(child.get_accState(0)));
}
catch (Exception)
{
}
if (ignoreInvisible)
{
if (childName != null
&& objName.Match(childName).Success
&& !childState.Contains("invisible"))
{
return child;
}
}
else
{
if (childName != null
&& objName.Match(childName).Success)
{
return child;
}
}
if (ignoreInvisible)
{
if (!childState.Contains("invisible"))
{
objToReturn = GetObjectByName(child, objName, ignoreInvisible);
if (objToReturn != default(IAccessible))
{
return objToReturn;
}
}
}
else
{
objToReturn = GetObjectByName(child, objName, ignoreInvisible);
if (objToReturn != default(IAccessible))
{
return objToReturn;
}
}
}
}
return objToReturn;
}
如何获取状态
这是一个棘手的部分。IAccessible.get_accState(object varChild)
返回一个状态 ID 号,该 ID 号是一个或多个状态的组合。例如,“MS Word 2007”在特定实例下有三个状态:focusable
、moveable
、sizeable
。但是,Win32 API GetStateText
只会针对状态 ID 返回第一个状态字符串。因此,我们必须多次调用此 API。下面的代码片段展示了我们的做法。
MSAAState.cs
public static string GetStateText(uint stateID)
{
uint maxLength = 1024;
var focusableStateText = new StringBuilder((int)maxLength);
var sizeableStateText = new StringBuilder((int)maxLength);
var moveableStateText = new StringBuilder((int)maxLength);
var invisibleStateText = new StringBuilder((int)maxLength);
//This pattern needs to be used to fetch other available cobination of states.
if (stateID == (MSAAStateConstants.STATE_SYSTEM_FOCUSABLE
| MSAAStateConstants.STATE_SYSTEM_SIZEABLE
| MSAAStateConstants.STATE_SYSTEM_MOVEABLE))
{
Win32.GetStateText(MSAAStateConstants.STATE_SYSTEM_FOCUSABLE,
focusableStateText, maxLength);
Win32.GetStateText(MSAAStateConstants.STATE_SYSTEM_SIZEABLE,
sizeableStateText, maxLength);
Win32.GetStateText(MSAAStateConstants.STATE_SYSTEM_MOVEABLE,
moveableStateText, maxLength);
return focusableStateText + "," + sizeableStateText + "," + moveableStateText;
}
var stateText = new StringBuilder((int)maxLength);
Win32.GetStateText(stateID, stateText, maxLength);
return stateText.ToString();
}
MSAAUIItem
回答这些问题并抽象出其他复杂性。
Office 2007 UI 自动化对象模型
顾名思义,此层特定于需要自动化的 Windows 应用程序。此层扩展了 MSAAUIItem
以定义 Office 2007 UI 控件。
OfficeUIItem
扩展了 MSAAUIItem
并充当核心 Office UI 项。所有其他项,如 OfficeRibbon
、OfficeToolBar
、OfficeRibbonTab
、OfficePropertyPage
和 MSWordWindow
都扩展了 OfficeUIItem
。
现在,让我们编写一些自动化代码。此代码将启动 MS Word 2007 并打开名为“Insert”的选项卡。然后,它将在“Pages”工具栏上运行“Cover Page”命令。选择特定选项卡很重要,因为只有这样 MS Word 才会刷新 UI 并加载该选项卡下的相应工具栏和控件。
Process.Start("winword");
MSWordWindow msWordMainWindow = new MSWordWindow("Document1 -");
msWordMainWindow.SelectTab("Insert");
msWordMainWindow.ToolBars["Pages"].Controls["Cover Page"].Invoke();
附带示例代码
本文附带的示例代码已在 Windows XP SP2 和 Windows 2008 上进行了测试。附带的代码将在 Windows XP 上运行,但在 Windows 2008 上可能会失败。原因是,我们用于获取可访问对象的 Win32 API 在这两个操作系统上是不同的。
Win32.cs
//Windows XP
[DllImport("oleacc.dll")]
internal static extern int AccessibleObjectFromWindow(
IntPtr hwnd,
uint id,
ref Guid iid,
[In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object ppvObject);
//Windows 2008
[DllImport("oleacc.dll", PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern object AccessibleObjectFromWindow(int hwnd,
int dwId, ref Guid riid);
结论
Microsoft Active Accessibility 并不是自动化 Windows UI 的全面解决方案。它存在一些限制。例如,使用可访问性,我们可以搜索 MS Word Ribbon 上的 TextBox
控件,但我们无法获取或设置其中的值,而这可以使用 Microsoft UIA 来完成。因此,MSAA 和 UIA 的结合可以为 Windows UI 自动化提供更有前景的解决方案。