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.
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 script development in the form of scripts on GW's "Script Central" which are in the category of "script development".
If you want a development environment that will give you a lot of help in writing your scripts, and if you own MS Word, check out the "MS Office" script, 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, regardlessf your development environment, check out the "Help Context" scrip from GW Micro, which gives you context-sensitive help for the WE object model. And finally, see the topic below on the "WEScriptFramework" from GW Micro, which provides you a way to get the basic part of your script automatically generated for you.
Having Hotkeys Trigger a Script
Virtually all scripts have this structure in common: they start running atsome 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, WE is unable to stop it or shut it down. 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 a script is not constantly executing, but is usually waiting for something to happen, is not one which is obvious to the new scripter.
Having a script wait for the user to press a particular hotkey is a very common way for a script to enter this wait state. It relies on capabilities of the WE object model, which allow the script to tell WE what keystroke is the hotkey, and which of the scripts 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 script.
When you have your script 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 script respond to a hotkey is the registerHotKey method of the keyboard object. once you know this, you go to the scripting help file 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 scripting help file:
' 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.
How Do I Get to a Property that I Can See in the Script Help File?
This is a common frustration. 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 script help file, you'll see one named "brailleTable", and that it has a property named "name", which you then read the help topic for. So, you decide that you probably 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 documentation 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 is both the type of object which contains this object, and the name of it's property or method. In this case, the help topic says:
A BrailleTable object can be obtained from a BrailleTables object's Active property, or Item method.
So 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 onthe 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:
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.
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 descretion 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 in XML text.
There is another use that XML files are often used for, because it is supported by WE to help make it easier, and that is to store different versions of all the possible messages and text a script might display or speak, in various languages. WE supports this by automatically selecting the appropriate language version of the text messages, based on the language that Windows is set to use. This is done without the script programmer 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 a Script to Examine or Manipulate the Data in the Window of Another Program
Writing a script 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 structure (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. This is very helpful to know, since it means when you are starting to look for a given window, you can 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 out it's 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.
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.
How Do 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 should install all of these scripts, 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 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, Focused Control, and Harvest Window from Jamal Mazrui, Focused Window Detective and MSAA Detective from Vic Beckley, and WE Event from GW Micro.
How Do You Locate the Object You Need to Work With 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
- focusedWindow object
- windows object collection
All of the 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 up in the properties of the application object, and read their documentation.
When you examine the window object, which all of the above objects return, 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 another 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. In order to get all of the text visible on the screen, you will need the overlap window object for the application.
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. This line of text may be the result of text from more than one window object, and/or more than one clip:
dim sText dim oMonitorPosition, oText Set oMonitorPosition = WECursor.Position Set oText = Text sText = oText.Line(oMonitorPosition).ClipsText
Points of note about this script example:
- first, the position of the WE cursor is determined, and returned to the script as a screenPoint object.
- 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).
- 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.
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 comes as 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 a part of 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. See the CLASS ... END CLASS command in any VBScript documentation for the details of what a class is capable of.
In short: a shared object defines 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 script manager you may have noticed that the script "help" dialog, along with it's "about" dialog and it's "hot key" dialog, are quite similar from one script to the next. This is because almost every script 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 a script. The documentation for what shared object classes are available from the GWToolKit can be access by going to the script manager, highlighting the GWToolKit, and pressing alt-H to open it's help information, and within this dialog, pressing alt-H again to open the help file. Here are the examples from the documentation topic for the standardHelpDialog class, and they will be discussed below the code:
' 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
' 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
' 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 script 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 script, or because it's script has not finished initializing. Any shared object must be part of a script, and when WindowEyes is first starting, all the scripts are trying to initialize. This may mean that any given shared object isn't quite done initializing at the time your script 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 script (after a significant delay), or, you may just get the significant delay. 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. You are not given any indication if the object is available, and initialization is especially slow, or if it's not installed. If it were installed, then you could possibly wait for it to finish initialization;
but you don't know how long to wait; and, if you simply sit there waiting on a particular shared object (for an unknown length of time), your script cannot be doing anything else, when in fact many scripts 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 exactly the case.
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, as it becomes available, or, when it no longer is available. If they are available at the time your script "hooks" this event, it will be called for each available shared object, so that your script 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 script may pause for up to 30 seconds waiting on the object. If the object's script should have an error and become unavailable, the event will inform your script that the object is no longer loaded, and you can limit the actions of your script, 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 script suddenly has an error when accessing a shared object which is no longer available. Using events in your script are detailed in the next section.
Having an event (such as an Action of Windows) Trigger a Script
Being Able to Localize a Script Using XML
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 to Obtain Information as to What Another Program is Doing
See the wiki article Custom MSAA Event Handling.
Adding Dialogs With Menus and Controls to Your Script
See the wiki article User Interface Techniques.