This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
cutting@microsoft.com Download the code (39KB) |
Dino Esposito |
A Quick Tour of WSH
WSH, now supported on
all Windows platforms but Windows CE, is an ActiveX® scripting engine hosted by
a Win32® executable called wscript.exe. This program is the default handler for .vbs
and .js files. To automate a task, just take the objects that perform it and arrange the calls using one of these high-level scripting languages. The concept is similar to batch files,
but far more powerful.
What makes WSH particularly attractive is its full support for COM objects. In addition, WSH includes its own object model with built-in objects for many commonly performed tasks. These objects can create shortcuts, access network nodes, read from and write to the registry, display popup messages, and so on.
In addition, WSH provides a generic collection object and some specific collections, such as the Arguments collection. This saves you from boring and repetitive coding when you need to extract and use parameters that the user wants your script code to process. You can, for example, have a VBScript file do work on some external data. With the old-style batch files, this was handled using the familiar %1, %2 syntax to get the nth argument. As you'll probably agree, the following approach is a large step forward:
If WScript.Arguments.Length >0 Then
shell.Popup WScript.Arguments.Item(0)
Else
shell.Popup "No argument specified."
End If
Overall, the native WSH object model is well designed and offers a reasonable set of built-in objects. For a WSH primer, look at the June 1998 Cutting Edge column, my upcoming Shell Programming book, or the July/August 1998 issue of MSDN News, where you'll find a useful two-page diagram of the whole object model.
Why Have a User Interface in WSH?
When you talk about the UI, normally you mean two things: interactive dialogs and shell support. By definition, a batch file runs without user interaction (in batch mode). But this is an outdated definition that doesn't apply to a graphical environment like Windows. Today, batch file are a way to automate tasks, regardless of whether those tasks are interactive. To avoid confusion, they're called macros instead of batch files. A systemwide task may require some input from the user, and this input cannot always be obtained through the usual prompt or InputBox script commands. You need a more general and flexible approach for asking users to enter data.
Another area of UI support in WSH relates to the hosting systemWindows 95, Windows 98, or Windows NT® 4.0 and higher. All these operating systems offer a programmable shell with features like context menus and drag and drop support. Why not add some kind of drag and drop support for VBScript and JScript files?
Suppose you have a script file that takes one or more file names as an argument. Wouldn't it be nice if you could select those files from the source folder, drop them onto your VBScript or JScript file, and have the script process them as normal command-line arguments? This is a shell feature that is not hard to write, though it applies more directly to document classes like .vbs or .js than to WSH itself. However, it makes interacting with WSH a lot easier.
Now let's see how to implement these features. In this column I will show you how to develop a COM object to extend WSH with a dialog-provider component, and create a DropHandler shell extension to pass the dropped files to the target .vbs or .js file.
A window.showModalDialog Replacement
If you're writing script code that will be used in HTML pages, you can exploit a method of the Microsoft Internet Explorer 4.0 window object called showModalDialog to display modal dialogs. Here is the code for an HTML page that causes a dialog like Figure 1 to appear.
<html>
<script>
function init() {
window.showModalDialog( "shortcut.htm" );
}
</script>
<body onload="init()">
</body>
</html>
When you load this HTML code, a modal window will pop up; it'll show the content of the specified HTML page as the user interface of the dialog. This technique is available under Win32 through the ShowHTMLDialog function exported by mshtml.dll, and documented in the Internet Client SDK (now part of MSDN). An insightful example of its use can be found in the samples\htmldlg subdirectory of the Internet Client SDK installation path. It's likely that the window.showModalDialog is implemented in the same manner as ShowHTMLDialog.
Figure: Displaying a Modal Dialog |
But if you need this functionality from within a WSH module where you can't access the Internet Explorer 4.0 window object, you must take a different approach. You'll need to extend WSH by writing a COM automation server.
In any programming scenario, you use dialogs when you want the user to return some kind of input. Commonly, you arrange some interactive controls (edit boxes, listboxes, checkboxes), give them some default values, and display the dialog. The user then interacts with it, possibly changing some of the default values, and closes the dialog. The modified controls should be able to return their values to the caller. In any Windows-based development environment, from Visual Studio® to Delphi, there are tools (resource editors) that let you graphically design the appearance of the dialog. The controls in a dialog are glued together with code. Moving all this logic into the WSH context requires you to handle more than script files. You need a standard file to describe the look of the dialog as well as a file to store the code that relates to the dialog. Plus, you need a way to import and export values from the dialog. The easiest way to wed controls with the code to handle them in a single file is through HTML.
Designing an HTML-based Dialog Provider
Figure 2 shows a piece of pseudocode that illustrates how this would work. The COM server will create a window that hosts a WebBrowser control to display the HTML page. Next, it will store a pointer to the browser's document object, and use this to programmatically access each of the page elements. If you compare this approach to traditional Win32 programming, you'll notice that there are few differences. Basically, there is an object reference instead of an HWND, and a DHTML string ID instead of a numeric window ID. |
Figure 3: Dialog-provider COM Server |
Of course, I assume that the author of the script is also the author of the page, so he or she will know each detail of the page and can decide on which IDs to use. The HTML page that is used as the dialog template will also contain the script code needed to integrate the various elements of the page. Figure 3 shows how the COM server works, and Figure 4 describes its programming interface. I've chosen to write the COM server with Visual Basic 5.0.
Behind the automation server whose ProgID is WshUI2.HtmlDialog, there's a form called frmWshUI that's actually created and displayed with the Create and Show methods. The source code for the server is shown in Figure 5; Figure 6 gives an overview of the Visual Basic project. |
Figure 6: Server Project |
The form frmWshUI is the core of the COM server. It embeds a WebBrowser control, a push button, and a status bar. It also exposes a handful of custom attributes as outlined in Figure 7. SetHtmlPage is the COM object method used to shield itself from the detailed structure of the form. This method causes the WebBrowser control to navigate to the specified page, namely the dialog template.
More importantly, the execution stops after this and waits for the document to load completely. Notice that at least in Visual Basic (and whenever you're working within a single thread), the entire module may hang if you use synchronization objects such as events to wait for the document. A loop like this |
g_bIsReady = False
While Not g_bIsReady
DoEvents
Wend
is less exciting, but it works fine. The boolean guard is set to True within the WebBrowser's DocumentComplete event. When SetHtmlPage returns, the COM object is ready to access the document object model (DOM) to initialize controls. The following code is used to implement the object's SetDlgItemText method:
Public Sub SetDlgItemText(ByVal sID As String, ByVal sText As String)
On Error Resume Next
g_doc.All(sID).innerText = sText
End Sub
I also defined similar methods to get and set special
HTML tags such as <IMG> and <A>. In this way, you can decide programmatically how certain page elements look
at runtime. An HTML page is not limited to tags; you can use ActiveX controls and scriptlets too. In this case, you need only assign them an ID. The rest is done by the server's GetObject function:
Public Function GetObject(ByVal sID As String) As Object
On Error Resume Next
Set GetObject = g_doc.All(sID)
End Function
This returns to the script code a reference to the object that you can use to set properties and invoke methods. This works fine with VBScript. Figure 8 shows the complete source code for frmWshUI.
A Sample HTML Dialog
Let's see how all this operates in practice. Suppose you
have the HTML template whose code is shown in Figure 9; this produces the screen shown in Figure 10. Now run the VBScript file shown in Figure 11.
Figure 10: Sample HTML Dialog |
The result is a dialog with a customized logo, text, and customized default input values (see Figure 12). The page itself is enhanced with cosmetic
features like a fading logo and edit boxes that change colors when they get the focus. You can see this if you compare Figures 10 and 12.
|
figure 12: The Dialog in Action |
There's another interesting feature here. We already mentioned a certain hidden ActiveX control. If you run the VBScript that creates the dialog shown in Figure 10, you will notice a tip attribute on some elements. Move the mouse over one of the input boxes and look at the status bar. Descriptive text will appear. This may not sound particularly exciting, but notice how it's done.
You have a Visual Basic form that hosts a WebBrowser control, which in turn hosts an HTML page. The Visual Basic form catches events directly from the DHTML page. How is it done? I described the technique in the article "Gone Fishin': Hooking the Internet Explorer 4.0 Object Model" in the December 1997 issue of MIND. I wrote an ActiveX control and optimized it to run within Internet Explorer (see Figure 13 The control is inserted at runtime through DHTML: |
Set doc = wb.Document
doc.body.insertAdjacentHTML "AfterBegin", STR_HTMLSPY
where wb is the name of the frmWshUI's WebBrowser control and STR_HTMLSPY is a string like this:
<object id=hook style='display:none'
classid='clsid:4939263D-4D80-11D2-BC00-AC6805C10E27'> </object>
Depending on your security settings, Internet Explorer might prompt you with a message about the control's safety when the page runs. Since this control is absolutely harmless, I've provided a .reg file to register it as safe for initialization and scripting. You can find it in the source code available here. This control grabs the DHTML events via a variable of type HTMLDocument with the WithEvents qualifier.
Private WithEvents g_document As HTMLDocument
•••
Set g_window = UserControl.Parent.Script
Set g_document = g_window.document
Still missing is a way to let the form know about it. I decided to have the form export a DoCallback function with the following structure:
Private Sub DoCallback( _
ByVal evName As String, _
ByVal srcID As String, _
ByVal param As String _
)
If Len(param) Then
sb.Caption = param
End If
RaiseEvent GotHTMLEvent(evName, srcID)
End Sub
The ActiveX control calls back the DoCallback function of the parent form, specifying information about the event. It received the reference to the form object through a preliminary call to its Activate method (see Figure 8 and Figure 13). As a result, when you move the mouse over a DHTML element, its description appears on the Visual Basic status bar.
Comparing HTML Dialogs
Now you have a custom dialog provider. Let's briefly compare this technique with the ShowHTMLDialog function mentioned at the beginning of this column. You have a different dialog layout with a predefined OK button. This is important because it means that you should avoid OK or Submit buttons in your HTML code. Since you are designing dialogs for WSH and not for HTML pages, this is reasonable. A WSH application is meant to perform actions outside HTML, so what you really need from a modal dialog is just a way to collect data interactively. You can close the dialog by clicking an OK or Close button. The frmWshUI's RetVal property allows you to distinguish which type it is.
Another difference is that this dialog is fully resizable. At this stage, the source code doesn't support predefined sizes or automatic positioning for the dialog. However, such features aren't difficult to code.
The third difference is that you're using an alternative approach to set and get data. The ShowHTMLDialog relies on HTML script capabilities. It receives a list of arguments from the caller and makes them available to the page through the dialogArguments collection. In contrast, this one directly reads and writes the HTML tag, and does it straight from the WSH environment.
Rewriting a WSH Example
To demonstrate the power of this approach, let's rewrite one of the most typical WSH examples: creating a shortcut. Assume you want to create a shortcut to a file either by passing its name through the command line or by typing the name in the input box. Here's how to write a VBScript file that accounts for possible command-line arguments.
if WScript.Arguments.Length >0 Then
for i=0 to WScript.Arguments.Length-1
DoCreateShortcut( WScript.Arguments.Item(i) )
next
else
DoCreateShortcut( "" )
end if
WScript.Quit
The code iterates through the arguments and calls an internal routine to actually create the shortcut. This routine (see Figure 14 ) will look much like the code in Figure 11 . The file name passed toDoCreateShortcut will be used to initialize the input box for the shortcut's target path. The code is structured in such a way that multiple files passed as arguments will be treated in a cascade.
Dropping onto VBScript or JScript Files
Now that you have HTML-based modal windows for WSH modules, you can arrange complex but easy-to-use dialogs to ask the user for some input. But don't stop there. By virtue of WSH, both VBScript and JScript files may be treated like executables. As with any other Win32-based executable, you can pass it command-line arguments via shell drag and drop. For example, you can select a file or a folder name and then drag and drop it onto the VBScript file in Figure 14 to create a shortcut to it.
Dragging and dropping one file over another is a shell feature that requires a shell extension module that implements a couple of COM interfaces: IPersistFile and IDropTarget. IPersistFile is the interface that's informed of the drag, and that transmits the name of the target file to the rest of the module. In other words, when you drag a file onto a VBScript or JScript file name, the shell looks for a registered drop handler extension and queries for IPersistFile. Next, it calls IPersistFile::Load with the name of the file currently under the mouse. This is a good opportunity for your code to store the name of the target file as member data.
IDropTarget is the standard COM interface to deal with drag and drop from the drop side. By supporting this interface, you enable Explorer (the Windows shell) to ask you what to do when four different circumstances occur: DragEnter, DragOver, DragLeave, and Drop.
DragEnter indicates that the mouse just entered the target area during a drag and drop operation. A shell extension should verify the type of data being carried, and accept or reject the action. This result will affect, for example, the mouse cursor shape. DragOver is an event raised after DragEnter when the mouse is moving over the target area. A target area in the shell context may be the rectangle occupied by an icon if your current folder settings use large icons, or the small space of an item name in a report listview.
As the name suggests, DragLeave is called when the mouse is moved outside the target area. Normally, you don't implement this function. Finally, the most interesting function of the IDropTarget interface is Drop. The body of this function will execute whenever the drop takes place. Everything you might want to do as the result of a drag and drop operation should be put here.
Notice that a shell extension is always registered to work on files of a certain document class. A document class is identified by file extension, meaning that the shell extension will apply only to .vbs and .js files.
Figure 15 shows the ATL code for a drop handler extension. The Drop function takes as its first argument a pointer to an IDataObject interface that denotes what's been dropped. Other arguments are the status of the keyboard, the point where the files have been released, and the final effect of the drag and drop operation. This value will be returned to the caller module (in this case, Explorer) to complete the drag side of the process. For example, the source code might delete the files that have been copied from one folder to another in preparation for a move operation.
For VBScript and JScript drop handlers, the drag and drop operation always will end with a copy return code. In general, the operations allowed are move, copy, and link. In this specific case, a copy code is fine since you don't want any other operation to take place.
During shell drag and drop, files are moved or copied via an IDataObject object, which is a kind of generic wrapper for clipboard data. The shell provides file names stored in CF_HDROP format. To extract those names, you need to use the DragQueryFile SDK function. Once you have the names of the dropped files, you should figure out how to pass them down to target VBScript or JScript files.
In the meantime, you concatenate all the names in a single string where each file name is separated by a space:
for( INT i=0; i<iNumOfFiles; i++ ) {
CHAR s[MAX_PATH];
DragQueryFile( hdrop, i, s, MAX_PATH );
PathQuoteSpaces( s );
lstrcat( pszBuf, s );
lstrcat( pszBuf, " " );
}
DragFinish( hdrop );
Then you open the VBScript or JScript file using ShellExecute. This function is capable of retrieving data from the registry and running the executable linked to files with the .vbs or .js extensionin this case, wscript.exe. Also, pass the string with all the selected file names as an argument. The list of names is delivered to the VBScript or JScript file through the Arguments collection.
Writing Shell Extensions with ATL
A shell extension is an in-process COM module that implements a couple of predefined interfaces. The interfaces that are implemented depend on the flavor of shell extension you've chosen. ATL does a good job of shielding you from the IUnknown and IClassFactory details and lets you concentrate on the specific interface to support. In many cases, you need to write an IXxxImpl.h file with the declaration of your own C++ class that inherits from the COM interfaces. (Sometimes, in fact, you get lucky and the IXxxImpl.h file you need is already available.) Once this is done, the rest is a simple matter of implementing a class derived from the one defined in IXxxImpl.h.
If you simply concatenate the file names you get from the shell in the previous code snippet and pass the resulting string to a VBScript or JScript file, chances are you will run into trouble with long file names. Command-line arguments are separated by spaces. If the command line has a long file name that includes spaces, the number of total arguments will not be detected correctly. To work around this, surround the
file name in quotes. The Shell Lightweight API, distributed as shlwapi.dll with Active Desktop and Windows 98, exports a ready-to-use function called PathQuoteSpaces that does just this.
Notes on Source Code and Compatibility
The source code for this month includes a Visual Basic 5.0 project group comprised of an ActiveX DLL, an ActiveX control, and a toy demo to test them. You will also find an ATL project to build a DropHandler shell extension with Visual C++. Once you compile the DLL, you will need to register it using regsvr32.exe.
The final version of the code was developed under Windows 95 with Visual Basic 5.0 and Visual C++ 5.0. I've also run it under Windows NT 4.0 and compiled the projects with Visual C++ 6.0 with Visual Basic 5.0 SP3. I encountered some minor problems doing this. Visual Basic 5.0 often failed
while compiling and returned an internal compiler error.
This was especially likely to happen when compiling the ActiveX DLL project.
Since Visual Basic and Visual C++ share some compiler components, I guessed there was a compatibility problem. To test this suspicion, I attempted to use a status bar control found in comctl32.ocx. First, I got an error on the library. Then, it informed me that I had a newer version somewhere on the disk and asked me to upgrade. I did so, but then ran into problems when coming back to Windows 95 and Visual Basic 5.0 on a Visual Studio 97 machine. My advice is to avoid mixing Visual Studio 97 and Visual Studio 6.0 tools. Visual Basic and Visual C++ are not completely separate products. Visual Studio is a really integrated environment!
Summary
WSH is a new technology that has already met with great success. In this column, I've shown ways to enhance its basic capabilities with some user interface extensions. I added a COM object to provide modal, full-featured, HTML-based dialogs and a shell extension to add drag and drop capability to VBScript and JScript files.