NAME
Win32::ActAcc - Active Accessibility client in Perl
SYNOPSIS
Note: We assume you're already familiar with Microsoft's Active Accessibility.
Tools
Explore your Active Accessibility world with Win32::ActAcc's utilities:
C:\> aadigger # Explore the window hierarchy (details: perldoc aadigger.pl)
C:\> aaWhereAmI # Mouse around and see the path to each accessible Object.
C:\> aaEvents # Watch WinEvents through Active Accessibility.
Script essentials
Now let's write a Perl script. You need this stuff at the top:
use Win32::OLE;
use Win32::ActAcc qw(:all);
Win32::OLE->Initialize();
use Win32::GuiTest; # recommended
Get Accessible Object from Desktop, Point, or HWND
Get the "root" Accessible Object:
$ao = Desktop();
- Note
-
A Perl
Win32::ActAcc::AO
object contains anIAccessible*
andchildID
.The object's precise subclass of AO reflects its role (Window, Client, Pushbutton, etc.).
Other ways to get an Accessible Object:
$ao = AccessibleObjectFromPoint($x, $y); # pixels from upper left
$ao = AccessibleObjectFromWindow($hwnd);
You can also get an Accessible Object from an event, such as when an application opens..
Invoke an app and watch for its window to appear
Here's how to invoke an app with system
and latch onto its main window by listening to events.
# Install event hook with first call to "clearEvents":
clearEvents();
# Give event hook time to permeate your computer:
# ...
# Start Notepad, but first discard the event backlog:
clearEvents();
system("start notepad.exe");
# Wait for Notepad to appear, as signaled by
# an EVENT_OBJECT_SHOW event associated with an
# Accessible Object whose name matches qr/Notepad$/,
# and make a note of that useful Accessible Object.
my $aoNotepad = waitForEvent
(
+{
'event'=>EVENT_OBJECT_SHOW(),
'ao_profile'=>qr/Notepad$/ # a 'window test': see below
},
# options:
+{
'timeout'=>30, # seconds
# (an hourglass buys a few more seconds)
'trace'=>1 # display events as troubleshooting aid
# Tip: Don't turn off 'trace' until your script works!
}
);
# The sentinel event might not be the last in the flurry of events.
# Wait for steady state before proceeding.
awaitCalm();
For jobs too elaborate for waitForEvent
, see "Events" below.
Accessible Object properties
Having found an Accessible Object, examine it:
my $hwnd = $ao->WindowFromAccessibleObject();
my $roleNumber = $ao->get_accRole();
my $roleText = GetRoleText( $roleNumber );
my $stateBits = $ao->get_accState();
my $name = $ao->get_accName();
my $value = $ao->get_accValue();
my $description = $ao->get_accDescription();
my $default_action = $ao->get_accDefaultAction();
my $help = $ao->get_accHelp();
my $f = $ao->get_accFocus();
my ($left, $top, $width, $height) = $ao->accLocation();
my $ks = $ao->get_accKeyboardShortcut();
my $id = $ao->get_itemID();
my $bp = $ao->get_nativeOM();
my @selections = $ao->get_accSelection();
visible
considers the STATE_SYSTEM_INVISIBLE
bit from get_accState
, among other factors - see "visible".
my $might_be_visible = $ao->visible();
Troubleshoot your script by printing out the Accessible Objects.
# Display class, name, state, location, ID, HWND, default action:
print "badger/limpid: " . $ao->describe() . "\n";
print "pilfer/bugle: $ao\n"; # same thing
# display summary of $ao and all its descendants
$ao->debug_tree();
GUI Manipulation
Active Accessibility alone is feeble, so be sure to see also "Using Active Accessibility with Win32::GuiTest".
# Selection and focus
$ao->accSelect(SELFLAG_TAKEFOCUS());
# doable action at this moment
my $action = $ao->get_accDefaultAction();
$ao->accDoDefaultAction();
If accDoDefaultAction
will do, then perhaps there is a particular action that the script would like to assert is default before executing it.
# Perl shortcut: Do named action iff it's the default -- otherwise die.
$ao->doActionIfDefault('Press'); # do-or-die
# Shorthand for the shortcut (for English-language Windows):
$ao->dda_Check();
$ao->dda_Click();
$ao->dda_Close();
$ao->dda_Collapse();
$ao->dda_DoubleClick();
$ao->dda_Execute();
$ao->dda_Expand();
$ao->dda_Press();
$ao->dda_Switch();
$ao->dda_Uncheck();
AO can simulate a click using the Windows API.
# Simulate click at center of an Accessible Object:
$ao->click(); # there's also $ao->rightclick()
Parents and Children
Find an Accessible Object's relatives:
my $p = $ao->get_accParent(); # query Active Accessibility
$p = $ao->parent(); # prefer cached weak-ref from iterator, if present
# Get child-count, then one child at a time:
my $kk = $ao->get_accChildCount();
my $ak = $ao->get_accChild(0); # etc.
# Get children in a list:
my @likely_visible_children = $ao->AccessibleChildren();
my @all = $ao->AccessibleChildren(0,0); # state-bits to compare, bit values
# Navigate turtle-style:
my $np1 = $ao->accNavigate(NAVDIR_FIRSTCHILD()); # etc. etc.
- Note
-
Win32::ActAcc's
AccessibleChildren
, with no arguments, screens out `invisible' and `offscreen' results by assuming the default arguments (STATE_SYSTEM_INVISIBLE()|STATE_SYSTEM_OFFSCREEN(), 0
).
Buggy apps may respond inconsistently to one or another technique of enumerating children. Unfortunately, you must program the script differently for each technique, so experimenting with more than one is tedious.
So why not use an Iterator instead?
Iterators
Here's how to visit an Accessible Object's children using an iterator:
my $iter = $ao->iterator();
$iter->open();
while ( my $aoi = $iter->nextAO() )
{
print "$aoi\n";
}
$iter->close();
Accessible Objects from iterators keep a weak reference to the "parent" that enumerated them, and can infer some state information from the parent's state.
my $p = $ao->iparent(); # parent as noted by iterator...
$p = $ao->parent(); # ... or get_accParent() if iparent=undef
# get state bits, including states implicit from parent
# (readonly, offscreen, invisible, unavailable):
my $allstate = $ao->istate();
The iterator for most windows uses a slow, but thorough, combination of AccessibleChildren
and accNavigate
. The iterator for menus and outlines can click through them and treat sub-items like children. You can select the best child-enumeration technique for each occasion. See details at "More on Iterators" below.
Win32::ActAcc's power tools -- dig
, tree
, menuPick
-- use iterators so as not to couple their mission to any specific child-enumeration technique.
Tree of Accessible Objects
Use tree
to traverse a hierarchy of Accessible Objects depth-first, calling a code-ref once for each Accessible Object (including the starting object). The code can control iteration using level
, prune
, stop
and pin
(see sample).
$ao->tree
(
sub
{
my ($ao, $monkey) = @_;
# $monkey->level() returns count of levels from root.
# $monkey->prune() skips this AO's children.
# $monkey->stop() visits NO more Accessible Objects.
# $monkey->pin() prevents closing any menus and outlines
# that tree() opened
# (applies only if flag 'active'=>1)
print ' 'x($monkey->level())."$ao\n";
},
#+{ 'active'=>1 } # optional iterator flags-hash
);
When tree
obtains an iterator for each Accessible Object it visits, tree
passes its second argument (an optional hash) to the iterator's constructor. (See "More on Iterators".)
Referring to an object by name or role
Supposing $ao
is a client area containing a Close button, here's how to find and press Close:
$aoBtnClose = $ao->dig
(
+[
"{push button}Close" # {role}name
]
);
$aoBtnClose->dda_Press();
If $ao
is a window, containing a client area, containing a Close button, just set forth both steps in the path to reach Close from $ao
:
$aoBtnClose = $ao->dig
(
+[
"{client}", # {role} only, name doesn't matter
"{push button}Close"
]
);
In a word, dig
follows a path of "Window Tests", and returns what it finds. See details at "Finding Accessible Objects using 'dig'".
You can run aadigger or aaWhereAmI interactively to reconnoiter and figure out a path to the interesting Accessible Object.
Menus
menuPick
uses Active Accessibility and Win32::GuiTest to manipulate standard Windows menu-bars and context-menus. Your mileage may vary with apps that use custom menus with cockeyed support for Active Accessibility.
# menuPick takes a ref to a list of window-tests,
# tracing a path from menubar to menu to submenu etc.,
# plus an optional flags hash.
$ao->menuPick(+[ 'Edit', qr/^Undo/ ], +{'trace'=>1} );
menuPick
can summon and operate a right-clicky context menu:
$ao->context_menu()->menuPick(+[ 'Paste' ]);
If Win32::GuiTest
has been loaded (as by use Win32::GuiTest;
), the active menu iterator closes menus when it's done with them.
Some menus contain items marked as invisible. Use the HASH form of the window-test to pick such an invisible item; the string and regex window-tests match only visible items.
Using Active Accessibility with Win32::GuiTest
Get an HWND or location from an Accessible Object and manipulate it with the Windows API:
use Win32::GuiTest;
# use a HWND
my $hwnd = $ao->WindowFromAccessibleObject();
my $name = Win32::GuiTest::GetWindowText($hwnd);
# use an (x,y) location
my ($left, $top, $width, $height) = $ao->accLocation();
Win32::GuiTest::MouseMoveAbsPix($left, $top);
DETAILS
Window Tests
A window-test examines an Accessible Object and returns a true or false value -- like Perl's file tests (-e, -f, etc.). Window-tests are used in waitForEvent
, dig
, menuPick
, and match
.
A window-test can take the form of a string, a regex, or a hash.
- String
-
The string must completely match the object's name, {role}, or {role}name. Matches "visible" objects only. You can't use the string form of window-test if you need to include a literal brace in the name. For the role, use whatever notation is convenient:
window # role text ROLE_SYSTEM_WINDOW # constant name WINDOW # last leg of constant name Win32::ActAcc::Window # Perl package for the role Window # last leg of package name value of ROLE_SYSTEM_WINDOW() # the role number
- Regular expression (qr/blabla/)
-
The regex matches the object's name, as in
$name=~qr/regex/
. Matches "visible" objects only. - Hash
-
Specifying a window-test as a hash is the most flexible way, as it can test not only the name and role, but also the state and other attributes of the Accessible Object, and even run a custom code-ref.
Hash members (all are optional; all present members must match the Accessible Object):
get_accRole
hash member-
role number
get_accName
hash member-
string (match entire) or regex
get_accValue
hash member-
string (match entire) or regex
get_accDescription
hash member-
string (match entire) or regex
get_accHelp
hash member-
string (match entire) or regex
get_accDefaultAction
hash member-
string (match entire) or regex
WindowFromAccessibleObject
hash member-
match an HWND number
visible
hash member-
a true value to match only "visible" objects. (Use a false value to match only invisible objects. Omit the 'visible' key if you don't care whether the object is visible.)
state_has
andstate_lacks
hash members-
or'd state bits
role_in
orrole_not_in
hash member-
LIST of roles (each item in the list uses any "{role}" notation (above), but without the braces)
code
hash member-
a code-ref to call if the other hash keys match. Return a true value to indicate a match.
Sample window-tests:
$b = $ao->match('Close'); # Is AO's name exactly Close?
$b = $ao->match( +{'get_accName'=>'Close'} ); # ... using a hash.
$b = $ao->match(qr/Close/); # Does AO's name match that regexp?
$b = $ao->match( +{'get_accName'=>qr/Close/} ); # ... using a hash.
$b = $ao->match('{ROLE_SYSTEM_PUSHBUTTON}Close'); # Is AO a pushbutton named Close?
$b = $ao->match('{push button}Close'); # ... using localized 'role text'
$b = $ao->match('{Pushbutton}Close'); # ... using ActAcc package name
$b = $ao->match( +{'get_accRole'=>ROLE_SYSTEM_PUSHBUTTON(), 'name'=>'Close'} ); # ...
$b = $ao->match( +{'rolename'=>'Pushbutton', 'get_accName'=>'Close'} ); # ...
$b = $ao->match
(
+{'code'=>
sub
{
my $ao = shift;
return $ao->match( qr/Bankruptcy in progress/ );
}
}
);
$b = $ao->match( +{'visible'=>1} );
There is more to the 'visible'=>1 test than meets the eye..
visible
my $might_be_visible = $ao->visible();
my $same_thing = $ao->match( +{'visible'=>1} );
The visible
function returns a true value if none of these reasons-for-being-invisible applies.
State bit `invisible' or `offscreen' is set
An ancestor's state includes `invisible' or `offscreen'. Note:
visible
does not callget_accParent
, which may lead to a cycle in a buggy app, but instead relies on the cached weak-ref from the iterator that found this Accessible Object.Location is undefined (unless state includes 'focusable')
Location is entirely negative
Height or width is zero
The algorithm overlooks other reasons-for-being-invisible, such as occlusion and inconspicuousness.
Finding Accessible Objects using 'dig'
dig
follows a path of "Window Tests", and returns what it finds.
Depending on its scalar or list context, and min and max options, dig
can perform various searches:
- Find all matching Accessible Objects (die if none)
-
Use
dig
in array context without specifying optionsmin
ormax
. - Find all matching Accessible Objects (if any)
-
Use
dig
in array context, specifying optionmin=0
. - Find first matching Accessible Object (die if none)
-
Use
dig
in scalar context, specifying neither optionmin
normax
, or specifying them both as 1. - Find first matching Accessible Object (if any)
-
Use
dig
in scalar context, specifyingmin=0
. If it finds no matching Accessible Object,dig
returnsundef
.
The optional second parameter is a hash of options:
- "min" option
-
Find this many objects, or die.
- "max" option
-
Stop looking after finding this many objects.
- "trace" option
-
If true, display the objects being examined.
-
dig
passes these along when it obtains an iterator for each Accessible Object it traverses.dig
sets the "active" flag unless the options hash specifies it as a non-true value.
Samples using dig
:
# Find one immediate child of $ao with role "client"; die if not found.
my $aoClient = $ao->dig( +[ '{client}' ] );
# Find one untitled Notepad within the Desktop's client area; die if not found.
my $someNewNotepad = Desktop()->
dig(+[
'{client}', # step 1
'{window}Untitled - Notepad' # step 2
]);
# Get results into a list: find *all* untitled Notepad
# windows in the Desktop's client area. Die if none found.
my @allNewNotepads1 =
Desktop()->
dig(+[
'{client}', # step 12
'{window}Untitled - Notepad' # step 2
]);
# Find all untitled Notepads, using a regex to match their name.
my @allNewNotepads2 =
Desktop()->
dig(+[ '{client}', # step 1
+{ # step 2:
'get_accRole'=>ROLE_SYSTEM_WINDOW(),
'get_accName'=>qr/^Untitled - Notepad$/
}
]);
# Find all untitled Notepads that contain an Application menubar.
my @allNewNotepads3 =
Desktop()->
dig(+[ '{client}', # step 1
+{ # step 2:
'get_accRole'=>ROLE_SYSTEM_WINDOW(),
'get_accName'=>qr/^Untitled - Notepad$/
},
+{ # step 3:
'get_accRole'=>ROLE_SYSTEM_MENUBAR(),
'get_accName'=>'Application'
},
+{ # step 4: back up!
'axis'=>'parent'
},
]);
# Find windows on desktop. Die if fewer than 2. Return at most 42.
my @upTo42Windows =
Desktop()->dig( +[
'{client}', # step 1
'{window}' # step 2
],
+{ # options
'min'=>2, # -die unless at least 2
'max'=>42, # -shortcut after 42
'trace'=>1 # -for troubleshooting
}
);
The active
, nav
, and perfunctory
options configure the iterator with which dig
enumerates each Accessible Object's children in its quest for potential matches..
More on Iterators
The default iterator uses both AccessibleChildren
and accNavigate
, which is slow but works with many applications.
my $iter = $ao->iterator();
Optional hints convey a preference for an iterator type, if it applies to the Accessible Object:
# operate menus and outlines, treating consequences as children
my $iter = $ao->iterator( +{ 'active'=>1 } );
# use AccessibleChildren
my $iter = $ao->iterator( +{ 'perfunctory'=>1 } );
# use accNavigate
my $iter = $ao->iterator( +{ 'nav'=>1 } );
For completeness, there is an iterator that uses get_accChildCount
and get_accChild
:
my $iter = new Win32::ActAcc::get_accChildIterator($ao);
Events
Win32::ActAcc installs a system-wide in-process event hook upon the first call to clearEvents
. Thereafter, events stampede through a circular buffer. You can watch by running aaEvents.
All Perl processes share one event hook and one circular buffer, but each Perl process keeps its own independent pointer into the buffer.
getEvent
retrieves one event from the circular buffer and advances the pointer:
# Retrieve one event from circular buffer (if any there be).
my $anEvent = getEvent();
if (defined($anEvent))
{
print "Event: $anEvent\n";
}
Scripts may getEvent
in a loop to watch for a specific sentinel event. Such a loop is included: waitForEvent
consumes events until one satisfies a hash-of-criteria (sample in the Synopsis) or a code-ref:
waitForEvent
(
sub
{
my $e = shift;
if ($$e{'event'} == EVENT_SYSTEM_FOREGROUND())
{
my $ao = $e->AccessibleObjectFromEvent(); # or getAO() for short
my $name = $ao->get_accName();
return $ao if ($name =~ qr/Notepad$/);
}
return undef;
},
# options:
+{
'timeout'=>30, # seconds
# (an hourglass buys a few more seconds)
'trace'=>1 # display events as troubleshooting aid
# Tip: Don't turn off 'trace' until your script works!
}
)
or die("Notepad didn't come to foreground in the allotted time.");
To prevent a stale event from triggering the exit condition, call clearEvents
before taking the action whose consequences the script will be looping in wait for.
SAMPLE
"eg\playpen.pl" demonstrates using Active Accessibility to inspect and manipulate a menu, a popup menu, a text-entry blank, a checkbox, a radio button, a spin button, tabs, a list box, a tree-list, a two-column list view, and suchlike.
Of course, playpen.pl depends on an application that presents such widgets. The applets that come with Windows change too often, so playpen.pl uses a simple C# app whose source code is in eg\playpen.
playpen.pl also depends on Win32::GuiTest.
Build the playpen C# app, then invoke playpen.pl to explore it:
> vcvars32 || rem ember to put 'csc' on the path
> cd eg\playpen
> build.cmd
> cd ..
> perl playpen.pl
TROUBLESHOOTING
If an Active Accessibility function unexpectedly returns undef
, check Perl's Extended OS Error special variable $^E
for clues.
Run your script with "perl -w".
If your script doesn't work, see whether the aadigger sample works.
If you see Windows error 0x800401f0 ("CoInitialize has not been called"), make sure your script starts off with Win32::OLE->Initialize().
If you get a Windows error number and want to know what it means, try using Win32::FormatMessage
.
If your script sometimes misses noticing an event that occurs very soon after your script calls clearEvents()
for the first time, insert a sleep
after that first clearEvents()
. Installing a WinEvent handler seems to take effect "soon", but not synchronously.
If you are desperate enough to insert "print" statements:
print "The Accessible Object is: $ao\n"; # shows several attributes
print "same as: ". $ao->describe() . "\n";
$ao->debug_tree(); # display $ao and all its descendants
If you are struggling to find the right event or window-test for use with waitForEvent
, dig
, tree
, or menuPick
, try using the trace
flag to evoke a lot of progress messages. Or, embed the interactive aadigger feature into your script:
# invoke interactive explorer feature starting at $ao:
use Win32::ActAcc::aaExplorer;
Win32::ActAcc::aaExplorer::aaExplore($ao);
If menuPick
doesn't work because your computer is too slow, increase the value of $Win32::ActAcc::MENU_SLOWNESS
. menuPick
relies on a hover delay to give the app a chance to update a menu-item object's default action.
If your script displays gibberish instead of Unicode text on the console, try writing to a file instead.
BUGS
It doesn't implement get_accHelpTopic
and accHitTest
.
menuPick
doesn't know how to choose commands that hid because you seldom use them.
You can't use a Win32::ActAcc "Accessible Object" with Win32::OLE.
It probably doesn't work multi-threaded.
Apps with a buggy IAccessible
implementation may cause the Perl process to crash.
COPYRIGHT
Copyright 2000-2004, Phill Wolf.
pbwolf@cpan.org
2 POD Errors
The following errors were encountered while parsing the POD:
- Around line 440:
=back doesn't take any parameters, but you said =back 4
- Around line 495:
=back doesn't take any parameters, but you said =back 4