Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

python pywin32 PyUserInput实现自动化脚本

2020/11/07 Python 自动化运维
Word count: 4,574 | Reading time: 19min

python pywin32 PyUserInput实现自动化脚本

pywin32用spy++工具查找到句柄,再结合PyUserInput就能很好地实现自动化脚本。

句柄是一个32位整数,在windows中标记对象用,类似一个dict中的key,详情参看这篇文章

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import win32gui
import win32con
import win32api

# 从顶层窗口向下搜索主窗口,无法搜索子窗口
# FindWindow(lpClassName=None, lpWindowName=None) 窗口类名 窗口标题名
handle = win32gui.FindWindow("Notepad", None)


# 获取窗口位置
left, top, right, bottom = win32gui.GetWindowRect(handle)

#获取某个句柄的类名和标题
title = win32gui.GetWindowText(handle)
clsname = win32gui.GetClassName(handle)

# 打印句柄
# 十进制
print(handle)
# 十六进制
print("%x" %(handle) )


# 搜索子窗口
# 枚举子窗口
hwndChildList = []
win32gui.EnumChildWindows(handle, lambda hwnd, param: param.append(hwnd), hwndChildList)

# FindWindowEx(hwndParent=0, hwndChildAfter=0, lpszClass=None, lpszWindow=None)
# 父窗口句柄 若不为0,则按照z-index的顺序从hwndChildAfter向后开始搜索子窗体,否则从第一个子窗体开始搜索。 子窗口类名 子窗口标题
subHandle = win32gui.FindWindowEx(handle, 0, "EDIT", None)

# 获得窗口的菜单句柄
menuHandle = win32gui.GetMenu(subHandle)

# 获得子菜单或下拉菜单句柄
# 参数:菜单句柄 子菜单索引号
subMenuHandle = win32gui.GetSubMenu(menuHandle, 0)

# 获得菜单项中的的标志符,注意,分隔符是被编入索引的
# 参数:子菜单句柄 项目索引号
menuItemHandle = win32gui.GetMenuItemID(subMenuHandle, 0)

# 发送消息,加入消息队列,无返回
# 参数:句柄 消息类型 WParam IParam
win32gui.postMessage(subHandle, win32con.WM_COMMAND, menuItemHandle, 0)

# wParam的定义是32位整型,high word就是他的31至16位,low word是它的15至0位。
# 当参数超过两个,wParam和lParam不够用时,可以将wParam就给拆成两个int16来使用。
# 这种时候在python里记得用把HIWORD的常数向左移16位,再加LOWORD,即wParam = HIWORD<<16+LOWORD。


# 下选框内容更改
# 参数:下选框句柄; 消息内容;
#参数下选框的哪一个item,以0起始的待选选项的索引;如果该值为-1,将从组合框列表中删除当前选项,并使当前选项为空;
# 参数CB_Handle为下选框句柄,PCB_handle下选框父窗口句柄
if win32api.SendMessage(CB_handle, win32con.CB_SETCURSEL, 1, 0) == 1:

# 下选框的父窗口命令
# 参数:父窗口句柄; 命令;
# 参数:WParam:高位表示类型,低位表示内容;参数IParam,下选框句柄
# CBN_SELENDOK当用户选择了有效的列表项时发送,提示父窗体处理用户的选择。 LOWORD为组合框的ID. HIWORD为CBN_SELENDOK的值。
win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, 0x90000, CB_handle)
# CBN_SELCHANGE当用户更改了列表项的选择时发送,不论用户是通过鼠标选择或是通过方向键选择都会发送此通知。LOWORD为组合框的ID. HIWORD为CBN_SELCHANGE的值。
win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, 0x10000, CB_handle)


# 设置文本框内容,等窗口处理完毕后返回true。中文需编码成gbk
# 参数:句柄;消息类型;
# 参数WParam,无需使用;
# 参数IParam,要设置的内容,字符串
win32api.SendMessage(handle, win32con.WM_SETTEXT, 0, os.path.abspath(fgFilePath).encode('gbk'))


# 控件点击确定,处理消息后返回0
# 参数:窗口句柄; 消息类型; 参数WParam HIWORD为0(未使用),LOWORD为控件的ID; 参数IParam 0(未使用),确定控件的句柄
win32api.SendMessage(Mhandle, win32con.WM_COMMAND, 1, confirmBTN_handle)


# 获取窗口文本不含截尾空字符的长度
# 参数:窗口句柄; 消息类型; 参数WParam; 参数IParam
bufSize = win32api.SendMessage(subHandle, win32con.WM_GETTEXTLENGTH, 0, 0) +1

# 利用api生成Buffer
strBuf = win32gui.PyMakeBuffer(bufSize)
print(strBuf)

# 发送消息获取文本内容
# 参数:窗口句柄; 消息类型;文本大小; 存储位置
length = win32gui.SendMessage(subHandle, win32con.WM_GETTEXT, bufSize, strBuf)

# 反向内容,转为字符串
# text = str(strBuf[:-1])

address, length = win32gui.PyGetBufferAddressAndLen(strBuf)
text = win32gui.PyGetString(address, length)
# print('text: ', text)

# 鼠标单击事件
#鼠标定位到(30,50)
win32api.SetCursorPos([30,150])

#执行左单键击,若需要双击则延时几毫秒再点击一次即可
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP | win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)

#右键单击
win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP | win32con.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)

def click1(x,y): #第一种
win32api.SetCursorPos((x,y))
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,x,y,0,0)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,x,y,0,0)

def click2(x,y): #第二种
ctypes.windll.user32.SetCursorPos(x,y)
ctypes.windll.user32.mouse_event(2,0,0,0,0)
ctypes.windll.user32.mouse_event(4,0,0,0,0)

def click_it(pos): #第三种
handle= win32gui.WindowFromPoint(pos)
client_pos =win32gui.ScreenToClient(handle,pos)
tmp=win32api.MAKELONG(client_pos[0],client_pos[1])
win32gui.SendMessage(handle, win32con.WM_ACTIVATE,win32con.WA_ACTIVE,0)
win32gui.SendMessage(handle, win32con.WM_LBUTTONDOWN,win32con.MK_LBUTTON,tmp)
win32gui.SendMessage(handle, win32con.WM_LBUTTONUP,win32con.MK_LBUTTON,tmp)

# 发送回车
win32api.keybd_event(13,0,0,0)
win32api.keybd_event(13,0,win32con.KEYEVENTF_KEYUP,0)

# 关闭窗口
win32gui.PostMessage(win32lib.findWindow(classname, titlename), win32con.WM_CLOSE, 0, 0)


# 检查窗口是否最小化,如果是最大化
if(win32gui.IsIconic(hwnd)):
# win32gui.ShowWindow(hwnd, win32con.SW_SHOWNORMAL)
win32gui.ShowWindow(hwnd, 8)
sleep(0.5)

# SW_HIDE:隐藏窗口并激活其他窗口。nCmdShow=0。
# SW_MAXIMIZE:最大化指定的窗口。nCmdShow=3。
# SW_MINIMIZE:最小化指定的窗口并且激活在Z序中的下一个顶层窗口。nCmdShow=6。
# SW_RESTORE:激活并显示窗口。如果窗口最小化或最大化,则系统将窗口恢复到原来的尺寸和位置。在恢复最小化窗口时,应用程序应该指定这个标志。nCmdShow=9。
# SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。nCmdShow=5。
# SW_SHOWDEFAULT:依据在STARTUPINFO结构中指定的SW_FLAG标志设定显示状态,STARTUPINFO 结构是由启动应用程序的程序传递给CreateProcess函数的。nCmdShow=10。
# SW_SHOWMAXIMIZED:激活窗口并将其最大化。nCmdShow=3。
# SW_SHOWMINIMIZED:激活窗口并将其最小化。nCmdShow=2。
# SW_SHOWMINNOACTIVE:窗口最小化,激活窗口仍然维持激活状态。nCmdShow=7。
# SW_SHOWNA:以窗口原来的状态显示窗口。激活窗口仍然维持激活状态。nCmdShow=8。
# SW_SHOWNOACTIVATE:以窗口最近一次的大小和状态显示窗口。激活窗口仍然维持激活状态。nCmdShow=4。
# SW_SHOWNORMAL:激活并显示一个窗口。如果窗口被最小化或最大化,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。nCmdShow=1。

感谢python win32api win32gui win32con 窗口句柄 发送消息 常用方法 键盘输入,代码主要来自于他

▲.需要注意在windows和mac下接口参数可能有所不同。

win32虽然也可控制键盘,但不如使用PyUserInput的方便。安装PyUserInput教程

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
from pymouse import PyMouse
from pykeyboard import PyKeyboard
#实例化
m = PyMouse()
k = PyKeyboard()

x_dim, y_dim = m.screen_size()
# 鼠标点击 参数:x,y,button=1(左键)、2(右键)、3(中间),次数
m.click(x_dim, y_dim, button=1,n=1)
# 键盘输入 参数:str,间隔
k.type_string('Hello, World!',interval=0)

# 按住一个键
k.press_key('H')
# 松开一个键
k.release_key('H')

# 相当于===>按住并松开,tap一个键
k.tap_key('e')
# tap支持重复的间歇点击键,参数:str,次数,间隔
k.tap_key('l',n=2,interval=5)

#创建组合键===>press_key和release_key结合使用
k.press_key(k.alt_key)
k.tap_key(k.tab_key)
k.release_key(k.alt_key)

# 特殊功能键
k.tap_key(k.function_keys[5]) # Tap F5
k.tap_key(k.numpad_keys['Home']) # Tap 'Home' on the numpad
k.tap_key(k.numpad_keys[5], n=3) # Tap 5 on the numpad, thrice

# Mac系统按键
k.press_keys(['Command','shift','3'])
# Windows系统按键
k.press_keys([k.windows_l_key,'d'])

其中pymouse的PyMouseEvent和pykeyboard的PyKeyboardEvent还可用于监听鼠标和键盘事件的输入
class Clickonacci(PyMouseEvent):
def __init__(self):
PyMouseEvent.__init__(self)
self.fibo = fibo()

def click(self, x, y, button, press):
'''Print Fibonacci numbers when the left click is pressed.'''
if button == 1:
if press:
print('Press times:%d'.format(press))
else: # Exit if any other mouse button used
self.stop()

C = Clickonacci()
C.run()

class TapRecord(PyKeyboardEvent):
def __init__(self):
PyKeyboardEvent.__init__(self)

def tap(self, keycode, character, press):
print(time.time(), keycode, character, press)

t = TapRecord()
t.run()
#这些对象是一个架构用于监听鼠标和键盘的输入;他们除了监听之外不会做任何事,需要继承重构他们#PyKeyboardEvent为编写完成,所以这里是一个继承PyMouseEvent的例子:

附录

查找窗体句柄

貌似在win32编程的世界里,包括窗口到文本框的所有控件就是窗体,所有的窗体都有独立的句柄。要操作任意一个窗体,你都需要找到这个窗体的句柄

1
2
3
4
5
6
FindWindow(lpClassName=None, lpWindowName=None):
自顶层窗口(也就是桌面)开始搜索条件匹配的窗体,并返回这个窗体的句柄。不搜索子窗口、不区分大小写。找不到就返回0
参数:
lpClassName:字符型,是窗体的类名,这个可以在Spy++里找到。
lpWindowName:字符型,是窗口名,也就是标题栏上你能看见的那个标题。
说明:这个函数我们仅能用来找主窗口。
1
2
3
4
5
6
7
8
FindWindowEx(hwndParent=0, hwndChildAfter=0, lpszClass=None, lpszWindow=None);
描述:搜索类名和窗体名匹配的窗体,并返回这个窗体的句柄。不区分大小写,找不到就返回0。
参数:
hwndParent:若不为0,则搜索句柄为hwndParent窗体的子窗体。
hwndChildAfter:若不为0,则按照z-index的顺序从hwndChildAfter向后开始搜索子窗体,否则从第一个子窗体开始搜索。
lpClassName:字符型,是窗体的类名,这个可以在Spy++里找到。
lpWindowName:字符型,是窗口名,也就是标题栏上你能看见的那个标题。
说明:找到了主窗口以后就靠它来定位子窗体啦

另外,python中找回来的句柄都是十进制整型,Spy++里显示的都是十六进制整型,这个要注意下,调试的时候用十六进制输出句柄,如下:print "%x" % (handle)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GetMenu(hwnd)
描述:获取窗口的菜单句柄。
参数:
hwnd:整型,需要获取菜单的窗口的句柄。
说明:获取的是插图中黄色的部分。
GetSubMenu(hMenu, nPos)
描述:获取菜单的下拉菜单或者子菜单。
参数:
hMenu:整型,菜单的句柄,从GetMenu获得。
nPos:整型,下拉菜单或子菜单的的索引,从0算起。
说明:这个可以获取插图中蓝色的部分;如描述所述,这个不仅可以获取本例中的下拉菜单,还可以获取子菜单。
GetMenuItemID(hMenu, nPos)
描述:获取菜单中特定项目的标识符。
参数:
hMenu:整型,包含所需菜单项的菜单句柄,从GetSubMenu获得。
nPos:整型,菜单项的索引,从0算起。
说明:这个获取的就是红色区域中的项目啦,注意,分隔符是被编入索引的,所以Open的索引是2而非1,而Exit的索引是9而非6。
1
2
3
4
5
6
7
8
PostMessage(hWnd, Msg, wParam, lParam)
描述:在消息队列中加入为指定的窗体加入一条消息,并马上返回,不等待线程对消息的处理。
参数:
hWnd:整型,接收消息的窗体句柄
Msg:整型,要发送的消息,这些消息都是windows预先定义好的,可以参见系统定义消息(System-Defined Messages)
wParam:整型,消息的wParam参数
lParam:整型,消息的lParam参数
说明:简单说,就是给指定程序发一个消息,这些消息都用整型编好号,作为windows的常量可以查询的。在这里,我们用的就是win32con这个库里定义的WM_COMMAND这个消息,具体的wParam和lParam是根据消息的不同而不同的。具体请根据MSDN查阅。

查阅MSDN的消息时,会发现有的wParam定义了low word和high word,这是什么呢?wParam的定义是32位整型,high word就是他的31至16位,low word是它的15至0位,如图。有时,一个消息只需要不超过两个参数,那wParam就可以当一个参数用。万一参数多了,wParam就给拆成了两个int16来使用。这种时候在python里记得用16进制把整形表示出来就比较清爽啦。

用了SendMessage而不是PostMessage,其区别就在于我们可以通过SendMessage取得消息的返回信息。因为对于我们要设置文本框信息的WM_SETTEXT信息来说,设置成功将返回True。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SendMessage(hWnd, Msg, wParam, lParam)
描述:在消息队列中加入为指定的窗体加入一条消息,直到窗体处理完信息才返回。
参数:
hWnd:整型,接收消息的窗体句柄
Msg:整型,要发送的消息,这些消息都是windows预先定义好的,可以参见系统定义消息(System-Defined Messages)
wParam:整型,消息的wParam参数
lParam:整型,消息的lParam参数
说明:wParam和IParam根据具体的消息不同而有不同的定义,详情参阅Part 2.
WM_SETTEXT 消息
描述:设置窗体的文本
参数:
wParam:未使用
lParam:一个指针,指向以null结尾的字符串。窗体文本将被设置为该字符串。
返回值:
如果成功设置,则返回1(MSDN原文是返回True)
说明:
上面的定义是直接从MSDN上翻译过来的,在Python的语境里面没有指针,你只需要把变量名作为lParam传入就好了。
另外,请注意编码,包含中文请用gbk编码,否则乱码。
1
2
3
4
5
6
WM_COMMAND 消息
描述:当用户选择了菜单(或按钮等控件的)命令,或控件发送通知到父窗口,或加速键击(accelerator keystroke is translated)时发送。
参数:根据情景不同而不同,在这里属于用户命令,参数配置如下
wParam:HIWORD为0(未使用),LOWORD为控件的ID
lParam:0(未使用)
返回值:如果窗体处理了消息,应返回0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
顺便,如果要获取目标文本框的内容呢,可以使用WM_GETTEXT,如下:

WM_GETTEXT消息:
描述:将窗体的文本内容复制到指定的buffer对象中
参数:
wParam:要复制字符的最大长度,包括截尾的空字节
lParam:用来保存字符串的buffer的指针
返回值:返回复制字符的数量,不包括截尾的空字节
利用win32gui.PyMakeBuffer(len, addr)可以造一个buffer对象,类似python3中的bytearray,lParam的返回值。而利用WM_GETTEXTLENGTH可以获取不含截尾空字节的文本长度的长度,可以用来设置Buffer的长度。完整的示例如下:

buf_size = win32gui.SendMessage(hwnd, win32con.WM_GETTEXTLENGTH, 0, 0) + 1 # 要加上截尾的字节
str_buffer = win32gui.PyMakeBuffer(buf_size) # 生成buffer对象
win32api.SendMessage(hwnd, win32con.WM_GETTEXT, buf_size, str_buffer) # 获取buffer
str = str(str_buffer[:-1]) # 转为字符串
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
Part 4:控件操作B——下拉
至于另存为图片,情况要稍微复杂一点,因为另存为图片的默认选项是BMP,特别不巧,我使用的FaceGen版本保存为BMP有BUG,不能成功保存,所以我们除了定位保存文件的路径以外,还需要对文件类型的下拉组合框(ComboBox进)行操作:

我们假设我们找到了组合框的句柄为CB_handle,我们可以用CB_SETCURSEL消息来更改当前的选项:

CB_SETCURSEL 消息
描述:
参数:
wParam:以0起始的待选选项的索引;如果该值为-1,将从组合框列表中删除当前选项,并使当前选项为空
lParam:未使用。
返回值:
更改选择成功将返回所设置选项的索引号。
只要给组合框发一个CB_SETCURSEL消息,你就会发现下拉列表的选项已经改变了。

这时点保存,你就会发现,这保存的跟之前的一样啊!根本没有变!

问题在哪里?

我们用鼠标或者键盘操作一下,是没有问题的,一旦更保存类型,保存窗口里的预览也会随之变化。所以,除了CB_SETCURSEL以外,一定还缺了点儿什么。

调用Spy++的消息机制查看手动操作,我们的下拉组合框除了渲染和点击,好像没有什么特别值得注意的。

那再看看父窗体呢?好像有点儿不太一样的东西:

CBN_SELENDOK 通知(notification code)
描述:当用户选择了有效的列表项时发送,提示父窗体处理用户的选择。父窗体通过WM_COMMAND消息接收这个通知。
参数:(作为WM_COMMAND的参数)
wParam:LOWORD为组合框的ID. HIWORD为CBN_SELENDOK的值。
lParam:组合框的句柄。
CBN_SELCHANGE 通知(notification code)
描述:当用户更改了列表项的选择时发送,不论用户是通过鼠标选择或是通过方向键选择都会发送此通知。父窗体通过WM_COMMAND消息接收这个通知。
参数:(作为WM_COMMAND的参数)
wParam:LOWORD为组合框的ID. HIWORD为CBN_SELCHANGE的值。
lParam:组合框的句柄。
说明:他们是WM_COMMAND消息wParam的high word(wParam的16-31位,详情参见Part 2)的常数之一,在Python中可以用位移操作将其移动到高位上(a<<16),再用加法加上低位的内容。
继续查MSDN的资料,我们发现,对于一个有效的选择,一定会发送这两个通知,发送完CBN_SELENDOK以后马上发送CBN_SELCHANGE。而且,使用CB_SETCURSEL消息时,CBN_SELCHANGE通知是不会被送达的!

问题就在这里,加上这两个消息之后,就能正常操作下拉菜单了。
1
2
3
4
5
if win32api.SendMessage(CB_handle, win32con.CB_SETCURSEL, format_dict[format], 0) == format_dict[format]:
win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, win32con.CBN_SELENDOK<<16+0, CB_handle) # 控件的ID是0,所以低位直接加0
win32api.SendMessage(PCB_handle, win32con.WM_COMMAND, win32con.CBN_SELCHANGE<<16+0, CB_handle)
else:
raise Exception("Change saving type failed")

Author: Mrli

Link: https://nymrli.top/2018/08/31/python-win32api-win32gui-win32con-PyUserInput实现自动化脚本/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Ubuntu下Sublime配置Python环境使用指导:
NextPost >
Python字典基本操作介绍
CATALOG
  1. 1. python pywin32 PyUserInput实现自动化脚本
    1. 1.1. win32虽然也可控制键盘,但不如使用PyUserInput的方便。安装PyUserInput教程
    2. 1.2. 附录
      1. 1.2.1. 查找窗体句柄