Python自动化工具:pywinauto
一、pywinauto的安装
(1)安装命令
pip install -U pywinauto/pip3 install -U pywinauto
(2)验证是否安装成功
from pywinauto.application import Application
二、pywinauto的使用
1.1介绍程序的backend
首先要判断程序是用什么语言写的?在实例化会有区别,主要是判断程序的backend?
程序的backend大致有两种:
(1)Win32 API(backend=“win32”)
(2)MS UI Automation(backend=“uia”)
1.2如何判断程序的backend?
推荐使用spy++和inspect来检查(spy++和inspect工具的下载地址:https://github.com/blackrosezy/gui-inspect-tool);
1.3如何使用inspect来判断backend的类别
将inspect左上角的下拉列表中切换到“UI Automation”,然后鼠标点一下你需要测试的程序窗体,inspect就会显示相关信息。
inspect中显示了相关的信息,如下图所示。说明backend为uia。
如果inspect中显示拒绝访问,说明该程序的backend应该是win32;
这里主要是限制自动化控制进程的范围。如一个程序有多个实例,自动化控制一个实例,而保证其他实例(进程)不受影响。
主要有两种对象可以建立这种入口点——
-->Application()
-->Desktop()
Application的作用范围是一个进程,如一般的桌面应用程序都为此类。
Desktop的作用范围可以跨进程。主要用于像win10的计算器这样包含多个进程的程序。这种目前比较少见。使用方法见:https://pywinauto.readthedocs.io/en/latest/getting_started.html#entry-points-for-automation
建立好入口后,需要连接进程;
3.1连接进程的方法
(1)使用Application对象的start()方法
start(self, cmd_line, timeout=app_start_timeout) # instance method:
cmd_line参数就是你使用命令行启动程序的命令语句(程序路径)
例如:
app = Application().start(r"F:pythonauto_toolsTerminal_部标_2014-01-12-11.exe")
(2)使用Application对象的connect()方法
-->使用进程ID (PID)进行绑定;
app = Application().connect(process=15860)
进程的PID可以在任务管理器中查看;
-->使用窗口句柄绑定
app = Application().connect(handle=0x00030F9A)
窗口句柄可以在Spy++中查看 :
-->使用程序路径绑定
app=Application().connect(path=r"F:pythonauto_toolsTerminal_部标_2014-01-12-11.exe")
-->使用标题、类型等匹配
app = Application().connect(title_re="Terminal_部标_2014-01-12.*", class_name="#32770 (对话框)")
三、使用pywinauto操作窗口
1.启动程序
(1)application.Application().start('路径+程序名/程序名')
app = application.Application().start('notepad.exe')
例子:
myapp = Application().start("notepad") myapp.__setattr__("name","notepad")
错误用法:
from pywinauto import application app = application.Application.start('notepad.exe')
原因:因为start方法必须是针对应用实例的方法, 忘记了实例化操作, ()符号;
(2)MenuSelect方法自动检索Notepad上的菜单选项
(3)decode(‘gb2312’)方法,python3使用encode('gb2312')方法是把中文强制转换为unicode编码,对于非英文的操作系统都是要转换的;
例如:点击“帮助->关于记事本”操作;
app.Notepad.MenuSelect('帮助->关于记事本'.decode('gb2312'))
附注:抛出异常
① AttributeError: 'str' object has no attribute 'decode'
主要原因:Python2和Python3在字符串编码上的区别(必须将字节字符串解码后才能),python在bytes和str两种类型转换,所需要的函数依次是encode(),decode()
解决方案:str通过encode()方法可以编码为指定的bytes
反过来,当从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法。反之,则使用encode()方法即可!正确用法如下:
app.Notepad.MenuSelect('帮助->关于记事本'.encode('gb2312').decode('gb2312'))
例如,对一个写字板app应用中的窗口,在英文操作系统中,其标题是“untitled - Notepad”
可以使用以下两种方式调用该窗体
app.Untitled
app.Notepad
对于关于窗口,其标题是“About Notepad”
可以使用以下名称调用该窗体
app.AboutNotepad
这里的app是你刚才实例的对象,Notepad是类名;
- 查找/调用窗口
通过工具spy++lite查看窗口的类名和标题文字...
这里先介绍官方文档的两种方法:
(1)通过top_dlg = app.top_window_() 来获取最上面的window(不推荐,如果有新进程,就会得到错误对象)
(2)通过find_dlg = app.window_(title_re = ‘ ’, class_name = ‘ ’) 方法获得,title_re和 class_name这两个可以单独使用也可以一块使用,因为有时没有标题文本,也有时一个窗口类名有多个对象;
例如:
“Edit”有时当一个对话框中有多个输入框时会有多个Edit类名,对于“关于记事本”我们可以通过以下代码获得-->
about_dlg = app.window_(title_re = u"关于", class_name = "#32770")
中文要进行unicode编码,这里也可以通过decode(‘gb2312’)方法实现,也可以这样(u”关于”)
这里通过print一下about_dlg可以确定我们得到的是一个什么对象?
例如:
<pywinauto.application.WindowSpecification object at 0x01F0A530>
说明我们得到的是一个application.WindowSpecification对象
(3)通过dlg_spec = app.window(title='')
或者app.window(title_re=’’)
- 定位对话框
例如:我要定位“关于记事本”的对话框
① 第一种方法
about_dlg= app.window_(title_re="关于",class_name="#32770") # 这里可以进行正则匹配title app.window_(title_re='关于“记事本”').window_(title_re='确定').Click()
② 第二种方法
ABOUT= '关于“记事本”' OK= '确定' # about_dlg[OK].Click() # app[ABOUT][OK].Click() app['关于“记事本”']['确定'].Click()
- 在窗口上找到某个按钮
4.1 例如:在”关于记事本”窗口上找到“确定”按钮(button)
① 第一种方法
(1)打印当前窗口的所有controller(控件和属性)
about_dlg.print_control_identifiers()
注意事项:
窗口的控件和属性打印失败的原因,有可能是因为程序启动的时候,但是窗口并没有马上出现,可以给一个合适的休眠时间即可。
例如:
在pywinauto中,对话框下面的是controller,button,checkbox,textbox等都是controller。
static,SysLink,button等是它类型,后面接的是title,都是unicode的,这里面就有没有title的controller,再后面的(L,T,R,B)是这个控件的位置,分别对应着左上右下;
例如:在”关于记事本”窗口上找到“确定”按钮;
可以通过app.window_()方法,传入的参数可以是title,也可以是class_name,所以我说这两个值相当重要,一直在用,这里的title支持正则表达式,非常方便,在app上先找到about_dlg,然后再about_dlg上找确定button,
app.window_(title_re = u'关于“记事本”').window_(title_re = u'确定'),然后通过Click()方法来单击这个button;
② 第二种方法(非英文系统下)
OK = u'确定' about_dlg[OK].Click()
这种方法需要在about_dlg下找到u’确定’
③ 第三种方法(不需要找about_dlg)
app[u'关于“记事本”'][u'确定'].Click()
- 特殊符号快捷键,对应码表
SHIFT |
+ |
CTRL |
^ |
ALT |
% |
空格键 |
{SPACE} |
BACKSPACE |
{BACKSPACE}、{BS} or {BKSP} |
BREAK |
{BREAK} |
CAPS LOCK |
{CAPSLOCK} |
DEL or DELETE |
{DELETE} or {DEL} |
DOWN ARROW |
{DOWN} |
END |
{END} |
ENTER |
{ENTER} or ~ |
ESC |
{ESC} |
HELP |
{HELP} |
HOME |
{HOME} |
INS or INSERT |
{INSERT} or {INS} |
LEFT ARROW |
{LEFT} |
NUM LOCK |
{NUMLOCK} |
PAGE DOWN |
{PGDN} |
PAGE UP |
{PGUP} |
PRINT SCREEN |
{PRTSC} |
RIGHT ARROW |
{RIGHT} |
SCROLL LOCK |
{SCROLLLOCK} |
TAB |
{TAB} |
UP ARROW |
{UP} |
+ |
{ADD} |
- |
{SUBTRACT} |
* |
{MULTIPLY} |
/ |
{DIVIDE} |
(1)使用type_keys()函数,这里需要的快捷键是Alt+T+P:
例如:
dlg_spec = app.window(title='屏幕录像专家 V2017') dlg_spec.type_keys('%TP')
(2)使用type_keys()函数,光标跳到最后
app[u"Terminal_部标_2014-01-12"]['Edit'].type_keys('{END}')
(3)使用type_keys()函数,删除输入框的内容
app[u"Terminal_部标_2014-01-12"]['Edit'].type_keys('{BACKSPACE}' * 20)
这里输入20个回退键
(4)使用TypeKeys()函数,输入文本,不会清空原来的数据
例如:app["标题"]['类名'].TypeKeys(u"119.23.142.92")
app[u"Terminal_部标_2014-01-12"]['Edit'].TypeKeys(u"119.23.142.92")
注意:如果输入框内初始有文本,需要将光标移到最后,然后进行删除操作,最后才能输入文本;
(5)app['Cy']['Edit3'].SetEditText('bbbb')
(6)#app['Cy']['Edit3'].SetText('bbbb')
(7)app['Cy']['Edit4'].set_edit_text('你好')
#(4)(5)(7)3种输入值,与.TypeKeys区别在于,这个如果文本框禁止输入也可强制输入
a=app['Cy']['Edit1'].WindowText()#获取值 b=app['Cy']['Edit3'].texts()#获取值,返回一个数组 c=app['Cy']['Edit4'].text_block()#获取值
6.操作窗口控件
常见的窗口程序的控件:输入框(Edit)、按钮(Button)、复选框(CheckBox)、单选框(RadioButton)、下拉列表(ComboBox).
关于各个控件的函数方法官网:https://pywinauto.readthedocs.io/en/latest/controls_overview.html
6.1 结合程序,控件的用法
大致用法分两步:
● 找到控件
● 操作控件
接下来,如何让程序找到控件?
(1)如何匹配控件
① 第一种方法
最简单的通过空间特征进行匹配(窗体也可以看成大控件),匹配窗口的方法除了前面提到的window()方法,还可以通过中括号加窗口名,例如:
dlg_spec = app.window(title=r'EXE/EXE 转 MP4')
dlg_spec = app[r'EXE/EXE 转 MP4']
除了title,还可以使用class或者title+class或者相近的text和类来匹配控件,
② 第二种方法
如果我们知道了程序的层次结构,然后类似寻到DOM元素一样一层一层的匹配;
那么我们要如何找到层次结构呢?
pywinauto提供了print_control_identifiers()函数来显示该窗体下所有控件的结构。
得到文件名后面的编辑框的属性为:
Edit - '' (L259, T88, R429, B107)
['EXE/EXE 转 MP4Edit', 'Edit3']
所以我们可以通过控件的text或者title来查找控件。如:
edit = dlg_spec[''] # 1 edit = dlg_spec['Edit2'] # 2 edit = dlg_spec.Edit2 # 3
注意,对于输入控件Edit,一般不建议使用text内容绑定,因为Edit的text内容会发生变化。另外,绑定的控件也可能不唯一。对于title,我这里可能理解不够,属性显示的是Edit3,但实际上绑定的时候用的却是Edit2,也就是数字要减一。
(2)如何操作控件
对于Edit控件,要么就是向里面写内容,要么就是读里面的内容。
参考地址:
https://blog.csdn.net/shawpan/article/details/78170200
① 转换文件
例如:我们要向Edit3写入要转换文件的路径(r’E: est test .exe’),这里的文件名在中间又加了空格;
edit.set_text(r'E: est test .exe') # 1
edit.type_keys(r'E: est test .exe',with_spaces = True) # 2
上述代码第一种方法是直接设置edit的text,而第二种是在里面模拟键盘输入(如果字符串中没有空格,可以省略后面的参数)
注意:使用第二种方法输入并没有什么效果,因为该编辑框设置了禁止输入(自己手动敲键盘,发现编辑框没有反应)
② 点击对话框的按钮
--> 第一种方法
about_dlg= app.window_(title_re="关于",class_name="#32770") # 这里可以进行正则匹配title
app.window_(title_re='关于“记事本”').window_(title_re='确定').Click()
使用格式:
app.window_(title_re='窗体标题').window_(title_re='Button名').Click()
--> 第二种方法
ABOUT= '关于“记事本”' OK= '确定' # about_dlg[OK].Click() # app[ABOUT][OK].Click() app['关于“记事本”']['确定'].Click()
使用格式:app['控件标题']['Button名'].Click()
--> 第三种方法
dlg_spec = app[r'EXE/EXE 转 MP4'] dlg_spec.Button0.click()
使用格式:匹配控件方法.类名.click()
--> 第四种方法
dlg_spec = app[r'EXE/EXE 转 MP4'] dlg_spec['浏览'].click()
使用格式:匹配控件方法 + ['Button名'] + . + click()
举个例子:
手动操作的步骤: 先在查找范围后的ComboBox中找到要转换的源程序所在的文件夹,中间的list中就会出现该文件中的所有文件和文件夹,选中源文件,文件名后面的Edit里面就会显示文件名,最后点击打开按钮完成选择。
如果按照手动操作的步骤进行模拟,脚本编写起来会比较复杂,这里我们使用了一个trick。直接在文件名的编辑框中输入源文件的绝对路径,然后点击打开按钮完成选择。
dlg_open = app['打开'] dlg_open.Edit.set_text(r'E: est test .exe') dlg_open['打开'].click()
有三点需要说明的地方:
1、 “打开”对话框中只有一个输入框,所以,使用Edit就可以绑定文件名的编辑框。
2、 编辑框是可编辑的,也就是说我们可以使用type_keys()函数模拟键盘输入文件路径,但是需要显式指明字符串中含有空格,不然空格会被忽略掉。而且需要先清空编辑框的内容。
dlg_open.Edit.type_keys(r'E: est test .exe')
图中为没有事先清理编辑框和没有显式指明含有空格的结果。
3、 若文件绝对路径输入错误,点击“打开”按钮会先跳到路径中最后一个文件夹,然后再点击“打开”,提示找不到文件的错误。
③ 勾选选项操作
现在变成了5帧/秒,建议扩频后不超过15帧/秒,也就是扩频3倍即可,默认参数也是3,所以只需要勾选上“自动扩频”即可。
dlg_spec.CheckBox0.check()
④ 处理对话框的确认按钮
我们需要做的就是找到这个对话框,点击里面的OK按钮;
app['屏幕录像专家'].Ok.click()
⑤ listview如何进行双击
使用DoubleClick()方法
⑥ 鼠标右击
使用RightClick()方法
⑦ 处理下拉列表
使用select()方法
终端类型选择“A08”,代码如下:
⑧ 按、移动、释放鼠标
Control.PressMouse/MoveMouse/ReleaseMouse()
- 等待窗口出现
实际操作中,我们不知道它什么时候会处理完?如上图的OK按钮只有处理完后弹窗后才能点击;
(1)等待法。首先预估一个所需的最长时间,保证此时已经处理完并弹窗,然后让程序等待这么长时间后在点击OK按钮;
import time ... time.sleep(100) app['屏幕录像专家'].Ok.click()
(2)查询法。
写个循环,一直查询是否存在“屏幕录像专家”弹窗,若存在,则退出循环。
...
注意,在查询的时候,最好不要用app[‘屏幕录像专家’].exists()。这个匹配不精准,如下图中的最后一个句柄。这个句柄在开启程序后就一直存在,且于我们要找的对话框title一样,所以我们在查找的时候需要加上class_name。
(3)查询等待法。
查询有个缺点就是如果一直没出现,就会一直等待。所以我们最好设置一个等待时间限。
使用模块中自带的wait函数就可以实现该功能了;
官网地址:
https://pywinauto.readthedocs.io/en/latest/wait_long_operations.html
- 举个例子code
(1)批量转换exe视频;
# -*- coding: utf-8 -*- """ Created on Wed Oct 4 16:52:13 2017 @author: x """ from pywinauto.application import Application program_path = r"D:Program Files (x86) lxsoft屏幕录像专家 共享版 V2017屏录专家.exe" app = Application().start(program_path) dlg_spec = app.window(title_re='屏幕录像专家.*',class_name='TMainForm') #dlg_spec.type_keys('%TP') dlg_spec.menu_select(r"转换工具->EXE/LXE转成MP4") dlg_spec = app[r'EXE/EXE 转 MP4'] dlg_spec.print_control_identifiers() # 打印该窗体下的所有控件结构 for line in open('ToBeConvert.txt'): filename = line.strip() # 去掉读取每一行时最后带着的空格和回车符 dlg_spec.Button3.click() # 点击“浏览”按钮 dlg_open = app.window(title=r'打开') # 获取“打开”对话框句柄 dlg_open.Edit.type_keys(filename,with_spaces = True) # dlg_open.Edit.set_text(filename) # 将文件绝对路径写入编辑框中 dlg_open.Button0.click() # 点击“打开”按钮 dlg_open.wait_not('visible') dlg_spec.CheckBox0.check() # 勾选自动扩帧 dlg_spec.Button0.click() # 点击“转换” app['另存为'].Button0.click() # 点击“另存为”对话框的“保存”按钮 app.window(title=r'屏幕录像专家',class_name='TMessageForm').Wait('enabled',timeout=300) # 等待转换结束 app.window(title=r'屏幕录像专家',class_name='TMessageForm').Ok.click() # 关闭转换完成后弹出的对话框
附注:
-1- 根据屏幕录像专家程序的安装位置修改变量‘program_path’的值。
-2- 在当前目录新建一个 ToBeConvert.txt 文档,每行写上一个带转换的源文件,目标文件目录默认与源文件相同。
E: est test .exe
E: est test1.exe
- 注意事项
在MenuSelect函数中不支持正则,完全是全文匹配,如我输入:
dig = app.Notepad.MenuSelect("编辑->替换".decode('gb2312')) 是找不到对象的
必须要:
dig = app.Notepad.MenuSelect("编辑(E)->替换(R)".decode('gb2312'))
得把(R) (E)写上才行,但是奇怪的是上面的“帮助->关于记事本”就不用输入;
(1)例1:
#! /usr/bin/env python #coding=gbk import time from pywinauto import application app = application.Application().start('notepad.exe') app.Notepad.MenuSelect('帮助->关于记事本'.encode('gb2312').decode('gb2312')) time.sleep(0.5) #这里有两种方法可以进行定位“关于记事本”的对话框 #top_dlg = app.top_window_() 不推荐这种方式,因为可能得到的并不是你想要的 about_dlg = app.window_(title_re = u"关于", class_name = "#32770")#这里可以进行正则匹配title #about_dlg.print_control_identifiers() app.window_(title_re = u'关于“记事本”').window_(title_re = u'确定').Click() app.Notepad.MenuSelect('帮助->关于记事本'.encode('gb2312').decode('gb2312')) time.sleep(0.5) #停0.5s 否则你都看不出来它是否弹出来了! ABOUT = u'关于“记事本”' OK = u'确定' #about_dlg[OK].Click() #app[ABOUT][OK].Click() app[u'关于“记事本”'][u'确定'].Click() app.Notepad.TypeKeys(u"杨彦星") dig = app.Notepad.MenuSelect("编辑(E)->替换(R)".encode('gb2312').decode('gb2312')) Replace = u'替换' Cancle = u'取消' time.sleep(0.5) app[Replace][Cancle].Click() dialogs = app.windows_()
(2)例2:
import time from pywinauto import application app = application.Application().start('notepad.exe') app.Notepad.MenuSelect('帮助->关于记事本') time.sleep(1) OK='确定' app['关于“记事本”']['确定'].Click() #或者app['关于“记事本”'][OK].Click() time.sleep(1) app.notepad.TypeKeys("输入测试文本") time.sleep(2) dig = app.Notepad.MenuSelect("编辑(E)->替换(R)") time.sleep(1) Replace='替换' Cancel='取消' app[Replace][Cancel].Click() #要不然就写成app['替换']['取消'].Click() time.sleep(1) dialogs = app.windows_()