python UIA實例


開源自己用python封裝的一個Windows GUI(UI Automation)自動化工具,支持MFC,Windows Forms,WPF,Metro,Qt

 

首先,大家可以看下這個鏈接 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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!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
1
2
#從window查找edit
edit  =  window.EditControl()

 修改EditControl內容並在當前文字后面模擬打字 

1
2
3
#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://github.com/yinkaisheng/Python-Automation-for-Windows


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM