接着上一篇的话题,因为要为 win32 程序写 UI 测试,自已也摸索出来一个大致的框架。在这里做一个小结:
以下代码都发表在公有领域(http://zh.wikipedia.org/zh-cn/Public_domain),你可以自由地使用它们。
1. 首先,你需要创建进程:
{ TCHAR cmdLine[1] = _T(""); STARTUPINFO si; ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&piPlanet, sizeof(PROCESS_INFORMATION)); si.cb = sizeof(STARTUPINFO); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_NORMAL; BOOL bRet = ::CreateProcess(_T("Planet.exe"), cmdLine, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE | NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &piPlanet); ASSERT(bRet); }
创建进程后,PROCESS_INFORMATION 结构体中就有了进程 id 和主线程 id 。
2. 获得主窗口句柄
主要通过 EnumWindows 来实现,如果窗口 title 是固定的,那么也可以用 FindWindowEx 。我这里由于 title 和 class name 都未知,用了一种比较搓的办法:先通过线程 id 随便找一个窗口,再用 GetParent 获取最顶层的窗口
// 一个回调函数,相当于 EnumWindows 的过滤器 static BOOL CALLBACK findWindowThreadIdProc(HWND hWnd, LPARAM param) { DWORD id = ::GetWindowThreadProcessId(hWnd, NULL); // 根据 HWND 得到线程id if (id == (DWORD) param) { hWndFound = hWnd; return FALSE; } return TRUE; } { hWndFound = NULL; // 找到指定的线程的第一个窗口: ::EnumWindows(findWindowThreadIdProc, piPlanet.dwThreadId); HWND hTmp; // 循环找到最顶层的窗口: while ((hTmp = ::GetParent(hWndFound)) != NULL) { hWndFound = hTmp; } }
当然,如果你提前就知道窗口的 class name ,就可以直接在回调函数中指定这个条件。
顺便说一下如何在 MFC 程序中指定 class name :让你的 CMainFrame 继承来自 CWnd 的 PreCreateWindow 函数,这个函数将在 MFC 调用 CreateWindow 之前调用:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if (!CFrameWnd::PreCreateWindow(cs)) return FALSE; WNDCLASS wndclass; ::GetClassInfo(::AfxGetInstanceHandle(), cs.lpszClass, &wndclass); wndclass.lpszClassName= _T("PlanetMainFrame"); // 将 class name 设为指定的值 VERIFY(::AfxRegisterClass(&wndclass)); cs.lpszClass = wndclass.lpszClassName; return TRUE; } // CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
另外,可以通过 VS 自带的 spy++ 查看桌面上所有窗口的 class name 。
3. 模拟点菜单
::PostMessage(hWnd, WM_COMMAND, MAKEWPARAM(CM_PROJECTPURCHASES, BN_CLICKED), NULL); Sleep(1000);
hWnd 是你的窗口句柄,CM_PROJECTPURCHASES 是对应的菜单 ID 。注意要 Sleep 足够的时间。
还要注意 PostMessage 和 SendMessage 的区别:SendMessage 必须要等相应程序处理完消息之后才返回,而 PostMessage 是异步的,立即返回。所以 PostMessage 需要 Sleep,而 SendMessage 则不需要。
4. 获得最表面的窗口
hWnd = ::GetForegroundWindow();
与 GetFocus() 不同,这个直接获得直观上最表层的那个窗口(比如点了菜单后弹出来的一个窗口),相当 imba 。而 GetFocus() 得到的句柄可能会是一个编辑框控件。
5. 关闭窗口
#define closeWindowX(w) {::PostMessage(w, WM_CLOSE, 0, 0);Sleep(500);}
然后就是 GetDlgItem() 之流了,再加上上篇文章介绍的模拟按键,基本上能搞定大部分的自动化 UI 测试任务。