资讯专栏INFORMATION COLUMN

Windows桌面程序自动化控制之uiautomation模块全面讲解

GT / 13437人阅读

摘要:大家好,我是小小明,经过个月断断续续的整理,模块桌面自动化控制的系统教程终于成型出炉了。模块所支持的剪切板操作的功能也远远超过常规的专门用于剪切板复制粘贴的库。很多程序没有实现,所以不支持自动化。

大家好,我是小小明,经过2个月断断续续的整理,uiautomation模块桌面自动化控制的系统教程终于成型出炉了。下面是本文档的目录大纲,大家可以感受一下:

简介

功能介绍

本文档大纲:

可以看到uiautomation模块除了核心功能UI控件的控制截图数据提取外,还支持全局热键注册剪切板操作管理员权限提权

在常规的模拟鼠标和键盘操作,我们一般使用pyautogui,uiautomation模块不仅能直接支持这些操作,还能通过控件定位方式直接定位到目标控件的位置,而不需要自己去获取对应坐标位置。uiautomation模块不仅支持任意坐标位置截图,还支持目标控件的截图,缺点在于截取产生的图片对象难以直接与PIL库配合,只能导出文件后让PIL图像处理库重新读取。对于能够获取到其ScrollItemPattern对象的控件还可以通过ScrollIntoView方法进行视图定位,与游览器的元素定位效果几乎一致。

在常规的热键功能,我们一般使用pynput实现,但现在有了uiautomation模块,热键注册会比pynput更简单功能更强。uiautomation模块所支持的剪切板操作的功能也远远超过常规的专门用于剪切板复制粘贴的库。更牛的是uiautomation模块能直接支持让你的python程序实现管理员提权。

基本上这个库的功能超过好几个专门针对某个功能的库。我们可以看看一下这个库自动化操作过程的动图效果:

掌握这个框架之后,你能够实现的自动化效果远不止如此。

这么优秀的框架你是否心动了呢?心动不如行动,学起来吧!!!

基本原理

uiautomation模块项目地址:https://github.com/yinkaisheng/Python-UIAutomation-for-Windows

uiautomation是yinkaisheng业余时间开发一个模块。封装了微软UIAutomation API,支持自动化Win32,MFC,WPF,Modern UI(Metro UI), Qt, IE, Firefox(version<=56 or >=60), Chrome谷歌游览器和基于Electron开发的应用程序(加启动参数–force-renderer-accessibility也能支持UIAutomation被自动化).

uiautomation只支持Python 3版本,依赖comtypes和typing这两个包,但Python不要使用3.7.6和3.8.1这两个版本,comtypes在这两个版本中不能正常工作(issue)。

UIAutomation的工作原理:

UIAutomation操作程序时会给程序发送WM_GETOBJECT消息,如果程序处理WM_GETOBJECT消息,实现UI Automation Provider,并调用函数
UiaReturnRawElementProvider(HWND hwnd,WPARAM wparam,LPARAM lparam,IRawElementProviderSimple *el),此程序就支持UIAutomation。

IRawElementProviderSimple 就是 UI Automation Provider,包含了控件的各种信息,如Name,ClassName,ContorlType,坐标等。

UIAutomation 根据程序返回的 IRawElementProviderSimple,就能遍历程序的控件,得到控件各种属性,进行自动化操作。若程序没有处理WM_GETOBJECT或没有实现UIAutomation Provider,UIAutomation则无法识别这些程序内的控件,不支持自动化。
很多DirectUI程序没有实现UIAutomation Provider,所以不支持自动化。

关于各控件所支持的控件模式,可参考:

https://docs.microsoft.com/zh-cn/windows/win32/winauto/uiauto-controlpatternmapping

在使用uiautomation模块前需要先安装:

pip install uiautomation

安装后会在python安装目录下的Scripts目录下得到一个automation.py脚本,可以使用它来准确获取目标窗口的控件结构信息。

automation.py脚本也可以从https://github.com/yinkaisheng/Python-UIAutomation-for-Windows/raw/master/automation.py下载。

当然使用windows自带的inspect.exe图形化工具来观察控件的树形结构更加,通过everything可以很快在系统中找到该工具。

⚠️ :inspect.exe工具获取到的控件类型可能与automation.py脚本打印的结果不太一样,如果发现控件实际不存在,要以automation.py脚本打印的结果为准。

控件控制入门:记事本操作

控件分析与可用参数

首先打开记事本窗口,并设置窗口前置:

import subprocessimport uiautomation as autosubprocess.Popen("notepad.exe")# 从桌面的第一层子控件中找到记事本程序的窗口WindowControlnotepadWindow = auto.WindowControl(searchDepth=1, ClassName="Notepad")print(notepadWindow.Name)# 设置窗口前置notepadWindow.SetTopmost(True)

运行上述代码后,会打开一个窗口前置的记事本程序。

控件可用参数说明:

  • searchFromControl = None:从哪个控件开始查找,如果为None,从根控件Desktop开始查找
  • searchDepth = 0xFFFFFFFF: 搜索深度
  • searchInterval = SEARCH_INTERVAL:搜索间隔
  • foundIndex = 1 :搜索到的满足搜索条件的控件索引,索引从1开始
  • Name:控件名字
  • SubName :控件部分名字
  • RegexName:使用re.match匹配符合正则表达式的名字,Name,SubName,RegexName只能使用一个,不能同时使用
  • ClassName :类名字
  • AutomationId: 控件AutomationId
  • ControlType :控件类型
  • Depth:控件相对于searchFromControl的精确深度
  • Compare:自定义比较函数function(control: Control, depth: int)->bool

searchDepth和Depth的区别:

searchDepth在指定的深度范围内(包括1~searchDepth层中的所有子孙控件)搜索第一个满足搜索条件的控件

Depth只在Depth所在的深度(如果Depth>1,排除1~searchDepth-1层中的所有子孙控件)搜索第一个满足搜索条件的控件

为了进一步操作该程序,我们可以使用inspect.exe工具或automation.py脚本分析控件结构。

通过inspect.exe工具分析控件时可以看到记事本的编辑区类型为DocumentControl

但uiautomation实际使用该类型查找控件时却会找不到控件报错。

下面我们使用automation.py脚本来分析目标窗口,我的Python安装目录为D:/Miniconda3所以automation.py脚本会存在于D:/Miniconda3/Scripts/automation.py

查看帮助信息:

>python D:/Miniconda3/Scripts/automation.py -hUIAutomation 2.0.15 (Python 3.7.4, 64 bit)usage-h      show command help-t      delay time, default 3 seconds, begin to enumerate after Value seconds, this must be an integer        you can delay a few seconds and make a window active so automation can enumerate the active window-d      enumerate tree depth, this must be an integer, if it is null, enumerate the whole tree-r      enumerate from root:Desktop window, if it is null, enumerate from foreground window-f      enumerate from focused control, if it is null, enumerate from foreground window-c      enumerate the control under cursor, if depth is < 0, enumerate from its ancestor up to depth-a      show ancestors of the control under cursor-n      show control full name, if it is null, show first 30 characters of control"s name in console,        always show full name in log file @AutomationLog.txt-p      show process id of controlsif UnicodeError or LookupError occurred when printing,try to change the active code page of console window by using chcp or see the log file @AutomationLog.txtchcp, get current active code pagechcp 936, set active code page to gbkchcp 65001, set active code page to utf-8examples:automation.py -t3automation.py -t3 -r -d1 -m -nautomation.py -c -t3

下面为了后续命令简化一点,我先将automation.py文件复制到cmd所在的当前目录。

执行以下命令:

python automation.py -t1 -d1

立马在1秒内将鼠标激活记事本窗口,可以看到控制台打印:

可以看到第一个控件的类型为EditControl

下面将鼠标移动到记事本的编辑框内之后,执行:

python automation.py -t0 -c

就得到编辑器的全部子控件信息:

下面我们使用uiautomation向记事本输入文本。

首先获取输入框:

edit = notepadWindow.EditControl()

方法1-使用EditControl支持的ValuePattern:

edit.GetValuePattern().SetValue("方法1")

该方法直接修改编辑框的文本内容。

方法2-发送按键指令输入文本:

edit.SendKeys("方法2")

该方法的输入效果比较像打字机输入。

方法3-复制文本后到剪切板粘贴:

auto.SetClipboardText("方法3")edit.SendKeys("{Ctrl}v")

获取当前编辑框中的文本:

print(edit.GetValuePattern().Value)

最后我们点击标题栏的关闭按钮(可以通过索引或名称查找目标按钮):

# 通过标题栏第三个按钮找到关闭按钮notepadWindow.TitleBarControl(Depth=1).ButtonControl(foundIndex=3).Click()

或:

# 通过标题栏查找名称为关闭的按钮notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name="关闭").Click()

最后保存并关闭:

notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name="关闭").Click()# 确认保存auto.SendKeys("{ALT}s")# 输入文件名,并快捷键点击保存auto.SendKeys("自动保存{ALT}s")# 如果弹出文件名冲突提示,则确认覆盖auto.SendKeys("{ALT}y")

完整代码:

import subprocessimport uiautomation as autosubprocess.Popen("notepad.exe")# 首先从桌面的第一层子控件中找到记事本程序的窗口WindowControl,再从这个窗口查找子控件notepadWindow = auto.WindowControl(searchDepth=1, ClassName="Notepad")print(notepadWindow.Name)# 设置窗口前置notepadWindow.SetTopmost(True)# 输入文本edit = notepadWindow.EditControl()auto.SetClipboardText("WIN98中的“98”是什么意思?")edit.SendKeys("{Ctrl}v")# 获取文本print("编辑框内容:",edit.GetValuePattern().Value)# 通过标题栏查找名称为关闭的按钮notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name="关闭").Click()# 确认保存auto.SendKeys("{ALT}s")# 输入文件名,并快捷键点击保存auto.SendKeys("自动保存{ALT}s")# 如果弹出文件名冲突提示,则确认覆盖auto.SendKeys("{ALT}y")

控件延迟搜索机制

底层COM对象方法:

⚠️ Control.Element返回IUIAutomation底层COM对象IUIAutomationElement, 基本上Control的所有属性或方法都是通过调用IUIAutomationElement COM API和Win32 API实现的。

延迟搜索控件:

当我们创建一个Control对象时,uiautomation并不会马上开始搜索控件,而是当使用其属性或方法,并且内部的Control.Element是None时uiautomation才开始搜索控件。如果在uiautomation.TIME_OUT_SECOND(默认为10)秒内找不到控件,uiautomation就会抛出一个LookupError异常。

也可以调用Control.Refind立马或重新开始搜索控件,例如:

edit = notepadWindow.EditControl()edit.Refind()
True

但是当控件不存在时,则会报出错误。

为了避免函数最终抛出异常,可以调用Control.Exists(maxSearchSeconds, searchIntervalSeconds, printIfNotExist)检查目标控件是否存在:

edit = notepadWindow.EditControl()edit.Exists()
True

Control.Refind和Control.Exists均会使Control.Element无效并触发重新搜索逻辑。

另一种检查目标控件是否存在的方法是auto.WaitForExist(control, timeout)

下面继续以记事本为对象演示这个机制。首先打开第一个记事本并获取输入控件:

import subprocessimport uiautomation as autoauto.uiautomation.SetGlobalSearchTimeout(2)  # 设置全局搜索超时时间为2秒subprocess.Popen("notepad.exe")window = auto.WindowControl(searchDepth=1, ClassName="Notepad")# 创建控件对象时并不会开始搜索控件edit = window.EditControl()

此时,控件window和edit还没有开始搜索,内部Control.Element的值为None。

第一次调用SendKeys时,才开始搜索控件window和edit:

# 第一次调用SendKeys时, 才开始搜索控件window和editedit.SendKeys("第一次调用")

搜索完毕后,才会开始执行发送按键方法,此时Control.Element有效。

第二次调用SendKeys不会触发搜索(Control.Element不为None):

edit.SendKeys("第二次调用")

然后我们清空输入的内容,并关闭刚才打开的记事本:

edit.GetValuePattern().SetValue("")window.GetWindowPattern().Close()

关闭后,此时虽然window和edit有值却已经无效了。

再次打开记事本,就必须重新搜索控件(否则无法操作新启动的窗口):

subprocess.Popen("notepad.exe")  # 运行第二个Notepadwindow.Refind()  # 必须重新搜索edit.Refind()  # 必须重新搜索

然后可以将其关闭:

window.GetWindowPattern().Close()

window和edit的Element再次失效。

Exists方法则既可以判断控件是否存在,也可以触发重新搜索:

subprocess.Popen("notepad.exe")  # 运行第三个Notepadif window.Exists(maxSearchSeconds=1.5, searchIntervalSeconds=0.3):  # 触发重新搜索    if edit.Exists(maxSearchSeconds=1.5):  # 触发重新搜索        edit.SendKeys("third notepad")  # 之前的Exists保证edit.Element有效        edit.SendKeys("{Ctrl}a{Del}")    window.GetWindowPattern().Close()else:    print("窗口1.5秒内未找到")

示例:连续打开三个记事本并关闭

完整代码与注释:

import subprocessimport uiautomation as auto# 设置全局搜索超时2秒auto.uiautomation.SetGlobalSearchTimeout(2)  subprocess.Popen("notepad.exe")window = auto.WindowControl(searchDepth=1, ClassName="Notepad")window.SetTopmost(True)edit = window.EditControl()# 当第一次调用SendKeys时, uiautomation开始在2秒内搜索控件window和edit# 因为SendKeys内部会间接调用Control.Element并且Control.Element值是None# 如果在15秒内找不到window和edit,会抛出LookupError异常try:    edit.SendKeys("第一个记事本程序")except LookupError as ex:    print("第一个记事本窗口在2秒内未找到")# 第二次调用SendKeys不会触发搜索, 之前的调用保证Control.Element有效edit.SendKeys("{Ctrl}a{Del}")# 关闭第一个Notepad, window和edit的Element虽然有值,但是无效了window.GetWindowPattern().Close()subprocess.Popen("notepad.exe")  # 运行第二个Notepadwindow.Refind()  # 必须重新搜索edit.Refind()  # 必须重新搜索edit.SendKeys("第二个记事本程序")edit.SendKeys("{Ctrl}a{Del}")window.GetWindowPattern().Close()  # 关闭第二个Notepad, window和edit的Element虽然有值,但是再次无效了subprocess.Popen("notepad.exe")  # 运行第三个Notepadif window.Exists(3, 1):  # 触发重新搜索    if edit.Exists(3):  # 触发重新搜索        edit.SendKeys("第三个记事本程序")  # 之前的Exists保证edit.Element有效        edit.SendKeys("{Ctrl}a{Del}")    window.GetWindowPattern().Close()else:    print("第三个记事本程序在三秒内不存在")

UIAutomation的常见功能

导包:

import uiautomation as auto

基本方法

显示桌面(相当于点击桌面右下角的按钮):

auto.ShowDesktop()

获取uiautomation已运行的时间:

auto.ProcessTime()

判断两个控件是否一致:

auto.ControlsAreSame(control1, control2)

鼠标点击指定坐标:

auto.Click(x, y)

右键单击:

auto.RightClick(x, y)

鼠标拖拽(鼠标从(x1,y1)位置按下鼠标拖动到(x2,y2)位置):

auto.DragDrop(x1, y1, x2, y2, moveSpeed=1)

moveSpeed参数决定了移动的速度。

关于鼠标滚轮操作详见后面的实例,被找到的控件调用鼠标点击方法不需要传入坐标。

获取窗口对象

获取桌面对象:

c = auto.GetRootControl()

返回运行当前python程序的控制台窗口对象:

cmdWindow = auto.GetConsoleWindow()

没有找到则返回None。

获取当前鼠标位置对应的窗口:

c = auto.ControlFromCursor().GetTopLevelControl()

ControlFromCursor返回了当前鼠标位置的控件,GetTopLevelControl获取了该控件对应的顶级窗口对象。

获取当前激活窗口对应的对象:

c = auto.GetForegroundControl().GetTopLevelControl()

控件查找方法

获取所有的子控件:

control.GetChildren()

获取首个子控件:

control.GetFirstChildControl()

获取最后一个子控件:

control.GetLastChildControl()

获取下一个兄弟控件:

control.GetNextSiblingControl()

获取前一个兄弟控件:

control.GetPreviousSiblingControl()

获取父控件:

control.GetParentControl()

获取顶层窗口控件:

control.GetTopLevelControl()

获取满足指定条件的祖先控件:

control.GetAncestorControl(func)

传入的函数参数要求:function(control: Control, depth: int) -> bool

当函数返回True时表示找到控件并返回,例如以下方法几乎可以得与GetTopLevelControl()相同的结果:

control.GetAncestorControl(lambda c, d: isinstance(c, auto.WindowControl))

窗口属性调整

假设获取到一个窗口对象:

win = auto.ControlFromCursor().GetTopLevelControl()

获取本地窗口句柄:

win.NativeWindowHandle

根据本地窗口句柄获取窗口控件对象:

win2 = auto.ControlFromHandle(win.NativeWindowHandle)

经测试,对象一致:

auto.ControlsAreSame(win, win2)
True

隐藏窗口:

win.Hide(0)

显示窗口:

win.Show(0)

窗口最小化:

win.Minimize()

窗口最大化:

win.Maximize()

判断窗口是否已经被最小化:

auto.IsIconic(win.NativeWindowHandle)

IsIconic进支持传入本地窗口句柄。

将最小化的窗口的恢复显示:

修改窗口的位置和大小,例如将某个窗口调整到最后一个屏幕的一半:

rects = auto.GetMonitorsRect()rect = rects[-1]win.MoveWindow(rect.left, rect.top,               rect.width()//2, rect.height()-30)

不过这种调整方法对于cmd这种命令行窗口无效,只能在获取其TransformPattern对象后,调用MoveResize方法来实现。上面的MoveWindow等价于:

transform_win = win.GetTransformPattern()transform_win.Move(rect.left, rect.top)transform_win.Resize(rect.width()//2, rect.height()-30)

移动窗口到屏幕中心位置:

win.MoveToCenter()

窗口置顶:

window.SetTopmost(True)

获取窗口标题并修改窗口标题:

win.SetWindowText(win.GetWindowText()+"|小小明")

获取运行当前python程序控制台窗口的标题:

auto.GetConsoleTitle()# auto.GetConsoleOriginalTitle()

设置运行当前python程序控制台窗口的标题:

auto.SetConsoleTitle("自定义控制台标题")

WalkTree遍历子控件

除了auto.WalkTree遍历目标控件外,还有auto.WalkControl遍历控件,区别在于auto.WalkTree必须传入自定义函数指定遍历的行为。auto.WalkControl将会在后面涉及可折叠类型的控件遍历时进行演示,下面给出一个简单的通过WalkTree遍历桌面的示例:

import uiautomation as autodef GetFirstChild(control):    return control.GetFirstChildControl()def GetNextSibling(control):    return control.GetNextSiblingControl()desktop = auto.GetRootControl()for control, depth in auto.WalkTree(desktop, getFirstChild=GetFirstChild, getNextSibling=GetNextSibling,                                    includeTop=True, maxDepth=2):    if not control.Name:        continue    print(" " * depth * 4, control.Name)

maxDepth指定了遍历深度,除了指定这两个方法以外还可以只转入getChildren方法:

def GetChildren(control):    return control.GetChildren()for control, depth, remain in auto.WalkTree(desktop,                                            getChildren=GetChildren,                                            includeTop=True,                                            maxDepth=2):    if not control.Name:        continue    print(" " * depth * 4, control.Name)

结果过滤的方逻辑我们还可以写到yieldCondition的传入函数中:

def yieldCondition(control, depth):    if control.Name:        return Truefor control, depth, remain in auto.WalkTree(desktop,                                            getChildren=GetChildren,                                            yieldCondition=yieldCondition,                                            includeTop=True,                                            maxDepth=2):    print(" " * depth * 4, control.Name)

在我电脑当前执行结果均为:

 桌面 1     任务栏         开始         在这里输入你要搜索的内容         开始         在这里输入你要搜索的内容         系统时钟, 23:02, ‎2021/‎11/‎15     test - Jupyter Notebook - 360安全浏览器 13.1         Chrome Legacy Window     一文掌握uiautomation的经典案例.md• - Typora         Typora     UIAutomation_demos – clipboard_test.py PyCharm     Program Manager

WalkTree的规则是当设置getChildren函数时,忽略getFirstChild和getNextSibling,否则使用这两个函数。设置yieldCondition函数时则开启额外的过滤。

甚至可以使用WalkTree方法计算全排列问题:

def NextPermutations(aTuple):    left, permutation = aTuple    ret = []    for i, item in enumerate(left):        nextLeft = left[:]        del nextLeft[i]        nextPermutation = permutation + [item]        ret.append((nextLeft, nextPermutation))    return retuniqueItems = list("abc")n = len(uniqueItems)count = 0for (left, permutation), depth, remain in auto.WalkTree((uniqueItems, []), NextPermutations,                                                        yieldCondition=lambda c, d: d == n):    count += 1    print
            
                     
             
               

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/123921.html

相关文章

  • 如何用Python下载百度指数的数据

    摘要:大家好我是小小明,今天给大家演示如何使用直接采集百度指数的数据。本文不演示如何使用自动化工具采集百度指数,为了采集更简单将直接读取并解析接口。 大家好我是小小明,今...

    crossea 评论0 收藏0
  • 超全的App 测试工具大全,收藏这篇就够了【附带官网|GitHub地址】

    摘要:三性能测试工具官网介绍腾讯开源的的随身调测平台,支持和。官网介绍腾讯游戏部门开发的移动全平台性能测试分析工具平台。百度的服务目前主要为收费服务。 随着移动互联网的高速发展,App 应用非常火,测试工程师也会接触到各种 app 应用。除了人工测试之外,也可以通过一些测试工具来提高我们的测试效率...

    MRZYD 评论0 收藏0
  • 使用Python控制手机(一)

    摘要:使用包控制手机时,需要在手机上安装在电脑中使用弹出运行框,在框内输入调用命令行窗口。具备较多功能,在此不再赘述,百度具有较多资料。调试工具用于查看手机当前状态用浏览器控制手机调试手机,还可以实时的查找控件中的属性以及其它属性值等等。 1. 配置Python环境变量 Python环境变...

    ralap 评论0 收藏0
  • 如何通过动化测试 应对App的频繁迭代

    摘要:自动化测试的流程是搭建框架设计测试用例编写脚本进行测试得出结果。它支持所有的事件操作,通过模拟器也能运行测试,很多自动化测试脚本编写客户端都封装了,包括百度云测腾讯优测等。 移动互联网在改变我们生活的同时,给各行各业都带来了翻天覆地的变化,就拿移动应用开发来说,传统软件的迭代周期往往以年、月计,而到了手机上,几乎每周都有新版本上线,一些勤快的应用甚至三五天就更新一个版本。如此频繁的迭代...

    YanceyOfficial 评论0 收藏0
  • RustDesk开源免费远程桌面软件支持Windows,苹果,安卓,Linux-替代TeamView

    摘要:今天要说的,是国人开发的一个免费开源的远程桌面软件,支持苹果安卓,全平台远程桌面软件。远程桌面软件,开箱即用,无需任何配置,完美替代和,支持电脑控制电脑,安卓控制电脑电脑控制安卓安卓控制安卓等。一说到远程桌面软件,自然首先想到的是TeamViewer,但是TeamViewer是商业软件,个人用户虽然是可以免费使用的,但是限制也多。今天要说的RustDesk,是国人开发的一个免费开源的远程桌面...

    番茄西红柿 评论0 收藏2637

发表评论

0条评论

最新活动
阅读需要支付1元查看
<