Основы PyWinAuto
Введение
pywinauto - это набор модулей
python
для автоматизации графического интерфейса
Microsoft Windows
.
В самом простом варианте он позволяет отправлять действия мыши и клавиатуры в диалоговые окна и элементы управления Windows.
Официальная документация
Установка
python -m pip install pywinauto
Collecting pywinauto Downloading pywinauto-0.6.8-py2.py3-none-any.whl (362 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 362.9/362.9 kB ? eta 0:00:00 Collecting six Using cached six-1.16.0-py2.py3-none-any.whl (11 kB) Collecting comtypes Downloading comtypes-1.2.0-py2.py3-none-any.whl (184 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 184.3/184.3 kB ? eta 0:00:00 Collecting pywin32 Downloading pywin32-306-cp311-cp311-win_amd64.whl (9.2 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.2/9.2 MB 21.8 MB/s eta 0:00:00 Installing collected packages: pywin32, comtypes, six, pywinauto Successfully installed comtypes-1.2.0 pywin32-306 pywinauto-0.6.8 six-1.16.0
Пример
В некоторых случаях нужно перейти в директорию с .exe файлом перед тем как запускать приложение
import os os.chdir("PATH")
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') # app = Application(backend='win32').start('')
python pywinauto_tutorial.py
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') app = Application(backend='uia').connect(title='Untitled - Notepad', timeout=100)
print_control_identifiers()
Чтобы управлять приложением, нужно получить список объектов - контроллеров. Сделать это можно с помощью print_control_identifiers()
print_control_identifiers() выводит на экран список всех контроллеров доступных из текущего состояния приложения а не вообще все контроллеры.
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') app = Application(backend='uia').connect(title='Untitled - Notepad', timeout=100) app.UntitledNotepad.print_control_identifiers()
Control Identifiers: Dialog - 'Untitled - Notepad' (L-1127, T248, R-579, B1034) ['Untitled - Notepad', 'Untitled - NotepadDialog', 'Dialog'] child_window(title="Untitled - Notepad", control_type="Window") | | Edit - 'Text Editor' (L-1118, T311, R-588, B996) | ['Edit', 'Edit0', 'Edit1'] | child_window(title="Text Editor", auto_id="15", control_type="Edit") | | | | ScrollBar - 'Vertical' (L-609, T311, R-588, B975) | | ['VerticalScrollBar', 'Vertical', 'ScrollBar', 'ScrollBar0', 'ScrollBar1'] | | child_window(title="Vertical", auto_id="NonClientVerticalScrollBar", control_type="ScrollBar") | | | | | | Button - 'Line up' (L-609, T311, R-588, B332) | | | ['Button', 'Line up', 'Line upButton', 'Button0', 'Button1'] | | | child_window(title="Line up", auto_id="UpButton", control_type="Button") | | | | | | Button - 'Line down' (L-609, T954, R-588, B975) | | | ['Button2', 'Line downButton', 'Line down'] | | | child_window(title="Line down", auto_id="DownButton", control_type="Button") | | | | ScrollBar - 'Horizontal' (L-1118, T975, R-609, B996) | | ['HorizontalScrollBar', 'ScrollBar2', 'Horizontal'] | | child_window(title="Horizontal", auto_id="NonClientHorizontalScrollBar", control_type="ScrollBar") | | | | | | Button - 'Column left' (L-1118, T975, R-1097, B996) | | | ['Button3', 'Column left', 'Column leftButton'] | | | child_window(title="Column left", auto_id="UpButton", control_type="Button") | | | | | | Button - 'Column right' (L-630, T975, R-609, B996) | | | ['Button4', 'Column rightButton', 'Column right'] | | | child_window(title="Column right", auto_id="DownButton", control_type="Button") | | | | Thumb - '' (L-609, T975, R-588, B996) | | ['Thumb'] | | StatusBar - 'Status Bar' (L-1118, T996, R-588, B1025) | ['StatusBar', 'Status Bar', 'Status BarStatusBar'] | child_window(title="Status Bar", auto_id="1025", control_type="StatusBar") | | | | Static - '' (L0, T0, R0, B0) | | ['Static', 'Static0', 'Static1'] | | …
Подробнее можно изучить control identifiers блокнота в статье «Автотестирование блокнота с pywinauto»
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') app = Application(backend='uia').connect( title='Untitled - Notepad', timeout=100 ) # app.UntitledNotepad.print_control_identifiers() text_editor = app.UntitledNotepad.child_window( title="Text Editor", auto_id="15", control_type="Edit" ).wrapper_object() text_editor.type_keys( "Subscribe to t.me/aofeed", with_spaces=True )
Часто контроллеров очень много и смотреть на их вывод в терминал неудобно.
Решается эта проблема записью в файл, который нужно указать как аргумент
app.MyApp.print_control_identifiers(filename="controls.txt")
Файл controls.txt будет создан в рабочей директории, то есть скорее всего рядом с запускаемым .exe файлом а не рядом со скриптом.
Объекты во вложенных меню
Когда мы в первый раз использовали
print_control_identifiers()
был получен список всех контроллеров, доступных из стартового окна.
Чтобы полуить списки контроллеров, которые доступны из подменю, нужно сперва выполнить
click_input()
на нужное меню и затем сразу же выполнить
print_control_identifiers()
Действуя по этому алгоритму можно создать библиотеку из всех доступных контроллеров.
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') app = Application(backend='uia').connect( title='Untitled - Notepad', timeout=100 ) # app.UntitledNotepad.print_control_identifiers() text_editor = app.UntitledNotepad.child_window( title="Text Editor", auto_id="15", control_type="Edit" ).wrapper_object() text_editor.type_keys( "Subscribe to t.me/aofeed", with_spaces=True ) file_menu = app.UntitledNotepad.child_window(title="File", control_type="MenuItem").wrapper_object() file_menu.click_input() app.UntitledNotepad.print_control_identifiers()
Control Identifiers: Dialog - '*Untitled - Notepad' (L-1512, T253, R641, B1009) ['*Untitled - Notepad', 'Dialog', '*Untitled - NotepadDialog'] child_window(title="*Untitled - Notepad", control_type="Window") | | Menu - 'File' (L-1503, T315, R-1234, B527) | ['File', 'FileMenu', 'Menu', 'Menu0', 'Menu1', 'File0', 'File1'] | child_window(title="File", control_type="Menu") | | | | MenuItem - 'New Ctrl+N' (L-1500, T318, R-1237, B342) | | ['New\tCtrl+N', 'New\tCtrl+NMenuItem', 'MenuItem', 'MenuItem0', 'MenuItem1'] | | child_window(title="New Ctrl+N", auto_id="1", control_type="MenuItem") | | | | MenuItem - 'New Window Ctrl+Shift+N' (L-1500, T342, R-1237, B366) | | ['MenuItem2', 'New Window\tCtrl+Shift+N', 'New Window\tCtrl+Shift+NMenuItem'] | | child_window(title="New Window Ctrl+Shift+N", auto_id="8", control_type="MenuItem") | | | | MenuItem - 'Open... Ctrl+O' (L-1500, T366, R-1237, B390) | | ['Open...\tCtrl+O', 'Open...\tCtrl+OMenuItem', 'MenuItem3'] | | child_window(title="Open... Ctrl+O", auto_id="2", control_type="MenuItem") | | | | MenuItem - 'Save Ctrl+S' (L-1500, T390, R-1237, B414) | | ['Save\tCtrl+SMenuItem', 'Save\tCtrl+S', 'MenuItem4'] | | child_window(title="Save Ctrl+S", auto_id="3", control_type="MenuItem") | | | | MenuItem - 'Save As... Ctrl+Shift+S' (L-1500, T414, R-1237, B438) | | ['MenuItem5', 'Save As...\tCtrl+Shift+S', 'Save As...\tCtrl+Shift+SMenuItem'] | | child_window(title="Save As... Ctrl+Shift+S", auto_id="4", control_type="MenuItem") | | | | MenuItem - 'Page Setup...' (L-1500, T445, R-1237, B469) | | ['Page Setup...MenuItem', 'MenuItem6', 'Page Setup...'] | | child_window(title="Page Setup...", auto_id="5", control_type="MenuItem") | | | | MenuItem - 'Print... Ctrl+P' (L-1500, T469, R-1237, B493) | | ['Print...\tCtrl+P', 'MenuItem7', 'Print...\tCtrl+PMenuItem'] | | child_window(title="Print... Ctrl+P", auto_id="6", control_type="MenuItem") | | | | MenuItem - 'Exit' (L-1500, T500, R-1237, B524) | | ['Exit', 'ExitMenuItem', 'MenuItem8'] | | child_window(title="Exit", auto_id="7", control_type="MenuItem") | | Edit - 'Text Editor' (L-1503, T316, R632, B971) | ['Edit', 'Edit0', 'Edit1'] | child_window(title="Text Editor", auto_id="15", control_type="Edit") | | | | ScrollBar - 'Vertical' (L611, T316, R632, B950) | | ['Vertical', 'ScrollBar', 'VerticalScrollBar', 'ScrollBar0', 'ScrollBar1'] | | child_window(title="Vertical", auto_id="NonClientVerticalScrollBar", control_type="ScrollBar") | | | | | | Button - 'Line up' (L611, T316, R632, B337) | | | ['Line up', 'Line upButton', 'Button', 'Button0', 'Button1'] | | | child_window(title="Line up", auto_id="UpButton", control_type="Button") | | | | | | Button - 'Line down' (L611, T929, R632, B950) | | | ['Line down', 'Line downButton', 'Button2'] | | | child_window(title="Line down", auto_id="DownButton", control_type="Button") | | …
Подробнее можно изучить control identifiers блокнота в статье «Автотестирование блокнота с pywinauto»
Пример поиска вложенного объекта
Откроем новое окно.
Для этого в предыдущем выводе найдём
… | | MenuItem - 'New Window Ctrl+Shift+N' (L-1500, T342, R-1237, B366) | | ['MenuItem2', 'New Window\tCtrl+Shift+N', 'New Window\tCtrl+Shift+NMenuItem'] | | child_window(title="New Window Ctrl+Shift+N", auto_id="8", control_type="MenuItem") …
Скопируем строку с clild_window, но вместо New Window Ctrl+Shift+N используем New Window\tCtrl+Shift+N
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') app = Application(backend='uia').connect( title='Untitled - Notepad', timeout=100 ) # app.UntitledNotepad.print_control_identifiers() text_editor = app.UntitledNotepad.child_window( title="Text Editor", auto_id="15", control_type="Edit" ).wrapper_object() text_editor.type_keys( "Subscribe to t.me/aofeed", with_spaces=True ) file_menu = app.UntitledNotepad.child_window( title="File", control_type="MenuItem" ).wrapper_object() file_menu.click_input() app.UntitledNotepad.print_control_identifiers() new_window = app.UntitledNotepad.child_window( title="New Window\tCtrl+Shift+N", auto_id="8", control_type="MenuItem" ).wrapper_object() new_window.click_input()
Диалоговое окно
Для простоты не будем открывать новое окно. Откроем блокнот, введём текст и с помощью контроллера close
… | | Button - 'Close' (L573, T254, R633, B291) | | ['CloseButton', 'Close', 'Button7'] | | child_window(title="Close", control_type="Button") …
закроем блокнот. Появится диалоговое окно Save, Don't Save и так далее.
В этот момент нужно распечатать контроллеры этого диалогового окна и использовать нужный.
from pywinauto.application import Application app = Application(backend='uia').start('notepad.exe') app = Application(backend='uia').connect( title='Untitled - Notepad', timeout=100 ) text_editor = app.UntitledNotepad.child_window( title="Text Editor", auto_id="15", control_type="Edit" ).wrapper_object() text_editor.type_keys( "Subscribe to t.me/aofeed", with_spaces=True ) file_menu = app.UntitledNotepad.child_window( title="File", control_type="MenuItem" ).wrapper_object() file_menu.click_input() close = app.UntitledNotepad.child_window( title="Close", control_type="Button" ).wrapper_object() close.click_input() app.UntitledNotepad.print_control_identifiers() dont_save = app.UntitledNotepad.child_window( title="Don't Save", auto_id="CommandButton_7", control_type="Button" ).wrapper_object() dont_save.click_input()
… Control Identifiers: Dialog - '*Untitled - Notepad' (L321, T173, R769, B851) ['Dialog', '*Untitled - Notepad', '*Untitled - NotepadDialog', 'Dialog0', 'Dialog1'] child_window(title="*Untitled - Notepad", control_type="Window") | | Dialog - 'Notepad' (L1042, T568, R1500, B745) | ['Notepad', 'Dialog2', 'NotepadDialog'] | child_window(title="Notepad", control_type="Window") | | | | Static - 'Do you want to save changes to Untitled?' (L1064, T619, R1427, B647) | | ['Static', 'Do you want to save changes to Untitled?', 'Do you want to save changes to Untitled?Static', 'Static0', 'Static1'] | | child_window(title="Do you want to save changes to Untitled?", auto_id="MainInstruction", control_type="Text") | | | | Static - '' (L0, T0, R0, B0) | | ['Static2'] | | child_window(auto_id="ContentText", control_type="Text") | | | | Button - 'Save' (L1174, T696, R1261, B725) | | ['Button', 'Save', 'SaveButton', 'Button0', 'Button1'] | | child_window(title="Save", auto_id="CommandButton_6", control_type="Button") | | | | Button - 'Don't Save' (L1269, T696, R1383, B725) | | ['Button2', "Don't SaveButton", "Don't Save"] | | child_window(title="Don't Save", auto_id="CommandButton_7", control_type="Button") | | | | Button - 'Cancel' (L1391, T696, R1478, B725) | | ['Cancel', 'Button3', 'CancelButton'] | | child_window(title="Cancel", auto_id="CommandButton_2", control_type="Button") | | | | TitleBar - '' (L1051, T571, R1491, B606) | | ['TitleBar', 'TitleBar0', 'TitleBar1'] | | | | | | Button - 'Close' (L1448, T569, R1492, B606) | | | ['Button4', 'CloseButton', 'Close', 'CloseButton0', 'CloseButton1', 'Close0', 'Close1'] | | | child_window(title="Close", control_type="Button") …
Контроллеры
Существует два набора контроллеров:
Поиск элемента для ввода текста
Допустим нужно ввести текст в такой элемент
Примерный порядок действий:
Запустить приложение.
Определить название (title) диалогового окна - в данном примере это App Name.
Распечатать контроллеры для диалогового окна
app = Application(backend='win32').start("path_to_exe") app = app.connect(best_match="App Name", timeout=3) dlg = app.AppName dlg.print_ctrl_ids()
Где-то в выдаче будет похожий результат
Control Identifiers: Dialog - 'App Name' (L735, T375, R1186, B777) ['App NameDialog', 'App Name', 'Dialog'] child_window(title="App Name", class_name="#32770") | … | ComboBox - '' (L819, T518, R1067, B539) | ['ComboBox', '&Destination:ComboBox', 'ComboBox0', 'ComboBox1'] | child_window(title="", class_name="ComboBox") | | | | Edit - '' (L822, T521, R1047, B536) | | ['&Destination:Edit', 'Edit', 'Edit0', 'Edit1'] | | child_window(title="", class_name="Edit") …
Если бы в поле был текст, например abc, то он бы отобразился так:
… | ComboBox - 'abc' (L819, T518, R1067, B539) | ['ComboBox', '&Destination:ComboBox', 'ComboBox0', 'ComboBox1'] | child_window(title="abc", class_name="ComboBox") | | | | Edit - 'abc' (L822, T521, R1047, B536) | | ['&Destination:Edit', 'Edit', 'Edit0', 'Edit1'] | | child_window(title="abc", class_name="Edit") …
Так как искомый элемент содержится внутри ComboBox можно перебрать все ComboBox с помощью found_index
Предположим, что нам повезло и нужный бокс имеет индекс 0.
destination = dlg.child_window(class_name="ComboBox", found_index=0) destination.print_ctrl_ids()
Control Identifiers: ComboBox - 'abc' (L819, T518, R1067, B539) ['ComboBox'] child_window(title="abc", class_name="ComboBox") | | Edit - 'abc' (L822, T521, R1047, B536) | ['Edit'] | child_window(title="abc", class_name="Edit")
Аналогичным способом найдём вложенное поле для ввода. Так как дочерный элемент всего один здесь тоже будет индекс 0.
text_input = destination.child_window(class_name="Edit", found_index=0) text_input.set_text("https://devhops.ru")
Результат:
Полный код
app = Application(backend='win32').start("path_to_exe") app = app.connect(best_match="App Name", timeout=3) dlg = app.AppName # dlg.print_ctrl_ids() destination = dlg.child_window(class_name="ComboBox", found_index=0) # destination.print_ctrl_ids() text_input = destination.child_window(class_name="Edit", found_index=0) text_input.set_text("https://devhops.ru")
app.windows()
Пример поиска по app.windows(). Подсмотрел здесь
… print([w.window_text() for w in app.windows()]) i = 0 for w in app.windows(): i += 1 print(i) print(dir(w)) print(w.children()) if i == 1: wind = w print(dir(wind)) print(wind.children()) children = wind.children() i = 0 for child in children: i += 1 print(dir(child)) print(child.texts) if i == 1: child.click()
requirements.txt
certifi==2024.2.2 charset-normalizer==3.3.2 comtypes==1.4.1 idna==3.7 pywin32==306 pywinauto==0.6.8 requests==2.31.0 six==1.16.0 typing_extensions==4.12.2 urllib3==2.2.1 WMI==1.5.1
Common Files
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
Автоматизация | |
pywinauto | |
Ошибки | |
Python |