mindtrove Collecting ideas since 1980

GUI Automation with pyAA

Microsoft Active Accessibility (MSAA) is a library for the Windows platform that allows client applications inspect, control, and monitor events and controls in graphical user interfaces (GUIs) and server applications to expose runtime information about their user interfaces. The purpose of MSAA is to make possible the adaptation of GUIs to other formats to improve software accessibility for users with visual, hearing, physical, and other impairments. In addition, MSAA can be used to good effect to automate GUI testing.

This tutorial introduces pyAA, an object oriented Python wrapper around the client-side functionality in MSAA. The tutorial demonstrates methods for obtaining references to interface objects, gives examples of inspecting and controlling GUI components, and describes how application and system events can be monitored. Timing, instability, and counting issues are also discussed.

Prerequisites

The MSAA Object Model

MSAA exposes the widgets of a graphical user interface as nodes in a tree structure that we will refer to as an accessible object model (AOM). Each node in the AOM represents some component while the edges in the tree represent connections between parent and child objects. From an overly simplistic point of view, the root node in the AOM is the Desktop window, its children are top level application windows, their children are container objects holding controls, and so on. In reality, the content of the AOM is much more complex. For example some components that appear on the screen are not represented in the AOM, invisible objects appear in the AOM but not on the screen, and the Desktop window is not always the root of the tree.

This diagram depicts a portion of a typical AOM tree. The Notepad application window is at the root of the tree. The first four of its seven immediate children are a menu bar, title, menu bar, and client. The client node has two immediate children, both of the window type. The first of these window nodes has seven children, the fourth of which is of the type editable text. This editable text node is the main document area in the Notepad application. Note that this important control is three levels deep in the AOM tree.

Accessible Objects

Programmatically, the components in the AOM tree are represented by instances of the AccessibleObject (AO) class in pyAA. The properties of this class give the name, value, role (i.e. type), state, etc. of the components it represents. The methods of the class support navigation among components, setting the focus and selection, and triggering actions.

The AccessibleObjectFromPoint function returns a reference to the topmost component at the given location but at the lowest level in the AOM tree. For instance, if two windows overlap at the given point, an AO representing a component in the top window will be returned. AOs for light child components such as items in a list, items in a tree, menu items in a menu, etc. are not returned by this function. Instead, AOs for their containing lists, trees, menus, etc. are returned instead.

From Window Handles

Another straightforward way of getting an AccessibleObject is by passing a window handle to the AccessibleObjectFromWindow function.

1
2
3
4
5
6
7
8
9
import pyAA
import win32gui
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
# we want the desktop window
objid = pyAA.Constants.OBJID_WINDOW
# get the object
ao = pyAA.AccessibleObjectFromWindow(hwnd, objid)
print ao.Name

The second parameter to this function specifies the part of a object that the returned AO should represent. See the pyAA documentation for the Constants class for a listing of all available object IDs.

From Nearby AOM Nodes

Once a reference to an AccessibleObject has been obtained, it can be used to get AOs for its parent and children in the AOM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pyAA
import win32gui    
 
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
# we want the desktop window
objid = pyAA.Constants.OBJID_WINDOW
# and its accessible object
top = pyAA.AccessibleObjectFromWindow(hwnd, objid)
print top.Name
print
 
# get all the children of the desktop
for c in top.Children:
  print c.Name
print
 
# get the parent of the first child
# it's the desktop again
p = top.Children[0].Parent
print p.Name

Likewise, methods exist to move through sibling nodes in the AOM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pyAA
import win32gui    
 
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
# this time we want the desktop client area
objid = pyAA.Constants.OBJID_CLIENT
top = pyAA.AccessibleObjectFromWindow(hwnd, objid)
 
# get the first application window
item = top.Navigate(pyAA.Constants.NAVDIR_FIRSTCHILD)
while 1:
  print item.Name, item.RoleText
  try:
    # try to get the next logical window
    item = item.Navigate(pyAA.Constants.NAVDIR_NEXT)
  except:
    break

In addition to moving logically through sibling nodes and to the first child node, pyAA supports spatial navigation up, down, left, and right as well as navigation to the last child node. Again, see the pyAA documentation for more details.

From Exact Paths

The AccessibleObject for a descendant node of the node represented by a currently held AO can be retrieved using an XPath-like notation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pyAA
import win32gui    
 
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
objid = pyAA.Constants.OBJID_CLIENT
top = pyAA.AccessibleObjectFromWindow(hwnd, objid)
 
# get the last object, which should be the program manager
pm = top.Navigate(pyAA.Constants.NAVDIR_LASTCHILD)
 
# get the list of desktop items
dlst = pm.ChildFromPath('/client[3]/window[0]/client[3]/window[0]/list[3]')
for c in dlst.Children:
  print c.Name

The ChildFromPath method accepts a string representing a path rooted at the AO object on which the method is called. Each segment of the path indicates a navigation down one level in the AOM. The text in the segment states the role of the node that should be selected next while the number in brackets indicates the node's absolute position in the list of children of its parent node. For example, the path /window[3]/client[10]/list[0] references the first child node (a list) of the tenth child node (a client) of the third child node (a window) of the current AccessibleObject.

An enhanced version of pyAA might support more flexible paths by allowing the programmer to specify names, values, states, and other properties in addition to roles.

From a Search

When the exact path to a desired descendant is not known, the FindOneChild and FindAllChildren methods can be used to locate a given component meeting some user specified criteria.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pyAA
import win32gui    
 
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
objid = pyAA.Constants.OBJID_CLIENT
top = pyAA.AccessibleObjectFromWindow(hwnd, objid)
 
# search for the desktop list
list_pred = (lambda node: node.Name == 'Desktop' and
             node.Role == pyAA.Constants.ROLE_SYSTEM_LIST)
dlst = top.FindOneChild(list_pred)
print 'All desktop items'
for c in dlst.Children:
  print c.Name
print
 
# search for all Python file icons on the desktop
py_pred = (lambda node: node.Name.find('.py') > -1 and
           node.Role == pyAA.Constants.ROLE_SYSTEM_LISTITEM)
print 'Desktop items ending in .py'
for c in dlst.FindAllChildren(py_pred):
  print c.Name

Of course, using these methods is much slower than specifying a known path since all descendant nodes must be searched until a match is found for FindOneChild or exhaustively for FindAllChildren.

From Events

Finally, AccessibleObjects can be obtained from system wide events such as window creation and destruction. This method is explained in detail in the Event watcher section below.

Properties and Methods

The examples so far have accessed just a few properties of the any AccessibleObject retrieved (e.g. Name, Children, Parent). Many other properties exist, however, and provide other bits of useful information about components on the screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pyAA
import win32gui  
 
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
objid = pyAA.Constants.OBJID_CLIENT
top = pyAA.AccessibleObjectFromWindow(hwnd, objid)
 
# search for the desktop list
list_pred = (lambda node: node.Name == 'Desktop' and
             node.Role == pyAA.Constants.ROLE_SYSTEM_LIST)
dlst = top.FindOneChild(list_pred)
 
# print all the information we can get about the desktop list
print 'Name:', dlst.Name
print 'Value:', dlst.Value
print 'State:', dlst.State
print 'State text:',dlst.StateText
print 'Role:', dlst.Role
print 'Role text:', dlst.RoleText
print 'Description:', dlst.Description
print 'Child count:', dlst.ChildCount
print 'Location and size:', dlst.Location
print 'Window handle:', dlst.Window
print 'Keyboard shortcut:', dlst.KeyboardShortcut
print 'Selected item:', dlst.Selection
print 'Process and thread IDs:', dlst.ProcessID
print 'Path rooted at desktop', dlst.Path
print 'Child ID:', dlst.ChildID

The AccessibleObject class also supports methods to set the focus, select and unselect list items, and perform the default action on a control to name a few.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pyAA
import win32gui  
 
# get the desktop window handle
hwnd = win32gui.GetDesktopWindow()
objid = pyAA.Constants.OBJID_CLIENT
top = pyAA.AccessibleObjectFromWindow(hwnd, objid)
 
# search for the desktop list
list_pred = (lambda node: node.Name == 'Desktop' and
             node.Role == pyAA.Constants.ROLE_SYSTEM_LIST)
dlst = top.FindOneChild(list_pred)
 
# select all items on the desktop
first = dlst.Navigate(pyAA.Constants.NAVDIR_FIRSTCHILD)
last = dlst.Navigate(pyAA.Constants.NAVDIR_LASTCHILD)
first.Select(pyAA.Constants.SELFLAG_TAKESELECTION|pyAA.Constants.SELFLAG_TAKEFOCUS)
last.Select(pyAA.Constants.SELFLAG_EXTENDSELECTION)

Event Watchers

Another fundamental component of the pyAA library is the Watcher class. Instances of this class allow an application to register callbacks for system wide events such as control name and value changes, control state changes, foreground window changes, and so forth. See the EVENT_SYSTEM_* constants in the pyAA documentation for a complete list of events.

The WindowWatcher class in pyAA derives from the base Watcher class to provide the ability to watch for window openings and closings. To use this class, a client application creates an instance and calls either the NotifyOnOpen or NotifyOnClose function. The return value of this function is a Deferred object, a placeholder for a result to be returned later. The client then calls the AddCallback method on the Deferred object to register one or more callbacks.

When an event occurs, the callbacks are invoked in the order in which they are registered. The first callback is passed the AccessibleObject associated with the event. The return value of this function is passed to the next callback and so on for all registered callbacks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import os, time
import pyAA
import pythoncom  
 
# global indicates if message pump continues
alive = True
 
def WindowClosed(ao):
  # quit the message pump
  global alive
  alive = False
 
# the ao is probably dead by now, so just return None
  return None
 
def WindowOpened(ao):
  # now watch for the window closing
  ww = pyAA.WindowWatcher()
  r = ww.NotifyOnClose(ao.Window)
  r.AddCallback(WindowClosed)
  r.AddCallback(Output, 'closed')
 
# whatever is returned by this function is passed
  # to the next registered callback function
  return ao.Name
 
def Output(name, action):
  print name, action
 
if __name__ == '__main__':
  # watch for instances of windows explorer
  ww = pyAA.WindowWatcher()
  r = ww.NotifyOnOpen(cls='CabinetWClass')
  # multiple callbacks can be added to deferred result
  r.AddCallback(WindowOpened)
  r.AddCallback(Output, 'opened')
 
# run windows explorer
  os.startfile('')
 
# a message pump is needed to receive events
  while alive:
    pythoncom.PumpWaitingMessages()
    time.sleep(0.1)

When the above example code is run, an instance of Windows Explorer is started. When the Explorer window appears, the example program prints the name of the window and waits for the window to close. When the Explorer window is manually closed, the example program prints a message and quits.

Watching for other kinds of events is possible by deriving a new class from Watcher. Setting a hook for system wide events is accomplished using the AddWinHookEvent method in the base class. The parameters to this method can include the type of event(s) to monitor, the function to call when the event(s) fire, the type of object to monitor, the thread or process to monitor, and the window handle to monitor. To unset an event hook, simply call the Release method in the base Watcher class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import os, time
import pyAA
import pythoncom  
 
class LocationWatcher(pyAA.Watcher):
  def Start(self):
    # create a deferred
    self.result = pyAA.Deferred()
    # register a hook to watch for window movement
    self.AddWinEventHook(callback=self.OnLocationEvent,
                         event=pyAA.Constants.EVENT_OBJECT_LOCATIONCHANGE,
                         obj_id=pyAA.Constants.OBJID_WINDOW)
    # return the deferred
    return self.result
 
def OnLocationEvent(self, event):
    # create a new deferred
    r = pyAA.Deferred()
    # return the event and the deferred
    self.result.Callback(event, r)
    # store the new deferred
    self.result = r
 
def Output(event, deferred):
  # print the object type and its location
  print event.ObjectName, event.AccessibleObject.Location
  # register this function again for the next event callback
  deferred.AddCallback(Output)
 
if __name__ == '__main__':
  # watch for instances of windows explorer
  ww = LocationWatcher()
  r = ww.Start()
  r.AddCallback(Output)
 
# a message pump is needed to receive events
  while 1:
    pythoncom.PumpWaitingMessages()
    time.sleep(0.05)

While the example program above is running, it will print the location of any window moved on the screen. There is no exit condition so it will continue to monitor location change events until closed.

Caveats

Timing

Components that are not fully initialized may sometimes appear in the AOM tree. When their properties are accessed through an AccessibleObject, a pyAA.Error exception or, worse, incorrect information may be returned. An application might choose to implement an intelligent time delay after any event that changes the state of the user interface to ensure all components in an interface are ready for use.

Instability

Any node in the AOM tree may change or be destroyed at any time, even while a client application is holding an AccessibleObject representing it. pyAA eases the pain of dealing with this very real possibility by throwing Python exceptions when a AccessibleObject is no longer valid. A client application must be prepared to handle the instability of the AOM by catching these exceptions, dropping held references to dead AccessibleObjects, and obtaining references to new AccessibleObjects for replaced AOM nodes.

Counting

The ChildCount property of the AccessibleObject class does not always give an accurate count of items in collections. For instance, a node with the role of list in the AOM may have a number of list items as its children representing the items in the list and a window object containing any column headers. In this case, the ChildCount property will be the number of list items plus one. The extra window object must be taken into account when an accurate count of the true list items is needed.

References

This tutorial introduces the basics of the pyAA library. Please refer to the full pyAA API documentation for more information.

Additional information about Microsoft Active Accessibility can be found on the following sites:

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment


No trackbacks yet.