Время на прочтение
14 мин
Количество просмотров 87K
Python библиотека pywinauto — это open source проект по автоматизации десктопных GUI приложений на Windows. За последние два года в ней появились новые крупные фичи:
- Поддержка технологии MS UI Automation. Интерфейс прежний, и теперь поддерживаются: WinForms, WPF, Qt5, Windows Store (UWP) и так далее — почти все, что есть на Windows.
- Система бэкендов/плагинов (сейчас их двое под капотом: дефолтный
"win32"
и новый"uia"
). Дальше плавно двигаемся в сторону кросс-платформенности. - Win32 хуки для мыши и клавиатуры (hot keys в духе pyHook).
Также сделаем небольшой обзор того, что есть в open source для десктопной автоматизации (без претензий на серьезное сравнение).
Эта статья — частично расшифровка доклада с конференции SQA Days 20 в Минске (видеозапись и слайды), частично русская версия Getting Started Guide для pywinauto.
- Основные подходы
- Координатный метод
- Распознавание эталонных изображений
- Accessibility технологии
- Основные десктопные accessibility технологии
- Старый добрый Win32 API
- Microsoft UI Automation
- AT-SPI (Linux)
- Apple Accessibility API
- Как начать работать с pywinauto
- Входные точки для автоматизации
- Спецификации окон/элементов
- Магия доступа по атрибуту и по ключу
- Пять правил для магических имен
Начнём с краткого обзора опен сорса в этой области. Для десктопных GUI приложений всё несколько сложнее, чем для веба, у которого есть Selenium. Вот основные подходы:
Координатный метод
Хардкодим точки кликов, надеемся на удачные попадания.
[+] Кросс-платформенный, легко реализуемый.
[+] Легко сделать «record-replay» запись тестов.
[-] Самый нестабильный к изменению разрешения экрана, темы, шрифтов, размеров окон и т.п.
[-] Нужны огромные усилия на поддержку, часто проще перегенерить тесты с нуля или тестировать вручную.
[-] Автоматизирует только действия, для верификации и извлечения данных есть другие методы.
Инструменты (кросс-платформенные): autopy, PyAutoGUI, PyUserInput и многие другие. Как правило, более сложные инструменты включают в себя эту функциональность (не всегда кросс-платформенно).
Стоит сказать, что координатный метод может дополнять остальные подходы. Например, для кастомной графики можно кликать по относительным координатам (от левого верхнего угла окна/элемента, а не всего экрана) — обычно это достаточно надежно, особенно если учитывать длину/ширину всего элемента (тогда и разное разрешение экрана не помешает).
Другой вариант: выделять для тестов только одну машину со стабильными настройками (не кросс-платформенно, но в каких-то случаях годится).
Распознавание эталонных изображений
[+] Кросс-платформенный
[+-] Относительно надежный (лучше, чем координатный метод), но всё же требует хитростей.
[-+] Относительно медленный, т.к. требует ресурсов CPU для алгоритмов распознавания.
[-] О распознавании текста (OCR), как правило, речи не идёт => нельзя достать текстовые данные. Насколько мне известно, существующие OCR решения не слишком надежны для этого типа задач, и широкого применения не имеют (welcome в комменты, если это уже не так).
Инструменты: Sikuli, Lackey (Sikuli-совместимый, на чистом Python), PyAutoGUI.
Accessibility технологии
[+] Самый надежный метод, т.к. позволяет искать по тексту, независимо от того, как он отрисован системой или фреймворком.
[+] Позволяет извлекать текстовые данные => проще верифицировать результаты тестов.
[+] Как правило, самый быстрый, т.к. почти не расходует ресурсы CPU.
[-] Тяжело сделать кросс-платформенный инструмент: абсолютно все open-source библиотеки поддерживают одну-две accessibility технологии. Windows/Linux/MacOS целиком не поддерживает никто, кроме платных типа TestComplete, UFT или Squish.
[-] Не всегда такая технология в принципе доступна. Например, тестирование загрузочного экрана внутри VirtualBox’а — тут без распознавания изображений не обойтись. Но во многих классических случаях все-таки accessibility подход применим. О нем дальше и пойдет речь.
Инструменты: TestStack.White на C#, Winium.Desktop на C# (Selenium совместимый), MS WinAppDriver на C# (Appium совместимый), pywinauto, pyatom (совместим с LDTP), Python-UIAutomation-for-Windows, RAutomation на Ruby, LDTP (Linux Desktop Testing Project) и его Windows версия Cobra.
LDTP — пожалуй, единственный кросс-платформенный open-source инструмент (точнее семейство библиотек) на основе accessibility технологий. Однако он не слишком популярен. Сам не пользовался им, но по отзывам интерфейс у него не самый удобный. Если есть позитивные отзывы, прошу поделиться в комментах.
Тестовый backdoor (a.k.a. внутренний велосипед)
Для кросс-платформенных приложений сами разработчики часто делают внутренний механизм для обеспечения testability. Например, создают служебный TCP сервер в приложении, тесты к нему подключаются и посылают текстовые команды: на что нажать, откуда взять данные и т.п. Надежно, но не универсально.
Основные десктопные accessibility технологии
Старый добрый Win32 API
Большинство Windows приложений, написанных до выхода WPF и затем Windows Store, построены так или иначе на Win32 API. А именно, MFC, WTL, C++ Builder, Delphi, VB6 — все эти инструменты используют Win32 API. Даже Windows Forms — в значительной степени Win32 API совместимые.
Инструменты: AutoIt (похож на VB) и Python обертка pyautoit, AutoHotkey (собственный язык, есть IDispatch COM интерфейс), pywinauto (Python), RAutomation (Ruby), win32-autogui (Ruby).
Microsoft UI Automation
Главный плюс: технология MS UI Automation поддерживает подавляющее большинство GUI приложений на Windows за редкими исключениями. Проблема: она не сильно легче в изучении, чем Win32 API. Иначе никто бы не делал оберток над ней.
Фактически это набор custom COM интерфейсов (в основном, UIAutomationCore.dll), а также имеет .NET оболочку в виде namespace System.Windows.Automation
. Она, кстати, имеет привнесенный баг, из-за которого некоторые UI элементы могут быть пропущены. Поэтому лучше использовать UIAutomationCore.dll напрямую (если слышали про UiaComWrapper на C#, то это оно).
Разновидности COM интерфейсов:
(1) Базовый IUknown — «the root of all evil». Самый низкоуровневый, ни разу не user-friendly.
(2) IDispatch и производные (например, Excel.Application
), которые можно использовать в Python с помощью пакета win32com.client (входит в pyWin32). Самый удобный и красивый вариант.
(3) Custom интерфейсы, с которыми умеет работать сторонний Python пакет comtypes.
Инструменты: TestStack.White на C#, pywinauto 0.6.0+, Winium.Desktop на C#, Python-UIAutomation-for-Windows (у них исходный код сишных оберток над UIAutomationCore.dll не раскрыт), RAutomation на Ruby.
AT-SPI
Несмотря на то, что почти все оси семейства Linux построены на X Window System (в Fedora 25 «иксы» поменяли на Wayland), «иксы» позволяют оперировать только окнами верхнего уровня и мышью/клавиатурой. Для детального разбора по кнопкам, лист боксам и так далее — существует технология AT-SPI. У самых популярных оконных менеджеров есть так называемый AT-SPI registry демон, который и обеспечивает для приложений автоматизируемый GUI (как минимум поддерживаются Qt и GTK).
Инструменты: pyatspi2.
pyatspi2, на мой взгляд, содержит слишком много зависимостей типа того же PyGObject. Сама технология доступна в виде обычной динамической библиотеки libatspi.so
. К ней имеется Reference Manual. Для библиотеки pywinauto планируем реализовать поддержку AT-SPI имеено так: через загрузку libatspi.so и модуль ctypes. Есть небольшая проблема только в использовании нужной версии, ведь для GTK+ и Qt приложений они немного разные. Вероятный выпуск pywinauto 0.7.0 с полноценной поддержкой Linux можно ожидать в первой половине 2018-го.
Apple Accessibility API
На MacOS есть собственный язык автоматизации AppleScript. Для реализации чего-то подобного на Python, разумеется, нужно использовать функции из ObjectiveC. Начиная, кажется, еще с MacOS 10.6 в предустановленный питон включается пакет pyobjc. Это также облегчит список зависимостей для будущей поддержки в pywinauto.
Инструменты: Кроме языка Apple Script, стоит обратить внимание на ATOMac, он же pyatom. Он совместим по интерфейсу с LDTP, но также является самостоятельной библиотекой. На нем есть пример автоматизации iTunes на macOs, написанный моим студентом. Есть известная проблема: не работают гибкие тайминги (методы waitFor*
). Но, в целом, неплохая вещь.
Как начать работать с pywinauto
Первым делом стоит вооружиться инспектором GUI объектов (то, что называют Spy tool). Он поможет изучить приложение изнутри: как устроена иерархия элементов, какие свойства доступны. Самые известные инспекторы объектов:
- Spy++ — входит в поставку Visual Studio, включая Express или Community Edition. Использует Win32 API. Также известен его клон AutoIt Window Info.
- Inspect.exe — входит в Windows SDK. Если он у вас установлен, то на 64-битной Windows можно найти его в папке
C:\Program Files (x86)\Windows Kits\<winver>\bin\x64
. В самом инспекторе нужно выбрать режим UI Automation вместо MS AA (Active Accessibility, предок UI Automation).
Просветив приложение насквозь, выбираем бэкенд, который будем использовать. Достаточно указать имя бэкенда при создании объекта Application.
- backend=»win32″ — пока используется по умолчанию, хорошо работает с MFC, WTL, VB6 и другими legacy приложениями.
- backend=»uia» — новый бэкенд для MS UI Automation: идеально работает с WPF и WinForms; также хорош для Delphi и Windows Store приложений; работает с Qt5 и некоторыми Java приложениями. И вообще, если Inspect.exe видит элементы и их свойства, значит этот бэкенд подходит. В принципе, большинство браузеров тоже поддерживает UI Automation (Mozilla по умолчанию, а Хрому при запуске нужно скормить ключ командной строки
--force-renderer-accessibility
, чтобы увидеть элементы на страницах в Inspect.exe). Конечно, конкуренция с Selenium в этой области навряд ли возможна. Просто еще один способ работать с браузером (может пригодиться для кросс-продуктового сценария).
Входные точки для автоматизации
Приложение достаточно изучено. Пора создать объект Application и запустить его или присоединиться к уже запущенному. Это не просто клон стандартного класса subprocess.Popen
, а именно вводный объект, который ограничивает все ваши действия границами процесса. Это очень полезно, если запущено несколько экземпляров приложения, а остальные трогать не хочется.
from pywinauto.application import Application
app = Application(backend="uia").start('notepad.exe')
# Опишем окно, которое хотим найти в процессе Notepad.exe
dlg_spec = app.UntitledNotepad
# ждем пока окно реально появится
actionable_dlg = dlg_spec.wait('visible')
Если хочется управлять сразу несколькими приложениями, вам поможет класс Desktop
. Например, в калькуляторе на Win10 иерархия элементов размазана аж по нескольким процессам (не только calc.exe
). Так что без объекта Desktop
не обойтись.
from subprocess import Popen
from pywinauto import Desktop
Popen('calc.exe', shell=True)
dlg = Desktop(backend="uia").Calculator
dlg.wait('visible')
Корневой объект (Application
или Desktop
) — это единственное место, где нужно указывать бэкенд. Все остальное прозрачно ложится в концепцию «спецификация->враппер», о которой дальше.
Спецификации окон/элементов
Это основная концепция, на которой строится интерфейс pywinauto. Вы можете описать окно/элемент приближенно или более детально, даже если оно еще не существует или уже закрыто. Спецификация окна (объект WindowSpecification) хранит в себе критерии, по которым нужно искать реальное окно или элемент.
Пример детальной спецификации окна:
>>> dlg_spec = app.window(title='Untitled - Notepad')
>>> dlg_spec
<pywinauto.application.WindowSpecification object at 0x0568B790>
>>> dlg_spec.wrapper_object()
<pywinauto.controls.win32_controls.DialogWrapper object at 0x05639B70>
Сам поиск окна происходит по вызову метода .wrapper_object()
. Он возвращает некий «враппер» для реального окна/элемента или кидает ElementNotFoundError
(иногда ElementAmbiguousError
, если найдено несколько элементов, то есть требуется уточнить критерий поиска). Этот «враппер» уже умеет делать какие-то действия с элементом или получать данные из него.
Python может скрывать вызов .wrapper_object()
, так что финальный код становится короче. Рекомендуем использовать его только для отладки. Следующие две строки делают абсолютно одно и то же:
dlg_spec.wrapper_object().minimize() # debugging
dlg_spec.minimize() # production
Есть множество критериев поиска для спецификации окна. Вот лишь несколько примеров:
# могут иметь несколько уровней
app.window(title_re='.* - Notepad$').window(class_name='Edit')
# можно комбинировать критерии (как AND) и не ограничиваться одним процессом приложения
dlg = Desktop(backend="uia").Calculator
dlg.window(auto_id='num8Button', control_type='Button')
Список всех возможных критериев есть в доках функции pywinauto.findwindows.find_elements(…).
Магия доступа по атрибуту и по ключу
Python упрощает создание спецификаций окна и распознает атрибуты объекта динамически (внутри переопределен метод __getattribute__
). Разумеется, на имя атрибута накладываются такие же ограничения, как и на имя любой переменной (нельзя вставлять пробелы, запятые и прочие спецсимволы). К счастью, pywinauto использует так называемый «best match» алгоритм поиска, который устойчив к опечаткам и небольшим вариациям.
app.UntitledNotepad
# то же самое, что
app.window(best_match='UntitledNotepad')
Если все-таки нужны Unicode строки (например, для русского языка), пробелы и т.п., можно делать доступ по ключу (как будто это обычный словарь):
app['Untitled - Notepad']
# то же самое, что
app.window(best_match='Untitled - Notepad')
Пять правил для магических имен
Как узнать эталонные магические имена? Те, которые присваиваются элементу перед поиском. Если вы указали имя, достаточно похожее на эталон, значит элемент будет найден.
- По заголовку (текст, имя):
app.Properties.OK.click()
- По тексту и по типу элемента:
app.Properties.OKButton.click()
- По типу и по номеру:
app.Properties.Button3.click()
(именаButton0
иButton1
привязаны к первому найденному элементу,Button2
— ко второму, и дальше уже по порядку — так исторически сложилось) - По статическому тексту (слева или сверху) и по типу:
app.OpenDialog.FileNameEdit.set_text("")
(полезно для элементов с динамическим текстом) - По типу и по тексту внутри:
app.Properties.TabControlSharing.select("General")
Обычно два-три правила применяются одновременно, редко больше. Чтобы проверить, какие конкретно имена доступны для каждого элемента, можно использовать метод print_control_identifiers(). Он может печатать дерево элементов как на экран, так и в файл. Для каждого элемента печатаются его эталонные магические имена. Также можно скопипастить оттуда более детальные спецификации дочерних элементов. Результат в скрипте будет выглядеть так:
app.Properties.child_window(title="Contains:", auto_id="13087", control_type="Edit")
Само дерево элементов — обычно довольно большая портянка.
>>> app.Properties.print_control_identifiers()
Control Identifiers:
Dialog - 'Windows NT Properties' (L688, T518, R1065, B1006)
[u'Windows NT PropertiesDialog', u'Dialog', u'Windows NT Properties']
child_window(title="Windows NT Properties", control_type="Window")
|
| Image - '' (L717, T589, R749, B622)
| [u'', u'0', u'Image1', u'Image0', 'Image', u'1']
| child_window(auto_id="13057", control_type="Image")
|
| Image - '' (L717, T630, R1035, B632)
| ['Image2', u'2']
| child_window(auto_id="13095", control_type="Image")
|
| Edit - 'Folder name:' (L790, T596, R1036, B619)
| [u'3', 'Edit', u'Edit1', u'Edit0']
| child_window(title="Folder name:", auto_id="13156", control_type="Edit")
|
| Static - 'Type:' (L717, T643, R780, B658)
| [u'Type:Static', u'Static', u'Static1', u'Static0', u'Type:']
| child_window(title="Type:", auto_id="13080", control_type="Text")
|
| Edit - 'Type:' (L790, T643, R1036, B666)
| [u'4', 'Edit2', u'Type:Edit']
| child_window(title="Type:", auto_id="13059", control_type="Edit")
|
| Static - 'Location:' (L717, T669, R780, B684)
| [u'Location:Static', u'Location:', u'Static2']
| child_window(title="Location:", auto_id="13089", control_type="Text")
|
| Edit - 'Location:' (L790, T669, R1036, B692)
| ['Edit3', u'Location:Edit', u'5']
| child_window(title="Location:", auto_id="13065", control_type="Edit")
|
| Static - 'Size:' (L717, T695, R780, B710)
| [u'Size:Static', u'Size:', u'Static3']
| child_window(title="Size:", auto_id="13081", control_type="Text")
|
| Edit - 'Size:' (L790, T695, R1036, B718)
| ['Edit4', u'6', u'Size:Edit']
| child_window(title="Size:", auto_id="13064", control_type="Edit")
|
| Static - 'Size on disk:' (L717, T721, R780, B736)
| [u'Size on disk:', u'Size on disk:Static', u'Static4']
| child_window(title="Size on disk:", auto_id="13107", control_type="Text")
|
| Edit - 'Size on disk:' (L790, T721, R1036, B744)
| ['Edit5', u'7', u'Size on disk:Edit']
| child_window(title="Size on disk:", auto_id="13106", control_type="Edit")
|
| Static - 'Contains:' (L717, T747, R780, B762)
| [u'Contains:1', u'Contains:0', u'Contains:Static', u'Static5', u'Contains:']
| child_window(title="Contains:", auto_id="13088", control_type="Text")
|
| Edit - 'Contains:' (L790, T747, R1036, B770)
| [u'8', 'Edit6', u'Contains:Edit']
| child_window(title="Contains:", auto_id="13087", control_type="Edit")
|
| Image - 'Contains:' (L717, T773, R1035, B775)
| [u'Contains:Image', 'Image3', u'Contains:2']
| child_window(title="Contains:", auto_id="13096", control_type="Image")
|
| Static - 'Created:' (L717, T786, R780, B801)
| [u'Created:', u'Created:Static', u'Static6', u'Created:1', u'Created:0']
| child_window(title="Created:", auto_id="13092", control_type="Text")
|
| Edit - 'Created:' (L790, T786, R1036, B809)
| [u'Created:Edit', 'Edit7', u'9']
| child_window(title="Created:", auto_id="13072", control_type="Edit")
|
| Image - 'Created:' (L717, T812, R1035, B814)
| [u'Created:Image', 'Image4', u'Created:2']
| child_window(title="Created:", auto_id="13097", control_type="Image")
|
| Static - 'Attributes:' (L717, T825, R780, B840)
| [u'Attributes:Static', u'Static7', u'Attributes:']
| child_window(title="Attributes:", auto_id="13091", control_type="Text")
|
| CheckBox - 'Read-only (Only applies to files in folder)' (L790, T825, R1035, B841)
| [u'CheckBox0', u'CheckBox1', 'CheckBox', u'Read-only (Only applies to files in folder)CheckBox', u'Read-only (Only applies to files in folder)']
| child_window(title="Read-only (Only applies to files in folder)", auto_id="13075", control_type="CheckBox")
|
| CheckBox - 'Hidden' (L790, T848, R865, B864)
| ['CheckBox2', u'HiddenCheckBox', u'Hidden']
| child_window(title="Hidden", auto_id="13076", control_type="CheckBox")
|
| Button - 'Advanced...' (L930, T845, R1035, B868)
| [u'Advanced...', u'Advanced...Button', 'Button', u'Button1', u'Button0']
| child_window(title="Advanced...", auto_id="13154", control_type="Button")
|
| Button - 'OK' (L814, T968, R889, B991)
| ['Button2', u'OK', u'OKButton']
| child_window(title="OK", auto_id="1", control_type="Button")
|
| Button - 'Cancel' (L895, T968, R970, B991)
| ['Button3', u'CancelButton', u'Cancel']
| child_window(title="Cancel", auto_id="2", control_type="Button")
|
| Button - 'Apply' (L976, T968, R1051, B991)
| ['Button4', u'ApplyButton', u'Apply']
| child_window(title="Apply", auto_id="12321", control_type="Button")
|
| TabControl - '' (L702, T556, R1051, B962)
| [u'10', u'TabControlSharing', u'TabControlPrevious Versions', u'TabControlSecurity', u'TabControl', u'TabControlCustomize']
| child_window(auto_id="12320", control_type="Tab")
| |
| | TabItem - 'General' (L704, T558, R753, B576)
| | [u'GeneralTabItem', 'TabItem', u'General', u'TabItem0', u'TabItem1']
| | child_window(title="General", control_type="TabItem")
| |
| | TabItem - 'Sharing' (L753, T558, R801, B576)
| | [u'Sharing', u'SharingTabItem', 'TabItem2']
| | child_window(title="Sharing", control_type="TabItem")
| |
| | TabItem - 'Security' (L801, T558, R851, B576)
| | [u'Security', 'TabItem3', u'SecurityTabItem']
| | child_window(title="Security", control_type="TabItem")
| |
| | TabItem - 'Previous Versions' (L851, T558, R947, B576)
| | [u'Previous VersionsTabItem', u'Previous Versions', 'TabItem4']
| | child_window(title="Previous Versions", control_type="TabItem")
| |
| | TabItem - 'Customize' (L947, T558, R1007, B576)
| | [u'CustomizeTabItem', 'TabItem5', u'Customize']
| | child_window(title="Customize", control_type="TabItem")
|
| TitleBar - 'None' (L712, T521, R1057, B549)
| ['TitleBar', u'11']
| |
| | Menu - 'System' (L696, T526, R718, B548)
| | [u'System0', u'System', u'System1', u'Menu', u'SystemMenu']
| | child_window(title="System", auto_id="MenuBar", control_type="MenuBar")
| | |
| | | MenuItem - 'System' (L696, T526, R718, B548)
| | | [u'System2', u'MenuItem', u'SystemMenuItem']
| | | child_window(title="System", control_type="MenuItem")
| |
| | Button - 'Close' (L1024, T519, R1058, B549)
| | [u'CloseButton', u'Close', 'Button5']
| | child_window(title="Close", control_type="Button")
В некоторых случаях печать всего дерева может тормозить (например, в iTunes на одной вкладке аж три тысячи элементов!), но можно использовать параметр depth
(глубина): depth=1
— сам элемент, depth=2
— только непосредственные дети, и так далее. Его же можно указывать в спецификациях при создании child_window
.
Примеры
Мы постоянно пополняем список примеров в репозитории. Из свежих стоит отметить автоматизацию сетевого анализатора WireShark (это хороший пример Qt5 приложения; хотя эту задачу можно решать и без GUI, ведь есть scapy.Sniffer
из питоновского пакета scapy). Также есть пример автоматизации MS Paint с его Ribbon тулбаром.
Еще один отличный пример, написанный моим студентом: перетаскивание файла из explorer.exe на Chrome страницу для Google Drive (он перекочует в главный репозиторий чуть позже).
И, конечно, пример подписки на события клавиатуры (hot keys) и мыши:
hook_and_listen.py.
Благодарности
Отдельное спасибо — тем, кто постоянно помогает развивать проект. Для меня и Валентина это постоянное хобби. Двое моих студентов из ННГУ недавно защитили дипломы бакалавра по этой теме. Александр внес большой вклад в поддержку MS UI Automation и недавно начал делать автоматический генератор кода по принципу «запись-воспроизведение» на основе текстовых свойств (это самая сложная фича), пока только для «uia» бэкенда. Иван разрабатывает новый бэкенд под Linux на основе AT-SPI (модули mouse
и keyboard
на основе python-xlib — уже в релизах 0.6.x).
Поскольку я довольно давно читаю спецкурс по автоматизации на Python, часть студентов-магистров выполняют домашние задания, реализуя небольшие фичи или примеры автоматизации. Некоторые ключевые вещи на стадии исследований тоже когда-то раскопали именно студенты. Хотя иногда за качеством кода приходится строго следить. В этом сильно помогают статические анализаторы (QuantifiedCode, Codacy и Landscape) и автоматические тесты в облаке (сервис AppVeyor) с покрытием кода в районе 95%.
Также спасибо всем, кто оставляет отзывы, заводит баги и присылает пулл реквесты!
Дополнительные ресурсы
За вопросами мы следим по тегу на StackOverflow (недавно появился тег в русской версии SO) и по ключевому слову на Тостере. Есть русскоязычный чат в Gitter’е.
Каждый месяц обновляем рейтинг open-source библиотек для GUI тестирования. По количеству звезд на гитхабе быстрее растут только Autohotkey (у них очень большое сообщество и длинная история) и PyAutoGUI (во многом благодаря популярности книг ее автора Al Sweigart: «Automate the Boring Stuff with Python» и других).
pywinauto
pywinauto is a set of python modules to automate the Microsoft Windows GUI.
At its simplest it allows you to send mouse and keyboard actions to windows
dialogs and controls, but it has support for more complex actions like getting text data.
Supported technologies under the hood: Win32 API (backend="win32"
; used by default),
MS UI Automation (backend="uia"
). User input emulation modules
mouse
and keyboard
work on both Windows and Linux.
Enjoying this?
Just star the repo or make a donation.
Your help is valuable since this is a hobby project for all of us: we do
new features development during out-of-office hours.
- In general the library tends to be cross-platform in the near future (Linux in 2018, macOS in 2019).
- Reliable text based «record-replay» generator is also a high priority feature under development.
- More feature requests and discusions are welcome in the issues.
Setup
- run
pip install -U pywinauto
(dependencies will be installed automatically)
Documentation / Help
- Short Intro on ReadTheDocs
- Getting Started Guide (core concept, Spy/Inspect tools etc.)
- StackOverflow tag for questions
- Mailing list
Simple Example
It is simple and the resulting scripts are very readable. How simple?
from pywinauto.application import Application app = Application().start("notepad.exe") app.UntitledNotepad.menu_select("Help->About Notepad") app.AboutNotepad.OK.click() app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
MS UI Automation Example
More detailed example for explorer.exe
:
from pywinauto import Desktop, Application Application().start('explorer.exe "C:\\Program Files"') # connect to another process spawned by explorer.exe # Note: make sure the script is running as Administrator! app = Application(backend="uia").connect(path="explorer.exe", title="Program Files") app.ProgramFiles.set_focus() common_files = app.ProgramFiles.ItemsView.get_item('Common Files') common_files.right_click_input() app.ContextMenu.Properties.invoke() # this dialog is open in another process (Desktop object doesn't rely on any process id) Properties = Desktop(backend='uia').Common_Files_Properties Properties.print_control_identifiers() Properties.Cancel.click() Properties.wait_not('visible') # make sure the dialog is closed
Dependencies (if install manually)
- Windows:
- pyWin32
- comtypes
- six
- Linux:
- python-xlib
- six
- Optional packages:
- Install Pillow (by
pip install -U Pillow
) to be able to callcapture_as_image()
method for making a control’s snapshot.
- Install Pillow (by
Packages required for running unit tests
- Pillow
- coverage
Run all the tests: python ./pywinauto/unittests/testall.py
Contribution
Pull requests are very welcome. Read Contribution Guide for more details about unit tests, coding conventions, etc.
Copyrights
Pywinauto for native Windows GUI was initially written by Mark Mc Mahon.
Mark brought many great ideas into the life using power of Python.
Further contributors are inspired of the nice API so that the development continues.
Starting from 0.6.0 pywinauto is distributed under the BSD 3-clause license.
Pywinauto 0.5.4 and before was distributed under the LGPL v2.1 or later.
- (c) The Open Source Community, 2015-2018 (0.6.0+ development)
- (c) Intel Corporation, 2015 (0.5.x maintenance)
- (c) Michael Herrmann, 2012-2013 (0.4.2)
- (c) Mark Mc Mahon, 2006-2010 (0.4.0 and before)
Normally, an application exposes a user interface (UI) for users, and an application programming interface (API) for programming.
- A human being uses keyboard and mouse to work with the user interface (UI)
- An application uses programming to work with the application programming interface (API)
The UI is designed for humans, and the API is designed for computers.
It is sometimes possible to use programming to control the user interface of another program — so your program acts as if it were using the keyboard and mouse. This technique is often called «UI automation«, and programs that do it are sometimes called «robots».
It’s a big topic, and it can be quite complex. It’s almost always better to use an API instead if you can: it’s faster, simpler, more reliable.
If you do need to use UI automation, there are a few different tools that can help.
You are asking about Python, so here are a few UI automation tools that work with Python:
- AutoIT is a standalone product, but you can use Python to script it.
- PyWinAuto is designed for use from Python.
- Sikuli uses computer vision to find parts of the screen. I believe it comes with a recording tool as well.
Just to repeat: UI automation is weird and hard. If you can possibly use an API instead, your life will be much easier.
Введение
Python имеет множество возможностей для создания стандартных типов файлов Microsoft Officeએ, включая Excelએ, Wordએ и PowerPointએ. Однако в некоторых случаях может оказаться слишком сложно использовать чистый подход Python для решения проблемы. К счастью, для python есть пакет “Python for Windows Extensions” (Python для расширений Windows), известный как pywin32, который позволяет нам легко получить доступ к Component Object Model (COM), компонентной объектной модели Windows, и управлять приложениями Microsoft через python. В этой статье будут рассмотрены некоторые базовые сценарии использования этого типа автоматизации и рассказывается о том, как приступить к работе с некоторыми полезными скриптами.
С веб-сайта Microsoft о модели компонентных объектов (COM):
Платформенно-независимая распределенная объектно-ориентированная система для создания двоичных программных компонентов, которые могут взаимодействовать. COM является базовой технологией для технологий Microsoft OLE Automationએ (составные документы) и ActiveXએ (компоненты с доступом в Интернет). COM-объекты могут быть созданы с помощью множества языков программирования.
Эта технология позволяет нам управлять приложениями Windows из другой программы. Многие из читателей этого блога, вероятно, видели или использовали VBAએ для некоторой автоматизации задачи Excel. COM — это основополагающая технология, поддерживающая VBA.
pywin32
Пакет pywin32 существует уже очень давно. Фактически, книга, посвященная этой теме, была опубликована в 2000 году Марком Хаммондом и Энди Робинсоном. Несмотря на то, что с тех пор уже прошло много лет (что заставляет меня чувствовать себя действительно старым), лежащие в основе технологии и концепции работают и сегодня. Pywin32 — это, по сути, очень тонкая оболочка python, которая позволяет нам взаимодействовать с COM-объектами и автоматизировать приложения Windows с помощью python. Сила этого подхода заключается в том, что вы можете делать практически все, что может делать приложение Microsoft, через python. Обратной стороной является то, что вам придется запускать это в системе Windows с установленным Microsoft Office. Прежде чем мы рассмотрим несколько примеров, убедитесь, что в вашей системе установлен pywin32 с помощью pip
или conda
.
Еще одна рекомендация: держите под рукой ссылку на страницу Тима Голдена. На этом ресурсе есть еще много подробностей о том, как использовать python в Windows для автоматизации и других административных задач.
Начиная
Все наши приложения начинаются с одинакового импорта и процесса активации приложения. Вот очень короткий пример открытия Excel:
import win32com.client as win32 excel = win32.gencache.EnsureDispatch('Excel.Application') excel.Visible = True _ = input("Press ENTER to quit:") excel.Application.Quit()
Как только вы запустите скрипт из командной строки, то должны увидеть, как открывается Excel. Когда вы нажмете ENTER, приложение закроется. Прежде чем мы действительно сделаем это приложение более полезным, необходимо изучить несколько ключевых концепций.
Первый шаг — импортировать клиента win32. Я использовал соглашение об импорте его как win32
, чтобы сделать фактический код отправки немного короче.
Магия этого кода заключается в использовании EnsureDispatch
для запуска Excel. В этом примере я использую gencache.EnsureDispatch
для создания статического прокси. Я рекомендую прочитать эту статью, если вы хотите узнать больше о статических и динамических прокси. Мне посчастливилось использовать этот подход для примеров, включенных в эту статью, но буду честен — я не слишком много экспериментировал с различными подходами к диспетчеризации.
Теперь, когда объект excel
запущен, нам нужно явно сделать его видимым, установив excel.Visible = TrueКод
.
win32 довольно умен и закроет Excel после завершения работы программы. Это означает, что если мы просто оставим код работать самостоятельно, вы, вероятно, не увидите Excel. Я включаю фиктивную подсказку, чтобы Excel оставался видимым на экране, пока пользователь не нажмет ENTER.
Я включаю последнюю строку excel.Application.Quit()
, как немного ремня и подтяжек. Строго говоря, win32 должен закрыть Excel, когда программа будет завершена, но я решил включить excel.Application.Quit()
, чтобы показать, как принудительно закрыть приложение.
Это самый простой подход к использованию COM. Мы можем расширить это несколько более полезных способов. В оставшейся части этой статьи будут рассмотрены некоторые примеры, которые могут быть полезны для ваших нужд.
Открыть файл в Excel
В своей повседневной работе я часто использую pandas для анализа и обработки данных, а затем вывожу результаты в Excel. Следующим шагом в этом процессе является открытие Excel и просмотр результатов. В этом примере мы можем автоматизировать процесс открытия файла, что может упростить его, чем попытки перейти в нужный каталог и открыть файл.
Вот полный пример:
import win32com.client as win32 import pandas as pd from pathlib import Path # Read in the remote data file df = pd.read_csv("https://github.com/chris1610/pbpython/blob/master/data/sample-sales-tax.csv?raw=True") # Define the full path for the output file out_file = Path.cwd() / "tax_summary.xlsx" # Do some summary calcs # In the real world, this would likely be much more involved df_summary = df.groupby('category')['ext price', 'Tax amount'].sum() # Save the file as Excel df_summary.to_excel(out_file) # Open up Excel and make it visible excel = win32.gencache.EnsureDispatch('Excel.Application') excel.Visible = True # Open up the file excel.Workbooks.Open(out_file) # Wait before closing it _ = input("Press enter to close Excel") excel.Application.Quit()
Вот результат в Excel:
Этот простой пример расширяет предыдущий, показывая, как использовать объект Workbooks для открытия файла.
Прикрепите файл Excel к Outlook
Другой простой сценарий, в котором полезен COM, — это когда вы хотите прикрепить файл к электронному письму и отправить его в список рассылки. В этом примере показано, как выполнять некоторые манипуляции с данными, открывать электронную почту Outlook,прикрепите файл и оставьте его открытым для дополнительного текста перед отправкой.
Вот полный пример:
import win32com.client as win32 import pandas as pd from pathlib import Path from datetime import date to_email = """ Lincoln, Abraham <>; """ cc_email = """ Franklin, Benjamin <> """ # Read in the remote data file df = pd.read_csv("https://github.com/chris1610/pbpython/blob/master/data/sample-sales-tax.csv?raw=True") # Define the full path for the output file out_file = Path.cwd() / "tax_summary.xlsx" # Do some summary calcs # In the real world, this would likely be much more involved df_summary = df.groupby('category')['ext price', 'Tax amount'].sum() # Save the file as Excel df_summary.to_excel(out_file) # Open up an outlook email outlook = win32.gencache.EnsureDispatch('Outlook.Application') new_mail = outlook.CreateItem(0) # Label the subject new_mail.Subject = "{:%m/%d} Report Update".format(date.today()) # Add the to and cc list new_mail.To = to_email new_mail.CC = cc_email # Attach the file attachment1 = out_file # The file needs to be a string not a path object new_mail.Attachments.Add(Source=str(attachment1)) # Display the email new_mail.Display(True)
Этот пример немного усложняется, но основные концепции те же. Нам нужно создать наш объект (в данном случае Outlook) и создать новое электронное письмо. Одним из сложных аспектов работы с COM является отсутствие согласованного API. Создание такого электронного письма не является интуитивным: new_mail = outlook.CreateItem (0)
Обычно требуется немного поисков, чтобы выяснить точный API для конкретной проблемы. Google и stackoverflow — ваши друзья.
После создания объекта электронной почты вы можете добавить получателя и список CC, а также прикрепить файл. Когда все сказано и сделано, это выглядит так:
Электронное письмо открыто, и вы можете добавить дополнительную информацию и отправить ее. В этом примере я решил не закрывать Outlook и позволить python обрабатывать эти детали.
Последний пример является наиболее сложным, но иллюстрирует мощный подход к объединению анализа данных Python с пользовательским интерфейсом Excel.
С помощью pandas можно создать сложный Excel, но такой подход может быть очень трудоемким. Альтернативным подходом было бы создание сложного файла в Excel, затем выполните манипуляции с данными и скопируйте вкладку данных в окончательный вывод Excel.
Вот пример панели инструментов Excel, которую мы хотим создать:
Да, я знаю, что круговые диаграммы ужасны, но я могу почти гарантировать, что кто-то попросит вас поместить их на панель инструментов в какой-то момент! Кроме того, в этом шаблоне была круговая диаграмма, и я решил оставить ее в окончательном виде вместо того, чтобы пытаться вычислить другую диаграмму. Было бы полезно сделать шаг назад и посмотреть на основной процесс, которому будет следовать код:
Приступим к работе с кодом.
import win32com.client as win32 import pandas as pd from pathlib import Path # Read in the remote data file df = pd.read_csv("https://github.com/chris1610/pbpython/blob/master/data/sample-sales-tax.csv?raw=True") # Define the full path for the data file file data_file = Path.cwd() / "sales_summary.xlsx" # Define the full path for the final output file save_file = Path.cwd() / "sales_dashboard.xlsx" # Define the template file template_file = Path.cwd() / "sample_dashboard_template.xlsx"
В этом разделе мы выполнили импорт, прочитали данные и определили все три файла. Следует отметить, что этот процесс включает в себя этап суммирования данных с помощью pandas и сохранения данных в файле Excel. Затем мы повторно открываем этот файл и копируем данные в шаблон. Это немного запутано, но это лучший подход, который я мог придумать для этого сценария.
Далее выполняем анализ и сохраняем временный файл Excel:
# Do some summary calcs # In the real world, this would likely be much more involved df_summary = df.groupby('category')['quantity', 'ext price', 'Tax amount'].sum() # Save the file as Excel df_summary.to_excel(data_file, sheet_name="Data")
Теперь мы используем COM, чтобы объединить временный выходной файл с нашей вкладкой панели управления Excel и сохранить новую копию:
# Use com to copy the files around excel = win32.gencache.EnsureDispatch('Excel.Application') excel.Visible = False excel.DisplayAlerts = False # Template file wb_template = excel.Workbooks.Open(template_file) # Open up the data file wb_data = excel.Workbooks.Open(data_file) # Copy from the data file (select all data in A:D columns) wb_data.Worksheets("Data").Range("A:D").Select() # Paste into the template file excel.Selection.Copy(Destination=wb_template.Worksheets("Data").Range("A1")) # Must convert the path file object to a string for the save to work wb_template.SaveAs(str(save_file))
Код открывает Excel и удостоверяется, что его не видно. Затем он открывает шаблон панели мониторинга и файлы данных. Он использует Range("A: D").Select()
для выбора всех данных, а затем копирует их в файл шаблона.
Последний шаг — сохранить шаблон как новый файл. Этот подход может быть очень удобным ярлыком, когда у вас есть ситуация, когда вы хотите использовать python для обработки данных, но вам нужен сложный вывод Excel. Возможно, сейчас у вас нет явной потребности в этом, но если вы когда-нибудь создадите сложный отчет Excel, этот подход будет намного проще, чем пытаться вручную кодировать электронную таблицу с помощью python.
Заключение
Я предпочитаю стараться как можно больше придерживаться python для повседневного анализа данных. Однако важно знать, когда другие технологии могут упростить процесс или повысить эффективность результатов. Технология Microsoft COM является зрелой технологией, и ее можно эффективно использовать через Python для выполнения задач, которые в противном случае были бы слишком сложными. Надеюсь, эта статья дала вам несколько идей о том, как включить эту технику в свой рабочий процесс. Если у вас есть какие-либо задачи, для которых вы хотите использовать pywin32, сообщите нам об этом в комментариях.
По материалам Automating Windows Applications Using COM
Sometimes we need to use Win32 APIs in Python. This article explains how to use Win32 APIs in Python.
To use Win32 APIs in Python we can use the following options.
Using the PyWin32 package
One of the most popular options is the PyWin32 library, which provides Python bindings for many Win32 API functions.
Here’s an example of using PyWin32 to call the MessageBox()
function from the Win32 API:
pip install pywin32
import win32api import win32con win32api.MessageBox(win32con.NULL, 'Hello from NoloWiz!', 'Test Window', win32con.MB_OK)
This code imports the win32api
and win32con
modules from the PyWin32 library. It then calls the MessageBox()
function from the Win32 API using the win32api.MessageBox()
function. The first argument to MessageBox()
is the handle to the owner window, which is set to win32con.NULL
to indicate that there is no owner window. The second argument is the message to display, and the third argument is the title of the message box. The fourth argument specifies the buttons to display in the message box, which is set to win32con.MB_OK
to display an OK button
In the above code, the win32con is a module in the pywin32 package that provides constants(win32con.MB_OK) used in Windows API calls. The win32con module contains constants for various Windows messages, window styles, keyboard codes, system parameters, and error codes.
Output
Use ctypes to call Win32 APIs
The ctypes is a foreign function library for Python that allows calling functions in shared libraries or DLLs and provides C compatible data types. We can use ctypes to call Win32 API functions. The ctypes is a standard library in Python, which means it is included with every Python installation.
Here is an example :
import ctypes user32 = ctypes.windll.user32 user32.MessageBoxW(None, 'Hello from NoloWiz!', 'Test Window', 0)
This code uses the windll attribute of the ctypes module to load the user32 DLL, which contains the MessageBoxW() function. It then calls the MessageBoxW() function using user32.MessageBoxW() function. The first argument to MessageBoxW() is the handle to the owner window. The second argument is the message to display, and the third argument is the title of the message box. The fourth argument specifies the buttons to display in the message box.
Note that the windll
object is used for calling functions that use the stdcall calling convention, while the cdll
object is used for calling functions that use the cdecl calling convention.
Conclusion
In conclusion, we can easily use the Win32 APIs using the pywin32 package and ctypes.