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.


September 1998

Microsoft Systems Journal Homepage

Control Spy Exposes the Clandestine Life of Windows Common Controls, Part II

Download ControlSpy.exe (2,914KB)

Mark Finocchio is on the Microsoft user interface developer support team. He specializes in windowing, messaging, user interface, and common controls. He can be reached at markfi@microsoft.com.

In the first part of this series of articles about the common controls (see "Control Spy Exposes the Clandestine Life of Windows Common Controls, Part I," MSJ July 1998), I introduced a utility suite called Control Spy, which I wrote to make my job on the Microsoft user interface developer support team easier. Control Spy is comprised of 22 programs, one for each control in COMCTL32.DLL, that let you learn about, test, and experiment with the common controls.
      In this article, I will cover the Control Spy features that were not discussed in Part I, and explain how to alter the source code to provide custom notification handlers and add custom code. I will also continue my discussion of the new and updated common controls that were introduced with Microsoft® Internet Explorer 4.0. See http://msdn.microsoft.com/downloads/files/40comupd.htm for a quick way to get the common controls without installing Internet Explorer.

Advanced Control Spy
      In Part I, I presented basic features of Control Spy such as how to change control styles on the fly and how to use the built-in parser to send any control-specific message to the control at runtime. To review, the parser handles the creation of data structures in memory (if needed), and cleans up afterwards. The result of the sent message (or API call) is displayed in the Return Value box. The parts of Control Spy that remain to be discussed are labeled in Figure 1.

Figure 1  Control Spy
Figure 1  Control Spy


      All the values displayed in the Return Value box can be copied into a log file. This is useful when you're sending many messages to the control and need to keep track of the results. Log files are written as standard text files so they can be viewed with any text editor.
      You specify a log file either by manually typing a file name in the Log File edit box or by navigating to the file with the Log File Browse button. To start or stop logging, select the Logging button. To erase the contents of the log file, just select the Log File Erase button.
      So far, I've explained how to manually send messages to a control, one at a time. But in some situations, you may want to repeatedly send the same group of messages—for example, when testing a modification to a control. You can accomplish this with statement lists.
      A statement list is simply a series of message statements listed one after the Other in a text file and separated by semicolons. (This is the Only situation in which semicolons are allowed.) You can use comments to make a statement list more readable. Comments always start with double slashes (//). When the parser encounters a double slash, it ignores everything that follows, up to the next line.
       Figure 2 shows a sample statement list for use with the ComboBoxEx Control Spy. To load a list, use the Statement List Open button. Control Spy will separate all the statements and display the M individually. You can move between statements with the scroll arrows, and use the Refresh button to reload the list. You will want to use the Refresh button if you used an editor to alter and save the list that was in use. The Stop button lets you stop using the statement list. If you choose Send while a statement list is open, Control Spy executes the current statement and moves to the next one. Select Send All to send all the statements consecutively from start to finish.
      The last area to discuss is how to handle callbacks. Callbacks require source code modification. Let's say you want to run a block of your code while using Control Spy. A source code function called ControlStateCallback facilitates this. First, choose a callback number, then select the Run Callback button. This will call the function ControlStateCallback with the number chosen through the interface as an argument. I've provided a switch statement so you can easily insert code into a case block.
      Comments in the function list all the handles you might need for custom code. I've defined global variables to give you access to the handle of the control, application instance, container window, and Control Spy main dialog. The callback function can also be called automatically with two other values: CSPY_STARTUP and CSPY_SHUTDOWN. CSPY_STARTUP occurs after the control has been created. CSPY_SHUTDOWN occurs before the control is destroyed. Controls are created and destroyed when Control Spy starts and closes, and when they are recreated via the Styles dialog.
      The source code for every Control Spy contains other customizable functions as well. The suite includes ready-to-build projects for each control located in the Control Spy Source directory. Each project contains three directories: Parser, Resource, and VC5Workspace. The Parser and Resource directories contain what you'd expect. The VC5Workspace directory contains the project files for Visual C++® 5.0. If you don't have Visual C++ 5.0, you can easily create projects for your compiler. The required source files are <ControlName>.c, Parser\lex_yy.c, and Parser\ytab.c. You must also include the resource file Resource\<ControlName>.rc. Finally, make sure to link in COMCTL32.LIB.
      If you build the project yourself, some warnings might be generated for lex_yy.c and ytab.c when you compile. This is because I didn't create the Se files directly (more on this later). They are harmless warnings that you can safely ignore, or you can eliminate the M by turning down the warning level for the Se files.
      The file with all the customizable functions is <ControlName>.c. the Se functions and some global variable definitions are located at the beginning of the file. the Se variables usually can be referenced through the parser and are available for you to customize.
      Image Lists are a good example of this. The first function in the file is InitialControlStyles. This lets you set the automatic startup styles for the control without having to use the Styles dialog. Next is a function that I've already mentioned: ControlStateCallback. Following that is the function ProcessControlNotification, which provides a place for your control notification handlers (the handlers called in response to WM_NOTIFY messages). The source already contains a switch statement and all the necessary pointer initialization and typecasting. Of course, it also has access to all the handles you might need for your code.
      The next function is ProcessControlCommand. Some of the controls still send commands (WM_COMMAND messages) to their parents. This function provides a place to handle those commands. In a few cases, a parent is notified of child events with standard Windows® messages. Control Spy defines ProcessWindowsMessage in those circumstances.
      The last customizable function, RegisterControl, demonstrates how the control is registered and contains a single call to InitCommon–ControlsEx. Most of the Se functions and global variables have already been populated and initialized. If you want to initialize the control differently, add different data, or handle notifications differently, just change the source code.
      Some of you may be wondering how Control Spy works under the surface—how flexible the parser really is. In general, the parser is only as functional as it needs to be to send all the messages to a particular control. Every Control Spy has a different implementation of the parser that understands only the constants that the control uses. Each defines only those keywords needed for the messages of the control. The keywords are usually mapped to internal global variables.
      The parser syntax is faintly similar to C (like the logical-OR operator), but it only supports what it needs. You can't use any other bitwise operators. Also, the parser recognizes many more types than are defined by C. For example, the parser defines TRUE as a boolean type and "Spy" as a string type.
      The parser provides strong type checking. If you try to use a number in an argument where a string is expected, a syntax error results. I wrote it this way because if Control Spy forced the user to use the messages correctly, it would provide a better learning experience.
      When the actual argument is within a structure, the parser uses weak type checking. For example, say you want to set the text color of a Tree View control to light blue. You would use:


 MSG (TVM_SETTEXTCOLOR, 0, RGB(0,0,255))
Strong type checking prevents you from doing this:

 MSG (TVM_SETTEXTCOLOR, 0, RGB(0,0,"Blue")) Syntax error ("Blue")!
However, weak type checking at the argument level lets you do this:

 MSG (TVM_SETTEXTCOLOR, 0, "Blue")   Not a syntax error.
This last statement doesn't make sense, but it doesn't produce a syntax error.
      If a control expects a string type as an argument, it's all right to pass it a NULL, if needed. However, if a structure expects a string, it's not okay to substitute a NULL unless it is documented as an appropriate value for the structure member.
      A good example of this is text callbacks. For some controls, you can substitute a LPSTR_TEXTCALLBACK value in place of a string. The parser would support this case. Since you can do things like passing NULL to controls, it is entirely possible to crash Control Spy if the message causes an illegal operation.
      You might be curious about the files I mentioned earlier—lex_yy.c and ytab.c—that weren't created by me directly. the Se files comprise the parser. lex_yy.c is the lexical analyzer that breaks down input into tokens and passes the M on to the parser. I used the compiler generation programs LEX and YACC to create the Control Spy interpreter. That's why they produce warnings if the warning level is too high. Again, the warnings are harmless.
      Now you are an expert at using Control Spy and can use its full potential for your benefit! Next, I will continue the discussion of common controls in the context of Control Spy. In the last article, I covered two of the seven new common controls that were released with Internet Explorer 4.0. Again, the controls I refer to as "new" are those that were technically in preview with Internet Explorer 3.0.

Flat Scroll Bar
      Flat scroll bars are enhanced standard scroll bars with different bevel types and colors (see Figure 3). They even support palettes. The Flat Scroll Bar control is strictly API-based. It uses functions rather than responding to messages. To use Flat Scroll Bars on a window, the window's handle must be passed to InitializeFlatSB. You don't need to specify the standard scroll bar styles (WS_HSCROLL and WS_VSCROLL) for the window.

Figure 3 Flat Scroll Bars
Figure 3 Flat Scroll Bars


      The control itself does not have its own window handle; it exists as an integral part of the window. If you use the Flat Scroll Bar API functions on a window that isn't using flat scroll bars, the calls will refer back to standard scroll bar functions. This lets you turn the flat scroll bar look on and off dynamically without having to write extra code to support both types. Standard scroll bar notifications (WM_HSCROLL and WM_VSCROLL) are generated regardless of the type of scroll bar.
      Run the Flat Scroll Bar Control Spy and try out the scroll bars. The notification messages that are displayed have the word Forward appended to the M. This is just a reminder that the notifications did not come by way of WM_NOTIFY or WM_COMMAND like they do with most of the Other controls. Try turning the flat scroll bars off dynamically:


 API:
     UnintializeFlatSB(window)
Then, try setting the horizontal scroll position using a Flat Scroll Bar API:

 API:
     FlatSB_SetScrollPos(window,SB_HORZ,0,TRUE)
The call was diverted to the normal scroll bar function, SetScrollPos. Now, start using flat scroll bars again:

 API:
     InitializeFlatSB(window)
      The Flat Scroll Bar control includes functions to change its appearance. The following code changes the horizontal scroll bar's appearance to Encarta mode and specifies a blue background:

 API:
     FlatSB_SetScrollProp(window,WSB_PROP_HBKGCOLOR,RGB(0,0,128),TRUE)
 API:
     FlatSB_SetScrollProp(window,WSB_PROP_HSTYLE,FSB_ENCARTA_MODE,TRUE)
      If you are adding custom notification handlers, keep in mind that a special ProcessWindowsMessage function has been added for Flat Scroll Bar's notifications in Control Spy. For more coloring options, check on the control's palette support. The source code defines a handle that can be used for custom palettes.

IP Address
      The Internet's influence continues to grow. For some applications, you need to know a user's IP address. The IP Address control (see Figure 4) allows quick entry and validation of IP addresses.
      The IP Address control has no class-specific styles. Even though it contains four edit control children, it doesn't support any edit control (EM) messages. It does, however, make use of three edit control notifications sent via WM_ COMMAND: set focus (EN_SETFOCUS), lost focus (EN_ KILLFOCUS), and content change (EN_CHANGE). To quickly check whether the control has a value, use IPM_ ISBLANK.

Figure 4 IP Address Control
       Figure 4 IP Address Control

      IP Address is a rather simple control. It only supports a handful of messages. Despite its simplicity, it does have a flaw. Try typing an alphabetic character in the field; it can't be done. This makes sense since an IP address doesn't contain alphabetic characters. Copy some text from somewhere else and paste it into the control. Now you can see the problem; characters can be pasted in the control. If you retrieve the value of the control, the field with alphabetic characters resolves to zero. To get around this problem, you must subclass the control and trap paste operations.
      The value of the control is set and retrieved with a single 32-bit number, and the MAKEIPADDRESS macro makes it easier to create this number. Control Spy supports this macro. Run the IP Address Control Spy and try setting the control:

 MSG (IPM_SETADDRESS,
      0,                            // Unused parameter
      MAKEIPADDRESS(198,105,232,37))// Address to set
Then, get the current address by supplying a pointer to a DWORD buffer:

 MSG (IPM_GETADDRESS,
      0,                    // Unused parameter
      ipbuffer)             // Pointer to integer buffer
Observe the Output in the Return Value box. Control Spy uses the IP Address field-extraction macros to get the values—for example, FIRST_IPADDRESS.
      You might need to limit the possible values by a range of IP addresses. This is done on a per-field basis. Say you want to limit the first field's range to the numbers between 100 and 150. Since fields are zero-based, the following message would be used:

 MSG (IPM_SETRANGE,
      1,                       // Limit range of Field 1
      MAKEIPRANGE(100,150))    // Range is 100 to 150
      Finally, keep in mind that you can use WM_SETFONT if you don't like the initial font chosen by the control (stock fonts are always readily available). Other controls also support this message. Check the Information section of the control to see if it looks at the WM_SETFONT message.

Month Calendar
      The Month Calendar control (see Figure 5) provides an intuitive interface for entering dates. Users can select a single day or a range of days. The Date and Time Picker control uses the Month Calendar control to query dates from the user.
      The Month Calendar is always centered in the rectangle specified as the creation size for the control. The size of the area needed to display the control depends on the font and can be retrieved, as I'll explain later. The control's interface is comprised of several different sections. Click the right or left scroll button to change the current month. The number of months skipped is set by MCM_SETMONTHDELTA. Click on the month to bring up a list of months, and select the year to open a spin box.
Figure 5 Month Calendar Control
       Figure 5 Month Calendar Control
      When the control is created, it defaults to the current system date. As with the Date and Time Picker, changing the date in the control does not change the system's date. There is no Month Calendar message for doing this, either. The Today section at the bottom of the control is customizable. If the user selects the Today area, the current date selection changes to the Today date. As with the Date and Time Picker control, the SYSTEMTIME structure is used to exchange data.
      The Today section can be turned off with the MCS_NOTODAY style. Further, to remove the Today circle, use MCS_NOTODAYCIRCLE. For multiple date selections, make sure to use the MCS_MULTISELECT style. Day State notifications are generated when you use the MCS_DAYSTATE style.
      A day displayed by the control can have one of two states: bold and not bold. MCN_GETDAYSTATE is sent by the control to ask what days it should display in bold. A month is represented by a value of type MONTHDAYSTATE (which is a fancy name for a DWORD). Each bit in the value represents a day. The BOLDDAY macro can be used to do the work of shifting the bits. Control Spy supports this macro, although with slight differences. It returns a value in Control Spy rather than updating a supplied variable. Thus, Control Spy's implementation takes one value (the day to make bold), not two.
      Portions of the previous and next months are usually displayed along with the current month, and the Se are usually a different color. So, at most, you will need an array of three MONTHDAYSTATEs to describe everything that is to be displayed in bold. To determine how many months are currently shown, use MCM_GETMONTHRANGE. To specify what days to make bold without providing a notification handler, use the MCM_SETDAYSTATE message.
      The Month Calendar control has a few quirks you will need to look out for. If there is no date range selected, the range of years differs when month scroll buttons are used rather than the year spin box. The year selection always stops at 1753 if it is being changed with the year spin box. To get around this problem, set a date range (I'll explain later).
      Another problem exists with the popup listbox that is used to select the current month. If the date range used spans less than a year, the month listbox will display all the months of the year as selectable. Also, there is no way to change the circle that's drawn around the Today date. Finally, you might think that the array of MONTHDAYSTATEs used in response to MCN_GETDAYSTATE would be allocated by the application. This is not the case; the array is supplied by the control.
      To familiarize yourself with the control, load up the Month Calendar Control Spy. Try setting the date range using MCM_SETRANGE:


 MSG (MCM_SETRANGE,
      GDTR_MIN|GDTR_MAX, // Set min and max date values
      systimearr(        // Pointer to SYSTEMTIME array
          // Starting date is 7/1/1972
          SYSTEMTIME(1972,7,0,1,0,0,0,0),
          // Ending date is 3/10/1976
          SYSTEMTIME(1976,3,0,10,0,0,0,0)))
The MCM_SETTODAY message is used to change the today date:

 MSG (MCM_SETTODAY,
      0,   // wParam is unused
      // Set today to January 1, 2000     
      SYSTEMTIME(2000,1,0,1,0,0,0,0))
Instead of handling notifications, set the bold state of the calendar days (make sure the MCS_ DAYSTATE style is in use):

 MSG (MCM_SETDAYSTATE,
      3,                  // Using 3 element array
      mdsarr(             // MONTHDAYSTATE array pointer
         BOLDDAY(31),     // Bold day 31 of last month
         BOLDDAY(15)|BOLDDAY(16),  // Bold day 15 and 16
         BOLDDAY(1)))     // Bold day 1 of next month
The size of the actual area that's used to display the calendar depends on the font used. To determine the rectangle's size, use the following:

 MSG (MCM_GETMINREQRECT,
      0,                    // wParam is unused
      RECT)                 // Pointer to RECT
      There are not many messages and styles for customizing the Month Calendar control interface. But if you really want something else to happen when the user clicks on some part of the control, you can use subclassing and hit testing. Hit testing is done via the MCM_HITTEST message:

 MSG (MCM_HITTEST,
      0,                  // wParam is unused
      // Pointer to part initialize MCHITTESTINFO
      MCHITTESTINFO(
          mchtisize,      // Size of MCHITTESTINFO
          POINT(45,50)))  // What's at (45,50)?
The return value is one of many values that specify where the point is on the control—for example, the scroll next month button.
      Finally, there are messages available for changing the colors of the control. Use MCM_SETCOLOR and MCM_ GETCOLOR for this. You can make color changes in different sections, and you can also use WM_SETFONT to make font changes.
      Now I'll continue my discussion of the updated common controls. Again, the Se updates come in the form of additional messages, notifications, styles, and structure members. Some controls have only minor changes, while others have undergone substantial revision.

List View
      The List View (see Figure 6) is a powerful control used to maintain and display collections of items. It can arrange items in four different ways: large icon, small icon, list, and details. It is capable of displaying millions of items, and at the same time is fast and flexible. List View controls are used all over the Windows shell. When you view your drives or roam around Control Panel, List Views are displaying the contents.
      The List View uses three Image Lists: one for large icons, one for small icons, and one for state. All of a List View item's properties are defined in an LVITEM structure. The structure members include the item label, image, overlay and state images, indention, and state.
      Don't confuse the state and the state image of an item. The state determines the item's appearance and functionality (for example, if it is selected, has focus, and so on). An item's state image is an image displayed next to its regular image in the control.

Figure 6 The List View
Figure 6 The List View

      The List View has quite a few messages. Several item-oriented messages make use of the LVITEM structure. the Se messages include LVM_INSERTITEM, LVM_SETITEM, LVM_GETITEM, LVM_SETITEMTEXT, and LVM_SETITEMSTATE. The state and text of an item can be changed with LVM_SETITEM, but it's more convenient to do this with the LVM_ SETITEMTEXT and LVM_SETITEM-STATE messages.
      You don't need to set the mask member of LVITEM with the Se messages to specify valid data. Also, when changing an item you specify its index in one of the parameters of the message, not within the LVITEM structure. When dealing with states, don't forget that the state member of LVITEM itself has a mask. If stateMask isn't set, all the state bits will be ignored.
      There are other sets of messages for report-mode columns, cursors, item position and spacing, color, and sorting. An item's index changes when items are added and deleted and is not dependent on screen position. If you need to identify items uniquely, use the user-definable member (lParam) of the LVITEM structure.
      The List View control has been updated in many ways. It now supports extended styles set via LVM_SETEXTENDEDLISTVIEWSTYLE. The new styles include full-row selection in report mode, one-click activation, images for subitems in report mode, hot underlining, and track selection ("hover" selection) of items. There are also extended styles to enable item checkboxes, use flat scroll bars, display gridlines, and generate tooltip text callback notifications.
      In report mode, the control uses a Header control for the column labels. Columns are maintained by the List View through the LVCOLUMN structure. This structure has been updated to accommodate the new features of the Header control: item images and column ordering. Messages specific to List View also have been added for column ordering: LVM_SETCOLUMNORDERARRAY and LVM_ GETCOLUMNORDERARRAY.
      Another new feature of the List View is the concept of "working areas"—rectangles defined within the control. While in icon or small icon mode, items are restricted to working areas. If an item is moved outside a working area, it is placed in the first working area (that is, the area with the index of 0). To make an item a member of another working area, use LVM_SETITEMPOSITION to move it to the new area.
      To create working areas, use LVM_SETWORKAREAS. This message takes a pointer to an array of rectangle structures whose coordinates are relative to the List View's client area. To restore the List View to its normal state, remove all working areas by using NULL for the array pointer.
      Working areas let you group items with something in common. You can force items to be displayed in specific regions of the control, and you can display scroll bars when they ordinarily wouldn't be displayed. The actual scrollable area of the control depends on item placement, not the size and position of the working areas. No matter how the working areas are arranged, the control will be scrollable so that all items can be reached.
      To determine if an item exists in a working area, you must compare the item's position with all the areas. If you don't already know the Se area boundaries, use the messages LVM_GETNUMBEROFWORKAREAS and LVM_ GETWORKAREAS to get this information.
      In virtual mode, a List View can display millions of items without a performance hit. Virtual List Views use text and image callbacks. With normal callbacks, the List View maintains information about its items. In virtual mode, the control maintains a minimum amount of information and no item-specific information.
      To place the control in virtual mode, use the LVS_ OWNERDATA style. When the control needs text and image information, it generates the LVN_GETDISPINFO notification. To optimize performance, the List View uses the LVN_OCACHEHINT notification to inform the application about index values that are good to have loaded and ready to go.
      Items can be added and removed in a virtual List View with LVM_INSERTITEM and LVM_DELETEITEM, respectively. The effect is no different than setting the current item count with LVM_SETITEMCOUNT. The virtual List View keeps track of the total number of items and their display; it's up to the application to give the Se items meaning based on the index.
      The virtual List View is a great way to present large amounts of data without sacrificing performance, but it does have some limitations. Some styles and messages are either partially supported or not supported at all while in virtual mode. Items can't be sorted while in virtual mode. the Only item state attributes maintained by the control are selection and focus (which isn't per item). LVM_SETITEM is not supported, so the Only other option is LVM_ SETITEMSTATE. LVM_GETNEXTITEM is limited, and LVM_SETITEMPOSITION isn't supported.
      List View has a few problems worth mentioning. When you are using a virtual List View, you must supply the text displayed for items. If you use a label so long that it runs into another item, the control will clip the text incorrectly. The result is icons that display overlapping parts of their text labels.
      Another problem involves the use of LVM_ GETNEXTITEM when there is only one item present in the control and nothing is selected. If the LVNI_ABOVE flag is used to search above an item, or the LVNI_SELECTED flag is used to find the next selected item, the application will lock. Make sure not to use LVM_GETNEXTITEM with the Se flags if there is a single unselected item in the list.
      Another problem to look out for concerns the LVS_EX_ CHECKBOXES style, which adds checkboxes next to each item. Checkboxes are automatically implemented with a state Image List. While in icon mode, the checkboxes can be toggled only if they are clicked on the upper-left area of the corresponding box.
      The last problem concerns setting the control's background color with LVM_SETBKCOLOR. The background color is not set correctly in report mode; only the color beneath the images is changed. You can use custom draw to get around this problem.
      With all this information in mind, let's try out a few things with List View Control Spy. First, let's set the attributes of an item. The LVITEM structure is important to understand—it is used with LVM_SETITEM. To see the effects of the following statement, make sure the LVS_ SHOWSELALWAYS style is on in the Styles dialog.

 MSG (LVM_SETITEM,
      0,              // wParam is unused
      LVITEM(         // Pointer to LVITEM structure
         LVIF_TEXT    // Mask: Text member is valid
         |LVIF_STATE  // Mask: State member is valid
         |LVIF_IMAGE, // Mask: Image member is valid
         // Set properties item with index 1
         1,           
         0,                 // No subitem
         LVIS_SELECTED/0/0, // Selected state flag
         // Don't forget the state mask!
         LVIS_SELECTED,
         // Text label is callback based 
         LPSTR_TEXTCALLBACK,
         // Text buffer size doesn't apply
         0,             
         // Image to display is callback based
         I_IMAGECALLBACK,
         // User data and indent (not in mask)
         0,0)) 
      After this message is sent, the text and image display of the second item in the List View will be callback-based, and the item will be selected. Notice the syntax for the state member. Since the state variable holds the item's state, state image, and overlay image, Control Spy defines a syntax with slashes so you can input the values. Internally, Control Spy shifts the bits to create the Overall value automatically.
      Now try a working area:

 MSG (LVM_SETWORKAREAS,
      1,           // Number of valid RECTs in our array
      rectarr(     // Pointer to RECT array (2 supplied)
          // Working area boundaries 
          RECT(40,40,500,500),
          // Second supplied RECT not used   
          RECT(0,0,0,0)))    
Nothing happens until you use LVM_ ARRANGE. After that, items are arranged in an area where they wouldn't normally be arranged.
      Some messages (like this one) expect variable-length arrays as parameters. It's possible to have more than two working areas—in fact, you can have up to LV_MAX_WORKAREAS. Control Spy defines an array of two RECT structures for this use. If you specify that the number of working areas is 1 (for the wParam), Control Spy ignores the second RECT structure.
      Finally, try putting the List View in virtual mode. Select Styles to view all the available styles for the control, then select LVS_OWNERDRAW and press Recreate Control. You must recreate the control since you cannot switch in and out of virtual mode dynamically. Once in virtual mode, notice that all the images and labels specify "callback." This is due to Control Spy's default handling of LVN_GETDISPINFO. Look at the List View Control Spy source code to see how to implement one of the Se handlers.

Progress Bar
      Progress Bar controls (see Figure 7) are useful for indicating the progress of a lengthy operation, and are used in many places throughout Windows. When you wait for pages to load in Internet Explorer, a Progress Bar shows how much has been loaded so far.
      Progress Bar messages allow you to set the length of the indicator, or it's "position." Positions are set (PBM_SETPOS) relative to a range of values (PBM_SETRANGE). PBM_ STEPIT advances the position based on the value set in PBM_SETSTEP. To advance it by an arbitrary amount, use PBM_DELTAPOS. Progress Bars default to a range between 0 and 100. Negative range extremes are allowed. Progress Bars generate no notifications.

Figure 7 Progress Bar
Figure 7 Progress Bar

      Progress Bars have been updated to support vertical bars (PBS_VERTICAL) and a new smooth look (PBS_SMOOTH). The new PBM_SETRANGE32 provides 32-bit range support. The colors of the Progress Bar can be changed with PBM_SETBARCOLOR and PBM_SETBKCOLOR.
      Load up Progress Bar Control Spy and try the Se messages to get familiar with the control:

 MSG (PBM_SETRANGE32, -125, 125)
The range is set between -125 and 125. Now, set the position in the middle:

 MSG (PBM_SETPOS, 0, 0)
Property Sheet
      The Property Sheet control (see Figure 8) presents information to the user in tabbed form. It's convenient because it uses standard dialog templates for the contents of each tab, and standard dialog callbacks. Property Sheets are used all over the Windows shell. When you view the properties of any object (for example, a file), the information is usually displayed in a Property Sheet. Property Sheets are also the basis of the "wizards" used by many applications.
Figure 8 Property Sheet
Figure 8 Property Sheet

      Programming with Property Sheets involves a mix of functions and messages. Property Sheets contain pages, and are created with the PropertySheet API or with CreatePropertySheetPage. Each page makes up one tab in the sheet. The PSM_ ADDPAGE message is used to add pages if they were created with CreatePropertySheetPage. It takes a page handle, which is returned by the function. Property Sheets usually exist as modal dialog boxes, but they can also exist as modeless dialogs.
      Before you can create a Property Sheet, you must fill out a PROPSHEEthe ADER structure. This structure defines all the Options available for creating the sheet. This is where you supply the title, pick what buttons to display, turn on the wizard mode, specify whether it's modeless (see the dwFlags member), specify what icons to display (see hIcon), and so on. You can even supply a watermark bitmap for display in the background (see hbmWatermark).
      Another important part of this structure is the ppsp member, which points to an array of PROPSHEETPAGE structures. Synonymous with PROPSHEEthe ADER, PROPSHEETPAGE specifies all the attributes of Property Sheet pages. Make sure to have this structure member point to at least one PROPSHEETPAGE structure.
      Before and after a Property Sheet and its pages are created, the application is informed of events with callbacks. You specify callback functions in the previously mentioned structures. Your Property Sheet callback (PropSheetProc) gets called before the Property Sheet is created and when it's being initialized. The pre-creation callback (when PSCB_PRECREATE is passed) gives you the Opportunity to alter the creation styles and position of the sheet.
      Property Sheet page callbacks (PropSheetPageProc) are called when pages are about to be created and destroyed. You can prevent pages from being created by handling the PSPCB_CREATE case of the callback.
      Property Sheet pages are similar to dialog boxes in that you must use dialog box callback procedures to handle messages coming from the page. Dialog box procedures are called in response to messages, much like standard Windows procedures. The difference is that the return value specifies whether the message was handled. The return value isn't sent back to the caller. To return a value from a handler, use SetWindowLong and the DWL_MSGRESULT index.
      Looking through the Property Sheet Control Spy source code, you can see that Property Sheets take more code to implement than the Other controls. This is because every page requires a dialog procedure and perhaps a callback.
      Once a Property Sheet is up and running, several messages are available for interacting with the control. As stated above, PSM_ADDPAGE adds a page to the control. To remove a page, use PSM_REMOVEPAGE. Pages are identified by an index or handle. A page's index changes as pages are added and removed. Handles are unique. Removing a page removes it from memory. PSM_REMOVEPAGE causes DestroyPropertySheetPage to be called. DestroyPropertySheetPage is available for your own use if the page wasn't initially added using PropertySheet.
      Various messages let you manipulate the buttons on the sheet. To programmatically select buttons, use PSM_APPLY and PSM_PRESSBUTTON. The PSM_CANCELTOCLOSE, PSM_SETFINISHTEXT, and PSM_SETTITLE messages are used to set text on different parts of the sheet. PSM_CHANGED and PSM_UNCHANGED enable and disable the Apply button, respectively.
      PSM_REBOOTSYSTEM and PSM_RESTARTWINDOWS don't actually reboot and restart the system. the Se messages cause special values (ID_PSREBOOTSYSTEM and ID_PSRESTARTWINDOWS) to be returned from PropertySheet when the sheet is modal. It's up to the application to reboot or restart.
      Activate a page with PSM_SETCURSEL, or use PSM_ SETCURSELID to activate based on a resource identifier. PSM_ISDIALOGMESSAGE passes messages to modeless Property Sheets from the application's message loop. This message does more internally to support Property Sheets than just call IsDialogMessage, which normally is used to handle dialog input. Keep this in mind when building message loops.
      Notifications of user interaction with the sheet are sent to the individual page dialog procedures, not the sheet itself. Depending on the notification and the response to the notification, some or all of the page procedures will be notified. Some notifications are sent when buttons are pressed—for example, PSN_APPLY, PSN_HELP, PSN_ QUERYCANCEL, and PSN_RESET. Others are specific to sheets in wizard mode, like PSN_WIZBACK, PSN_WIZNEXT, and PSN_WIZFINISH.
      You can stop operations like Apply and Cancel by returning the appropriate value. For example, pages will receive PSN_APPLY in index order as long as no page returns PSNRET_INVALID_NOCHANGEPAGE to stop the process. Control Spy provides default handling of all the notifications to keep the Property Sheet from closing. As a result, you won't ever see notifications like PSN_RESET, which is sent when the sheet is about to be destroyed.
      Property Sheets have been updated slightly. A new notification, PSN_GETOBJECT, has been added to allow a page to be an OLE drop target. The PROPSHEEthe ADER structure has been modified to include members for watermarks. Additionally, PROPSHEETPAGE has been updated with members for displaying headers and subtitles.
      When you use Property Sheets, make sure to look out for a few things. When a Property Sheet is created and the first page is displayed, any other pages in the sheet do not yet exist. Only when the user selects another page for the first time will it be created.
      Another potential problem area is Property Sheet page handles, which are used to identify pages. the Se handles are returned from CreatePropertySheetPage calls. Handles for pages created indirectly via a PropertySheet call can't be retrieved.
      One final tip: if you need access to the underlying Tab control used by the Property Sheet, you can use the PSM_GETTABCONTROL notification.
      Try the following messages in Property Sheet Control Spy to get more familiar with the control:

 MSG (PSM_CANCELTOCLOSE, 0, 0)
The Cancel button is disabled and the OK button now displays Close. Now, set the title:

 MSG (PSM_SETTITLE, 0, "My Property Sheet")
Status Bar
      Most of the applications you interact with every day include Status Bar controls (see Figure 9). Status Bars are divided into parts that can be used to display text and icons. The Status Bar controls support the set of shared common control styles prefixed with CCS.
      Both CreateWindowEx and CreateStatusWindow can be used for the control's creation. CreateStatusWindow is provided as a convenience. It doesn't accept control dimensions since they are ignored (as long as the CCS_NORESIZE style isn't used). When in simple mode, the control's multiple panes are replaced with one long pane. This mode is usually used for temporarily displaying information such as file loads and saves.
Figure 9 Status Bar Control
Figure 9 Status Bar Control

      Status Bar parts are defined by an array that is sent to the control with the SB_SETPARTS message which I'll explain shortly. To set the text that is displayed in a part, use SB_SETTEXT. This message also includes drawing options for the border displayed around the text (such as SBT_POPOUT). Use SB_SIMPLE to move in and out of simple mode. Make sure to use 255 as the part index if you are displaying text while in simple mode; otherwise, it won't display.
      You can retrieve information about the text displayed in a part with SB_GETTEXT and SB_GETTEXTLENGTH. The DrawStatusText function supplies text drawing options. You can use this function with a handle to a device context (for example, the Status Bar's) to display text anywhere and with any of the border styles. You can obtain the location of parts with SB_GETRECT. Parts are referenced by their index; index 0 is always the leftmost part.
      Status Bars have been updated in several ways. Icons are now supported. To assign an icon to a part, use SB_SETICON. Tooltips are now supported with the SBT_TOOLTIPS style. You can set and retrieve the tooltip text associated with a part by using SB_SETTIPTEXT and SB_GETTIPTEXT. Tooltips will not be displayed if all the text can be seen in the part, or if the control is in simple mode. SB_ISSIMPLE has been added so you can find out if the control is in simple mode. The notification SBN_SIMPLEMODECHANGE has been added as well. Finally, to change the background color of the control, use SB_SETBKCOLOR.
      To demonstrate the control, let's use Control Spy to set the control's parts, text, and icons. Load up Status Bar Control Spy and try the following message:

 MSG (SB_SETPARTS,
      // Use 2 element array
      2,                      
      // Part right edge positions
      INTARR { 100,185,0,0,0,0,0,0 }) 
Control Spy automatically allocates enough room for an 8-element array, and initializes it using the syntax shown above. This example only uses two of the elements. Each element defines the right edge of the part. If an element used -1, the right edge of the part would extend to the right edge of the control. To set the icon and text, try this message:

 MSG (SB_SETTEXT, 1, "My Text!")
Control Spy defines two icons internally for use:

 MSG (SB_SETICON, 1, Icon0)
Tab
      Tab controls (see Figure 10 ) provide an interface for defining multiple pages within the area of the control. A very common use for Tab controls is within Property Sheets, which use the M to define their pages. The Tab control is also used in an unexpected place: the Windows taskbar. Tab controls support the CCS styles. A Tab control item is an individual tab. Although it may appear otherwise, a Tab control is only one window; items are redrawn when they are selected and given focus.
Figure 10 Tab Control
Figure 10 Tab Control

      An item's properties are defined in the TCITEM structure, and TCM_INSERTITEM, TCM_SETITEM, and TCM_ GETITEM make use of it. Images are assigned to the Tab control though an Image List. This Image List is associated with the control through TCM_SETIMAGELIST. Various messages are available to query the control for information on its items. the Se messages include TCM_GETITEMCOUNT, TCM_GETITEMRECT, TCM_GETROWCOUNT, TCM_GETCURFOCUS, and TCM_GETCURSEL.
      Selecting an item in a Tab control causes it to gain focus. So why are there separate messages for getting the focused and selected item? The Tab control button style, TCS_BUTTONS, makes tabs appear as pushbuttons.
      Different messages and styles let you set the way tabs are displayed. TCS_FIXEDWIDTH forces all the tabs to be displayed with the same width. To force tabs to fill entire rows, use TCS_RIGHTJUSTIFY. TCS_SINGLELINE and TCS_MULTILINE determine whether multiple rows are used. You can also change the look of the tabs with TCM_SETMINTABWIDTH, TCM_SETPADDING, and TCM_SETITEMSIZE.
      As I stated earlier, Property Sheets use Tab controls. Property Sheets create child dialog boxes from your templates as siblings of its child Tab control. The Tab control's display area (the area of the control not occupied by tabs) must be sized to accommodate the Se dialog boxes. To query the control for display and window size information, use TCM_ADJUSTRECT.
      The Tab control now supports extended styles with the TCM_SETEXTENDEDSTYLE message. Two extended styles are available: TCS_EX_FLATSEPARATORS (draw separators between flat buttons while in button mode), and TCS_EX_REGISTERDROP (activate drop target request notifications).
      Several new regular styles have been added to the control. To display tabs at the bottom of the control, use TCS_BOTTOM. While in button mode, TCS_FLATBUTTONS cause the buttons to be drawn with the new flat look. TCS_MULTISELECT allows for multiple button selection. You can automatically highlight items under the cursor with TCS_HOTTRACK. TCS_RIGHT and TCS_VERTICAL have been added to support vertical tab display. To display tabs on opposite sides of the control, use TCS_ SCROLLOPPOSITE. Finally, tab items can now have states: TCIS_BUTTONPRESSED and TCIS_HIGHLIGHTED.
      Some Tab control problems are worth noting. If the control is disabled, nothing is grayed out. Also, do not use & characters in item labels—they cause underlines to show under the adjacent letter (like accelerator keys). This may be what you want, but underlines don't show if the control is vertical.
      Finally, if you need to remove images from the associated Image List dynamically, don't get a handle to the underlying Image List and delete it using Image List functions. Instead use TCM_REMOVEIMAGE. This allows each tab item to retain its image, even though Image List indexes are changing.
      When you insert tabs, an index is required to place the new tab. Keep in mind that indexes are not dependent on display position. Also, indexes change as tabs are inserted and removed. Run Tab Control Spy and try the following message:

 MSG (TCM_INSERTITEM,
      6,                // Insert at index position 6
      TCITEM(           // Pointer to TCITEM structure
            TCIF_IMAGE  // Mask: Image member is valid
            |TCIF_TEXT, // Mask: Text member is valid
            0,0,        // State members not used
                        // (unused, not in mask)
            "My Tab",   // Label to display
            0,          // No need for buffer size
            -1,         // Don't use an image
            0))         // User defined data (unused,
                        // not in mask)
A new tab will be inserted at the current display location of index 6.
      The hit testing feature is useful if more control is required over the Tab control:

 MSG (TCM_HITTEST,
      0,                // wParam is unused
      // Pointer to TCHITTESTINFO structure
      TCHITTESTINFO(    
                   POINT(10,15), // Point for hit test
                   0))           // Hit location will be
                                 // filled by control
The control will report where the hit occurred.
      For more insight on how the control works, try out some of the styles. TCS_TOOLTIPS enables tooltip support. Notice the TTN_NEEDTEXT notifications; the parent will receive the M for requested text. Try out TCS_BUTTONS as well. Look familiar?

To Be Continued...
      The Control Spy suite and this article were written to provide new insight into the common controls I've discussed the advanced features of Control Spy and explained how to customize its source code. I've also continued my discussion of what COMCTL32.DLL has to offer you. The next article in this series will conclude the discussion on the new and updated common controls and, for completeness, review the controls that haven't changed.

From the September 1998 issue of Microsoft Systems Journal.