开源自己用python封装的一个Windows GUI(UI Automation)自动化工具,支

写在前面的话:此工具是本人玩UI自动化测试,感觉用着最好用,最爽的一款python写的软件推荐给大家
首先,大家可以看下这个链接 Windows GUI自动化测试技术的比较和展望 。
这篇文章介绍了Windows中GUI自动化的三种技术:Windows API, MSAA - Microsoft Active Accessibility, UIAutomation
用脚本语言AutoIT实现自动化就是第一种技术Windows API, 查找窗口句柄实现的。
用工具Spy++查看程序,如果Spy++能识别程序窗口中的控件就能用这种技术。
python中也有一个UI自动化测试模块pywinauto,也是用这种技术实现的。 
但Windows API实现的自动化不支持WPF程序、Windows 8中的Metro程序。
用UIAutomation实现的自动化支持微软提供的各种界面开发框架,如MFC, Windows Forms, WPF, Metro App。也部分支持Qt。
该技术的详细介绍可以参考CodeMagazine上的一篇文章

 
Creating UI Automation Client Applications  
官方文档 msdn: UI Automation Client Programmer's Guide
我就是根据这个用Python和C++对UIAutomation做了一层封装,方便我自己的使用,可以快速开发自动化脚本。
UIAutomation支持平台包括Windows XP(SP3),Windows Vista, Windows 7, Windows 8、8.1、10。
安装使用uiautomation,支持Python2,Python3,x86,x64。
 运行pip install uiautomation,安装后在c:\python36\scripts目录里会有automation.py,在cmd里运行automation.py -h。
或者在github上下载源码https://github.com/yinkaisheng/Python-UIAutomation-for-Windows,在cmd里运行源码根目录里的automation.py -h。
运行源码demos目录里的操作计算器的脚本 demos\automation_calculator.py看下运行效果。
 
 
下面通过QQ2013做下实验(spy++获取不到QQ窗口内的控件,可以对比一下):
 
然后运行最新版QQ2013, 先保持在qq登录界面
 
运行cmd,cd到工具的目录,输入automation.py -t3回车,然后3秒内切换到qq的登录界面
 
cmd窗口中就显示了qq窗口中的控件信息

运行automation.py遍历控件时,支持下列参数
 
-t int value, 延迟时间time秒,延迟指定秒数再遍历控件,
-r, 从树的根部(root: Desktop)遍历,如果不指定,从当前窗口遍历
-d, int Value, 遍历控件树的的深度depth,如果不指定,遍历整个树,和-c一起使用时,可以为负值
-f, 遍历焦点focus控件,如果不指定,从当前窗口遍历
-c, 遍历光标下的控件,如果不指定,从当前窗口遍历,如果同时指定-d, -d可以为负值,比如-d-2表示从光标下控件向上找到两层父控件,遍历这个父控件
 
-a, 获取光标下控件及其所有祖先(ancestor)控件
 
-n, 显示控件的完整name, 如果不指定,只显示前30个字符
-m, 显示控件更多more属性,默认只显示控件的几个属性
例子:
automation.py –t3, 3秒后遍历当前窗口所有控件
automation.py –d2 –t3, 3秒后遍历当前窗口前三层控件
automation.py –r –d1 –t0 -n, 0秒后遍历root的第一层子控件,并显示控件完整名称
 
automation.py –c –t3, 3秒后遍历鼠标光标下面的控件信息
 
 automation.py –c –t3 -d-2, 3秒后遍历鼠标光标下面的控件向上两层的父控件
 
 下面是在Windows 8中运行automation.py –r –d1 –t0的例子, 如下图


 在UIAutomation中控件树的根部是 桌面Desktop, 上面的命令输入了了 -r(root)参数,就从根部枚举窗口,参数-d1,只枚举桌面的第一层子控件。
 
在Windows 8中,如果要查看Metro App的控件信息,必须当前窗口要在Metro界面才能枚举,如果Metro App被切换到后台,则获取不到它的控件。
 
先运行automation.py -t5, 在5秒内切换到Metro App, 等待几秒钟,查看cmd,就能看到Metro App的控件信息。
 
automation模块同时会把显示的信息写入到文件@AutomaitonLog.txt,方便查看。
 
还有,在Windows 7及更高版本系统中,最好以管理员来运行cmd,再调用python,我发现有些程序用普通权限获取不到全部控件信息。
 
 
 
登录QQ2013,再一次枚举它的窗口,如图
另一个操作QQ群导出所有群成员详细信息的例子 :
 
使用python UIAutomation从QQ2017(v8.9)群界面获取所有群成员详细资料
 
 
下面介绍下用uiautomaton模块自动化操作系统记事本notepad.exe的一个例子,
 
先用automaiton.py遍历记事本窗口,知道它的控件树结构,再开始写代码



python2.7代码如下(在代码demos目录automation_notepad_py2.py, automation_notepad_py3.py):

#!python2
# -*- coding: utf-8 -*-
import os, sys, time
import subprocess
import uiautomation as automation
 
def testNotepadCN():
    consoleWindow = automation.GetConsoleWindow()
    consoleWindow.SetActive()
    automation.Logger.ColorfulWriteLine('\nI will open <Color=Green>Notepad</Color> and <Color=Yellow>automate</Color> it. Please wait for a while.')
    time.sleep(2)
    automation.ShowDesktop()
    #打开notepad
    subprocess.Popen('notepad')
    #查找notepad, 如果name有中文,python2中要使用Unicode
    window = automation.WindowControl(searchDepth = 1, ClassName = 'Notepad', RegexName = u'.* - 记事本')
    #可以判断window是否存在,如果不判断,找不到window的话会抛出异常
    #if window.Exists(maxSearchSeconds = 3):
    if automation.WaitForExist(window, 3):
        automation.Logger.WriteLine("Notepad exists now")
    else:
        automation.Logger.WriteLine("Notepad does not exist after 3 seconds", automation.ConsoleColor.Yellow)
    screenWidth, screenHeight = automation.Win32API.GetScreenSize()
    window.MoveWindow(screenWidth // 4, screenHeight // 4, screenWidth // 2, screenHeight // 2)
    window.SetActive()
    #从window查找edit
    edit = window.EditControl()
    edit.Click(waitTime = 0)
    #python2中要使用Unicode, 模拟按键
    edit.SetValue(u'hi你好')
    edit.SendKeys(u'{Ctrl}{End}{Enter}下面开始演示{! 4}{ENTER}', 0.2, 0)
    edit.SendKeys('{Enter 3}0123456789{Enter}', waitTime = 0)
    edit.SendKeys('ABCDEFGHIJKLMNOPQRSTUVWXYZ{ENTER}', waitTime = 0)
    edit.SendKeys('abcdefghijklmnopqrstuvwxyz{ENTER}', waitTime = 0)
    edit.SendKeys('`~!@#$%^&*()-_=+{ENTER}', waitTime = 0)
    edit.SendKeys('[]{{}{}}\\|;:\'\",<.>/?{ENTER}', waitTime = 0)
    edit.SendKeys(u'™®①②③④⑤⑥⑦⑧⑨⑩§№☆★○●◎◇◆□℃‰€■△▲※→←↑↓〓¤°#&@\^_―♂♀{ENTER}{CTRL}a')
    window.CaptureToImage('Notepad.png')
    edit.SendKeys('Image Notepad.png was captured, you will see it later.', 0.05)
    #查找菜单
    window.MenuItemControl(Name = u'格式(O)').Click()
    window.MenuItemControl(Name = u'字体(F)...').Click()
    windowFont = window.WindowControl(Name = u'字体')
    windowFont.ComboBoxControl(AutomationId = '1140').Select(u'中文 GB2312')
    windowFont.ButtonControl(Name = u'确定').Click()
    window.Close()
    if automation.WaitForDisappear(window, 3):
        automation.Logger.WriteLine("Notepad closed")
    else:
        automation.Logger.WriteLine("Notepad still exists after 3 seconds", automation.ConsoleColor.Yellow)
 
    # buttonNotSave = ButtonControl(searchFromControl = window, SubName = u'不保存')
    # buttonNotSave.Click()
    # or send alt+n to not save and quit
    # automation.SendKeys('{Alt}n')
    # 使用另一种查找方法
    buttonNotSave = automation.FindControl(window,
        lambda control, depth: control.ControlType == automation.ControlType.ButtonControl and u'不保存' in control.Name)
    buttonNotSave.Click()
    subprocess.Popen('Notepad.png', shell = True)
    time.sleep(2)
    consoleWindow.SetActive()
    automation.Logger.WriteLine('script exits', automation.ConsoleColor.Cyan)
    time.sleep(2)

首先用subprocess.Popen('notepad') 调用notepad
 
先写查找notepad窗口的代码了
#查找notepad, 如果name有中文,要使用Unicode
window = WindowControl(searchDepth = 1, ClassName = 'Notepad', SubName = u'记事本')

searchDepth = 1, 表示只查找树的的第一层子控件,不会遍历整个树查找,所以能很快找到notepad的窗口。
查找控件可以指定如下参数 ClassName, WindowControl, AutomationId, Name , SubName,foundIndex,前四个都是cmd里显示的内容,
SubName可以指定如果Name中含有SubName这个字符串,也算查找成功。
foundIndex表示第几个符合查找条件的控件,不指定的话默认是1。
 
然后再查找EditControl
#从window查找edit
edit = window.EditControl()

修改EditControl内容并在当前文字后面模拟打字 
#python2中要使用Unicode, 模拟按键
edit.SetValue(u'hi你好')
edit.SendKeys(u'{Ctrl}{End}{Enter}下面开始演示{! 4}{ENTER}', 0.2, 0)

另一个例子, 操作QQ登录界面的一段代码,这里没有写调用的代码,先手动启动qq2013,保持在登录界面,
# -*- coding:utf-8 -*-
from automation import *

time.sleep(2)
#查找qq窗口,searchDepth = 1,设置查找深度为1,查找Desktop的第一层子窗口就能很快找到QQ
qqWindow = WindowControl(searchDepth = 1, ClassName = 'TXGuiFoundation', Name = 'QQ2013')
if not qqWindow.Exists():
    shellTray = Control(searchDepth = 1, ClassName = 'Shell_TrayWnd')
    qqTray = ButtonControl(searchFromControl = shellTray, Name = 'QQ')
    if qqTray.Exists():
        qqTray.Click()
        time.sleep(1)
if not qqWindow.Exists():
    Logger.WriteLine(u'Can not find QQ window, please put it in front first!' % edit.CurrentValue(), ConsoleColor.Red)

#查找QQ帐号Edit,设置searchFromControl = qqWindow,从qqWindow开始查找子控件
#foundIndex = 1,表示查找第一个符合要求的控件,子窗口中的第一个Edit
edit = EditControl(searchFromControl = qqWindow, foundIndex = 1)
edit.Click()
Win32API.SendKeys('{Ctrl}A')
Logger.Write('Current QQ is ')
#获取edit内容
Logger.WriteLine(u'%s' % edit.CurrentValue(), ConsoleColor.DarkGreen)
time.sleep(1)
#查找第二个Edit,即密码Edit
edit = EditControl(searchFromControl = qqWindow, foundIndex = 2)
edit.Click()
Logger.Write('Current QQ password is ')
#获取password内容
Logger.WriteLine('%s' % edit.CurrentValue(), ConsoleColor.DarkGreen)
Logger.WriteLine('Only get stars. password can not be got', ConsoleColor.DarkGreen)
time.sleep(1)
Logger.WriteLine('Now let\'s show the buttons of QQ')
time.sleep(2)
#遍历QQ窗口内的所有Button
buttonIndex = 1
button = ButtonControl(searchFromControl = qqWindow, foundIndex = buttonIndex)
while button.Exists():
    l, t, r, b = button.BoundingRectangle
    Logger.WriteLine('button %d, position (%d,%d,%d,%d)' % (buttonIndex, l, t, r, b))
    button.MoveCursorToMyCenter()
    time.sleep(1)
    buttonIndex += 1
    button = ButtonControl(searchFromControl = qqWindow, foundIndex = buttonIndex)

Logger.WriteLine('\r\nLook, the last button\'s position are all 0, it may be invisible.', ConsoleColor.Yellow)
button = ButtonControl(searchFromControl = qqWindow, foundIndex = 4)
button.Click()
menu = Control(searchDepth = 1, ControlType = ControlType.MenuControl, Name = u'TXMenuWindow')
if (menu.Exists()):
    menuItem = MenuItemControl(searchFromControl = menu, Name = u'隐身')
    menuItem.Click()
    time.sleep(1)
button = ButtonControl(searchFromControl = qqWindow, foundIndex = 8)
button.Click()
time.sleep(1)
button = ButtonControl(searchFromControl = qqWindow, Name = u'取消')
button.Click()



Windows 8 中自动化操作系统Metro日历的一段代码
from automation import *

def main():
    RunMetroApp(u'日历')
    canlendarWindow = WindowControl(ClassName = MetroWindowClassName, Name = u'日历')
    t = time.localtime()
    day = t.tm_mday

    text1 = TextControl(searchFromControl = canlendarWindow, foundIndex = 1, Name = str(day))
    text2 = TextControl(searchFromControl = canlendarWindow, foundIndex = 2, Name = str(day))
    if text2.Exists(1) and text2.BoundingRectangle[0] > 0:
        text2.Click()
    else:
        text1.Click()
    location = EditControl(searchFromControl = canlendarWindow, AutomationId = 'LocationTextbox')
    location.Click()
    Win32API.SendKeys(u'南京')
    title = EditControl(searchFromControl = canlendarWindow, AutomationId = 'EventTitleTextbox')
    title.Click()
    Win32API.SendKeys('Hi')
    content = EditControl(searchFromControl = canlendarWindow, AutomationId = 'NotesTextboxContent')
    content.Click()
    Win32API.SendKeys('Nice to meet you!', 0.2)
    canlendarWindow.MetroClose()
    ShowDesktop()

if __name__ == '__main__':
    main()

测试Firefox:
Windows版Firefox(Version<=56)是用DirectUI实现的,只能看到一个窗口句柄,但是用UIAutomation就能看到网页里所有元素控件。但最新版Firefox57版本采用了新的Rust内核,不支持UIAutomation了。
现在是2018年,我用最新版的Firefox60测试发现该版本支持UIAutomation了。
 
UIAutomation的工作原理是:
当你用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,就能遍历程序的控件,得到控件各种属性,进行自动化操作。
所以如果你发现UIAutomation不能识别一些程序内的控件或部分不支持,这并不是UIAutomation的问题,
是程序作者没有处理WM_GETOBJECT或没有实现UIAutomation Provider,或者故意不想支持UIAutomation。
 
很多DirectUI程序都没有实现UIAutomation Provider,所以不支持自动化,要想支持自动化,必须程序作者修改源码支持。
 
源代码下载,最新代码支持py2, py3, x86, x64
 



原文来自:https://www.cnblogs.com/Yinkaisheng/p/3444132.html

您可能还会对下面的文章感兴趣: