November 1996
Previewing the Common Controls DLL for Microsoft Internet Exlorer 4.0, Part II
Strohm Armstrong
Strohm Armstrong is a support engineer at Microsoft specializing in user and shell technologies.
Click to open or copy the LISTVIEW project files.
Click to open or copy the VLISTVW project files.
In the first part of this series, I covered some of the new features of the common controls DLL that is part of Microsoft® Internet Explorer (IE) 4.0. I focused on the new features such as custom draw and the new controls such as the CoolBar and the Date/Time Picker.
This month I will talk about the enhancements to already existing controls. I will cover the enhanced ListView control, which can hold a seemingly infinite amount of items, the enhanced Header control, the enhanced Tab control, the enhanced ToolTip control, and the enhanced TrackBar control, to name a few. In addition, I will provide a couple of sample programs that show correct usage of the enhanced controls, and also use some of the brand new controls discussed in Part I.
Just as with the sample in Part I, you will need to get the new common controls DLL. (Just install IE 3.0 to get the IE 4.0 Common Controls DLL-yes, you read that right. The DLL is not redistributable, so downloading IE 3.0 is the only way to get it right now.)
Virtual ListViewThe ListView control now has a method called Virtual ListView that allows a large number of items to be contained in the control. The control is created like any other ListView, except you add the LVS_OWNERDATA style.
After you create the control, you tell it how many items will be in the control by sending it the LVM_SETITEMCOUNT message. When you send this message, you are telling the control how many items will appear to be in the control; you never actually insert any items into the control. This message takes an int for the wParam that is the number of items the control will contain. The lParam is zero or a combination of the flags that specify display options: LVSICF_NOINVALIDATEALL means the ListView control will not repaint unless affected items are currently in view and LVSICF_NOSCROLL means the ListView control will not change the scroll position when the item count changes.
From this point on, any item displayed in the ListView will do so on a callback basis. The control will send the LVN_GETDISPINFO notification for any item displayed. You process this notification just as you would a standard ListView control-by supplying the requested display information. The lParam for this notification is a pointer to an LV_DISPINFO structure. The mask member of this structure specifies the information the control is seeking.
Two notification messages are specific to the Virtual ListView. The first, LVN_ODFINDITEM, is sent when the control needs to find a particular item. Say, for instance, the user typed something while focus is in the control. The lParam for this notification is a pointer to an NMLVFINDITEM structure. This structure contains an NMHDR structure (hdr), an index at which to start the search (iStart), and an LVFINDINFO (formerly LV_FINDINFO) structure (lvfi). This information is supplied to enable you to find the requested item. You should return the index of the requested item, or -1 if the item was not found.
The second notification is LVN_ODCACHEHINT. This notification is sent when the ListView is about to ask for a range of items. The lParam for this notification is a pointer to an NMLVCACHEHINT structure. This structure contains an NMHDR structure (hdr), an int that indicates the beginning of the range (iFrom), and an int that indicates the end of the range of items (iTo). On this notification, you should load the specified items into your local cache if you are maintaining one. It is still possible to get an LVN_GET-DISPINFO for an uncached item, so your application must take this into account.
The VListVw SampleMy next sample application demonstrates the Virtual ListView (see Figures 1 and 2). This application displays a list of 100,000,000 items in a ListView. (The code is available from the sources listed on page 5.) Keep in mind that users will line up by the millions at the local software store demanding a refund if you have the gall to present them with a list containing 100,000,000 items, but I digress.
Figure 1 Vitual ListView Sample Application
During initialization, VListVw calls InitCommonControlsEx with dwICC of the INITCOMMONCONTROLSEX structure set to ICC_LISTVIEW_CLASSES to initialize the ListView class. In the CreateListView function, CreateWindowEx creates the ListView. Since I am creating a virtual ListView, the LVS_OWNERDATA style bit is added to the window styles for the control. The small and large image lists are then created and initialized using the Image List APIs. The image lists are assigned to the ListView by sending the control the LVM_SETIMAGELIST message once for the small icons and once for the large icons (as always, wParam indicates which size icon you are passing in). After the ListView is created, VListVw then calls InitListView, which sets up the column information using the LVM_INSERTCOLUMN message. InitListView then calls InsertListViewItems, which clears any items already in the list and then sends the LVM_SETITEMCOUNT message to the ListView to tell the control how many items it will contain.
When the user selects a different view, the application simply calls SwitchView with the desired view style. SwitchView uses GetWind-owLong with GWL_STYLE to retrieve the current style settings. The view bits are then cleared and the new view-style bit is set. SetWindowLong with GWL_STYLE is then used to set the new style in the control.
New ListView FeaturesItems in a ListView can now be indented. Item indenting is only supported in report view. The LVITEM structure is the same as the previous LV_ITEM structure with the addition of an int member (iIndent) that specifies how much to indent an item. Just set iIndent to the number of indent units you wish. An indent unit is the width of one item image. The item will be indented by iIndent*<image width> pixels.
The ListView also supports extended styles. As with the ComboBoxEx control, these extended styles should not be confused with extended window styles. Figure 3 lists the extended styles supported by the ListView. The extended styles are set by sending the LVM_SETEXTENDEDLISTVIEWSTYLES message to the control. The lParam for this message is a DWORD that contains the extended ListView styles to be set. To retrieve the extended ListView styles, send the control an LVM_GETEXTENDEDLISTVIEWSTYLES message. The return value of this message is a DWORD that contains the current extended ListView styles. All of the new ListView messages are explained in Figure 4.
The ListView SampleThe ListView sample demonstrates the extended ListView styles and item indenting (see Figures 5 and 6). There is even a custom draw option for good measure.
Figure 5 List View Sample Application
During initialization, the ListView sample calls InitCommonControlsEx with dwICC of the INITCOMMONCONTROLSEX structure set to ICC_LISTVIEW_CLASSES, which initializes the ListView class. In the CreateListView function, CreateWindowEx creates the ListView. The small and large image lists are then obtained from the shell using the SHGetFileInfo API. The image lists are assigned to the ListView by sending the control the LVM_SETIMAGELIST function once for the small icons and once for the large icons. After the ListView is created, the sample calls InitListView, which sets up the column information. InitListView then calls InsertListViewItems, which clears any items already in the list and then inserts one item for each entry in the system image list.
When the user selects a different view from the menu, the application calls SwitchView with the desired view style. SwitchView uses GetWindowLong with GWL_STYLE to retrieve the current style settings. The view bits are then cleared and the new view-style bit is set via SetWindowLong. When the user selects Indent from the menu, the current indenting value for each selected item is retrieved with the LVM_GETITEM message. The indent value is then incremented and the new indent value is set with the LVM_SETITEM message. The same thing happens when Unindent is selected, except the indent value is decremented.
When the user selects an extended ListView style from the menu, the program checks to see if that style is already set. If not, the program calls AddExStyle to add that extended style. If the style is already set, the program calls RemoveExStyle to remove the extended style. AddExStyle retrieves the extended ListView style by sending the LVM_GETEXTENDEDLISTVIEWSTYLE message to the control. The new style is added to the style flags and then the new styles are set by sending the LVM_SETEXTENDEDLISTVIEWSTYLE message to the control.
All WM_NOTIFY messages are processed through the ListViewNotify function in my sample. When custom draw is enabled, ListViewNotify will return CDRF_NOTIFYITEMDRAW to NM_CUSTOMDRAW with dwDrawStage set to CDDS_PREPAINT. This causes the ListView to send NM_CUSTOMDRAW with dwDrawStage set to CDDS_ ITEMPREPAINT for each item that needs to be painted. When the CDDS_ITEMPREPAINT notification is received, the item index is checked. If the index is odd, then the function will create a bold version of the existing ListView font and select this font into the HDC provided in the NMLVCUSTOMDRAW structure. The text color for these items will be set to red and the text background color will be set to white. For those odd items, the return value is a combination of the CDRF_NEWFONT and CDRF_NOTIFYPOSTPAINT flags. Returning CDRF_NEWFONT tells the control that the font was changed, so it will recalculate the text extents. The CDRF_NOTIFYPOSTPAINT flag tells the control to notify you when it has completed painting this item. This notification will be sent with dwDrawStage equal to CDDS_ITEMPOSTPAINT. When you receive the CDDS_ITEMPOSTPAINT notification, you get the currently selected font and delete it. If the item index is even, then only the default font will be used, the text color will be set to blue, and the text background color will be set to white. This will cause all of the odd items to be drawn with a bold font with a red text color and a white background color. All of the even items will be drawn with the default font, with a blue text color and a white background color. When you run the sample, notice how the default selection colors override the colors you set during the custom draw cycle. Unfortunately, you are unable to alter the color of selected items unless you go full-blown ownerdraw.
Enhanced Header ControlThe Header control now has many enhancements. These include drag and drop manipulation, item ordering, hot tracking, image list support, callback items, and the ability to place text, bitmaps, and images in a header item.
To obtain drag and drop manipulation of the Header items, you only have to specify the HDS_DRAGDROP style bit when creating the control. When this style is specified, the user can rearrange the Header items by dragging and dropping them with the mouse. This works in conjunction with item ordering, which allows you to specify or retrieve the order of the Header items.
To obtain the item-ordering information, send the control an HDM_GETORDERARRAY message. This message takes a pointer to an array of integers as the lParam and the number of array elements pointed to by the lParam in the wParam. To allocate the array, you should obtain the number of items from the Header using the HDM_GETITEMCOUNT message and then allocate an array of n integers. After sending the message, the array will contain the order of the items in the Header control. The order of the items is given as shown from left to right. For example, if item two was the left item, item zero was the next item, and item one was on the right, the array would contain item two at index zero, item zero at index one, and item one at index two.
You can set the item ordering by sending the HDM_SETORDERARRAY message. This message takes the same parameters as the HDM_GETORDERARRAY message. Before sending the message, you place the item indexes in the array as you want them to appear from left to right.
To obtain hot tracking in a Header, you only have to add the HDS_HOTTRACK style bit to the window styles of the control when it is created. This causes the Header to highlight the item the mouse is hovering over.
To use an image list with the Header control, you first assign the image list. This is done by sending the HDM_SETIMAGELIST message to the control. The lParam for this message is the handle to the image list to be set for the control. You can get the handle to the image list by sending the control the HDM_GETIMAGELIST message. Returned from this message is the handle to the image list. The HDITEM structure, which replaces the HD_ITEM structure, includes two new int members, iImage and iOrder. iImage is used to specify an image index that will be displayed with the item. Figure 7 shows details on the HDITEM structure.
When adding items to a Header control, you can now specify that the text or image or both will be provided on a callback basis. When filling in the HDITEM structure, you can specify LPSTR_TEXTCALLBACK for the pszText member and I_IMAGECALLBACK for the iImage member. The control will then send HDN_GETDISPINFO notifications to request the display information. The lParam for this notification is a pointer to an NMHDDISPINFO structure (see Figure 8).
Progress BarThe Progress Bar now supports full 32-bit ranges. To set the 32-bit range, you send the Progress Bar a PBM_SETRANGE32 message. The wParam for this message is a DWORD that specifies the low end of the range and the lParam is a DWORD that specifies the high end. This message returns a DWORD value that contains the previous 16-bit low limit in the low word and the previous 16-bit high limit in the high word. If the previous ranges were 32-bit values, the return value consists of the low words of both 32-bit limits. To retrieve the entire high and low 32-bit range, use the PBM_GETRANGE message, which takes a BOOL as the wParam that specifies the return value of the message. If wParam is FALSE, then the message returns the 32-bit high range. If wParam is TRUE, the message returns the 32-bit low range. The lParam is a pointer to a PBRANGE structure that receives both 32-bit high and low-range values. The PBRANGE structure consists of two integers, iLow and iHigh. iLow designates the low end of the Progress Bar range and iHigh designates the high end. If you are only interested in the return value from this message, you can pass NULL for the lParam.
Tab ControlThe Tab control now allows you to place the tabs along any side of the control. The control also supports hot tracking by adding the TCS_HOTTRACK style bit to the window styles of the control when it is created. Hot tracking tabs highlight the tab the mouse cursor is hovering over.
By default, the tabs are placed along the top of the control. To place them on the bottom of the control, add the TCS_BOTTOM style bit when the control is created. To place the tabs on the left side of the control, add the TCS_VERTICAL style bit when the control is created. And if you want the tabs on the right, add both TCS_VERTICAL and TCS_RIGHT styles at create time.
The Tab control also has a new feature that causes the unused rows of tabs to be displayed on the opposite side of the control. This feature is obtained by adding the TCS_SCROLLOPPOSITE style bit when the control is created. Figures 9 and 10 show how this style works.
Figure 9 Regular Tabs
Figure 10 New Tab Style
The TC_ITEM and TC_ITEMHEADER structures have been renamed to TCITEM and TCITEMHEADER to follow current naming conventions. To ensure backwards compatibility, the old structure names can still be used, but use TCITEM and TCITEMHEADER in your new projects.
TreeViewBesides the support for custom draw explained in the previous article, only one enhancement has been made to the TreeView control: a TreeView parent item can now be partially expanded. This has the effect of expanding the item and displaying the children, but leaving the + symbol in the button next to the parent. This is accomplished by adding the TVIS_EXPANDPARTIAL item style to the parent item. To support partial expanding, the TVE_EXPANDPARTIAL action flag has been added for use with the TVM_EXPAND message. Specifying this flag will cause the item to be partially expanded. This is useful in situations where the items in a TreeView are being obtained from a source that could possibly encounter an error. For example, suppose that the items are being obtained from a database and an error occurs after only five items are obtained and added. The five items will be displayed, but the + symbol will still be displayed. This indicates to the user that more information is available. When the user clicks the + symbol again, you can attempt to perform the query again. You need to be careful here that you do not add duplicate items. You need to either clear the items belonging to this item or exclude the existing items from being duplicated.
Track BarThe Track Bar now supports up to two buddy windows and ToolTips. Track Bar buddy windows are sibling controls that the Track Bar will position for you. The Track Bar will automatically place the buddy windows centered around the control at the Track Bar's extents, but it will not make other modifications to these controls.
To set a buddy window, you send the Track Bar the TBM_SETBUDDY message. The wParam for this message is a BOOL indicating where to position the buddy window. If this value is FALSE, then the buddy window will be positioned to the right of the Track Bar if the Track Bar control uses the TBS_HORZ style. If the Track Bar uses the TBS_VERT style, the buddy will be positioned below the Track Bar control. If this value is TRUE, then the buddy window will be positioned to the left of the Track Bar if the Track Bar control uses the TBS_HORZ style. If the Track Bar uses the TBS_VERT style, the buddy will be positioned below the Track Bar control.
The lParam for this message is the window handle of the buddy window. To retrieve a buddy window handle, you send the Track Bar a TBM_GETBUDDY message. The wParam for this message is the same as that for the TBM_SETBUDDY message. The return value is the window handle of the desired buddy window. The message will return NULL if no buddy window has been set at the selected location.
To implement ToolTips in the Track Bar, add the TBS_TOOLTIPS style bit when creating the control. Alternatively, you can create your own ToolTip and assign it to the Track Bar by sending the Track Bar the TBM_SETTOOLTIPS message. This message takes the window handle of the ToolTip control as the wParam. To retrieve the ToolTip handle, send the Track Bar a TBM_GETTOOLTIPS message. This message returns the handle to the ToolTip control. By default, the ToolTip will display the current position of the Track Bar.
ToolBarThe ToolBar has two new styles that provide different methods of displaying the ToolBars: TBSTYLE_FLAT and TBSTYLE_LIST. Figure 11 shows a ToolBar with the TBSTYLE_FLAT style. Figure 12 shows a ToolBar with the TBSTYLE_LIST style. The TBSTYLE_FLAT style also makes the ToolBar transparent, which causes the ToolBar buttons to be displayed on the client area of the window underneath the ToolBar. The ToolBar also supports hot tracking by default.
Figure 11 ToolBar with TBSTYLE_FLAT
Figure 12 ToolBar with TBSTYLE_LIST
A ToolBar's style can now be changed dynamically by sending the ToolBar a TB_SETSTYLE message. This message takes the new ToolBar style flags for the lParam. The return value from this message is not used. To retrieve the ToolBar styles, you can send the ToolBar a TB_ GETSTYLE message. This message returns the current ToolBar styles.
You can easily obtain the rectangle of a ToolBar item by sending the ToolBar a TB_GETRECT message. The wParam for this message is the item identifier, and the lParam is a pointer to a RECT structure. This message returns a Boolean value indicating the success or failure of the message.
The ToolBar now supports the use of image lists. There are three image lists that the ToolBar can use. The first is for displaying normal or default images on the ToolBar. This image list is set using the TB_SETIMAGELIST message. This message takes the HIMAGELIST of the new default image list as the lParam and returns the HIMAGELIST of the previous default image list or NULL if no previous image list was set. The default image list can be retrieved using the TB_GETIMAGELIST message. This message returns the HIMAGELIST of the current default image list, or NULL if no default image list is set.
The second image list is for displaying disabled images on the ToolBar. This image list is set using the TB_SETDISABLEDIMAGELIST message. This message takes the HIMAGELIST of the new disabled image list as the lParam and returns the HIMAGELIST of the previous disabled image list, or NULL if no previous image list was set. The disabled image list can be retrieved using the TB_GETDISABLEDIMAGELIST message. This message returns the HIMAGELIST of the current disabled image list or NULL if no disabled image list is set.
The third image list is for displaying hot-tracking images when the mouse cursor is over a ToolBar item or when the mouse button is pressed on a ToolBar item. This image list is set using the TB_SETHOIMAGELIST message. This message takes the HIMAGELIST of the new hot-tracking image list as the lParam and returns the HIMAGELIST of the previous hot-tracking image list or NULL if no previous image list was set. The hot-tracking image list can be retrieved through the TB_GETHOTIMAGELIST message. This message returns the HIMAGELIST of the current hot-tracking image list or NULL if no hot-tracking image list is set.
There is a new set of system-defined images that can be used with the ToolBar. These are IDB_HIST_SMALL_ COLOR and IDB_HIST_SMALL_COLOR. These image sets can be used exactly like the other system-defined image sets. See the TBADDBITMAP structure definition in the Win32 SDK documentation for details on how to use these image sets. The history image sets contain the following images: HIST_BACK, HIST_FORWARD, HIST_ FAVORITES, HIST_ADDTOFAVORITES, and HIST_VIEWTREE.
The TB_LOADIMAGES message was added to allow the programmer to load a bitmap into the ToolBar's image list. This message takes a bitmap resource identifier as the wParam and the HINSTANCE of the module that contains the bitmap resource for the lParam. The ToolBar's default image list must already have been set using the TB_SETIMAGELIST message for the TB_LOADIMAGES to have any effect. The system-defined image sets can also be used with this message by passing the image set constant (IDB_XXX) as the wParam and HINST_COMMCTRL as the lParam.
The ToolBar makes implementation of drop-down ToolBar buttons easier. To gain this effect, simply add the TBSTYLE_DROPDOWN style bit to the button's style. When you do this, a combo box-like drop-down arrow will be drawn next to the button and, when the button is pressed, the parent window will receive a WM_NOTIFY message with the code equal to TBN_DROPDOWN. During this notification, you can send the ToolBar the TB_GETRECT message to determine where to place the menu and then call TrackPopupMenu to display the menu.
The updated ToolBar supports extended item features. You now have more control over the size of the ToolBar items. You can set the minimum and maximum button widths by sending the ToolBar the TB_SETBUTTONWIDTH message. This message takes a DWORD value as the lParam, where the low WORD of the lParam indicates the minimum width of the buttons and the high WORD indicates the maximum width of the buttons. The current size of the ToolBar buttons can be obtained by sending the ToolBar a TB_GETBUTTONSIZE message. This message takes no parameters and returns a DWORD, where the low WORD contains the width of each ToolBar button and the high WORD contains the height of each ToolBar button.
ToolBar item text can now be wrapped and you can control the number of lines that will be displayed by sendingtheTB_SETMAX-TEXTROWS message. This message takes an integer as the wParam, which indicates the maximum number of text rows that can be displayed. To retrieve the number of text rows currently being displayed, send the ToolBar a TB_GETTEXTROWS message. This message returns an integer that contains the number of text rows currently displayed. If an item's text will not fit into the width provided in the TB_SETBUTTONWIDTH message and the maximum number of text rows is one, then the text will be displayed truncated with an ellipsis (...).
The starting point for all of the buttons can now be adjusted by sending the ToolBar a TB_SETINDENT message. This message takes the indent value as the wParam. The indent is the number of pixels that the first button will be indented.
ToolTipThe ToolTip now allows the tip to track along with the mouse while the mouse is moving instead of just remaining in the same place. This is accomplished with a combination of the TTM_TRACKACTIVATE and TTM_TRACKPOSITION messages. The TTM_TRACKACTIVATE message takes a Boolean value that indicates if the tracking is being activated as the wParam and a pointer to a TOOLINFO structure as the lParam. If wParam is zero, then tracking for this tool is being disabled. If wParam is nonzero, then tracking for this tool is being enabled. The cbSize, hwnd, and uId members of the TOOLINFO must be filled out before sending the message. The ToolTip uses the hwnd and uId members to determine which tool is affected by this message.
To implement tracking ToolTips, you need to use the TTM_TRACKPOSITION message instead of TTM_RELAYEVENT to pass mouse messages to the ToolTip. The TTM_TRACKPOSITION message takes a DWORD that contains the x and y position, in screen coordinates, where the tracking ToolTip will be displayed as the lParam. The low word contains the x coordinate and the high word contains the y coordinate. ToolTip tracking is not supported for windows that the ToolTip subclasses. If you want to allow tracking for other windows, then you will have to subclass the windows yourself and send the TTM_ TRACKPOSITION messages when appropriate.
You now have more control over the appearance of the text within the ToolTip than ever before. In addition to being able to perform the custom draw techniques outlined in Part I of this article, you can set the margins used in the ToolTip, specify the maximum width of the ToolTip control, set the text color, and set the background color.
To set the margins, you send the ToolTip a TTM_SETMARGIN message. This message takes a pointer to a RECT structure that specifies the new margins as the lParam. The left member of the RECT structure specifies the distance between the left edge of the ToolTip window and the left edge of the text. The top member of the RECT structure specifies the distance between the top edge of the ToolTip window and the top edge of the text. The right member of the RECT structure specifies the distance between the right edge of the ToolTip window and the right edge of the text. The bottom member of the RECT structure specifies the distance between the bottom edge of the ToolTip window and the bottom edge of the text. All of these values are in pixels. To retrieve the current margins, you send the ToolTip a TTM_GETMARGIN message. This message takes a pointer to a RECT structure as the lParam. This RECT structure will contain the current margins after the message is sent.
The maximum width of the ToolTip is specified by sending the TTM_SETMAXTIPWIDTH message to the ToolTip. This message takes an integer that specifies the new maximum width of the ToolTip control as the lParam. If the length of the text exceeds this value, the text will be wrapped and left-justified to fit within the ToolTip. If the length of a single word is longer than the width of the ToolTip, then the ToolTip will be expanded to fit the widest word. The maximum width value is not changed, however. To obtain the current maximum width, you should send the ToolTip a TTM_GETMAXTIPWIDTH message. This message returns an integer value that indicates the maximum width setting.
To set the text and background colors displayed in a ToolTip, you send the control TTM_SETTIPTEXTCOLOR and TTM_SETTIPBKCOLOR messages. Both of these messages take a COLORREF that specifies the new color as the wParam. To obtain these colors, use the TTM_GETTIPTEXTCOLOR and TTM_GETTIPBKCOLOR messages. Both of these messages return a COLORREF that specifies the currently set color.
In Windows 95, you can set the ToolTip delay time by sending the ToolTip a TTM_SETDELAYTIME message. What was conspicuously missing was a message to retrieve the delay time. That has now been remedied with the addition of the TTM_GETDELAYTIME message. This message takes a constant identifier, which specifies which delay time is being requested for the wParam. It returns an integer value that indicates the requested delay time in milliseconds. The requested delay time can be TTDT_AUTOPOP, TTDT_INITIAL, or TTDT_RESHOW. These are the same flags, and have the same meaning, as those specified in the TTM_SETDELAYTIME topic in the Win32 SDK documentation.
The TTM_POP message has also been added to cause the ToolTip to no longer be displayed. This message takes no parameters. The ToolTip receiving the message will be the one that is no longer displayed. This message does not permanently disable the ToolTip, but is only in effect until the next time a ToolTip will be displayed. This message is useful in cases where you want the ToolTip to disappear at a time that it would not normally do so. As an example, if you wanted your application to control how long a ToolTip is displayed, you would send the ToolTip the TTM_POP message when you no longer wanted the ToolTip to be displayed.
ConclusionThe common controls DLL was originally written to satisfy the needs of the Windows¨ 95 shell and not much more. Over the past year, lots of feedback on the controls have resulted in significant improvements to this DLL. Since many new applications from Microsoft are using these widgets, the natural evolution of the common controls DLL also follows along.
From the November 1996 issue of Microsoft Systems Journal.