|
文章目录简介功能介绍基本原理控件控制入门:记事本操作控件分析与可用参数控件延迟搜索机制示例:连续打开三个记事本并关闭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-Windowsuiautomation是yinkaisheng业余时间开发一个模块。封装了微软UIAutomationAPI,支持自动化Win32,MFC,WPF,ModernUI(MetroUI),Qt,IE,Firefox(version=60),Chrome谷歌游览器和基于Electron开发的应用程序(加启动参数–force-renderer-accessibility也能支持UIAutomation被自动化).uiautomation只支持Python3版本,依赖comtypes和typing这两个包,但Python不要使用3.7.6和3.8.1这两个版本,comtypes在这两个版本中不能正常工作(issue)。UIAutomation的工作原理:UIAutomation操作程序时会给程序发送WM_GETOBJECT消息,如果程序处理WM_GETOBJECT消息,实现UIAutomationProvider,并调用函数UiaReturnRawElementProvider(HWNDhwnd,WPARAMwparam,LPARAMlparam,IRawElementProviderSimple*el),此程序就支持UIAutomation。IRawElementProviderSimple就是UIAutomationProvider,包含了控件的各种信息,如Name,ClassName,ContorlType,坐标等。UIAutomation根据程序返回的IRawElementProviderSimple,就能遍历程序的控件,得到控件各种属性,进行自动化操作。若程序没有处理WM_GETOBJECT或没有实现UIAutomationProvider,UIAutomation则无法识别这些程序内的控件,不支持自动化。很多DirectUI程序没有实现UIAutomationProvider,所以不支持自动化。关于各控件所支持的控件模式,可参考:https://docs.microsoft.com/zh-cn/windows/win32/winauto/uiauto-controlpatternmapping在使用uiautomation模块前需要先安装:pipinstalluiautomation安装后会在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脚本打印的结果为准。控件控制入门:记事本操作控件分析与可用参数首先打开记事本窗口,并设置窗口前置:importsubprocessimportuiautomationasautosubprocess.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:控件AutomationIdControlType:控件类型Depth:控件相对于searchFromControl的精确深度Compare:自定义比较函数function(control:Control,depth:int)->boolsearchDepth和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查看帮助信息:>pythonD:Miniconda3Scriptsautomation.py-hUIAutomation2.0.15(Python3.7.4,64bit)usage-hshowcommandhelp-tdelaytime,default3seconds,begintoenumerateafterValueseconds,thismustbeanintegeryoucandelayafewsecondsandmakeawindowactivesoautomationcanenumeratetheactivewindow-denumeratetreedepth,thismustbeaninteger,ifitisnull,enumeratethewholetree-renumeratefromrootesktopwindow,ifitisnull,enumeratefromforegroundwindow-fenumeratefromfocusedcontrol,ifitisnull,enumeratefromforegroundwindow-cenumeratethecontrolundercursor,ifdepthisbool当函数返回True时表示找到控件并返回,例如以下方法几乎可以得与GetTopLevelControl()相同的结果:control.GetAncestorControl(lambdac,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对象后,调用Move和Resize方法来实现。上面的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遍历桌面的示例:importuiautomationasautodefGetFirstChild(control):returncontrol.GetFirstChildControl()defGetNextSibling(control):returncontrol.GetNextSiblingControl()desktop=auto.GetRootControl()forcontrol,depthinauto.WalkTree(desktop,getFirstChild=GetFirstChild,getNextSibling=GetNextSibling,includeTop=True,maxDepth=2):ifnotcontrol.Name:continueprint(''*depth*4,control.Name)maxDepth指定了遍历深度,除了指定这两个方法以外还可以只转入getChildren方法:defGetChildren(control):returncontrol.GetChildren()forcontrol,depth,remaininauto.WalkTree(desktop,getChildren=GetChildren,includeTop=True,maxDepth=2):ifnotcontrol.Name:continueprint(''*depth*4,control.Name)结果过滤的方逻辑我们还可以写到yieldCondition的传入函数中:defyieldCondition(control,depth):ifcontrol.Name:returnTrueforcontrol,depth,remaininauto.WalkTree(desktop,getChildren=GetChildren,yieldCondition=yieldCondition,includeTop=True,maxDepth=2):print(''*depth*4,control.Name)在我电脑当前执行结果均为:桌面1任务栏开始在这里输入你要搜索的内容开始在这里输入你要搜索的内容系统时钟,23:02,2021/11/15test-JupyterNotebook-360安全浏览器13.1ChromeLegacyWindow一文掌握uiautomation的经典案例.md•-TyporaTyporaUIAutomation_demos–clipboard_test.pyPyCharmProgramManagerWalkTree的规则是当设置getChildren函数时,忽略getFirstChild和getNextSibling,否则使用这两个函数。设置yieldCondition函数时则开启额外的过滤。甚至可以使用WalkTree方法计算全排列问题:defNextPermutations(aTuple):left,permutation=aTupleret=[]fori,iteminenumerate(left):nextLeft=left[:]delnextLeft[i]nextPermutation=permutation+[item]ret.append((nextLeft,nextPermutation))returnretuniqueItems=list("abc")n=len(uniqueItems)count=0for(left,permutation),depth,remaininauto.WalkTree((uniqueItems,[]),NextPermutations,yieldCondition=lambdac,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()forxinrange(width):foryinrange(height):color=((x-width)**2+(y-height)**2)*255//(width**2+height**2)bitmap.SetPixelColor(x,y,0xFF00FF|color<<24)cost = auto.ProcessTime()- startprint(f'SetPixelColor 逐像素设置 {width}x{height} 图片的颜色耗时 {cost:.3f}s')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()- startprint(f'SetPixelColorsOfRect 设置 {width}x{height} 图片矩形区域的颜色,耗时 {cost:.3f}s')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,85rects =[((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('Title
Hello
testhtml
')设置图片到剪切板,只需要将Bitmap设置到剪切板即可,下面演示通过图片文件路径构造Bitmap并设置到剪切板:withauto.Bitmap.FromFile(path)asbmp: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:是否输出到文件,默认为TrueprintToStdout:是否输出到控制台,默认为TruelogFile:日志文件所在位置,默认为当前目录下的@AutomationLog.txt文件printTruncateLen:日志截断大小,每条输出超过长度限制时在控制台的输出会被截断。设置该值小于等于0时则不截断。对于第二个颜色参数,我们可以直接通过auto.ConsoleColor中的变量来获取对应常量,例如:auto.Logger.Write('测试输出',auto.ConsoleColor.Yellow)可以看一下支持的颜色列表:colors=[colorforcolorindir(auto.ConsoleColor)ifnotcolor.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('一段文本红色,黑色蓝色结束')不过以上命令的颜色效果只在有标准控制台的窗口输出才有效。全局热键与多线程在常规的场景中,我们一般使用pynput实现全局热键的注册,但实际上pynput相对于uiautomation库的热键功能是比较难用。在uiautomation注册全局热键非常简单,只需要调用auto.RunByHotKey传入快捷键和函数即可。下面演示一个简单的例子,按下快捷键分别打印文本:importthreadingfromthreadingimportEventimportuiautomationasautodefdemo1(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结束程序:MainThread4404mainRegisterhotkey('','VK_F2')successfullyRegisterhotkey('Control','VK_1')successfullyRegisterhotkey('Control|Shift','VK_2')successfullyRegisterexithotkey('Control','VK_D')successfully----------hotkey('','VK_F2')pressed----------Thread-14608demo1forfunctiondemo1exits,hotkey('','VK_F2')----------hotkey('Control','VK_1')pressed----------Thread-212468demo2forfunctiondemo2exits,hotkey('Control','VK_1')----------hotkey('Control|Shift','VK_2')pressed----------Thread-35428demo3forfunctiondemo3exits,hotkey('Control|Shift','VK_2')Exithotkeypressed.Exit由于每次按下热键都会启动独立的线程执行该函数,从打印日志可以看到每次按下执行函数的线程都不同。RunByHotKey的参数列表如下:RunByHotKey(keyFunctionsict[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还可以继续:importthreadingfromthreadingimportEventimportuiautomationasautodefdemo1(stopEvent:Event):foriinrange(10):ifstopEvent.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)测试一下:Registerhotkey('Control','VK_S')successfullyRegisterstophotkey('Control','VK_E')successfullyRegisterexithotkey('Control','VK_D')successfully----------hotkey('Control','VK_S')pressed----------01234----------stophotkeypressed----------退出热键被按下,结束!forfunctiondemo1exits,hotkey('Control','VK_S')Exithotkeypressed.Exit需要注意在线程中需要使用控件对象相关方法时,要在新线程中进行相应的初始化:auto.InitializeUIAutomationInCurrentThread()...auto.UninitializeUIAutomationInCurrentThread()也可以使用with语句简化代码:withauto.UIAutomationInitializerInThread(): pass...和pass替换为线程中实际操作的代码。⚠️:否则部分场景下会报错。管理员提权模板代码为:if__name__=='__main__':ifauto.IsUserAnAdmin():main()else:print('RunScriptAsAdmin',sys.executable,sys.argv)auto.RunScriptAsAdmin(sys.argv)将原本存在于__main__代码块中的内容存放于main()方法中即可。通过实例学习UI自动化控制win10计算器自动计算自动控制计算器更简单的方法是复制粘贴需要计算的字符串到计算器,但本节的目的为了演示点击按钮计算的效果。首先启动计算器并设置窗口置顶:importuiautomationasautoimportosimportsysimporttimeimportsubprocess#显示搜索控件所遍历的控件数和搜索时间auto.uiautomation.DEBUG_SEARCH_TIME=True#设置全局搜索超时时间为1秒auto.uiautomation.SetGlobalSearchTimeout(1)#创建计算器窗口控件calcWindow=auto.WindowControl(searchDepth=1,ClassName='ApplicationFrameWindow',Compare=lambdac,d:c.Name=='Calculator'orc.Name=='计算器',desc='计算器窗口')ifnotcalcWindow.Exists(0,0):subprocess.Popen('calc')#设置窗口前置calcWindow.SetTopmost(True)2021-09-2123:10:34.990[17]->{ClassName:'ApplicationFrameWindow',desc:'计算器窗口',ControlType:WindowControl}TraverseControls:2,SearchTime:1.004s[23:10:33.984967-23:10:34.990062]⚠️Compare参数可以传入自定义搜索函数,两个参数分别为控件对象和搜索深度。上述代码能够适配Name属性可能为中文或英文的情况。不过ClassName参数足够定位到计算器窗口,Compare参数可以直接删除。Desc是无效的属性,可以在debug日志中打印出来,当然任何无效属性都可以在debug日志中打印出来。calcWindow.Exists(0,0)判断了计算器窗口是否已经存在,防止重复打开新的窗口。切换到科学计算器:#调低waitTime可以加快点击速度calcWindow.ButtonControl(AutomationId='TogglePaneButton',desc='打开导航').Click(waitTime=0.01)calcWindow.ListItemControl(AutomationId='Scientific',desc='选择科学计算器').Click(waitTime=0.01)clearButton=calcWindow.ButtonControl(AutomationId='clearEntryButton',desc='点击CE清空所有输入')ifclearButton.Exists(0,0):clearButton.Click(waitTime=0)else:calcWindow.ButtonControl(AutomationId='clearButton',desc='点击C清空所有输入').Click(waitTime=0)2021-09-2123:22:28.966[3]->{AutomationId:'TogglePaneButton',desc:'打开导航',ControlType:ButtonControl}TraverseControls:121,SearchTime:0.061s[23:22:28.906224-23:22:28.966385]2021-09-2123:22:29.518[5]->{AutomationId:'Scientific',desc:'选择科学计算器',ControlTypeistItemControl}TraverseControls:133,SearchTime:0.077s[23:22:29.442885-23:22:29.518933]2021-09-2123:22:29.876[12]->{AutomationId:'clearButton',desc:'点击C清空所有输入',ControlType:ButtonControl}TraverseControls:51,SearchTime:0.029s[23:22:29.846955-23:22:29.876364]⚠️清空按钮在输入框有数和没有数时,按钮的和AutomationId不一样,所以先判断其中一种情况的按钮是否存在,不存在则切换到另一种形式的按钮进行点击。其中按钮的AutomationId属性可以通过inspect.exe工具或automation.py脚本获取。下面只演示四则运算,先通过inspect.exe工具获取每个按钮控件对应的AutomationId:然后通过auto.WalkControl遍历所有控件,从而缓存每个字符对应的按钮控件:id2char={'num0Button':'0','num1Button':'1','num2Button':'2','num3Button':'3','num4Button':'4','num5Button':'5','num6Button':'6','num7Button':'7','num8Button':'8','num9Button':'9','decimalSeparatorButton':'.','plusButton':'+','minusButton':'-','multiplyButton':'*','divideButton':'/','equalButton':'=','openParenthesisButton'','closeParenthesisButton':')'}char2Button={}forc,dinauto.WalkControl(calcWindow,maxDepth=4):ifc.AutomationIdinid2char:char2Button[id2char[c.AutomationId]]=c然后封装一下对四则运算表达式计算的函数:defcalc(expression):expression=''.join(expression.split())ifnotexpression.endswith('='):expression+='='forcharinexpression:char2Button[char].Click(waitTime=0)time.sleep(0.1)calcWindow.SendKeys('{Ctrl}c',waitTime=0.1)returnauto.GetClipboardText()测试一个四则表达式:calc('1234*(4+5+6)-78/90.8')需要截屏的话可以调用如下命令:calcWindow.CaptureToImage('calc.png',x=7,y=0,width=-14,height=-7)⚠️x和y表示起始坐标,由于win10的计算器存在7像素的阴影,所以可以去掉。width和height为负数时,表示原宽度减少指定值。垂直方向上只有下方有阴影,水平方向上左右均有阴影。关闭程序只需调用:calcWindow.GetWindowPattern().Close()完整代码:importuiautomationasautoimportosimportsysimporttimeimportsubprocess#显示搜索控件所遍历的控件数和搜索时间auto.uiautomation.DEBUG_SEARCH_TIME=True#设置全局搜索超时时间为1秒auto.uiautomation.SetGlobalSearchTimeout(1)#创建计算器窗口控件calcWindow=auto.WindowControl(searchDepth=1,ClassName='ApplicationFrameWindow',desc='计算器窗口')ifnotcalcWindow.Exists(0,0):subprocess.Popen('calc')#设置窗口前置calcWindow.SetTopmost(True)#调低waitTime可以加快点击速度calcWindow.ButtonControl(AutomationId='TogglePaneButton',desc='打开导航').Click(waitTime=0.01)calcWindow.ListItemControl(AutomationId='Scientific',desc='选择科学计算器').Click(waitTime=0.01)clearButton=calcWindow.ButtonControl(AutomationId='clearEntryButton',desc='点击CE清空所有输入')ifclearButton.Exists(0,0):clearButton.Click(waitTime=0)else:calcWindow.ButtonControl(AutomationId='clearButton',desc='点击C清空所有输入').Click(waitTime=0)id2char={'num0Button':'0','num1Button':'1','num2Button':'2','num3Button':'3','num4Button':'4','num5Button':'5','num6Button':'6','num7Button':'7','num8Button':'8','num9Button':'9','decimalSeparatorButton':'.','plusButton':'+','minusButton':'-','multiplyButton':'*','divideButton':'/','equalButton':'=','openParenthesisButton'','closeParenthesisButton':')'}char2Button={}forc,dinauto.WalkControl(calcWindow,maxDepth=4):ifc.AutomationIdinid2char:char2Button[id2char[c.AutomationId]]=cdefcalc(expression):expression=''.join(expression.split())ifnotexpression.endswith('='):expression+='='forcharinexpression:char2Button[char].Click(waitTime=0)time.sleep(0.1)calcWindow.SendKeys('{Ctrl}c',waitTime=0.1)returnauto.GetClipboardText()result=calc('1234*(4+5+6)-78/90.8')print('1234*(4+5+6)-78/90.8=',result)result=calc('3*3+4*4')print('3*3+4*4=',result)result=calc('2*3.14159*10')print('2*3.14159*10=',result)calcWindow.CaptureToImage('calc.png',x=7,y=0,width=-14,height=-7)calcWindow.GetWindowPattern().Close()结果:1234*(4+5+6)-78/90.8=18509.1409691629955947136563876653*3+4*4=252*3.14159*10=62.8318产生的截图:运行过程中的原速动图:窗口的拖拽与缩放下面我们使用一个记事本窗口作为前置窗口就行演示,首先打开记事本窗口:importsysimporttimeimportsubprocessimportuiautomationasauto#设置全局搜索超时时间为1秒auto.uiautomation.SetGlobalSearchTimeout(1)note=auto.WindowControl(searchDepth=1,ClassName='Notepad')ifnotnote.Exists(0,0):subprocess.Popen('notepad.exe')note.SetActive()note.SetTopmost(waitTime=0)调整窗口位置和大小后,输入文本:#移动窗口的位置,前两个参数表示左上角的位置,后两个参数编程窗口大小note.MoveWindow(300,70,480,400)edit=note.EditControl()edit.SendKeys('{Ctrl}{End}{Enter2}我是置顶窗口!!!n我将遮住其他窗口.')打开计算器窗口并调整:#打开计算器窗口并调整:calcWindow=auto.WindowControl(searchDepth=1,ClassName='ApplicationFrameWindow')ifnotcalcWindow.Exists(0,0):subprocess.Popen('calc')calcWindow.SetActive()calcWindow.MoveWindow(100,100,320,500)再进行本例最精彩的部分,以拖拽方式移动窗口:#鼠标从位置(240,110)拖拽到(840,110)auto.DragDrop(240,110,840,110,waitTime=0.1)#再拖拽回去auto.DragDrop(840,110,240,110,waitTime=0.1)最后打印文本并关闭窗口:calcWindow.GetWindowPattern().Close()edit.SendKeys('{Ctrl}{End}{Enter2}关闭计算器窗口')vp=edit.GetValuePattern()print('当前文本:',vp.Value)#激活记事本窗口note.SetActive()note.GetWindowPattern().Close()#确认不保存auto.SendKeys('{ALT}n')完整代码:importsysimporttimeimportsubprocessimportuiautomationasauto#设置全局搜索超时时间为1秒auto.uiautomation.SetGlobalSearchTimeout(1)note=auto.WindowControl(searchDepth=1,ClassName='Notepad')ifnotnote.Exists(0,0):subprocess.Popen('notepad.exe')note.SetActive()note.SetTopmost(waitTime=0)#移动窗口的位置,前两个参数表示左上角的位置,后两个参数编程窗口大小note.MoveWindow(300,70,480,400)edit=note.EditControl()edit.SendKeys('{Ctrl}{End}{Enter2}我是置顶窗口!!!n我将遮住其他窗口.')#打开计算器窗口并调整:calcWindow=auto.WindowControl(searchDepth=1,ClassName='ApplicationFrameWindow')ifnotcalcWindow.Exists(0,0):subprocess.Popen('calc')calcWindow.SetActive()calcWindow.MoveWindow(100,100,320,500)#鼠标从位置(240,110)拖拽到(840,110)auto.DragDrop(240,110,840,110,waitTime=0.1)#再拖拽回去auto.DragDrop(840,110,240,110,waitTime=0.1)#打印文本并关闭窗口:calcWindow.GetWindowPattern().Close()edit.SendKeys('{Ctrl}{End}{Enter2}关闭计算器窗口')vp=edit.GetValuePattern()print('当前文本:',vp.Value)#激活记事本窗口note.SetActive()note.GetWindowPattern().Close()#确认不保存auto.SendKeys('{ALT}n')运行效果(图片使用了全局色键压缩,导致颜色失真):管理员提权操作并读取设备管理器栏目数据由于必须要有管理员权限才能读取设备管理器,在没有管理员权限的控制台需要进行管理员身份提权。本案例涉及很多小知识点,下面先拆分来单独列出来。获取屏幕大小:auto.GetScreenSize()(1920,1080)获取当前运行的程序的控制台并移动:sw,sh=auto.GetScreenSize()cmdWindow=auto.GetConsoleWindow()ifcmdWindow:cmdTransformPattern=cmdWindow.GetTransformPattern()cmdTransformPattern.Move(sw//2,0)cmdTransformPattern.Resize(sw//2,sh*3//4)打开设置管理器并修改窗口大小和位置:subprocess.Popen('mmc.exedevmgmt.msc')mmcWindow=auto.WindowControl(searchDepth=1,ClassName='MMCMainFrame')mmcTransformPattern=mmcWindow.GetTransformPattern()mmcTransformPattern.Move(0,0)mmcTransformPattern.Resize(sw//2,sh*3//4)下面我们的目标是提取出设备管理器的每一项的信息:tree=mmcWindow.TreeControl()foritem,depthinauto.WalkControl(tree,includeTop=False):ifnotisinstance(item,auto.TreeItemControl):continueitem.GetSelectionItemPattern().Select(waitTime=0.01)pattern=item.GetExpandCollapsePattern()ifpattern.ExpandCollapseState==auto.ExpandCollapseState.Collapsed:pattern.Expand(waitTime=0.01)print(''*(depth-1)*4,item.Name)部分结果:DESKTOP-IS8QJHFIDEATA/ATAPI控制器Intel(R)300SeriesChipsetFamilySATAAHCIController便携设备Seagate1.81TB .....音频输入和输出24B1W1G5(英特尔(R)显示器音频)RealtekDigitalOutput(RealtekHighDefinitionAudio)立体声混音(RealtekHighDefinitionAudio)扬声器(RealtekHighDefinitionAudio)上述代码执行后,滚动条已经到底底部,下面我们可以执行以下代码让滚动条回到顶部:treeScrollPattern=tree.GetScrollPattern()treeScrollPattern.SetScrollPercent(-1,0)SetScrollPercent传入的两个参数表示将滚动条移动到指定百分比位置:horizontalPercent:横向位置百分比verticalPercent:纵向位置百分比传入-1表示不移动,由于没有横向滚动条,所以第一个参数传入了-1。当然也可以移动到底部:treeScrollPattern.SetScrollPercent(-1,100)也可以使用鼠标滑轮实现:#滚动条未到顶就使鼠标滑轮不停往上滑whiletreeScrollPattern.VerticalScrollPercent>0:tree.WheelUp(waitTime=0.01)#滚动条未到底就使鼠标滑轮不停往下滑whiletreeScrollPattern.VerticalScrollPercent1329304
|
|