Skip to Main ContentWindow-Eyes  Braille Sense  CCTV  Voice Sense  SyncBraille  Support  Training
GW Micro - Unleashing the power of your mind's eye.
 

So you want to write a script? (part 2: scripting examples) - GWWiki

Jump to: navigation, search



This is part 2 of the article So you want to write a script?, which you may want to go to if you haven't read through it thoroughly first.

Contents

Scripting Varius Types of Issues

The remaining portion of this article is going to focus on various problems or goals that scripting might solve, and show examples of how they may be done. In each case all the WE features used will be explained. I hope in this way to be able to demonstrate all the WE scripting features and capabilities. I plan to order the issues from simpler to more complex, but I believe it's important that you read them all, and not just the one which may pertain to your immediate plans.

You should also look at the aids to app development in the form of apps on GW's "App Central" which are in

the category of "app development".

If you want a development environment that will give you a lot of help in writing your apps, and if you own MS Word, check out the "MS Office" app, which utilizes the VBA development environment of Word for VBScript development, giving you code completion (a.k.a. IntelliSense), syntax checking, object browsing, and more. Also, see the topic below on the "WEScript Framework Wizard" from GW Micro, which provides you a way to get the basic part of your app automatically generated for you.

And finally, when you're ready to publish your app to the world, see the article here in the wiki Walk-through for Creating an App.


Using the App Developer Reference Manual

"In my app, how Do I use a Property which I Can See in the App Developer Reference manual?" This is a commonly asked question and frustration, especially of those who are new to the idea of object oriented programming. For example: suppose you want to speak the names of all the braille tables currently in use.

If you look at the list of objects in the App Developer Reference manual, you'll see one named "brailleTable", and that it has a property named "name", which you then read the help topic for. You decide you want to speak the property "Name", and make a note of it, but, the help file only lists the properties and methods for the brailleTable object right? no clue as to how you use it?

Luckily, this isn't exactly the case. In addition to the documentation entry for each property or method, there is a documentation topic for the object itself. you get to it the same way you always do: you press <enter> on the object name in the tree list, and then press F6 to change panes to read the help text entry for the object itself.

This information always contains a list of other objects which have this object type as one of their properties, or a method which returns this object type.

This is a very common feature of object models in object-oriented programming: objects contain other objects, which in turn contain further objects, and so on. it makes it very difficult sometimes to come up with the logic which will take you from the object that you know how to get to, to the one which you need to get to.

The information provided in the help topic for the object itself, is both the type of object which contains this object, and the name of it's property or method which you would use to get this type of object. In this case, the help topic says:

A BrailleTable object can be obtained from a BrailleTables object's Active property, or Item method.


Next, you click on the link in the help topic to the object type which contains this object (brailleTables in this case), and then you can click on the links of the properties and methods which return the object type you're interested in (in this case, you'd click on the "active" property and the "item" property. You decide that "active" is the one holding what you need, so you place it, along with a dot dereference operator, in front of the "name" property which you noted down earlier, and now you have "active.name".

You can press backspace from the help topic on "active" and you're returned to the help topic on "brailleTables". And in here, just like the previous object, is a description of the object types along with their properties or methods, which contain a brailleTables object. And again, you can click on this object type to go to it's help topic, and find the properties and methods which were mentioned (here it's "translationTables"), and click on it to read it's help topic. It seems correct, so you note it down, with a dot operator, in front of what you have previously noted, and now you have translationTables.active.name

You keep repeating this "back tracking" process until you can't go any further, or until you reach an object which you know how to obtain. In many cases, what you will find is that you have ended up in a root level object, which you recognize from the previous list earlier in this article, or by it's help topic telling you this. In such a case you know that you need do nothing to obtain it, that's largely the point of root level objects. In this case, we end up with:

speak activeSettings.braille.TranslationTables.active.name


Of course it's not always this easy; many times what you need isn't vailable in a root level object, and you must use a series of methods to obtain the right objects, in order for you to get to your desired goal. The rest of this article will hopefully provide you with the skills and knowledge to do just that.

Having Hotkeys Trigger an App

Virtually all apps have this structure in common: they start running at some point,they perform their initialization, and then they go into a state where they are waiting for something to happen; usually that something ultimately is the user prforming some action. When a script is in this "wait state" the WE script manager will show it's status as "running", which is a little confusing as it's not actively doing anything, it's just waiting for something to happen.

If a script however is always executing, and never goes into a wait state, then it's very likely something is wrong with it's design (such as an infinite loop), as there is no reason for a script to constantly execute, endlessly, unless perhaps you're calculating Pi out to forever! Unfortunately, when a script is constantly executing, it is impossible to do anything else with it (such as communicate with it). This is why the wait state is so important for a script, it then allows it to receive messages, information, and instructions from WE, the user, or the oprating system. This important point, that an app is not constantly executing, but is usually waiting for something to happen, is not one which is obvious to the new developer.


Having an app wait for the user to press a particular hotkey is a very common way for an app to enter this wait state. It relies on capabilities of the WE object model, which allow the app to tell WE what keystroke is the hotkey, and which of the app's subroutines or functions should automatically be executed, when the hotkey is pressed. WE also supresses the keystroke so that Windows or any application doesn't see it, and try to react to it. The other type of keystroke which WE can be set to react to is a "cursorKey". The only difference isn't that this key is used to manipulate the cursor position, although it often is, but is instead that a cursorKey is allowed to go to the application as well as triggering a routine in your app.

When you have your app call a method, and pass it the name of one of your subroutines or functions which it is to cause to be executed, then this subroutine or function is known as a "callback". A callback is required to be defined in a very specific way that the method which you are calling specifies. that is, the method you call may specify in it's documentation that the callback should b a function, with 3 parameters, defined in this order ... and that the function should return this kind of value under these circumstances. When that is the case, you cannot deviate from those instructions; you must create a function, with the specified parameters (except where they are described as being optional), which returns the specified data, if you want to use the method.


In this case, the method you need to use in order to have your app respond to a hotkey is the registerHotKey method of the keyboard object. once you know this, you can go to the App Developer Reference manual and lookup objects, the keyboard object, and under it methods, and finally the registerHotKey method. in it's documentation will be a specification as to how you will need to setup your callback routine for this method to use. Here is an example based on one from the App Developer Reference Manual:

' this example is a fully complete script.
dim registeredKey 
Set registeredKey = keyboard.registerHotKey("Control-Shift-Windows-H", "SpeakHelloWorld")

Sub SpeakHelloWorld()
      Speak "Hello world!"
End Sub

Items to note about this example are:

  • this is a complete and fully functional script.
  • when you run it, pressing control-shift-windows-H will cause a message to be spoken.
  • the speak method was able to be used without specifying the WE application object, or the parent object of the speak method, because those objects are root level objects (the parent object of the speak method is the speech object).
  • the result of the call to the registerHotkey method must be stored in a variable, and this variable must be a global variable, in order for the hotkey to continue functioning. if the variable holding the result is destroyed, such as when it's a local variable and the subroutine or function ends, then the hotkey will stop functioning. this is not the only method which relies on it's result being stored in a global variable, and so you will need to pay attention to the documentation of each method for this requirement.
  • the result of the registerHotkey method is an object, and because it's being stored in a variable, you must use the "set" command to do so, instead of just assigning it directly to the variable.
  • the hotkey being registered only remains registered as a hotkey for this session of WE running; it is not saved permanently anywhere.
  • as a general rule, you should not hard-code the name of the hotkey you wish to use directly into the registerHotkey command in your script; this does not give the user the opportunity to change it to anything else. see the section on the GWToolkit (below) for a collection of functions and subroutines which will make it much easier for you to store the hotkey name in a file, and to allow the user to change it to some other keystroke.
  • even though this script uses the Speak method, it works with braille displays as well; the Speak method also displays any text on the braille display (for 2 seconds), and has no trouble if you're not running a speech synthesizer at all.

What is the XML file used for?

Often, you will see a script file accompanied by a .xml file, when it's installed. However, it's usually not clear what these .xml files are for?

An XML file is an ASCII text file, in a specific format, which is essentially a database which can hold many different types of data at once. Scripts use them to hold their data; they do this because when updating a script, it's often easier to update the XML file than to change the script program file itself.

In addition to holding data (such as the text of the help messages, default values for hotkeys, the text of the about box, etc.), all of which are completely at the Discretion of the programmer as to which data should be stored in the XML file, WE does have one very specific use for XML files: they are used to hold the text which defines the look and feel of menus and dialogs used by the script. Every other piece of information could be stored in some other way (which is the choice of the programmer), but the dialogs and menus that WE can create for you must be designed using the custom XML commands defined in the App Developer's Reference manual.

This file is automatically created for you by the WE Script Framework app (to hold the strings and hotkeys of the app it's creating for you), and a second app written by GW named UIDesign will edit this file for you, allowing you to easily create WE XML dialogs, menus, etc., and even allowing you to edit the strings portion. Between these two apps you as an app designer actually never need to learn the XML commands which are defined in the WE App Developer Reference manual.

There is another function that XML files are extremely useful for, because it is supported by WE to help make it easier, and that is to store different versions of all the possible dialogs menus and controls, messages and text an app might display or speak, in various languages. WE supports this by automatically selecting the appropriate language version of the item, based on the language that Windows is set to use. This is done without the app developer having to put forth any extra effort to allow for these different languages (other than that of getting the text initially translated, and then stored in the XML file under the appropriate language identification).


Writing an App to Examine the Text in the Window of a Program

Writing an app which does something with the information found somewhere on the screen is quite a common scripting task. You may want to examine it, or even change it. The WE object model has quite a variety of objects which allow you to do this, and quite a large variety of methods which help you in finding the exact object (such as the correct window), that you want to interact with.

To read text from the screen, a window object is almost always involved. A major complication however is that Windows has many more window objects than are evident by looking at the screen. To Windows, a window object can mean many more things than just the area of the screen associated with an application such as Notepad, and they are always nested in a parent-child relationship, which can run to any depth. In fact, any window at all is ultimately a child, at some depth, of the desktop window. (See this summary of window relationships and terminology from Microsoft).

This is very helpful to know, since it means when you are starting to look for a given window, one approach is to start with the desktopWindow object, which is a root level property, and you can begin searching all of it's children windows for the one you need.

When examining the window objects owned by a given application, you might be surprised at first when you find that there are usually many more than the one you see on the screen. For instance, Notepad turns out to have, not just the one window you might expect, but actually four. One is the main client area for the file, one is the status-bar at the bottom of the main editing area, and I'm not sure what the other 2 even are for.

Many applications will have at least one window object for every control they can display, along with one more to hold them all, and to act as the main window which is visible on the screen. In WE object terms, this main window is the "overlap" window for the application, and each of the others becomes the focusedWindow as you tab through the controls.

Many of these window objects have no text at all, and others may only hold a small amount (such as the contents of an editbox). In these cases, their borders are usually not visible, and so the window itself is not visible.

What Tools Exist to Help You Find Out What's Available for You to Work With in a Given Application?

Before we go on to talk about how your program may find the objects it needs to work with, you will need to know what objects are even possible for you to use in achieving your scripting goals. It turns out many people have written scripts which are designed to help you discover information on the window structure, Windows and WindowEyes events, and MSAA information for a given application. You may find it very helpful to install all of these apps, and familiarize yourself with each one; what it has to offer you, and how it works. While they often duplicate one another in the information that they can provide, you'll probably find one who's presentation is more comfortible for the way you work than some of the others.

One of the oldest offerings, and perhaps the most important, is the immediate mode script (Immed). It allows you to type in commands, or short sequences of commands, without having to write and run a script, just to see what will happen. It's described as a "real-time debugging environment for VBScript and JScript. If, for instance, you'd like to see what the title and the classname of the notepad window are, you can open notepad, and then invoke the immediate mode script, and type in commands such as:

print activeWindow.title 

And you will be shown the notepad window's title. There are times when you need to prefix the print command with an at sign, to insure it will use the application which was active before you invoked the immediate mode dialog, when running your command, so see the immediate mode's help documentation for details.


One of the newest offerings is the TreeView script, from GW Micro. It will display for you a structure of the Windows, or the MSAA objects, for a given application, as well as some of the most common properties for each.

Others include Virtual Explorer, Focus Control, and Harvest Window from Jamal Mazrui, Focused Window Detective and MSAA Detective from Vic Beckley, and WE Event from GW Micro. It's very likely you'll need to use tools such as these, in order to determine what's available for you to work with, before you attempt to write your script.

How Do You Locate the Object You Need to Work With in Your Script?

Knowing which object you want to work with is only part of the battle; next, you have to be able to get to it, or find it, in your script. Finding the correct window object that you need can be quite a challenge. The WE object model has many objects and methods for you to find the window object that you need to work with; some of the most commonly used are:

  • activeWindow object
  • desktopWindow object
  • clientInformation.overlap window object (only for use with program-specific apps)
  • focusedWindow object
  • windows object collection

All of the objects above, which don't have an object and a dot operator preceeding them, are available as properties of the application root level object, and so need not have any object dereference in order to use them in your script. If you're not sure how to find the window object you need, try looking each of the above objects up in the App Developer Reference manual, as properties of the application object, and read their documentation. In addition to what each points to directly, all of them have properties which are a windows object; the windows object has many methods which help you "filter", or search for, the window objects which you need.


Example 1

Suppose you open NotePad. You make sure the format is not set to wrap lines under the format menu, and then you make sure the status bar is visible under the view menu. When you try to read the status bar, which is showing you which line and column you are positioned at, you find that WindowEyes doesn't speak anything. (This is not the case at all, but just an excuse for when you might need an app, or at least a scripting example!). There must be something non-standard about the NotePad status bar right? however, this is a perfect application for a script, and so you decide to write one, which will read the status bar for NotePad when you press a hotkey.

First, you should use some tool such as TreeView, to examine the NotePad windows, when NotePad is running. When you do this, you will see NotePad has 4 child windows (all of which will be listed in it's .children property, and most of which will be listed in it's .directChildren property), and one of them has a name indicating that it is a status bar. It's name is msctls_statusbar32 and it's classname is the same. These properties are usually unique to each window, and so are a very good way to search for a particular window. (Sometimes the title is unique and unchanging, and so it's also a good way to search, but as in this case, often the title can be blank). That's really convenient for our example; it means it's likely this window only contains the text of the status bar, and so if we can find this window, all we need to do is to speak it's contents in order to hear the status bar. I believe other script tools which examine windows will actually show you each window's text contents, and you could verify with those tools that this window indeed contains only status bar information.

In this example we won't go through the entire script; we'll assume you have the hotkey portion working, and we'll only write a subroutine which finds, and speaks, the NotePad status bar.

You should understand about program-specific apps. Your app will be defined so that it is specifically tied to NotePad, and so, it will only be run when NotePad is started, and it will be shutdown when NotePad is shutdown. Furthermore, your hotkey will be defined so that it can only fire when NotePad is the active program (or application). Therefore, this subroutine we are writing can assume that the activeWindow object must point to a NotePad window. It may not necessarily be pointing to the main NotePad window, so we will need to use a property of the activeWindow named overlap, which does point to the main window for the window object being used (activeWindow in this case). Since the status bar window is a child of the main NotePad window, we need to use one of the properties of a window object which hold a list of all it's children; these properties are of type windows, and they have methods which help you find a particular child window.

When you examine the window object, you'll see a property named "clips". This is a collection of all the strings of text in this particular window (see the main WE help file for a definition of the term "clip"). This is one way to examine the text of an application: find the correct window object which you need, and then use it's clips collection to examine all it's text, or find the particular clip you need, and examine it's text. In addition to the clips collection, which may be many individual portions of text, the clips object also has a property (clipsText) which will return all of the text of a window object at once. This may not be all of the text which is visible on the screen, it's only all of the text that this particular window object contains.

sub speakStatusBar()
dim objMainWindow
dim objStatusBarWindow
dim objResults

set objMainWindow = activeWindow.overlap
 set objResults = objMainWindow.children.filterByName("msctls_statusbar32")
if not objResults is nothing then
if objResults.count > 0 then
' found the window!
set objStatusBarWindow = objResults(1) ' this gets the first window of the collection which was returned by the search
' now speak it's contents
speak objStatusBarWindow.clips.clipstext
end if
end if

end sub


Example 2

Sometimes though the text you want to examine can best be specified by it's location on the screen; in particular, by it's location within the overlap window of the application. In this case, you may not want to use all the text in a window object, but just the text at a certain place on the display. The place is usually specified by screen point X and Y coordinants, or by 4 coordinants which define an enclosing rectangle (by defining the location of it's top, bottom, left, and right sides). This has the possibility to return to you text contained in multiple child windows, or in multiple clipps, which are physically adjacent to one another on the screen, but which may not be stored together in one easily accessed data structure. When this is what you need, you probably should use the text root level object, and it's line method, as in the example below.

In this example, the WECursor object's position is used to retrieve a line of text, from whatever window object the WECursor object is positioned within. (The mouse object could be used just as well for this example).

This line of text may be the result of text from more than one window object, and/or more than one clip:
 Note however that if you try this example from the immediate window, you may not get back the text you expect, since the immediate window itself could be "hiding" the window you are trying to work with. Also note that the text object doesn't give you access to the text of any hidden window.

dim sText 
dim oMonitorPosition, oText
Set oMonitorPosition = WECursor.Position
Set oText = Text ' we must make a copy of this object in order to work with it
sText = oText.Line(oMonitorPosition).ClipsText

Points for the Example Above

  • first, the position of the WE cursor is determined, and returned to the script as a screenPoint object. (note that both the WECursor and mouse objects can return a position object which is a screenPoint, but only one of them may be active at a time; you can check and/or set the activeCursorType property of the application object to insure that the desired cursor object is active). Also note that the cursor object (the insertion point) also has a position property, but it returns one of type windowPoint instead of screenPoint, so you will need to convert this to a screenPoint before passing it into the .line() method.
  • next, this position is passed into the line method of the text object, which returns a line of text (as represented by a clips collection), where the line is specified by the position passed in, and, the bounderies of the window containing the screenPoint (this is not obvious from reading the script, but is explained in the documentation of the line method of the text object). This prevents .line from giving you text which may have "bled over" from windows next to, or under, the one you are working with.
  • the line method actually returns a clips object, and one property of the clips object is the clipsText string, which contains all of the text in all of the clips, concatenated together into one long string. it's this string which is actually assigned to a variable in the script example.
  • A copy of the Text root level object is made, into the oText variable. This is done because some of the methods of the Text object keep track of data (such as the last position you used as input to one of it's methods),so that they can then implement functionality such as "previous line". This requires the object to be able to modify itself to store these values, and in such cases, if you don't make a copy of the object (which your script then owns), the copy of the object owned by WE will not allow your script to modify it; therefore, functionality such as "previous line" will not function properly, as the previous position will not be saved. The MSAAEventSource is another such object which may require you to make a copy of it in order to use it fully.


Using Shared Objects and the GWToolkit to Make Scripting Easier

A shared object is a library of properties and methods, each related set of which is stored in a single object definition. It's purpose is to provide code for commonly needed functions, which can be shared between scripts. At the time of the writing of this portion of the article, the 2 commonly used shared object collections are those in the GWToolKit (which is written by GW Micro and is part of the installation of WindowEyes), and the Homer shared object by Jamal Mazrui (which can be downloaded from script central).

The idea of shared objects is an extension of the hosted script capabilities of WindowEyes, and is not specific to the VBScript or JScript languages. The definitions of the objects which are shared however do depend upon the abilities in each language to define a class. (for VBScript, See the CLASS ... END CLASS command in the VBScript documentation for the details of what a class is capable of).

In short: a shared object defines procedures, functions, and/or one or more classes each with it's own set of properties and methods, and then makes these classes available for use by other scripts.

For example, In the app manager or the App menu, you may have noticed that the "help" dialog, along with it's "about" dialog and it's "hot key" dialog are quite similar from one app to the next. This is because almost every app makes use of a class from the GWToolKit (named standardHelpDialog) which is a shared object; and the methods and properties of this shared object are used to display these dialogs and the help information for an app. The documentation for what shared object classes are available from the GWToolKit can be accessed by going to the app manager, highlighting the GWToolKit, and pressing alt-H (or selecting it from the App menu) to open it's help information, and within this dialog, pressing alt-H again to open the help file. More information on use of the GW toolkit can be found in an article of the support knowledgebase at article # 1115.

Here are the examples from the documentation topic for the standardHelpDialog object, and they will be discussed below the code:

Example 1

' This example creates a help dialog without keyboard capture support
Set StandardHelpDialog = SharedObjects("com.GWMicro.GWToolkit.StandardHelpDialog").NewDialog
StandardHelpDialog.HelpTitle = "My Custom Help"
StandardHelpDialog.HelpText = "This is the text of my custom help information."
StandardHelpDialog.Show

Example 2

' This example creates a help dialog with keyboard capture support
Set StandardHelpDialog = SharedObjects("com.GWMicro.GWToolkit.StandardHelpDialog").NewDialog
StandardHelpDialog.INIFileName = "myINI.ini"
StandardHelpDialog.INISectionName = "config"
StandardHelpDialog.INIKeyName = "hotkey"
StandardHelpDialog.HelpTitle = "My Custom Help"
StandardHelpDialog.HelpText = "This is the text of my custom help information."
StandardHelpDialog.Show

Example 3

' This example creates a hotkey manager button in the Script Help dialog when using
the StandardHelpDialog object and the HotkeyManager object.
' first create the hotKeyManager dialog object and initialize it
Set HotkeyManager = SharedObjects("com.GWMicro.GWToolkit.HotkeyManager").NewDialog
HotkeyManager.INIFileName = "myINI.ini"
HotkeyManager.INISectionName = "Hotkeys"
Set HotkeyManager.KeyStrings = Strings("myXMLFile.xml")
' now create the standardHelpDialog object and initialize it, and finally display it
Set StandardHelpDialog = SharedObjects("com.GWMicro.GWToolkit.StandardHelpDialog").NewDialog
StandardHelpDialog.INIFileName = "myINI.ini"
StandardHelpDialog.INISectionName = "hotkeys"
Set StandardHelpDialog.UseHotkeyManager = HotkeyManager
StandardHelpDialog.HelpTitle = "My Custom Help"
StandardHelpDialog.HelpText = "This is the text of my custom help information."
StandardHelpDialog.ShowHelp

Points for the Examples Above

  • The line "Set StandardHelpDialog = SharedObjects("com.GWMicro.GWToolkit.StandardHelpDialog").NewDialog"

actually does two distinct operations, which may be clearer if they were separated into two commands:

Set shClass = SharedObjects("com.GWMicro.GWToolkit.StandardHelpDialog")
set StandardHelpDialog  = shClass.NewDialog

The first command gets the class object, for the standardHelpDialog, from the sharedObjects collection. The second command invokes the newDialog method, which returns an object whose type is standardHelpDialog. this object is ready to be used now that it has been created by the newDialog method; before then, it was only a class definition.

  • An important point here is that the examples above, for clarity of the example, assume that the shared objects exist and are ready to be used. Actually, neither condition is a given. When your app goes to make use of a shared object, it may not be available because the user hasn't installed it, or because there's a problem with it's app, or because it's app has not finished initializing. Any shared object must be part of an app, and when WindowEyes is first starting, all the apps are trying to initialize. This may mean that any given shared object isn't quite done initializing at the time your app tries to make use of it.

In either case simply accessing the shared object as the examples above do may cause an error in your app (after a 30 second delay when your app is unresponsive).

This is because the default behavior when you access a shared object in the sharedObjects collection is to wait up to 30 seconds for it to become available, and then return "nothing" if it did not become available. If you simply sit there waiting on a particular shared object (for an unknown length of time), your app cannot be doing anything else, when in fact many apps can perform their primary function while waiting on a shared object such as the standardHelpDialog to become available. That is, they could if they had some way of doing so, and then trying to make use of the shared object only when it was available. Which, it turns out, is possible when using a more sophisticated method of accessing shared objects.

The recommended way of accessing a shared object is to make use of the sharedObjects object's "onStateChange" event. This event will be called for each shared object which is available, as it becomes available, or when it no longer is available. If they are available at the time your app "hooks" this event, it will be called for each available shared object, so that your app will know the objects are available for use. When you make use of the onStateChange event, you can then safely access the sharedObjectscollection for a given object, once you know it's available, without any worry that your app may pause for up to 30 seconds waiting on the object.

If the object's app should have an error and become unavailable, the event will inform your app that the object is no longer loaded, and you can limit the actions of your app, or stop it from executing altogether (after possibly notifying the user). If you do not make use of the onStateChange Event, you may find your app suddenly has an error when accessing a shared object which is no longer available. Using events in your app is detailed in the next section, but is essentially the same as the example above showing a hotkey "callback" function.


Having an event (such as an Action of Windows) Trigger an App

An event is very much like the example above showing how to have an app notified when a hotkey is pressed. In that example, a specific subroutine or function of your script is executed when the hotkey is pressed. Having your script respond to an "event" is much like having it respond to a hotkey; that is, you must setup a specific subroutine or function to be executed, and it's header must be defined exactly as shown in the documentation for the event, and finally you must "connect" this subroutine or function to the event so that it will be executed. (being "connected" is analogous to setting up a callback in the previous example).

Only objects can have events for you to connect to, therefore, the first part of doing this is to find the object type which hosts the event that you need. You do this by knowing what type of object that it is. The events for any object type are detailed in the WindowEyes object model documentation, but in their own subsection under each object, just as it's properties and it's methods are each listed in their own subsection.

As a very informal set of definitions: an object's properties contain information which you may use; an object's methods will execute some specific action (similar to running a subroutine or a function), usually related to that object, and an object's events will notify you when something specific happens involving that object by executing a specific subroutine or function in your script.

For instance: you may want to know when the window you are working with closes. When it does, you may wish to speak a message to the user informing them that it has closed. To do this your script would need to have a window object for the window under discussion. then, you would lookup in the object model to see what events are available for the window object type; if you find the type of event you need, you would then get the header definition from the documentation for a handler subroutine or function for it.

Example 1

Suppose you had written a script to run when Notepad runs; that is, you made it application specific, and you therefore can count on the activeWindow property pointing to a notepad window. Now suppose that you want to know when the main Notepad window closes. To do this, after checking the events for the window object, you see that you need to connect to the onClose event:

dim notepadWindow
dim npWinConnection
set notepadWindow = activeWindow.overlap
npWinConnection = connectEvent(notepadWindow, "onClose", "closing")
sub closing(windowHandle)
speak "notepad just closed, hope you saved everything!"
end sub

And the above code snipit does just this, by using the connectEvent() method (of the "Script" root level object) to "hook" or "connect" the "onClose" event of a specific window. Why did we store the result of the connectEvent() call in a variable? Because it can then be used at some time later to stop monitoring the onClose event for that window if we no longer wanted to do that. We would stop it with the command:

disconnect npWinConnection

If the window had closed, could we then hook some other event? No! It's quite likely that very soon after the window closes, the notepadWindow variable will no longer point to a valid window; and if it is empty, or not pointing at a valid object of type window, then you cannot hook any of it's events. For that matter, you cannot hook the onClose event until after the Set command which initializes the notepadWindow variable, because until then, the variable is empty.

Writing event handlers, as these particular subroutines or functions are called, takes some special care. There are often hidden rules or conditions which are not usually present in ordinary subroutines or functions. For example, many events are structured so that your subroutine or function can change the outcome of the event (which is still in progress when your routine is called). In such situations, it means either Windows or WindowEyes must wait on your subroutine or function to complete, before it can carry on completing the event. In these cases, you must setup your routine so that it executes very quickly; if you don't, you may cause WindowEyes or Windows to have unexpected delays, or to behave in unexpected ways which may cause an error.

How do you tell if this event is a type which causes WindowEyes to wait? Usually you can tell by examining the documentation. If it clearly states that you can change the outcome by returning a value as a function, or by setting one or more of the parameters, then you'll know for certain this is one of the special cases. Even if it requires you to write your handler as a function instead of a subroutine, then you should suspect that it is one of the special cases.

The onClose event above is not one of these cases; if it had been however, it could be that the window could not close until your routine completed. if you wrote your routine so that it took a long time to run, then the application might appear to hang when the user tried to close the window. So what is "a long time?" Are there commands which cannot be used because they take a "long time"? This is a hard question to answer. Usually you do not have to worry, unless you use some command which causes Windows or WindowEyes to do some sort of input or output operation, since those commands can take a very long time. Such commands including opening a window or dialog, speaking, reading from or writing to a file, running a program, just as a few examples.

Suppose you do not wish to effect the outcome, yet you wish to use one of the types of commands which may take a "long time", in a special case event handler; is there a way around this? Yes, there is. You can use the "queue" command, inside of the event handler, to start a subroutine running at a slightly later time, which will be where you can use the commands which may take a long time. The event handler can then return immediately, while the queued subroutine will run later, not holding it up.

Example 2

Suppose what you want to do is to write to an .ini file, the lasttime anything is spoken. In order to know when something is spoken, you will need to use the "onSpeak" event of the "Speech" object; however, this is one of the special cases where you can change the outcome of the event, so you cannot take a long time; therefore, you cannot write to the .ini file in this event handler. What you must do then is to make use of the "queue command to execute a subroutine, which will execute the write to the .ini file at a slightly later time:

dim c
c = connectEvent(speech, "onSpeak", "spoken")
sub spoken(theString)
spoken = vbNull ' tells WindowEyes there's no change in what's spoken
queue "logIt"
end sub
sub logIt
INIFile("myFile.ini").text("speech", "last spoken").text = now
end sub

The above example will execute a function, named "spoken", each time WindowEyes speaks. This function will return a value telling WindowEyes that it does not want to effect what is about to be spoken, and then it will "queue" a logging subroutine, which will execute some fraction of a second later, and write the time of the speech to the .ini file. Because it is executing later, it doesn't hold up the processing of the onSpeak event.


Example 3

You can't have too many examples of event handlers! This example shows the use of a window event which fires whenver any child window at any level is created; it also shows the use of a window searching method, which can search all existing windows by title.

The purpose of this app is to warn the user if Notepad opens the same file more than once (wich Notepad does allow to happen, and so it's very easy to have the same file in several different states of being edited).

option explicit
dim  n
' now use the window object's onChildCreate event, and do it on the desktop window itself, to look for new windows
n = connectEvent ( desktopWindow , "onChildCreate", "myNewWindowHandler")
' end of main body
sub myNewWindowHandler(win)
' this is the event handler for the onChildCreate event of the desktop window
' since we may say something, and we also need to pause slightly to allow the application to have time enough to set the window title, we have to use the queue method
queue "checkWin", win
end sub
sub checkWin(win)
dim objWins , objCurWin
sleep 500 ' give application half a second to initialize window
' below tests to see if we have a top level window
if win.style.overlapped or win.style.MDIChild then  
' it is an application top-level window
  if win.className = "Notepad" then
' it's a notepad document window
  set objWins = windows.filterByTitle(win.title) ' returns a collection of currently open windows with a given title from the root level property named Windows (which is a collection of all open windows)
  for each objCurWin in objWins
      if objCurWin.handle <> win.handle then
 ' this isn't the window being opened at the moment
        if objCurWin.className = "Notepad" then
' it's a notepad document window
          silence ' helps this warning message stand out verbally
          sleep 1000 ' pause one second after going quiet
          speak "Warning! " & win.title & " is opened more than once!"
        end if '  objCurWin.className =  = "Notepad"
      end if ' objCurWin.handle <> win.handle
    next
  end if '  win.className = "Notepad"
end if '  win.style.overlapped or win.style.MDIChild
end sub


Using the WE Script Framework Wizard to Help Develop Your Script

When you're ready to publish your script to the world, you may wonder how everyone achieves those consistent looking help dialogs for their scripts? and for that matter, how do they get an about box and a way to change the hotkeys in there? How do they get the description of what their script does into the script manager? How do they make it so that their script shuts down when WE shuts down; how do they arrange to use shared objects as the shared objects become available during the WE startup process?

These are all aspects of issues which all scripts should handle. Luckily, if you're programming in VBScript or JScript, these issues are almost identical from one script to the next (except for the details of your script), and so GW has written a script (named WE Script Framework Wizard, you will need to download and install it before you can use it) which helps you write a script. It doesn't by any means write an entire script for you , but it does take the above issues and many others, and asks you some questions, and when you're done answering it's questions, it will generate a partial script for you.

This partial script is sometimes called a "skeleton" or a "framework". it may be a complete script in that it will load and run without causing an error, but it won't do anything very useful; at least at first.

The idea is that once you have generated a skeleton to meet your needs, you will take the scripting code you have developed so far, and begin to insert it, at the right locations, in the skeleton script. You may have to edit some of the lines of code that were generated as part of the skeleton, but this can save you a lot of time you would otherwise spend in tediously reprogramming these same features into each of your scripts. Not only that, but for new scripters it's a great way to learn details of what a good script should have, and learn how to accomplish them.

Start by reading the help text of the WE Script Framework Wizard script, in the script manager. When you're ready, run the "scripting wizard" function it provides.

Then, read the generated script code from start to finish before you do anything to it. Decide where the code you have already written should best be inserted into the skeleton.


Using MSAA

While use of the window object and it's events can provide your app information as to what's happening with windows on the desktop, and their associated programs, it's MSAA which gives you information as to what's happening to the controls and other objects within a particular window, or associated with a particular process. This is done via the events of two important objects of the WE object model: MSAAEventSource, and MSAAEventBlock.

It's important to note that use of these objects can yield a flood of information, unless they are filtered in some way (such as limiting their reports to those associated with a particular process). While there exists a property for filtering the MSAAEventSource reports to a particular process, your app cannot simply assign a value to this property of the MSAAEventSource object. This is because this object isn't "owned" by your app, but by Window-Eyes itself. In order to get an MSAA object your app can modify (and so filter with) you will need to make a copy, into a global variable, of the MSAAEventSource object like so:

set myMSAA = MSAAEventSource
myMSAA.process = clientInformation.applicationProcess
myConection = connectEvent(myMSAA, "onObjectFocus", "myOnObjectFocus")

The example above sets up MSAA to filter events to those for the process which caused the app to begin running, and then sets up an event handler for the onObjectFocus MSAA event.

In the example above, the variable myMSAA must be global, or the filtering and the event handling will stop working as soon as the current sub/function has completed executing.

If you would like to know more about how to replace the way Window-Eyes handles a particular MSAA event with one of your own, see the wiki article Custom MSAA Event Handling.

Adding Dialogs With Menus and Controls to Your Script

See the wiki article User Interface Techniques.


This page was last modified on 8 May 2012, at 21:49.

This page has been accessed 39,558 times.


Text Size:
Decrease Text Size Increase Text Size

Personal tools

Powered by MediaWiki
Public Domain
© 2013 GW Micro, Inc. All Rights Reserved.
GW Micro, Inc.    725 Airport North Office Park    Fort Wayne, IN 46825
Ph: 260-489-3671 Fax: 260-489-2608    www.gwmicro.com    sales@gwmicro.com    support@gwmicro.com
Hours: M-F, 8a-5p, EDST