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 (92KB) |
Dino Esposito |
Figure 1: Dynamic HTML Browser |
The Shell Namespace
If you want Windows 95 and Windows NT 4.0 to treat your documents in a special fashion, you have one option: write some extensions to the system's shell. This basically consists of implementing a handful of COM interfaces that Windows Explorer will call back when accomplishing a given task for a given kind of document.
You can write a namespace extension to enrich the collection of shell objects. Accessories, Recycle Bin, Control Panel, and My Briefcase are all symbols held by the shell and which form its namespace. The root of this domain is the Desktop object. Directories and files are no longer the only components of the system; they still exist, but Windows 95 and Windows NT 4.0 integrate them as part of a more generalized layout. Any object that's part of the shell namespace can be viewed and accessed through Windows Explorer, which serves as the shell manager as well as the program you use to browse the file system. It's always active and working behind the scenes, though you can't see it.
Windows Explorer handles objects by asking them to do everything needed: showing the content, enumerating the items, refreshing the window, responding to users clicking on menus and toolbars, drag and drop, and so on. Windows Explorer is responsible for the results of what other pieces of software do. Keep in mind that the Windows shell doesn't know (and never learns) how to handle your documents. It includes only a minimal set of common functionality. If you want to improve the shell support for a certain kind of document, then it's up to you to provide the module to do it. This new module must provide certain standard COM interfaces for the connection to take place.
Namespace Extensions
You can customize the shell by adding new symbols, which is accomplished by creating special folders as Internet Explorer does in Figure 2. In the Temporary Internet Files folder, the list of the files downloaded from the net appears as if it were a regular directory. Instead, looking at the actual content of the Windows\Temporary Internet Files folder, you'll see four hidden directories whose files Windows Explorer interprets and presents in a customized manner.
Figure 2: Custom Folders |
A new symbol may or may not be fully integrated into the Windows Explorer frame. If it is integrated, you can browse it as you would any other standard folder (like a directory). Such extensions are called nonrooted and display in Explorer's right-hand pane. Nonrooted means the extension is part of the existing hierarchy of objects. Conversely, rooted extensions require another instance of Windows Explorer to be started from a specific root path. Your view is limited to the subtree under the root path and you can't go up to a higher level. Furthermore, rooted extensions will always create a new window on the desktop. If the root is a file name (like the sample discussed here) the extension is nearly identical to an external viewer except it runs in the same process space as the Windows Explorer.
For example, these lines will launch CABVIEW (an example of a namespace extension included with the Win32® SDK) on the mini.cab file:
|
Whether you make your extension rooted or not depends upon what you want to do with it and the data you want to integrate with the shell. A wise (but not strict) rule to keep in mind is to create a nonrooted object if it represents a collection of something and would be an expandable node in a hierarchical structure. Otherwise, if you just want to offer a customized view of single objects (without running external apps), then the rooted approach might be better. In this case, the root path will be the file or directory itself. |
Figure 3: Custom Nonrooted Extension |
Figure 3 shows how a custom, nonrooted extension will appear. Note the modified toolbar, which sets up a new button with the Internet Explorer 4.0 icon. Now you can open up the MIND homepage more easily than ever! From the coder's perspective there are no differences between rooted and nonrooted extensions; they are mainly distinguished by the information you save in the registry. For a better understanding of the fundamentals of namespace extensions, I recommend that you read "Extending the Windows Explorer with Name Space Extensions," by David Campbell (MSJ, July 1996). You will learn, for example, that extending the way Windows Explorer works on a file requires a rooted extension because the current shell doesn't support browsing into file content. This feature is currently planned as part of Windows 98 (codenamed Memphis).
Shell Extensions
Writing a Namespace Extension
|
Figure 4: Classes and Interfaces |
The core of the extension is the CHtmlFolder class, which inherits from both IShellFolder and IPersistFolder. Figure 5 shows a complete list of methods you might want to implement. Not all of them are strictly necessary, except in a fairly complex extension. The checkmarks indicate which methods are usually worth it in a nontrivial implementation. The IPersistFolder interface is intended to provide a couple of entry points during the folder initialization. The Initialize function gets called when Windows Explorer is creating the folder object. It receives a PIDL (an identifier representing a relative path to the parent folder). This value, whose type is ITEMIDLIST, is guaranteed to be unique and meaningful only within the parent's domain. File names are a special case of PIDLs. To convert PIDLs into readable path names, call: |
|
The namespace extension that is under construction here (htmlview.dll) is designed to affect single HTML file names. The line shown above, excerpted from IPersistFolder::Initialize, saves the document name into a CHtmlFolder custom data member I'll be using later. If you look closely at the source code for the CHtmlFolder class (see htmlfold.cpp), you'll notice that many IShellFolder methods aren't implemented and return the E_NOTIMPL error code. Don't let COM programming scare you: there is rarely the need to write a lot of code for all the methods exposed by an interface. IShellFolder provides Windows Explorer with all it needs to integrate your documents into its frame window and menus. It's responsible for the creation of more specialized objects that will take care of item enumeration, displaying content, setting and retrieving attributes, plus the other UI stuff I mentioned earlier.
View and Enumeration
When CreateViewWindow gets called, you can add buttons to the toolbar via the usual messages and structures. To send messages to the browser's UI objects you would use IShellBrowser::SendControlMsg, specifying which UI object you want: |
|
As shown above, you have to specify the UI object, add your own bitmaps to the toolbar, and assign their progressive index to the custom buttons. The folder's menu is a resource shared between Windows Explorer and any namespace extension. You must create a new menu and pass it to the Windows Explorer via IShellBrowser::InsertMenuSB to let it populate as needed. Then you can add your own customization using the common API calls. Be sure to identify the submenus (File, Edit, View, Tools, Help) by ID instead of by position numbers (which might change), and assign your items IDs in the recommended range. If you fail to heed this advice, your code may work in the short run but not in future versions of the shell. Not surprisingly, the Windows Explorer installed by Active Desktop has a new Go menu right in the middle that will break noncompliant menu code. When you're finished modifying the menu, set it via IShellBrowser::SetMenuSB. Status messages to follow the same rules as in ordinary apps. You can show status text with SendControlMsg and the FCW_STATUSBAR flag, but don't forget to first turn your string into wide chars. The complete source code for the view object is in htmlview.cpp. After setting up the view, an instance of a new class is created to fill out the treeview control. |
|
It adds two main nodes: File Information and HTML Contents. The former subtree contains system information about the file like name, size, and creation date.
Working with the Registry
|
|
ShellExecute is a Win32 API call that lets you spawn executables as well as document files. While launching an EXE requires just a call into CreateProcess, things get more complicated for documents. The pszCmd argument takes a string such as Open, Print, or Explore and represents the actions you can take over the document. Each verb refers to a certain path in the registry where a command string is stored. One of these verbs is assigned the default attribute. Windows Explorer retrieves this string and passes it up to ShellExecute. What happens with HTML files when you have Internet Explorer 4.0 installed? Suppose you click on myfile.htm. Usually, the default verb is Open, which displays in bold on the document's context menu. These settings are saved in the registry, as shown in Figure 8. When a verb is executed, the command line shown in the right pane will execute, using the actual file name. You can add custom commands too, and have them appear on the context menu. |
Figure 8: HTML Registry Commands |
I added a new DHTML verb with a descriptive name of View Dynamic Content. It refers to the following line for execution: |
|
{CLSID} is the actual CLSID of the extension. If you set DHTML as the default item, the pseudocode seen above will become the following: |
|
The actual system command, instead, will be : |
|
How is the connection between code and HTML files established? Open the Registry Editor under HKEY_CLASSES_ROOT and search for the ".htm" node. It should exist if you have any program registered to open HTML files. The entry's default value points to another registry path, under which you'll find a \Shell subtree with command strings. For Internet Explorer, this entry is named htmlfile (see Figure 8). This value might be different if you're using Netscape Navigator or some other browser as your default.
A C++ Class for Dynamic HTML
|
|
The m_pMSHTML member is more intuitively expressed as the Document property of the WebBrowser control. The Load method puts the server to work on a specific HTML document. Once the file name is converted into wide characters, you call IPersistMoniker or IPersistFile to read it in. The proper interface to use is a distinction made on the first five bytes of the name. If it begins with "http:", use URL monikers. Otherwise, the simpler IPersistFile::Load would suffice (see Figure 10). At this point, I thought I was finished with my program. After loading successfully, the code attempted to access the Internet Explorer 4.0 object model to save pointers to the collections of links, tags, images and so on. Initially, it worked fine. Then things began to go bad. I did all my testing with a very simple HTML page, but as soon as I chose a more complicated one, everything crashed. After a bit of investigation, I figured out the cause of the problem, thanks to the WalkAll sample in the Internet Client SDK. It isn't safe for your software to access anything through the Internet Explorer 4.0 object model before it's ready. The Internet Explorer 4.0 document object raises a specific event when the initialization stage ends, so you can reliably make your calls. This translates into three more steps:
IPropertyNotifySink is intended to give your software (as a sink object) the opportunity to react to property value changes. Each time a property is about to modify its content, the object sends notifications by invoking OnRequestEdit and OnChanged. OnRequestEdit gives the sink a chance to either allow or reject the change. OnChanged arrives only after a new value has been set. The property to hook on is ReadyState. When OnChanged gets called, it is passed the DISPID of the involved property. |
|
The code shows how to get the current value of a given property. V_I4 extracts the lVal internal field from a VARIANT variable and returns a 32-bit value. Next, compare the ready state against all the feasible constants and post an internal message to the current thread: |
|
A sort of synchronization between the document server and your module is absolutely necessary. I adopted the same solution as in the WalkAll sample mentioned earlier. Using a Win32 sync object like an event should work as well. CDynHtml::Load has a message loop governed by GetMessage and DispatchMessage (see Figure 10). The module ends up here just after loading the file into memory and exits only when WM_EX_DOCUMENTREADY arrives. (The message is user-defined in DynHtml.h.) The window handle should be set to null. This effect is due to PostThreadMessage and confirms that the message you examine is exactly the one you were waiting for. At this point, accessing the Internet Explorer 4.0 object model should be absolutely safe.
Walking the Document Object Model
|
|
OK, so I've obtained the pColl reference. Now I need a way to walk it, item by item. When iterating on a single element I want to analyze its information and display it in a fashion that may vary quite a bit. One possible approach is to define some EnumXxx functions, nearly identical to the SDK's EnumWindows, EnumFonts, and so on. In CDynHtml I add EnumLinks, EnumObjects, EnumScripts, EnumApplets, and EnumImages. Each accepts a pointer to a specific callback function and a 32-bit buffer for custom data. Figure 9 shows the declarations. All the enumerators have a common skeleton. They scan the list represented by the IHTMLElementCollection pointer and pass the nth field down to the callback. Getting the nth item of a collection demonstrates all the complexity and the slowness of low-level COM programming, especially when compared to the same Visual Basic code. Such collections export the item function, which requires a couple of VARIANTs and an IDispatch pointer. While the first VARIANT takes the index of the item, the second seems to have no function. |
|
The pElem variable is what the callback functions will receive. This fragment represents what happens when executing the last line of the previous Visual Basic code. CDynHtml offers a few other benefits. For example, it has a GetTitle member that returns the title of the document as an ANSI string. GetSource reads the HTML source code, minus the header. If you want to know how many items form a given collection, just call GetNumOf specifying an ID for each supported list. This implementation of CDynHtml recognizes only links, applets, controls, scripts, and images.
Putting it All Together
|
figure 11: HTML Content Treeview |
When the EnumXxx functions get called to complete the treeview, they receive a pointer to a given interface. Each callback can make a different use of it, according to the information it is expected to provide. MyLinkProc and MyImageProc, for example, simply call get_href to return the name of the object referenced, whether it's a mail address, Web site, or GIF file name. MyObjectProc extracts the CLASSID attribute of the ActiveX control and reads the description from the registry. MyAppletProc does the same with the Name attribute. As for scripts, I assume they always begin with <!-- and have the proc name enclosed in a couple of CR/LF characters. I also assume I'll find a LANGUAGE attribute. This is often true, though a more precise algorithm is welcome.
The New Explorer
Summary
|
|
and swap the default item for context menus from Open to View Dynamic Content (see Figure 12). |
figure 12: Custom Context Menu |
This article is based on Internet Explorer 4.0 Platform Preview 2. It is possible that some elements might have changed slightly upon the final release of Internet Explorer 4.0. |