📜 ⬆️ ⬇️

Old new pywinauto: automate Windows GUI in Python with install / uninstall example

image
One day, while searching for a tool to automate GUI testing, I came across an interesting python package pywinauto . And although it supports only native controls and partially Windows Forms, it is quite suitable for our tasks.
The history of pywinauto originates somewhere around 1998, when Mark McMahon wrote a C-language utility for GUI Automation for his needs (it took about two years), and then, in 2005, rewrote it in Python in three months . The power of python manifested itself in all its glory: the interface pywinauto turned out to be simple and expressive. The tool was actively developed from 2006 to 2010. During the years of calm, in 2011-2012 a kind person moden-py wrote a GUI helper to view the hierarchy of windows and generate pywinauto code called SWAPY (the binaries are here ).
In the meantime, the world was changing. Our team switched to 64-bit binaries, and the pywinauto clone earned on 64-bit Python. In the main branch, the project did not develop for four years and was rather outdated. In 2015, with Mark’s consent, we managed to breathe new life into the project. Now pywinauto officially lives on a githaba , and thanks largely to the comrade airelil, unit tests run on the AppVeyor CI server .

At the moment we have released 3 new releases of the 0.5.x line (the last one is 0.5.2). Major improvements over 0.4.2:



For the past four years, our team has been successfully using pywinauto to test internal software, including complex graphical custom controls. For them, there are custom wrappers that use the HwndWrapper.SendMessage method and the RemoteMemoryBlock class (which, incidentally, has been improved along the way). But this is a topic for a separate analysis, because I did not see any open examples of custom controls for pywinauto.
')
For now let's look at some of the features of pywinauto with a specific example.

Install / uninstall automation example


Often there is a task to automate the installation / removal of software on 100,500 test machines. Let us show how this can be done on the example of 7zip (demo example!). It is assumed that the 64-bit installer is pre-downloaded from www.7-zip.org and lies, for example, in the same folder as our scripts. On test machines, the User Account Control (UAC) is disabled to zero (usually an isolated subnet, which does not harm security).

Installation


This is how the install script install_7zip.py looks (by link - an updated version):

 from __future__ import print_function # for py2/py3 compaibility import sys, os # assume the installer is placed in the same folder as the script os.chdir(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))) import pywinauto app = pywinauto.Application().Start(r'msiexec.exe /i 7z920-x64.msi') Wizard = app['7-Zip 9.20 (x64 edition) Setup'] Wizard.NextButton.Click() Wizard['I &accept the terms in the License Agreement'].Wait('enabled').CheckByClick() Wizard.NextButton.Click() Wizard['Custom Setup'].Wait('enabled') Wizard.NextButton.Click() Wizard.Install.Click() Wizard.Finish.Wait('enabled', timeout=30) Wizard.Finish.Click() Wizard.WaitNot('visible') if os.path.exists(r"C:\Program Files\7-Zip\7zFM.exe"): print('OK') else: print('FAIL') 


With the installation, everything is quite simple, but there are a couple of unobvious moments. To enable the check box to accept the license, we use the CheckByClick () method, which appeared in pywinauto 0.5.0, because many check boxes do not process the WM_CHECK message, and only respond to a real click.

The long wait for the installation process itself is provided by the Wait () method with an explicit parameter timeout = 30 (in seconds). That is, the Wizard.Finish object Wizard.Finish is just a button description, and it is not associated with a real button until Wait () or any other method is called. Strictly speaking, a Wizard.Finish.Click() call is equivalent to a longer Wizard.Finish.WrapperObject().Click() call (it usually occurs implicitly) and is almost equivalent to Wizard.Finish.Wait('enabled').Click() . It was possible to write in one line, but sometimes it is worth emphasizing the importance of the Wait () method.

Deletion


The script for uninstall_7zip.py removal is somewhat more complicated, because you have to go into the control panel, in the section “uninstall programs”. If desired, using explorer.exe, you can automate other tasks.

 from __future__ import print_function import pywinauto pywinauto.Application().Start(r'explorer.exe') explorer = pywinauto.Application().Connect(path='explorer.exe') # Go to "Control Panel -> Programs and Features" NewWindow = explorer.Window_(top_level_only=True, active_only=True, class_name='CabinetWClass') try: NewWindow.AddressBandRoot.ClickInput() NewWindow.TypeKeys(r'Control Panel\Programs\Programs and Features{ENTER}', with_spaces=True, set_foreground=False) ProgramsAndFeatures = explorer.Window_(top_level_only=True, active_only=True, title='Programs and Features', class_name='CabinetWClass') # wait while list of programs is loading explorer.WaitCPUUsageLower(threshold=5) item_7z = ProgramsAndFeatures.FolderView.GetItem('7-Zip 9.20 (x64 edition)') item_7z.EnsureVisible() item_7z.ClickInput(button='right', where='icon') explorer.PopupMenu.MenuItem('Uninstall').Click() Confirmation = explorer.Window_(title='Programs and Features', class_name='#32770', active_only=True) if Confirmation.Exists(): Confirmation.Yes.ClickInput() Confirmation.WaitNot('visible') WindowsInstaller = explorer.Window_(title='Windows Installer', class_name='#32770', active_only=True) if WindowsInstaller.Exists(): WindowsInstaller.WaitNot('visible', timeout=20) SevenZipInstaller = explorer.Window_(title='7-Zip 9.20 (x64 edition)', class_name='#32770', active_only=True) if SevenZipInstaller.Exists(): SevenZipInstaller.WaitNot('visible', timeout=20) if '7-Zip 9.20 (x64 edition)' not in ProgramsAndFeatures.FolderView.Texts(): print('OK') finally: NewWindow.Close() 


Here are some key points.

When you launch explorer.exe, a launching process (launcher) is created briefly, which checks that explorer.exe (worker) is already running. Such a bunch of “launcher-> worker” is sometimes found. Therefore, we separately connect to the workflow explorer.exe by the connect () method.

After clicking on the address bar ( AddressBandRoot ), the so-called in-place edit box appears (only at the time of entry). When calling the TypeKeys() method, TypeKeys() must specify the parameter set_foreground=False (appeared in 0.5.0), otherwise the in-place edit box will disappear from the radar. For all in-place controls it is recommended to set this parameter to False.

Further, the list of programs is initialized for a long time, however the ListView control itself is available and a simple call to ProgramsAndFeatures.FolderView.Wait('enabled') does not guarantee that it has already been completely initialized. Lazy (lazy) initialization goes in a separate thread, so you need to monitor the CPU activity of the entire explorer.exe process. To do this, pywinauto 0.5.2 implements two methods: CPUUsage() , which returns the CPU load in percent, and WaitCPUUsageLower() , waiting until the CPU load falls below the threshold (2.5% by default). The idea of ​​implementing these methods was suggested by the article comrade JOHN_16: “We monitor the completion of the CPU loading process” .

By the way, the call item_7z.EnsureVisible() magically scrolls the list so that the desired item is visible. No special work with the scroll bar is needed.

Several Wait and WaitNot calls mean that you need to wait for a certain window to open or close for a relatively long time (longer than the default). However, some waitnot calls are inserted just for control. This is a good practice.

“After all, life is both simpler and more complicated ...”


Of course, this was just an example. In the case of 7zip, everything is solved much easier. Run cmd.exe as Administrator and execute a simple line (works with any UAC level):
  wmic product where name="7-Zip 9.20 (x64 edition)" call uninstall 

Of course, the installer zoo is not limited to .msi packages, and the range of automation tasks is very wide.

What is most often asked


If earlier the main question was about Python 3 and 64 bits, now support WPF and a number of other non-native applications that support the UI Automation API are on the agenda. Practices in this direction is. Any help in adapting various back-end'ov under the interface pywinauto we welcome.

Source: https://habr.com/ru/post/266459/


All Articles