如何使用
转眼间学 Python 也有接近 4 年的事情了。
自从学了这门所谓的编程语言之后,一切皆对象的思维方式影响了我的工作、生活,顺带着影响着人生观等。所以说,专注一件事情的同时,顺带着把其他的一些东西醒悟了,这也算是一件圆满的答复吧。
- 本人学习、整理的笔记个人YY为比较直观,用词刺眼,如有冒犯,多多见谅。
网上介绍 Python 的太多,我就用最实际的来说:我个人认为硬件技术的发展使得目前的硬件问题变得性能过剩,加上最近几年网上大多数“专家”也发现了摩尔定律可能性或者存在被打破的规律,so,不用多说,学 Python 还是学 other 那是需求者要说的事情;假设你不喜欢蛋糕,每天路过一个蛋糕店,且这个蛋糕店能让你免费领取、试吃新鲜刚做好蛋糕,对于你自己而言,东西再好、免费也是白搭。所以说,一门技术对于一个人来说,最重要的第一点是用途,其次就是易学、易用。
与我来说,Python 在我实际当中本就有实现的用途,其次就是易学与易用。
那么本文的主角 Pywinauto 顾名思义,就是利用 Python 在电脑上进行软件的 GUI 自动化,模拟人工操作;那么,你的流程可以先这样:
- 利用面向对象的思维方式,先将重复性且有规律的工作内容进行分类;
- 利用流程思维(思维导图的方式)将工作类进行流程设计(面向过程);
- 使用 Python Pywinauto 库、os 模块(或者 Pathlib)、win32clipboard(像手动操作复制图片、或者文字需要的)……
[这里只列举了我自己常用的几个]
以上只是自己的开头,接下来是正文的开始,我自己从 Pywinauto 官方文档 - 点击传送 里使用 沉浸式翻译 插件进行摘录保存,里面根据自己的理解修改了一定量的东西,再加上根据自己的阅读习惯进行一定量的排版。
推荐辅助工具:Windows - SDK - inspect,微软官方:https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ 点击 Download the installer 等待下载好,安装时尽管点下一步。
inspect 安装之后先重启电脑,否则打开 inspect 可能会卡死并且该软件不工作。重启后请在我的电脑依次打开:C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64 (注意:10.0.22621.0 是我自己目前电脑最新的版本号,请根据你的电脑来,否则就不是说依次打开了!) 在 x64 的文件夹下找到 inspect.exe 建议右击 - 发送到 - 桌面快捷方式 。
定义
一些重要的定义可能对初学者有所帮助。
- 对话框是一个包含其他几个 GUI 元素/控件(如按钮、编辑框等)的窗口。对话框不一定是主窗口。主窗体顶部的消息框也是一个对话框。主窗体也被pywinauto视为对话框。
- 控件是层次结构中任何级别的 GUI 元素。该定义包括窗口、按钮、编辑框、网格、网格单元格、条形图等。
- Win32 API 技术(pywinauto 中的“win32”后端)为每个控件提供标识符。这是一个称为句柄的唯一整数。
- UI 自动化 API(pywinauto 中的“uia”后端)可能不会为每个 GUI 元素提供窗口句柄。这样的元素对“win32”后端是不可见的。但是
Inspect.exe
如果可用,可以显示属性NativeWindowHandle
。
如何指定可用的应用程序实例
Application()
实例是与要自动化的应用程序相关的所有工作的联系点。因此,应用程序实例需要连接到进程。有两种方法可以做到这一点:
start(self, cmd_line, timeout=app_start_timeout) # instance method:
or:
connect(self, **kwargs) # instance method:
start()
在应用程序未运行且需要启动它时使用。按以下方式使用它:
app = Application().start(r"c:\path\to\your\application -a -n -y --arguments")
timeout
参数是可选的,只有在应用程序需要很长时间才能启动时才需要使用它。
connect()
在要自动化的应用程序已启动时使用。若要指定已在运行的应用程序,需要指定以下选项之一:
process
: 应用程序的进程 ID,例如app = Application().connect(process=2341)
handle
: 应用程序窗口的窗口句柄,例如app = Application().connect(handle=0x010f0c)
path
: 进程的可执行文件的路径(GetModuleFileNameEx
用于查找每个进程的路径,并与传入的值进行比较),例如app = Application().connect(path=r"c:\windows\system32\notepad.exe")
- 或指定窗口的参数的任意组合,这些参数将传递给
pywinauto.findwindows.find_elements()
函数。例如:app = Application().connect(title_re=".*Notepad", class_name="Notepad")
注意:应用程序必须准备就绪,然后才能使用
connect()
。没有像在start()
之后找到应用程序时那样的超时或重试。
因此,如果你在pywinauto之外启动应用程序,你需要休眠或编程一个等待循环,以等待应用程序完全启动。
如何指定应用程序的对话框
一旦应用程序实例知道它连接到哪个应用程序,就需要指定要处理的对话框。
有许多不同的方法可以做到这一点。最常见的方法是使用项目或属性访问权限根据对话框的标题选择对话框。例如
dlg = app.Notepad
or equivalently或同等
dlg = app['Notepad']
下一个最简单的方法是要求 top_window()
例如
dlg = app.top_window()
这将返回应用程序顶级窗口中具有最高 Z 顺序的窗口。
注意:这目前尚未经过测试,因此我不确定它是否会返回正确的窗口。它肯定是应用程序的顶级窗口 - 它可能不是 Z 顺序中最高的窗口。
如果这还不够控制,那么您可以使用可以传递的 findwindows.find_windows()
相同参数,例如
dlg = app.window(title_re="Page Setup", class_name="#32770")
最后,为了拥有您可以使用的最大控制权
dialogs = app.windows()
这将返回应用程序的所有可见、已启用的顶级窗口的列表。然后,您可以使用模块中 handleprops
的一些方法选择所需的对话框。一旦你有了你需要的手柄,然后使用
app.window(handle=win)
注意:如果对话框的标题很长 - 则输入属性访问权限的时间可能很长,在这种情况下,它通常更容易使用
app.window(title_re=".*Part of Title.*")
如何在对话框中指定控件
有多种方法可以指定控件,最简单的方法是
app.dlg.control
app['dlg']['control']
第二个更适合非英语操作系统,您需要传递 unicode 字符串,例如 app[u'your dlg title'][u'your ctrl title']
该代码从以下各项为每个控件构建多个标识符:
- title
- friendly class
- title + friendly class
如果控件的标题文本为空(删除非字符后),则不使用此文本。相反,我们查找控件上方和右侧最接近的标题文本。并附加友好类。所以列表变成了
- friendly class
- closest text + friendly class
为对话框中的所有控件创建一组标识符后,我们就会消除它们的歧义。
使用 WindowSpecification.print_control_identifiers()
方法
例如:
app.YourDialog.print_control_identifiers()
Sample output示例输出
Button - Paper (L1075, T394, R1411, B485)
'PaperGroupBox' 'Paper' 'GroupBox'
Static - Si&ze: (L1087, T420, R1141, B433)
'SizeStatic' 'Static' 'Size'
ComboBox - (L1159, T418, R1399, B439)
'ComboBox' 'SizeComboBox'
Static - &Source: (L1087, T454, R1141, B467)
'Source' 'Static' 'SourceStatic'
ComboBox - (L1159, T449, R1399, B470)
'ComboBox' 'SourceComboBox'
Button - Orientation (L1075, T493, R1171, B584)
'GroupBox' 'Orientation' 'OrientationGroupBox'
Button - P&ortrait (L1087, T514, R1165, B534)
'Portrait' 'RadioButton' 'PortraitRadioButton'
Button - L&andscape (L1087, T548, R1165, B568)
'RadioButton' 'LandscapeRadioButton' 'Landscape'
Button - Margins (inches) (L1183, T493, R1411, B584)
'Marginsinches' 'MarginsinchesGroupBox' 'GroupBox'
Static - &Left: (L1195, T519, R1243, B532)
'LeftStatic' 'Static' 'Left'
Edit - (L1243, T514, R1285, B534)
'Edit' 'LeftEdit'
Static - &Right: (L1309, T519, R1357, B532)
'Right' 'Static' 'RightStatic'
Edit - (L1357, T514, R1399, B534)
'Edit' 'RightEdit'
Static - &Top: (L1195, T550, R1243, B563)
'Top' 'Static' 'TopStatic'
Edit - (L1243, T548, R1285, B568)
'Edit' 'TopEdit'
Static - &Bottom: (L1309, T550, R1357, B563)
'BottomStatic' 'Static' 'Bottom'
Edit - (L1357, T548, R1399, B568)
'Edit' 'BottomEdit'
Static - &Header: (L1075, T600, R1119, B613)
'Header' 'Static' 'HeaderStatic'
Edit - (L1147, T599, R1408, B619)
'Edit' 'TopEdit'
Static - &Footer: (L1075, T631, R1119, B644)
'FooterStatic' 'Static' 'Footer'
Edit - (L1147, T630, R1408, B650)
'Edit' 'FooterEdit'
Button - OK (L1348, T664, R1423, B687)
'Button' 'OK' 'OKButton'
Button - Cancel (L1429, T664, R1504, B687)
'Cancel' 'Button' 'CancelButton'
Button - &Printer... (L1510, T664, R1585, B687)
'Button' 'Printer' 'PrinterButton'
Button - Preview (L1423, T394, R1585, B651)
'Preview' 'GroupBox' 'PreviewGroupBox'
Static - (L1458, T456, R1549, B586)
'PreviewStatic' 'Static'
Static - (L1549, T464, R1557, B594)
'PreviewStatic' 'Static'
Static - (L1466, T586, R1557, B594)
'Static' 'BottomStatic'
此示例取自 test_application.py
注意:通过此方法打印的标识符已通过使标识符唯一的过程运行。因此,如果您有两个编辑框,它们的标识符中都列出了“编辑”。
实际上,虽然第一个可以称为“Edit”、“Edit0”、“Edit1”,而第二个应该称为“Edit2”。注意:您不必准确!。假设我们从上面的例子中举一个例子
Button - Margins (inches) (L1183, T493, R1411, B584) 'Marginsinches' 'MarginsinchesGroupBox' 'GroupBox'
假设您不喜欢其中任何一个
GroupBox
- 太通用了,它可以是任何组框Marginsinches
而且MarginsinchesGroupBox
- 这些看起来不对,省略“英寸”部分会更好好了,现在该代码将您使用的标识符与对话框中的所有可用标识符进行最佳匹配。
例如,如果闯入调试器,则可以看到如何使用不同的标识符(Pdb) print app.PageSetup.Margins.window_text() Margins (inches) (Pdb) print app.PageSetup.MarginsGroupBox.window_text() Margins (inches)
这也将迎合错别字。尽管您仍然必须小心,因为对话框中有 2 个相似的标识符,但您使用的拼写错误可能与您想到的控件更相似。
如何在英语以外的应用语言中使用 pywinauto
由于 Python 在代码中不支持 unicode 标识符,因此不能使用属性访问来引用控件,因此必须使用项访问或显式调用 window()
。
所以而不是写
app.dialog_ident.control_ident.click()
你必须写
app['dialog_ident']['control_ident'].click()
或显式使用 window()
app.window(title_re="NonAsciiCharacters").window(title="MoreNonAsciiCharacters").click()
查看此检查 examplesmisc_examples.py get_info()
的示例
如何处理未按预期响应的控件(例如 OwnerDraw 控件)
某些控件(尤其是 Ownerdrawn 控件)不会按预期响应事件。例如,如果您查看任何 HLP 文件并转到“索引”选项卡(单击“搜索”按钮),您将看到一个列表框。
在此上运行 Spy 或 Winspector 将向您显示它确实是一个列表框 - 但它是所有者绘制的。这意味着开发人员已告诉 Windows,他们将覆盖项目的显示方式并自行完成。
在这种情况下,他们已经做到了无法检索字符串:-(。
那么这会导致什么问题呢?
app.HelpTopics.ListBox.texts() # 1
app.HelpTopics.ListBox.select("ItemInList") # 2
- 将返回一个空字符串列表,这意味着 pywinauto 无法获取列表框中的字符串
- 这将失败并出现 IndexError,因为 ListBox 的 select(string) 方法在 Texts 中查找项,以了解它应该选择的项的索引。
以下解决方法适用于此控件
app.HelpTopics.ListBox.select(1)
这将选择列表框中的第 2 项,因为它不是字符串查找,它可以正常工作。
不幸的是,即使这样也并不总是有效。开发人员可以使控件不响应标准事件(如 Select)。在这种情况下,选择列表框中的项目的唯一方法是使用 TypeKeys() 的键盘模拟。
这允许您将任何击键发送到控件。因此,要选择您将使用的第 3 项
app.Helptopics.ListBox1.type_keys("{HOME}{DOWN 2}{ENTER}")
{HOME}
将确保突出显示第一项。{DOWN 2}
然后将突出显示向下移动两个项目{ENTER}
将选择突出显示的项目
如果应用程序广泛使用了类似的控件类型,则可以通过从 ListBox 派生一个新类来简化它的使用,该类可以使用有关特定应用程序的额外知识。
例如,在 WinHelp 示例中,每次在列表视图中突出显示项时,其文本都会插入到列表上方的 Edit 控件中,您可以从那里获取项的文本,例如
# print the text of the item currently selected in the list box
# (as long as you are not typing into the Edit control!)
print app.HelpTopics.Edit.texts()[1]
如何访问系统托盘(又名 SysTray,又名“通知区域”)
在时钟附近有代表正在运行的应用程序的图标,此区域通常称为“系统托盘”。事实上,这个区域有许多不同的窗口/控件。包含图标的控件实际上是一个工具栏。
它是具有类 TrayNotifyWnd 的窗口中的 Pager 控件的子项,该窗口位于另一个具有类Shell_TrayWnd的窗口中,所有这些窗口都是正在运行的 Explorer 实例的一部分。谢天谢地,你不需要记住所有这些:-)。
重要的是要记住,您正在“Explorer.exe”应用程序中查找一个具有“Shell_TrayWnd”类的窗口,该窗口具有标题为“通知区域”的工具栏控件。
获取此操作的一种方法是执行以下操作
import pywinauto.application
app = pywinauto.application.Application().connect(path="explorer")
systray_icons = app.ShellTrayWnd.NotificationAreaToolbar
任务栏模块提供对系统托盘的非常初步的访问。
它定义了以下变量:
- explorer_app:
defines an Application()
定义连接到正在运行的资源管理器的 Application() 对象。您可能不需要直接使用它。 - TaskBar: 任务栏
任务栏的句柄(包含“开始”按钮、“快速启动”图标、正在运行的任务等的栏) - StartButton:开始按钮:
我想你可能知道这是什么! - QuickLaunch: 快速启动
带有快速启动图标的工具栏 - SystemTray:系统托盘
包含时钟和系统托盘图标的窗口 - Clock: 时钟
clock时钟 - SystemTrayIcons:SystemTrayIcons:
表示系统托盘图标的工具栏 - RunningApplications: 运行应用程序
表示正在运行的应用程序的工具栏
我还在模块中提供了两个函数,可用于单击系统托盘图标:
ClickSystemTrayIcon(button)
:
您可以使用它左键单击系统托盘中的可见图标。我不得不特别说可见图标,因为可能有许多不可见的图标显然无法单击。按钮可以是任何整数。如果指定 3,则它将找到并单击第 3 个可见按钮。
(现在这里几乎没有执行错误检查,但此方法将来很可能会被移动/重命名。RightClickSystemTrayIcon(button)
:
类似于ClickSytemTrayIcon
,但执行右键单击。
通常,当您单击/右键单击图标时,您会看到一个弹出菜单。此时要记住的是,弹出菜单是应用程序自动化的一部分,而不是资源管理器的一部分。
e.g.例如:
# connect to outlook
outlook = Application.connect(path='outlook.exe')
# click on Outlook's icon
taskbar.ClickSystemTrayIcon("Microsoft Outlook")
# Select an item in the popup menu
outlook.PopupMenu.Menu().get_menu_path("Cancel Server Request")[0].click()
COM 线程模型
默认情况下,如果在导入 pywinauto 之前未定义其他模型,则 pywinauto 会在 init 上设置客户端多线程 COM 模型 (MTA)。该模型可以由另一个导入的模块隐式设置,也可以通过 sys.coinit_flags
显式指定。
通过显式设置单螺纹分隔模型来覆盖 MTA 的示例。
import sys
sys.coinit_flags = 2 # COINIT_APARTMENTTHREADED
import pywinauto
请注意,COM 模型的最终值将赋值回 sys.coinit_flags
。这是为了避免与其他模块发生冲突。的 sys.coinit_flags
可能值:
0
- 多线程公寓模型 (MTA)2
- 单线程公寓模型 (STA)
More info:更多信息:
-
关于 Microsoft COM 线程模型:了解和使用 COM 线程模型: Understanding and Using COM Threading Models
-
关于pywinauto MTA的内部讨论。discussion