移动测试开发 Windows - Python 应用:使用消息操作窗口
在 Windows 端经常会使用到一些带界面的工具,但在实现自动化的时候对这些工具的自动化如果使用位置点击的方式,那么自动化的稳定性不太好,并且实现起来也不方便,除了界面点击的方式是否还有其它更简单、稳定的方式呢?今天就介绍一种通过消息操作界面的方法。
Windows 系统是建立在事件驱动的机制上的,通过消息的传递来实现的,消息提供了应用程序之间、应用程序与 windows 系统之间进行通信的手段。
一、Windows 窗口
窗口是位于屏幕中的一个区域,它用于接收用户的输入,然后以文本或图形的形式显示输出。比如一个应用程序,一般都是以图形界面的形式展示,这个图形界面就是应用程序的窗口,其中可能还包括工具栏、按钮、滚动条、输入框等等,这些也是窗口,通常被称为 “子窗口” 或 “控件窗口” 或 “子窗口控件”。
以《Windows 程序设计》中的一个简单窗口程序的示例代码,说明窗口的创建、窗口消息循环、窗口消息处理函数。
HELLOWIN.C
/*--------------------------------------------------------------------------------------------
HELLOWIN.C——Displays“Hello, Windows 98!”in client area(c) Charles Petzold, 1998
---------------------------------------------------------------------------------------------*/
# include < windows.h >
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT (" HelloWin ");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;//窗口类
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc=WndProc;
wndclass.cbClsExtra=0;
wndclass.cbWndExtra=0;
wndclass.hInstance=hInstance;
wndclass.hIcon=LoadIcon (NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
wndclass.hbrBackground=(HBRUSH) GetstockObject (WHITE_BRUSH);
wndclass.lpszMenuName=NULL;
wndclass.lpszclassName= szAppName;
if (!RegisterClass (&wndclass)) //窗口类的注册
{
MessageBox (NULL, TEXT(“This program requires Windows NT !*”),SzAppName, MB_ICONERROR);
return 0;
}
//窗口的创建
hwnd = CreateWindow ( szAppName,//窗口类名
TEXT(“Hello程序”),//窗口标题
WS_OVERLAPPEDWINDOW,//窗口风格
CW_USEDEFAULT,// x的初始位置
CW_USEDEFAULT,// y的初始位置
CW_USEDEFAULT,//初始x大小
CW_USEDEFAULT,//初始y大小
NULL,//父窗口句柄
NULL,//窗口菜单句柄
hInstance,//程序实例句柄
NULL);//创建参数
ShowWindow (hwnd, iCmdShow); //窗口的显示
Updatewindow (hwnd); //窗口客户区重绘
//消息循环,从消息队列中获取消息
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
return msg.wParam;
}
//窗口过程,决定了窗口客户区的显示内容以及窗口如何对用户的输入做出响应。即消息的处理。
LRESULT CALLBACK WndProc (HWND hwnd, UINT message,WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch(message)
{
case WM_CREATE:
PlaySound (TEXT (" hellowin .wav"), NULL, SND_FILENAME I SND_ASYNC);
return 0;
case MWM_PAINT:
hdc = BeginPaint (hwnd, &ps);
GetClientRect (hwnd,&rect);
Drawtext (hdc, TEXT("Hello, Windows 98 !”), -1, &rect ,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint (hwnd, &ps);
return0;
case MWM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefwindowProc (hwnd, message, wParam, lParam);
}
概括来说,Windows 系统中有两种消息队列:
• 系统消息队列
• 应用程序消息队列
发生事件时,Windows 先将触发的消息放入系统消息队列,之后根据消息的 hwnd 值将消息复制到相应的应用程序消息队列,接着应用程序中的消息循环在其消息队列中检索每个消息并发送给相应的窗口处理函数。
二、Windows 消息
一个消息,是系统定义的一个 32 位的值,他唯一的定义了一个事件,比如单击鼠标就是发送一个消息。消息的相关信息包含在 MSG 结构体中,在 Windows 中 MSG 结构体定义如下:
typedef struct tagMsg
{
HWND hwnd; //接受该消息的窗口句柄
UINT message; //消息常量标识符,也就是我们通常所说的消息号
WPARAM wParam; //32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; //32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; //消息创建时的时间
POINT pt; //消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;
1. windows 消息类型可以分为以下两大类:
①. 系统定义的消息:非用户定义的消息,范围在【0x0000,0x03ff】之间,又可以分为三小类:
窗口消息:与窗口的内部运作有关,创建窗口,绘制窗口,销毁窗口,一般以 WM_开头,如 WM_CREATE, WM_SIZE, WM_MOUSEMOVE 等标准的 Windows 消息
命令消息:一般特指 WM_COMMAND 消息,与处理用户请求有关,通常由控件或者菜单产生。
通知消息:特指 WM_NOTIFY 消息。通常指一个窗口内的子控件发生了一些事情,需要通知父窗口。通知消息只适用于标准的窗口控件(按钮、列表框、组合框、编辑框,以及化公共控件树状视图、列表视图)。
②. 应用定义的消息
WM_USER :【0X0400-0X7FFF】, 用户自定义的消息范围。
WM_APP :【0X8000-0XBFFF】,用于程序之间的消息通信。
RegisterWindoMessage :【0XC000-0XFFFF】消息类型的定义在 Winuser.h 中。
2. 消息的发送
消息的发送有 3 种方式:发送、寄送和广播。
发送消息的函数有 SendMessage、SendMessageCallback、SendNotifyMessage、SendMessageTimeout;
寄送消息的函数主要有 PostMessage、PostThreadMessage、PostQuitMessage;
广播消息的函数有 BroadcastSystemMessage、BroadcastSystemMessageEx。
常用的发送消息的函数是 PostMessage 和 SendMessage,但为防止程序卡住可以使用 SendMessageTimeout 代替 SendMessage。三个函数的定义是:
函数原型:B00L PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
参数:
hWnd:其窗口程序接收消息的窗口的句柄。
Msg:指定被寄送的消息。
wParam:指定附加的消息特定的信息。
IParam:指定附加的消息特定的信息。
返回值:如果函数调用成功,返回非零值:如果函数调用失败,返回值是零。若想获得更多的错误信息,请调用GetLastError函数。
备注: 如果发送一个低于WM_USER范围的消息给异步消息函数(PostMessage.SendNotifyMessage,SendMesssgeCallback),消息参数不能包含指针。否则,操作将会失败。函数将再接收线程处理消息之前返回,发送者将在内存被使用之前释放。
函数原型:LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam);
参数:
hWnd:其窗口程序将接收消息的窗口的句柄。
Msg:指定被发送的消息。
wParam:指定附加的消息指定信息。
IParam:指定附加的消息指定信息。
返回值:返回值指定消息处理的结果,依赖于所发送的消息。是同步消息函数。
SendMessageTimeout:
函数功能:该函数将指定的消息发送到一个或多个窗口。此函数为指定的窗口调用窗口程序,并且,如果指定的窗口属于不同的线程,直到窗口程序处理完消息或指定的超时周期结束函数才返回。如果接收消息的窗口和当前线程属于同一个队列,窗口程序立即调用,超时值无用。
函数原型:LRESULT SendMessageTimeout(HWND hwnd,UINT Msg,WPARAM wParam,LPARAM IParam,UINTfuFlags,UIUT uTimeout,LPDWORD lpdwResultult);
参数:
hWnd:其窗口程序将接收消息的窗口的句柄。
Msg:指定被发送的消息。
wParam:指定附加的消息指定信息。
IParam:指定附加的消息指定信息。
fuFlags;指定如何发送消息。此参数可为下列值的组合:
SMTO_ABORTIFHUNG:如果接收进程处于“hung”状态,不等待超时周期结束就返回。
SMTO_BLOCK:阻止调用线程处理其他任何请求,直到函数返回。
SMTO_NORMAL:调用线程等待函数返回时,不被阻止处理其他请求。
SMTO_NOTIMEOUTIFNOTHUNG:Windows 95及更高版本:如果接收线程没被挂起,当超时周期结束时不返回。
uTimeout:为超时周期指定以毫秒为单位的持续时间。如果该消息是一个广播消息,每个窗口可使用全超时周期。例如,如果指定5秒的超时周期,有3个顶层窗回未能处理消息,可以有最多15秒的延迟。
IpdwResult:指定消息处理的结果,依赖于所发送的消息。
返回值:如果函数调用成功,返回非零值。如果函数调用失败,或超时,返回值是零。若想获得更多的错误信息,请调用GetLastError函数。如果GetLastError返回零,表明函数超时。
三、代码示例
PostMessage,SendMessage,SendMessageTimeout 调用 win32gui 中的接口,返回值与上文中函数原型有区别,具体可参考https://yiyibooks.cn/__trs__/meikunyuan6/pywin32/pywin32/PyWin32/win32gui.html的文档
以 Dbgview.exe 打开 Filter 在 exclude 中写入过滤字符串,举例:
1. 按钮点击,提供 2 种点击方式
def buttonClickFunc1(hwnBbutton):
'''点击按钮方法1,使用WM_LBUTTONDOWN 和WM_LBUTTONUP 完成一次点击,
hwnBbutton 是要点击的按钮的句柄。
SendMessage也可用。'''
rstDown = win32gui.PostMessage(hwnBbutton, win32con.WM_LBUTTONDOWN, 0, 0)
rstUp = win32gui.PostMessage(hwnBbutton, win32con.WM_LBUTTONUP, 0, 0)
def buttonClickFunc2(hwndParent, buttonID):
'''点击按钮方法2,使用命令消息WM_COMMAND ,
hwndParent 是按钮所在的父窗口,
SendMessage也可用。
buttonID传入10进制,如果是16进制要用int("buttonID", 16)转换一下,如int("3EC", 16) '''
rst = win32gui.PostMessage(hwndParent, win32con.WM_COMMAND, win32con.BN_CLICKED<<16 | buttonID, buttonID)
备注:buttonID可以通过Spy++查看。
2. 菜单选择,提供 2 种操作方式
①. 通用方法:找到菜单项的 ID,发消息给菜单项,打开菜单项
def getMenuItemText(menu, idx):
'''获取menu中标题的内容'''
import win32gui_struct
mii, extra = win32gui_struct.EmptyMENUITEMINFO()#新建一个空结构
win32gui.GetMenuItemInfo(menu, idx, True, mii)#将子菜单内容取到结构中
#解包结构
fytpe, fstate, wid, hsubmenu, hbmpchecked, hbmpuchecked, dwitemdata, text, hbmpitem = win32gui_struct.UnpackMENUITEMINFO(mii)
return text
def opeMenu(hwndParent, indexTab, num, textStr):
'''
indexTab:菜单中第indexTab个tab,tab是从0开始编号的;
num:第indexTab个tab中第num个菜单项,菜单项是从0开始编号的,注意,在菜单中横线也要计数;
textStr:第num个菜单项的标题内容;
'''
hwnd_menu = win32gui.GetMenu(hwndParent)#取得menu句柄
hwnd_menu_sub_file = win32gui.GetSubMenu(hwnd_menu, indexTab)#取到menu的子菜单第indexTab项的句柄
ID_Setting = win32gui.GetMenuItemID(hwnd_menu_sub_file, num)#取到子菜单第indexTab项第num个的ID
text_Setting = getMenuItemText(hwnd_menu_sub_file, num)
if text_Setting == textStr:#取到标题的内容对比,一致继续操作
win32gui.PostMessage(hwndParent, win32con.WM_COMMAND, ID_Setting, 0)
return True
②. 快捷键方法:
如果有快捷键的菜单项,可以通过快捷键选择,这种操作比上面的方法简单很多:
win32gui.PostMessage(phwnd, win32con.WM_COMMAND, ID, 0)
ID 就是快捷键的代码。可以通过 Spy++ 抓取。
3. 输入框输入
win32gui.SendMessageTimeout(hwndEdit, win32con.WM_SETTEXT, 0, testStr, win32con.SMTO_NORMAL, 1000)#
4.获取输入框内容
def geteditdata(hwnd,id):
hwnd_file = win32gui.GetDlgItem(hwnd,id)
bufLen = win32gui.SendMessage(hwnd_file, win32con.WM_GETTEXTLENGTH, 0, 0) + 1
buffer = array.array('b', b'\x00\x00' * bufLen)
text_len = win32gui.SendMessage(hwnd_file, win32con.WM_GETTEXT, bufLen, buffer)
text = win32gui.PyGetString(buffer.buffer_info()[0], bufLen - 1)
return text
5. 完整代码
#coding:gbk
'''
python3
'''
import os
import win32con, win32gui
import time
import subprocess
import commctrl
#启动进程
def startProgram(tool_path):
try:
cmd = r'%s' % tool_path
rst = subprocess.Popen(cmd, shell = True)
return rst
except Exception as e:
print (e)
return
#获取父窗口句柄
def findParentWindow(wndClass=None, wndText=None):
hwnd = win32gui.FindWindow(wndClass, wndText)
#print(hwnd)
return hwnd
#获取子窗口句柄
def findSubWindow(hwndParent, ID = None, wndClass=None, wndText=None):
if ID:
itmehwnd = win32gui.GetDlgItem(hwndParent, ID)
print (itmehwnd)
return itmehwnd
hwnd = win32gui.FindWindowEx(hwndParent, None, wndClass, wndText)
if not hwnd:
print ('%s获取失败' % wndClass)
return False
return hwnd
#点击按钮
def buttonClickFunc1(hwnBbutton):
'''点击按钮方法1,SendMessage也可用'''
rstDown = win32gui.PostMessage(hwnBbutton, win32con.WM_LBUTTONDOWN, 0, 0)
rstUp = win32gui.PostMessage(hwnBbutton, win32con.WM_LBUTTONUP, 0, 0)
if rstDown and rstUp is not None:
return False
return True
#输入框输入字符串
def editSetText(hwndEdit, testStr):
#WM_SETTEXT不能用PostMessage,PostMessage参数中不能有指针。异步消息,不等待
rst = win32gui.SendMessageTimeout(hwndEdit, win32con.WM_SETTEXT, 0, testStr, win32con.SMTO_NORMAL, 1000)
if rst[0]==1 and rst[1]==1:
return True
return False
#用快捷键打开菜单项
def opeToolTab(hwndParent, ID):
rst = win32gui.PostMessage(phwnd, win32con.WM_COMMAND, ID, 0)
print(rst)
if __name__ == '__main__':
tool_path = r'C:\Users\Administrator\Desktop\Dbgview.exe'
rst = startProgram(tool_path)
if not rst:
print("step 1:start Fail")
sys.exit()
print("step 1:start OK")
time.sleep(3)
phwnd = findParentWindow("dbgviewClass")#以窗口类名查找父窗口
print('step 2: find phwnd %s' % phwnd)
opeToolTab(phwnd, 40022)#打开菜单项。40022是Dbgview中Ctrl+L这个快捷键的虚拟代码,即Filter
time.sleep(3)
hwndsub = findParentWindow("#32770", "DebugView Filter")#查找Filter子窗口
print ("step 4:find hwndsub %s" % hwndsub)
hwndEdit = findSubWindow(hwndsub, ID = 0x3EC, wndClass="ComboBox", wndText=None)# Filter子窗口中exclude对应的edit是combobox的子窗口,可以用combobox直接settext。所以这里找的是combobox的句柄
print ("step 5:find hwndEdit %s" % hwndsub)
editSetText(hwndEdit, 'testStr')#将'testStr'字符串设置到exclue中
print ("step 6:setText")
time.sleep(3)
hwnBbutton = findSubWindow(hwndsub, ID = None, wndClass="Button", wndText="&OK")#查找Filter子窗口中OK按钮的句柄
print ("step 7:find hwnBbutton %s" % hwnBbutton)
buttonClickFunc1(hwnBbutton) #点击Filter子窗口中OK按钮关闭子窗口
print ("step 8:click button")
参考:
https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa
https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
https://docs.microsoft.com/en-us/windows/win32/winmsg/messages-and-message-queues
https://bbs.pediy.com/thread-256282.htm
https://zhuanlan.zhihu.com/p/42992978