前言
打算使用代码段+有序列表解释的形式记录使用细节,目的是记录下来一套之后可以复刻的自动化流程
pywinauto官方文档Contents — pywinauto 0.6.8 documentation
application
from pywinauto.application import Application
app = Application(backend="uia").start("notepad.exe")
# 附带命令行参数的指令
app = Application(backend='uia').start('notepad.exe "C:\\path\\to\\your\\file.txt"')
- 使用application来定位要自动化操作的应用程序
- Application()创建一个Application对象,设置backend来选择uia技术或win32技术,win32 api支持(MFC、VB6、VCL、简单的WinForms控件和大多数旧的遗留应用程序),uia支持(WinForms,WPF,应用商店,Qt5,浏览器)
- Application().start()可以打开参数指定的应用程序并返回一个Application对象,参数可以传绝对地址,相对地址(登记在环境变量中),或者一个附带命令行参数的指令
window specification
from pywinauto import mouse
dlg = app.window(title_re="[\s\S]*Microsoft Edge[\s\S]*")
# 打印控件树
dlg.print_control_identifiers()
# 方法1
new_page_spec = dlg.child_window(title_re="[\s\S]*新建标签页[\s\S]*", control_type="Button")
# 方法2
new_page_spec = dlg.chile_window(auto_id="view_29")
# 方法1 能确定大致位置后,可以用来点击难定位的控件
rect = new_page_spec.recangle()
center_x = (rect.left + rect.right) // 2
center_y = (rect.top + rect.bottom) // 2
mouse.move(coorders=(center_x, center_y))
mouse.click(coorders=(center_x, center_y))
# 方法2
new_page_spec.click_input()
- window()定位窗口 用title或者title_re找到窗口就好,window()返回WindowSpecification对象
- child_window用来找窗口下具体的控件,控件的定位方法可以用title_re, control_type, auto_id, class_name, found_index等来定位;其中found_index是当匹配到多个控件时决定 所选控件的索引
- auto_id可以精准定位到控件,而且相对稳定,长期内不会变化,但是有些控件没有auto_id(AutomationId)
- 也可以引用pywinauto.mouse控制鼠标对特定坐标进行点击
- 知晓桌面程序内部的控件信息可以使用pywinauto自带的方法(但是打印出来的信息有限,而且较深的控件树可能没办法全部展示到输出中,不知道怎么用比较好);win自带的Inspect.exe是windows SDK提供的工具之一,可以到Windows SDK - Windows app development | Microsoft Developer这个网址,只勾选"Debugging Tools for Windows"来下载,优点是可以像F12一样用鼠标来直接将眼见的某一控件在Inspect中从控件树里定位出来(跟踪焦点)。
watch focus/watch cursor
- (跟踪焦点)watch focus ,图中第一个红框标记出来的虚线正方形图标,控制跟踪焦点功能的开关,打开以后tree view和data view会根据鼠标点击的焦点切换显示对象。
- (跟踪光标)watch cursor,图中第二个红框标记出来的鼠标图标,控制跟踪光标的开关,打开以后tree view和data view根据鼠标停留的位置切换显示对象
- 跟踪光标不一定能准确地定位到想要的控件,比如期望的控件是按钮,实际上定位的是按钮上的文本(不过点击操作的话大概是一样的?实际没试过)。
- 可以通过关闭跟踪功能来锁住想看的控件信息
wrapper
spec.wrapper_object().minimize() # while debugging
spec.minimize() # in production
none_spec=app.child_window(title='我不存在') #不会报错
none_spec.wrapper_object() #ElementNotFound
# 常用操作
spec.click() #点击 页面上可能看不到该控件 鼠标不会移动到控件上面 无法触发鼠标相关ui
spec.click_input() #点击 鼠标瞬移到控件上并点击
rect = spec.rectangle() #获取矩形信息 包括rect.left rect.right rect.top rect.bottom在内的绝对坐标 以屏幕像素为单位
spec.minimize() #最小化
spec.restore() #将窗口置顶
spec.maximize() #最大化窗口
spec.exists() #判断控件是否存在 十分好用
spec.is_enabled() #看控件是否启用 可以用在分页表格,不断点击下一页直到按钮不可用(即全部点完)
spec.window_text() #看控件标题 可以用在时间等可以用正则确定的控件上,定位后取页面上的实际数值
- window()和child_window() 返回的是WindowSpecification对象
- WindowSpecification需要通过wrapper_object()来操作实际的窗口(或者控件),该方法返回一个wrapper对象
- python允许省略wrapper_object()的调用,所以上述1,2行效果是一样的
- 当获取一个页面上不存在的控件时,child_window()不会报错,wrapper_object()会报ElementNotFound错误
control_type
control_type参数值 | 中文名 | Inspect内部显示为 |
---|---|---|
Button | 按钮 | UIA_ButtonControlTypeId |
Text | 文本 | UIA_TextControlTypeId |
List | 列表 | UIA_ListControlTypeId |
ListItem | 列表项 | UIA_ListItemControlTypeId |
Hyperlink | 链接 | UIA_HyperlinkControlTypeId |
Menu | 菜单 | UIA_MenuControlTypeId |
RadioButton | 单选按钮 | UIA_RadioButtonControlTypeId |
Edit | 编辑 | UIA_EditControlTypeId |
遇到问题及使用技巧
def getMidPoint(rect):
return (rect.left + rect.right)//2, (rect.top + rect.bottom)//2
# 4 点击溢出ListItem
# spec为带滚轮的List控件
def getMidPoint(rect):
return (rect.right+rect.left)//2, (rect.top+rect.bottom)//2
item = spec.child_window(title=title, control_type='ListItem')
spec_rect = spec.rectangle()
if item.exists():
rect = item.rectangle()
while rect.left == 0: # 当ListItem在窗口中溢出时,获取到的rectangle为(0,0,0,0)
mouse.move(coords=(spec_rect.right - 4, spec_rect.bottom - 4))
mouse.click(coords=(spec_rect.right - 4, spec_rect.bottom - 4)) # 点击滚动槽向下滚动
item = spec.child_window(title=title, control_type='ListItem')
rect = item.rectangle()
mouse.move(getMidPoint(rect)) # while循环结束 鼠标会移动到溢出ListItem上
# 5
# 5-2
for i in range(n):
item = spec.child_window(title="重复窗口名", found_index=i)
spec_rect = spec.rectangle()
rect = item.rectangle()
while rect.left == 0:
mouse.move(coords=getMidPoint(spec_rect))
mouse.scroll(coords=getMidPoint(spec_rect), wheel_dist=-1) # -1是向下滚动滚轮一次 比点击滚动槽稳定一些
item = spec.child_window(title="重复窗口名", found_index=i) # ElementNotFoundError
rect = item.rectangle()
item.click_input()
#5-4
for i in range(10):
print(f"{i} {spec.child_window(title="重复窗口名", control_type='ListItem', found_index=i).exists()}")
# print:
# 0 True
# 1 True
# 2 True
# 3 True
# 4 True
# 5 True
# 6 True
# 7 False
# 8 False
# 9 False
#5-5
for i in range(n):
item = spec.child_window(title="重复窗口名", found_index=i)
spec_rect = spec.rectangle()
rect = item.rectangle()
scroll = rect.left == 0
while scroll:
mouse.move(coords=getMidPoint(spec_rect))
mouse.scroll(coords=getMidPoint(spec_rect), wheel_dist=-1)
rect = item.rectangle()
# 实际子窗口可能有不存在,或者存在但是rect.left==0 需要考虑两种需要滚动的情况
# 还要考虑到子窗口完全显示出来以后再点击,否则点击的位置可能在spec窗口之外
scroll = (not spec.child_window(title="重复窗口名", found_index=i).exists()
or rect.left == 0 or rect.bottom > spec_rect.bottom)
mouse.move(coords=getMidPoint(rect))
mouse.click(button='left', coords=getMidPoint(rect))
WindowSpecification.child_window()不只能搜索到当前对象的直接子层的对象,也能搜索到更深层次的对象。在实际使用时可能会遇到各种各样的问题,当无法搜索到想要定位的对象时,需要检查自己是否有如下问题:
- 名称是否写对 使用title参数时,要求控件的Name要和参数值完全匹配,当识别不到相应控件时,检查控件Name前后是否包含空格字符。(建议使用title时直接从Inspect.exe中复制Name值到代码中)
- 匹配到多个对象 当匹配到多个对象的时候,直接执行操作会报错(比如There are 2 elements that match the criteria)
- 搜索条件太宽泛时会报错pywinauto.timings.TimeoutError
- (点击溢出ListItem)带有滚动条的List中,所有ListItem都在控件树中,即item_spec.exists() == True,但是溢出在窗口外的ListItem,无法使用item_spec.click_input()点击;可以使用rectangle()来判断ListItem是否溢出,存在但页面上找不到的元素rectangle()会返回(0,0,0,0),如果ListItem确定溢出则想办法向下滚动;对于带有滚动条的窗口,可以通过点击滚动槽的方式将窗口向下滚动(主窗口可以直接用pyautogui键入space);实在滚动不了的窗口就考虑用click()直接点击,不移动鼠标,缺点是需要相关鼠标ui时无法触发。
- (带有滚动条的窗口中点击满足同一规则的多个子窗口的问题)
- 需求:我希望在一个List中,依次点击同一类的List,由于List过长,所以我需要边检查某个ListItem是否存在边向下滚动滚动条
- 想法:通过found_index=None来访问子窗口,并捕获There are n elements that match the criteria错误,使用re库来获得重复的子窗口数n(貌似pywinauto没有提供直接获取重复元素数的方法);遍历i从0到n-1,访问found_index=i的子窗口;在当某一子窗口的rectangle().left==0时,说明子窗口不在当前窗口中,需要向下滚动滚动条,并重新获取这个子窗口
- 问题:当重复地给item赋值时,再次执行的child_window因为滚动过窗口(可能导致了控件树变化),无法识别出新显现出的子窗口。比如捕获到There are 7 elements, 但窗口只放得下前3个子窗口,当滚动过窗口后,再次执行chile_window则只能匹配到这3个窗口,当调用child_window(title='重复窗口名', found_index=3)即第四个子窗口时,会报loading:found_index is specified as 3, but 3 window/s found错误
- 验证:在没有滚动滚动条,即spec窗口初次加载时,对7个子窗口进行存在验证,发现此时全部子窗口都是存在的,区别只是rectangle() == (0,0,0,0)
- 实操:避免对item重复赋值,向下滚动并对item调用rectangle()来判断子窗口是否出现
- 使用aa = ord('从inspect Name字段复制下来的某特殊字符')来查看特殊字符的编码,并用unicode格式输出:f"U{aa:04X}"; 得到Unicode编码以后,用title='\uxxxx'即可找到对应特殊字符的控件