MSAA的全称是Microsoft Active Accessibility(微软活动辅助功能)。这是类似DCOM技术。技术模型是这样的,UI程序可以暴露出一个Interface,方便另一个程序对其进行控制。 MSAA技术的初衷是为了方便残疾人使用Windows 程序。比如盲人看不到窗口,但是盲人可以通过一个USB读屏器连接到电脑上, 读屏器通过UI程序暴露出来的这个Interface,就可以获取程序信息,通过盲文或者其它形式传递给盲人。
MSAA提供了如此方便的功能, UI自动化测试自然可以借用这项技术。MSAA暴露出来的Interface叫做 IAccessible(可访问到的)。测试程序和目标UI程序互操作流程如下:
1. 测试程序调用Windows API: AccessibleObjectFromWindow,传入目标UI程序HWND。
2. AccessibleObjectFromWindow函数向UI程序发送WM_GETOBJECT消息。
3. UI程序创建实现了IAccessible的内部类,然后通过LresultFromObject API把IAccessible 接口返回给测试程序。
4. 测试程序拿到IAccessible接口,开始调用IAccessible接口函数操作测试目标。
IAccessible接口里面的几个关键函数是:
IAccessible::get_accChild/ IAccessible::get_accParent:通过这两个函数,调用者可以浏览目标程序的窗口关系树,定位到UI元素。
IAccessible::accLocation/I Accessible::accHitTest:读取和分辨目标元素的屏幕位置。
IAccessible::accName/ I Accessible:: accSelect:读取元素的名字,对UI元素进行指定的操作,比如选取Listbox里面的某一项等等。
IAccessible::accValue :开发人员可以自定义value属性的实现。比如针对折线图控件,开发人员可以在accValue中返回折线的坐标数列。
MSAA的理念类似于test hook。 通过主动让UI程序暴露一个接口来让调用者控制。 在具体使用中,测试人员往往是结合MSAA和Win32 API操作,取长补短。一方面对于UI元素丰富的属性,比如style,钩选状态,是否最大化和模拟用户输入等,继续采用Win32 API。 另一方面用MSAA的优势来弥补Win32 API的一些不足,比如:
由于MSAA有自己的get_accChild方法,使其控件树关系并不一定要和Win32 HWND关系对应一致。对于自绘窗口,虽然说只有一个HWND, 但是开发人员可以通过实现IAccessible接口来实现逻辑上的层次关系。比如Excel中就可以通过IAccessible把多个cell的子 IAccessible接口暴露给调用者。
IAccessible的实现是由开发者提供, 开发者可以灵活地根据实际情况决定方法的实现。比如前面提到了折线图控件可以返回坐标数列。对于.NET WinForm, 微软在Framework中就提供了IAccessible的默认实现,这样在具体实现中,就可以处理.NET动态维护HWND的细节等
MSDN:http://msdn.microsoft.com/en-us/library/dd318466%28VS.85%29.aspx
IAccessibleHWND hwnd = ::FindWindowW( L"CalcFrame",L"Calculator");
IAccessible *paccMainWindow = NULL;
AccessibleObjectFromWindow(hwnd, OBJID_WINDOW, IID_IAccessible, (void**)&paccMainWindow);
long count = 0;
paccMainWindow->get_accChildCount(&count);
其他方法可参考:https://docs.microsoft.com/zh-cn/windows/win32/api/oleacc/nn-oleacc-iaccessible?redirectedfrom=MSDN
下面介绍如何获取子控件的IAccessible接口,在这个之前先讲点其它的东西:
获取控件如果使用Findwindow(FindwindowEx)我们需要知道的控件的ClassName和CaptionName,这两个信息可以通过Spy++等工具获取,但我们在测试工具中会常见到Role等很多属性,先看这些信息是从什么地方来的,在microsoft的站点上下载Inspect32.exe运行后如:
上面告诉了我们一个控件的详细信息,现在我们只关心classname, role, name。
下面开始实现获取子控件IAccessible接口的代码,先看函数定义:
BOOL Find (IAccessible* paccParent,
LPSTR szName,
LPSTR szRole,
LPSTR szClass,
IAccessible** paccChild,
VARIANT* pvarChild)
BOOL Find (
IAccessible* paccParent,
LPSTR szName,
LPSTR szRole,
LPSTR szClass,
IAccessible** paccChild,
VARIANT* pvarChild)
{
HRESULT hr;
long numChildren;
unsigned long numFetched;
VARIANT varChild;
int indexCount;
IAccessible* pCAcc = NULL;
IEnumVARIANT* pEnum = NULL;
IDispatch* pDisp = NULL;
BOOL found = false;
char szObjName[MAX_PATH], szObjRole[MAX_PATH], szObjClass[MAX_PATH], szObjState[MAX_PATH];
//Get the IEnumVARIANT interface
hr = paccParent -> QueryInterface(IID_IEnumVARIANT, (PVOID*) & pEnum);
if(pEnum)
pEnum -> Reset();
// Get child count
paccParent -> get_accChildCount(&numChildren);
for(indexCount = 1; indexCount Next(1, &varChild, &numFetched);
else
{
varChild.vt = VT_I4;
varChild.lVal = indexCount;
}
// Get IDispatch interface for the child
if (varChild.vt == VT_I4)
{
pDisp = NULL;
hr = paccParent -> get_accChild(varChild, &pDisp);
}
else
pDisp = varChild.pdispVal;
// Get IAccessible interface for the child
if (pDisp)
{
hr = pDisp->QueryInterface(IID_IAccessible, (void**)&pCAcc);
hr = pDisp->Release();
}
// Get information about the child
if(pCAcc)
{
VariantInit(&varChild);
varChild.vt = VT_I4;
varChild.lVal = CHILDID_SELF;
*paccChild = pCAcc;
}
else
*paccChild = paccParent;
// Skip invisible and unavailable objects and their children
GetObjectState(*paccChild, &varChild, szObjState, sizeof(szObjState));
if(NULL != strstr(szObjState, "unavailable"))
{
if(pCAcc)
pCAcc->Release();
continue;
}
GetObjectName(*paccChild, &varChild, szObjName, sizeof(szObjName));
GetObjectRole(*paccChild, &varChild, szObjRole, sizeof(szObjRole));
GetObjectClass(*paccChild, szObjClass, sizeof(szObjClass));
if ((!szName || !strcmp(szName, szObjName)) && (!szRole || !strcmp(szRole, szObjRole)) && (!szClass || !strcmp(szClass, szObjClass)))
{
found = true;
*pvarChild = varChild;
break;
}
if(!found && pCAcc)
{
// Go deeper
found = Find(pCAcc, szName, szRole, szClass, paccChild, pvarChild);
if(*paccChild != pCAcc)
pCAcc->Release();
}
}
// Clean up
if(pEnum)
pEnum -> Release();
return found;
}
看获取控件状态的函数:
UINT GetObjectState(IAccessible* pacc, VARIANT* pvarChild, LPSTR lpszState, UINT cchState)
{
HRESULT hr;
VARIANT varRetVal;
*lpszState = 0;
VariantInit(&varRetVal);
hr = pacc->get_accState(*pvarChild, &varRetVal);
if (!SUCCEEDED(hr))
return(0);
DWORD dwStateBit;
int cChars = 0;
if (varRetVal.vt == VT_I4)
{
// Convert state flags to comma separated list.
for (dwStateBit = STATE_SYSTEM_UNAVAILABLE; dwStateBit < STATE_SYSTEM_ALERT_HIGH; dwStateBit get_accChildCount(&count);
IAccessible* paccControl = NULL;
VARIANT varControl;
if (Find(paccMainWindow, "Open:", "editable text", "Edit", &paccControl, &varControl))
{
paccControl->put_accValue(varControl, CComBSTR("123"));
}
运行结果: