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

Posted zhangliang0000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows桌面程序自动化控制之uiautomation模块全面讲解相关的知识,希望对你有一定的参考价值。

文章目录

  • 简介

  • 功能介绍

  • 基本原理

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

  • 控件分析与可用参数

  • 控件延迟搜索机制

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

  • UIAutomation的常见功能

  • 基本方法

  • 获取窗口对象

  • 控件查找方法

  • 窗口属性调整

  • WalkTree遍历子控件

  • Bitmap位图对象的使用

  • 对多个显示器分别截屏

  • 剪切板操作

  • 自带的Logger日志输出类

  • 全局热键与多线程

  • 管理员提权

  • 通过实例学习UI自动化

  • 控制win10计算器自动计算

  • 窗口的拖拽与缩放

  • 管理员提权操作并读取设备管理器栏目数据

  • 记事本文本输入与字体调整

  • wireshark抓包数据读取

  • PDF目录折叠展开提取器

简介

功能介绍


本文档大纲:

可以看到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 subprocess
import uiautomation as auto

subprocess.Popen('notepad.exe')# 从桌面的第一层子控件中找到记事本程序的窗口WindowControl
notepadWindow = 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:Miniconda3Scriptsautomation.py

查看帮助信息:

>python D:Miniconda3Scriptsautomation.py -h
UIAutomation 2.0.15 (Python 3.7.4, 64 bit)
usage
-h      show commandhelp
-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 controls

if 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.txt
chcp, get current active code page
chcp 936, set active code page to gbk
chcp 65001, set active code page to utf-8

examples:
automation.py -t3
automation.py -t3 -r -d1 -m -n
automation.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('Ctrlv')

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

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('ALTs')# 输入文件名,并快捷键点击保存
auto.SendKeys('自动保存ALTs')# 如果弹出文件名冲突提示,则确认覆盖
auto.SendKeys('ALTy')

完整代码:

import subprocess
import uiautomation as auto

subprocess.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('Ctrlv')# 获取文本print("编辑框内容:",edit.GetValuePattern().Value)# 通过标题栏查找名称为关闭的按钮
notepadWindow.TitleBarControl(Depth=1).ButtonControl(searchDepth=1, Name='关闭').Click()# 确认保存
auto.SendKeys('ALTs')# 输入文件名,并快捷键点击保存
auto.SendKeys('自动保存ALTs')# 如果弹出文件名冲突提示,则确认覆盖
auto.SendKeys('ALTy')

控件延迟搜索机制


底层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 subprocess
import uiautomation as auto
auto.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和edit
edit.SendKeys('第一次调用')

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

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

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

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

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

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

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

subprocess.Popen('notepad.exe')# 运行第二个Notepad
window.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('CtrlaDel')
    window.GetWindowPattern().Close()else:print("窗口1.5秒内未找到")

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


完整代码与注释:

import subprocess
import 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('CtrlaDel')# 关闭第一个Notepad, window和edit的Element虽然有值,但是无效了
window.GetWindowPattern().Close()

subprocess.Popen('notepad.exe')# 运行第二个Notepad
window.Refind()# 必须重新搜索
edit.Refind()# 必须重新搜索
edit.SendKeys('第二个记事本程序')
edit.SendKeys('CtrlaDel')
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('CtrlaDel')
    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 auto


defGetFirstChild(control):return control.GetFirstChildControl()defGetNextSibling(control):return control.GetNextSiblingControl()


desktop = auto.GetRootControl()for control, depth in auto.WalkTree(desktop, getFirstChild=GetFirstChild, getNextSibling=GetNextSibling,
                                    includeTop=True, maxDepth=2):ifnot control.Name:continueprint(' '* depth *4, control.Name)

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

defGetChildren(control):return control.GetChildren()for control, depth, remain in auto.WalkTree(desktop,
                                            getChildren=GetChildren,
                                            includeTop=True,
                                            maxDepth=2):ifnot control.Name:continueprint(' '* depth *4, control.Name)

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

defyieldCondition(control, depth):if control.Name:returnTruefor 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方法计算全排列问题:

defNextPermutations(aTuple):
    left, permutation = aTuple
    ret =[]for i, item inenumerate(left):
        nextLeft = left[:]del nextLeft[i]
        nextPermutation = permutation +[item]
        ret.append((nextLeft, nextPermutation))return ret


uniqueItems =list("abc")
n =len(uniqueItems)
count =0for(left, permutation), depth, remain in auto.WalkTree((uniqueItems,[]), NextPermutations,
                                                        yieldCondition=lambda c, d: d == n):
    count +=1print(count, permutation)
1 ['a', 'b', 'c']
2 ['a', 'c', 'b']
3 ['b', 'a', 'c']
4 ['b', 'c', 'a']
5 ['c', 'a', 'b']
6 ['c', 'b', 'a']

可以看到已经顺利的计算出正确的结果。

Bitmap位图对象的使用


默认新建的图片为空白透明图片:

width, height =500,500# 创建一张透明图片
bitmap = auto.Bitmap(width, height)

然后我们可以设置一点颜色,首先以逐像素遍历的方式操作:

width, height =500,500# 创建一张透明图片
bitmap = auto.Bitmap(width, height)
start = auto.ProcessTime()for x inrange(width):for y inrange(height):
        color =((x-width)**2+(y-height)**2)*255//(width**2+height**2)
        bitmap.SetPixelColor(x, y,0xFF00FF| color <<24)
cost = auto.ProcessTime()- start
print(f'SetPixelColor 逐像素设置 widthxheight 图片的颜色耗时 cost:.3fs')
bitmap.ToFile('tmp.png')
SetPixelColor 逐像素设置 500x500 图片的颜色耗时 0.648s

上述代码遍历每个像素点,通过SetPixelColor方法设置颜色。可以看到耗时达到0.6秒以上,能否快一点呢?

SetPixelColorsOfRect方法可以直接设置整个区域的颜色:

start = auto.ProcessTime()
colors =[]for x inrange(width):for y inrange(height):
        color =((x-width)**2+(y-height)**2)*255//(width**2+height**2)
        colors.append(0xFF00FF| color <<24)
bitmap.SetPixelColorsOfRect(0,0, width, height, colors)
cost = auto.ProcessTime()- start
print(f'SetPixelColorsOfRect 设置 widthxheight 图片矩形区域的颜色,耗时 cost:.3fs')
bitmap.ToFile('tmp.png')
SetPixelColorsOfRect 设置 500x500 图片矩形区域的颜色,耗时 0.460s

显然设置整个区域的颜色会更快一些。

经测试使用GetPixelColor方法获取到的颜色值可能会因为负数补码导致获取到的值与当初设置的不一致。我们可以通过GetAllPixelColors方法获取原生数组后,然后计算偏移量获取颜色值:

colors = bitmap.GetAllPixelColors()defgetPixelColor(x, y):return colors[x*width+y]
    
getPixelColor(10,10)
4110352639

可以通过控件的ToBitmap方法对该控件截图获取Bitmap对象,传入参数可以决定截取的范围,例如我们截图桌面范围480*360范围内(不传参则获取整个控件)的图片:

root = auto.GetRootControl()
bitmap = root.ToBitmap(0,0,480,360)
bitmap.ToFile('tmp.png')
Image("tmp.png")

ToBitmap方法也可以使用Bitmap.FromControl方法替代:

bitmap = auto.Bitmap.FromControl(control, x, y, width, height)

裁切图片:

with bitmap.Copy(150,100,80,102)as subBitmap:
    subBitmap.ToFile('tmp.png')
    display(Image("tmp.png"))

结果:成功裁切出 极速PDF阅读器 的图标。

若需要同时裁切多个部分,可以使用GetPixelColorsOfRects方法:

width, height =75,85
rects =[((width*i,0, width, height))for i inrange(3)]
colors = bitmap.GetPixelColorsOfRects(rects)for nativeArray in colors:with auto.Bitmap(width, height)as subBitmap:
        subBitmap.SetPixelColorsOfRect(0,0, width, height, nativeArray)
        subBitmap.ToFile('tmp.png')
        display(Image("tmp.png"))

X轴翻转:

with bitmap.RotateFlip(auto.RotateFlipType.RotateNoneFlipX)as bmp:
        bmp.ToFile('tmp.png')
        display(Image("tmp.png"))

Y轴翻转:

with bitmap.RotateFlip(auto.RotateFlipType.RotateNoneFlipY) as bmp:
    bmp.ToFile('tmp.png')
    display(Image("tmp.png"))

90度旋转:

with bitmap.Rotate(90)as bmp:
    bmp.ToFile('tmp.png')
    display(Image("tmp.png"))

30度旋转(非90度整数倍 旋转)

with bitmap.Rotate(30)as bmp:
    bmp.ToFile('tmp.png')
    display(Image("tmp.png"))

对多个显示器分别截屏


结合前面的方法,我们可以对桌面截屏,对鼠标下的控件截屏,对当前激活窗口截屏等等。

基本都是在获取相应控件后调用如下方法:

control.CaptureToImage(savePath)

获取桌面控件前面已经演示,下面看看如何同时获取多个屏幕的截图:

c = auto.GetRootControl()
rects = auto.GetMonitorsRect()print(rects)for rect in rects:
    c.CaptureToImage('tmp.png', rect.left, rect.top,
                     rect.width(), rect.height())
    display(Image("tmp.png"))

核心点就是通过GetMonitorsRect获取所有屏幕的坐标范围数组,截图时指定坐标范围即可。

剪切板操作


通常我们会使用pyperclip对文本进行复制粘贴,但实际上uiautomation所支持的剪切板操作会更加丰富,不仅支持纯文本,还支持富文本和图片。

涉及文件的剪切板操作,个人已经在**《UI自动化控制微信发送文件》**一文中实现将文件设置到剪切板中。

获取当前剪切板的内容格式:

import uiautomation as auto

formats = auto.GetClipboardFormats()print(formats)
49282: 'html Format', 49402: 'Rich Text Format', 13: 'CF_UNICODETEXT', 1: 'CF_TEXT', 49287: 'UniformResourceLocator', 50062: 'JAVA_DATAFLAVOR:application/x-java-jvm-local-objectref; class=c', 16: 'CF_LOCALE', 7: 'CF_OEMTEXT'

读取剪切板时,我们可以根据当前剪切板的格式分别作不同的处理:

formats = auto.GetClipboardFormats()for k, v in formats.items():if k == auto.ClipboardFormat.CF_UNICODETEXT:print("文本格式:", auto.GetClipboardText())elif k == auto.ClipboardFormat.CF_HTML:
        htmlText = auto.GetClipboardHtml()print("富文本格式:", htmlText)elif k == auto.ClipboardFormat.CF_BITMAP:
        bmp = auto.GetClipboardBitmap()print("位图:", bmp)

不过更关键的是设置数据到剪切板。

设置文本到剪切板:

auto.SetClipboardText('Hello World')

设置富文本到剪切板:

auto.SetClipboardHtml('<h1>Title</h1><br><h3>Hello</h3><br><p>test html</p><br>')

设置图片到剪切板,只需要将Bitmap设置到剪切板即可,下面演示通过图片文件路径构造Bitmap并设置到剪切板:

with auto.Bitmap.FromFile(path)as bmp:
    auto.SetClipboardBitmap(bmp)

而根据文件路径设置文件到剪切板已通过win32clipboard实现,详见:**《UI自动化控制微信发送文件》**一文。

自带的Logger日志输出类


uiautomation自带了日志输出类,有时我们希望输出不仅到控制台,同时输出到文件时,可以直接使用uiautomation自带的方法。

基础输出:

auto.Logger.Write(
    log: Any,
    consoleColor:int=-1,
    writeToFile:bool=True,
    printToStdout:bool=True,
    logFile:str=None,
    printTruncateLen:int=0,)
  • log: 要输出的日志内容

  • consoleColor: 文本在控制台输出的颜色

  • writeToFile: 是否输出到文件,默认为True

  • printToStdout: 是否输出到控制台,默认为True

  • logFile: 日志文件所在位置,默认为当前目录下的@AutomationLog.txt文件

  • printTruncateLen: 日志截断大小,每条输出超过长度限制时在控制台的输出会被截断。设置该值小于等于0时则不截断。

对于第二个颜色参数,我们可以直接通过auto.ConsoleColor中的变量来获取对应常量,例如:

auto.Logger.Write('测试输出', auto.ConsoleColor.Yellow)

可以看一下支持的颜色列表:

colors =[color for color indir(
    auto.ConsoleColor)ifnot color.startswith("__")]print(colors)
['Black', 'Blue', 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGray', 'DarkGreen', 'DarkMagenta', 'DarkRed', 'DarkYellow', 'Default', 'Gray', 'Green', 'Magenta', 'Red', 'White', 'Yellow']

auto.Logger.WriteLine函数与auto.Logger.Write几乎等价,只是少了printTruncateLen参数。

可以使用auto.Logger.ColorfullyWrite方法对指定部分的文本修改颜色:

auto.Logger.ColorfullyWrite('一段文本<Color=Red>红色</Color>,黑色<Color=Blue>蓝色</Color>结束')

不过以上命令的颜色效果只在有标准控制台的窗口输出才有效。

全局热键与多线程


在常规的场景中,我们一般使用pynput实现全局热键的注册,但实际上pynput相对于uiautomation库的热键功能是比较难用。

在uiautomation注册全局热键非常简单,只需要调用auto.RunByHotKey传入快捷键和函数即可。下面演示一个简单的例子,按下快捷键分别打印文本:

import threading
from threading import Event

import uiautomation as auto


defdemo1(stopEvent: Event):
    thread = threading.currentThread()print(thread.name, thread.ident,"demo1")defdemo2(stopEvent: Event):
    thread = threading.currentThread()print(thread.name, thread.ident,"demo2")defdemo3(stopEvent: Event):
    thread = threading.currentThread()print(thread.name, thread.ident,"demo3")if __name__ =='__main__':
    thread = threading.currentThread()print(thread.name, thread.ident,"main")
    auto.RunByHotKey((0, auto.Keys.VK_F2): demo1,(auto.ModifierKey.Control, auto.Keys.VK_1): demo2,(auto.ModifierKey.Control | auto.ModifierKey.Shift, auto.Keys.VK_2): demo3,, waitHotKeyReleased=False)

以上代码分别注册了快捷键F2,Ctrl+1和Ctrl+Shift+2。下面测试运行该程序并分别按下这三个快捷键,最后按下Ctrl+D结束程序:

MainThread 4404 main
Register hotkey ('','VK_F2') successfully
Register hotkey ('Control','VK_1') successfully
Register hotkey ('Control|Shift','VK_2') successfully
Register exit hotkey ('Control','VK_D') successfully
----------hotkey ('','VK_F2') pressed----------
Thread-14608 demo1
<Thread(Thread-1, started 4608)>for function demo1 exits, hotkey ('','VK_F2')----------hotkey ('Control','VK_1') pressed----------
Thread-212468 demo2
<Thread(Thread-2, started 12468)>for function demo2 exits, hotkey ('Control','VK_1')----------hotkey ('Control|Shift','VK_2') pressed----------
Thread-35428 demo3
<Thread(Thread-3, started 5428)>for function demo3 exits, hotkey ('Control|Shift','VK_2')
Exit hotkey pressed. Exit

由于每次按下热键都会启动独立的线程执行该函数,从打印日志可以看到每次按下执行函数的线程都不同。

RunByHotKey的参数列表如下:

RunByHotKey(keyFunctions: Dict[Tuple[int,int],(...)-> Any],
                stopHotKey: Optional[Tuple[int,int]]=None,
                exitHotKey: Tuple[int,int]=(ModifierKey.Control, Keys.VK_D),
                waitHotKeyReleased:bool=True)

其中exitHotKey表示程序退出的快捷键,默认为Ctrl+D。waitHotKeyReleased表示是否等待弹起后执行,经测试设置为False不等待更佳。

stopHotKey表示产生退出事件的快捷键,当我们设置该参数并在运行中按下该快捷键,函数的参数stopEvent将会被设置,调用.is_set()将会返回True。

我们可以在热键需要执行一个耗时较长的循环操作时,在循环中判断该事件是否被设置,设置就退出循环。

演示一个简单的示例,按下Ctrl+S启动热键,每0.5秒打印一个数字直到10,按下Ctrl+E则中断热键执行。重新按下Ctrl+S还可以继续:

import threading
from threading import Event

import uiautomation as auto


defdemo1(stopEvent: Event):for i inrange(10):if stopEvent.is_set():print("退出循环")breakprint(i, end=" ")
        stopEvent.wait(0.5)if __name__ =='__main__':
    thread = threading.currentThread()print(thread.name, thread.ident,"main")
    auto.RunByHotKey((0, auto.Keys.VK_S): demo1,, stopHotKey=(0, auto.Keys.VK_E),
        waitHotKeyReleased=True)

测试一下:

Register hotkey ('Control','VK_S') successfully
Register stop hotkey ('Control','VK_E') successfully
Register exit hotkey ('Control','VK_D') successfully
----------hotkey ('Control','VK_S') pressed----------01234----------stop hotkey pressed----------
退出热键被按下,结束!
<Thread(Thread-1, started 10016)>for function demo1 exits, hotkey ('Control','VK_S')
Exit hotkey pressed. Exit

需要注意在线程中需要使用控件对象相关方法时,要在新线程中进行相应的初始化:

auto.InitializeUIAutomationInCurrentThread()...
auto.UninitializeUIAutomationInCurrentThread()

发布windows 7桌面池之自动桌面池即时克隆

在Horizon View  Web 控制台中的目录下拉菜单找到桌面池,点击添加,打开添加桌面池向导,选择类型为自动桌面池

技术分享图片

2、设置用户分配类型

技术分享图片

3、在vCenter Server页面选择即时克隆

技术分享图片

4、设置桌面池标识

技术分享图片

5、设置桌面池设置

技术分享图片

6、配置桌面池命名模式、置备时间安排、桌面池尺寸等

技术分享图片

7、配置存储优化

技术分享图片

8、vCenter 设置配置默认镜像、虚拟机位置、资源设置等

技术分享图片

9、客户自定义

技术分享图片

10、检查配置,并点击完成。

技术分享图片

11、通过client,可以看到很多的任务,在进行中

技术分享图片

12、创建完成之后,在client看到有很多的文件

技术分享图片

13、通过VMware horizon View web控制台看到桌面池已经置备好了,状态为可用状态

技术分享图片

14、为桌面池授权,并验证登录

技术分享图片

15、通过认证用户登录验证测试

技术分享图片

16、选择桌面池

技术分享图片

17、登录成功

技术分享图片

18、通过web 页面访问验证

技术分享图片

19、选择桌面池

技术分享图片

20、用户登录正常

技术分享图片

21、通过horizon web 控制台可以看到会话记录

技术分享图片

基于instance clone的桌面到此结束,后面进行基于Windows 10的桌面发布进行说明

以上是关于Windows桌面程序自动化控制之uiautomation模块全面讲解的主要内容,如果未能解决你的问题,请参考以下文章

发布windows 7桌面池之自动桌面池 View Composer链接克隆

Python基础系列讲解-自动控制windows桌面

Python基础系列讲解-自动控制windows桌面

自动化之Katalon桌面端

如何从另一个桌面应用程序控制 Windows 服务

发布windows 10桌面池之自动桌面池 View Composer链接克隆