Kraig Brockschmidt
{ewc navigate.dll, ewbutton, /Bcodeview /T"Click to open or copy the code samples from this article." /C"samples_2}
Controls are variants of the standard window available in the MicrosoftÒ WindowsÔ operating system. Windows1 offers a standard set of controls and the ability to extend this set with custom controls. Like standard controls, custom controls are simply child windows dedicated to a single purpose. You can make custom controls available to multiple applications by implementing them in a DLL and making them accessible as system resources; such windows can greatly simplify application development since they are self-contained, reusable modules.
Writing a custom control is as straightforward as writing a small application: you register a class, create windows of that class, and process messages in a window procedure. Like most Windows-based applications, it involves calling functions from the Windows KERNEL (memory management), USER (window management and messaging), and GDI (graphics) modules. The difficulty in creating a good control is in designing complete interfaces for both users and application developers. These interfaces should include the visuals, control styles, functional messages, and the control’s application programming interface (API). The success of the control depends largely on the completeness and usability of these interfaces.
A control is an object that communicates with the user, an application, and the system (see Figure 1). The control’s visuals describe the complete end user interface, while the control’s messages and exported functions make up its API. A control’s window procedure is used for communication with the system and an application. Applications can call API functions directly or send messages to the control’s window procedure which then calls control API functions. The system sends messages to inform the control window procedure of an event or a state change (see Figure 2).
Figure 1
Figure 2 Control Messages
Message Type | Origin | Destination | Examples |
Event | System | Control | WM_KEYDOWN messages when keys are pressed; WM_LBUTTONDOWN when the user clicks the left mouse button. |
State Change | System | Control | WM_SIZE when a control's dimensions change; WM_MOVE when the control's position changes. |
Function | Application | Control | Instructs the control to perform a standard task, like WM_GETTEXT, or a control-specific task, like EM_SETSEL, in edit controls. Message API functions are simply callable functions that can perform the same types of tasks as function messages. |
Notification | Control | Application | WM_COMMAND with a notification code, WM_VSCROLL, or WM_HSCROLL. Edit controls send WM_COMMAND with an EN_CHANGE notification code each time the edit control's contents change. |
Every standard control in the Windows environment performs these kinds of functions. There are three types of standard controls. Simple controls are for display only. Input controls process Boolean (or in the case of scroll bars, scalar) user input, and complex controls allow direct manipulation of the data (see Figures 3, 4, and 5). Developers already familiar with the standard controls offered by Windows may want to skip the next few sections.
Figure 3 Simple Controls
Control | Function | Data |
Static (general) | Display | Any data structure |
Static Text | Display, mnemonic activation | Character array: char szText[20]; |
Static Rect, Frame | Display | A Windows RECT structure |
Group Box | Display, mnemonic activation | Array of characters and a RECT structure |
Icon | Display | Handle to an icon, HICON |
Figure 4 Input Controls
Control | Function | Data |
Push button, momentary | Initiate action, display label of the action | Function call, pointer to function |
Push button, on/off | Select/display inclusive/exclusive options | A BOOL for a single control; an array of BOOLs for a set of controls. BOOL represents two possible states. |
Push button, on/off/other | Select/display inclusive/exclusive options allowing an undetermined state | A WORD for a single control; array of WORDs for a set of controls. A WORD can represent more than two states. |
Radio button | Select/display mutually exclusive options | Array of BOOLs, where only one BOOL is TRUE at any time. |
Check box, 2-state | Select/display mutually inclusive options | A BOOL for a single control; an array of BOOLs for a set of controls. BOOL represents two possible states. |
Check box, 3-state | Select/display mutually inclusive options allowing an undetermined state | A WORD for a single control; array of WORDs for a set of controls. A WORD can represent more than two states. |
Scroll bar | Initiate motion of displayed data | Any data. If the data is numeric, motion means increment or decrement. For graphical data, such as an HBITMAP, motion means scrolling the bitmap in some direction. |
Figure 5 Complex Controls
Control | Function | Data |
Edit control, single-line | String (contiguous) manipulation | Character array: char szText[n] where n is reasonably small. |
Edit control, multiple-line | Text (non-contiguous) manipulation, linked text structures | |
List box, single-selection | Select/display mutually exclusive options, manage linked list, directory search | Linked list, where the indexes of the next and previous list items are the links to those items; disk directory. |
List box, multiple-selection | Select/display mutually inclusive options, manage linked list, directory search | Linked list, as above; disk directory |
Combo box, single-selection | Select/display mutually exclusive options, manage linked list, directory search | Linked list, as above; disk directory, |
Combo box, multiple-selection | Select/display mutually inclusive options, manage linked list, directory search | Linked list, as above; disk directory, |
Simple controls can be thought of as "read-only" controls.They display data that can be any type from a simple BOOL to a complex data structure. All Windows static controls--text, frames, rectangles, and icons--also fall into this category (see Figure 3). Of course, the static class could easily be expanded to display data like a polygon, a floating point number, or something even more complex. Static controls do not process keyboard or mouse input. Exceptions to this are static text and group boxes, which activate the next enabled nonstatic control when the underlined character in the control’s text is pressed. A Windows group box consists of a character array and a RECT. But this control should be considered static (though it has a BS_GROUPBOX style indicating that it is of the button class) because it only displays data and does not process input.
Input controls (see Figure 4) combine the display function of simple controls with the ability to respond to keyboard and mouse input. Input controls include push buttons, check boxes, radio buttons, and scroll bars.
Push buttons can be either momentary switches or on/off switches. Momentary push buttons represent the initiation of an action; each button is labeled with text or an icon to represent the action. On/off push buttons represent the initiation or termination of an action and indicate a state. For example, the Microsoft Excel Toolbar has a button labeled B for boldfacing text. When that button is pressed, the currently selected text is boldfaced and the button stays down to indicate the bold state.
A push button-type control may also have a third "undetermined" state that is neither on nor off like the 3-state check boxes described below. (This state is not yet supported under Windows).
Check boxes and radio buttons allow selection from a fixed set of options: check boxes offer mutually inclusive choices (where no choice is an option) whereas radio buttons represent mutually exclusive choices (at least one must always be selected). Since mutual exclusion requires that one option is always selected, a single radio button is useless. A single check box has the same functionality as a single on/off switch. 3-state check boxes support a third, undetermined state, where the check box is neither checked nor unchecked.
Scroll bars usually represent both relative and absolute motion of some data display and indicate the current position of the scrollable data. The effect of motion depends on the type of data displayed; an integer type may be incremented or decremented where a bitmap may just shift position.
So far I have covered controls that display data or that accept input and translate it into an action or state change. Complex controls (see Figure 5) have all these qualities and more. They either support a variable number of options or allow users to directly manipulate the data contained in the control. The predefined complex controls are edit controls, list boxes, and combo boxes.
Edit controls come in two styles: single-line and multiline. Single-line edit controls are used for string manipulation. The string is manipulated and stored as a linear array of characters. Insertions and deletions move blocks of characters (the C run-time library string functions let you perform these operations), and become memory-intensive as the size of the linear array increases. Multiline edit controls store text in linked lists of structs. Each line of text is represented by a struct containing a character array and variables to maintain its position in the document. Insertions and deletions do not move memory, but rather update pointers to the new, previous, and next blocks of data.
Both types of edit controls contain basic character manipulation functions, such as copy, cut, and paste. Edit controls are the only standard controls that allow users to modify existing data directly (combo boxes, which also let you do this, contain edit controls). Overwrite is unsupported in Windows edit controls.
The next complex control, the list box, is used to present a variable list of options and comes in two forms: the single-selection or standard list box, where the user can select one option, and the multiple-selection list box, which is generated using the LBS_MULTIPLESEL style. A list box can be thought of as a scrollable window where each item in the window is accompanied by a radio button (single-selection) or a check box (multiple-selection). Applications send messages to add and remove items. However, since the visual interface does not allow an end user to change the list box’s contents, applications often provide push buttons to let the user add or delete items.
A list box uses a doubly linked list to hold its items. When an application adds or removes an item, these links are automatically updated. An application can store data in the list using LB_SETITEMDATA, retrieve data using LB_GETITEMDATA, and sort the list. You can create a hidden list box if you want to use the linked-list functionality without the visual interface.
List boxes also have the ability to list files that match certain attributes and file extensions that you specify. They also include functionality to find the first file and find the next files in an MS-DOSÒ directory.
What is called in Windows a drop-down list combo box (CBS_DROPDOWNLIST) is really more of a list box than a combo box. The drop-down list box is, in all senses, a list box that simply takes less space in its quiescent state. As in a normal list box, the user may only select from the displayed items; to allow the user to enter a new item, you have to use a combo box.
The combo box is the most complex standard control. It’s a combination of a list box and a single-line edit control. Combo boxes are like list boxes, but they also include a "none-of-the-above" entry where the user may enter a completely different item. The same combo box functionality could be implemented with a set of check boxes or radio buttons and an edit control. Combo boxes have an expanded style (CBS_SIMPLE) and a compact style (CBS_DROPDOWN).
Almost every serious developer has reached a point where standard controls can’t do the job completely, but a custom control would be expensive to implement. To justify creating a custom control, you must analyze the function it will perform and determine whether that function is already provided in a standard control. Using a standard control minimizes coding and lets you maintain a common user interface.
If you need to modify only the appearance of a standard control, use an owner-draw variant if available. Owner-draw controls require a small amount of extra coding, allowing you to concentrate on the visuals. You should try to maintain some visual similarity to the standard control to avoid confusing users.
If you require a slight change in the behavior (not the visual interface) of a standard control, use subclassing. Subclassing lets you do things like ignore the mouse in a list box or create a numbers-only edit control; it also involves a small amount of extra coding. You should avoid excessive subclassing, that is, intercepting and modifying a lot of the functionality of a control, since it is extremely difficult code to maintain. A subclassed control should behave similarly to the standard control so users can predict how the control will perform. If you need to change the visual interface and make a slight modification to the control’s behavior, you can subclass an owner-draw control.
If an owner-draw variant of a control is unavailable or your control’s behavior is quite different from the behavior of any standard control, your best choice is to write a custom control. This involves much more coding and requires you to design the visual interface.
If you’re going to write a custom control, you must decide whether to implement the control as private (in an application) or global (in a DLL).
A private control is available only to the application that contains it. This is suitable when the control is proprietary, when a separate DLL is unacceptable, or if the control’s code is incompatible with a DLL.
You can create a system extension that is shareable among all applications by implementing a global control in a DLL. To do this you need to register your class with the CS_GLOBALCLASS style. Global controls are the only choice when two or more applications will use the control.
When you decide to create a custom control, you should think about four design areas. First consider the control API functions. These should provide access to the functionality of the control without the need for a control window. The API should completely describe the functionality of the control. Concentrating on the API first helps you create an API that is independent of messages, window styles, and visuals. Second, determine the messages and message API functions that will be used in the control. These cause specific actions in the control window. Next, enumerate the different styles that will effect variations in the base control, such as orientation and behavior. Finally, after you know all the possible styles the control may have, sketch the visuals for each style.
Once these parts of the control are carefully thought-out, implementation is very straightforward and relatively painless. On the other hand, jumping into the implementation without any planning will invariably result in misguided decisions, an incomplete interface, excess debugging time, and a control that doesn’t fully meet your needs.
I’ll demonstrate these design guidelines with my MicroScroll custom control (see Figure 6). MicroScroll is similar to a miniature scroll bar; it’s designed to be placed next to an edit control and used as a spin button. A spin button allows a user to cycle through a set of values.
Figure 6 MUSTEST displays a MicroScroll control using MUSCROLL.DLL
The code compiles into MUSCROLL.DLL and includes code to interface with the Windows Software Development Kit (SDK) Dialog Editor. The Dialog Editor interface is described in the Windows 3.0 Guide to Programming, chapter 20, section 20.25 and also in "Extending the WindowsÔ 3.0 Interface with Installable Custom Controls", MSJ (Vol. 5, No. 4). The interface consists of three exports from your DLL: an Info function, a Style function, and a Flags function, as contained in MicroScroll’s CTLDLGED.C source file. These functions must be exported as ordinals 2, 3, and 4. Note that the Windows 3.0 SDK Dialog Editor incorrectly ignores any style changes in the custom control’s Styles dialog, which the Dialog Editor invokes by calling the DLL’s Style function. This prevents you from being able to change the vertical or horizontal style of the MicroScroll control, for example. This problem is corrected in the Windows SDK 3.1 Dialog Editor.
The MUSTEST program demonstrates how to create a spin button using the MicroScroll control. (All the source code can be found on any MSJ bulletin board--Ed.) Figure 6 shows two types of spin buttons implemented in MUSTEST.
A control’s classname must be unique in the system, even for private controls. The only exception is that two applications can register a class with identical names as long as each class does not have the CS_GLOBALCLASS style. For global controls implemented in a DLL, the module name in the control’s definition file must also be unique within the system. Since "MicroScroll" is too long for a module name, the sample control uses MUSCROLL.
Control API functions deal strictly with the control’s data manipulation. A control window has no functionality in itself but identifies an instance of data that the control manipulates through the control’s API. Furthermore, if a control DLL exports these functions, then an application can call these functions to manipulate the same data without the control.
The Windows lstrlen function is a good example of a possible control API. To illustrate this, let’s look at an application that uses a push button control. The push button control window retains its window text in memory, and when the control window procedure receives a WM_GETTEXTLENGTH message, it retrieves a pointer to its text, calls lstrlen, and returns the string length to the application:
LONG ButtonWndProc(HWND hWnd, WORD iMsg, WORD wParam,
LONG lParam)
{
LPSTR psz;
WORD cch;
switch (wParam)
{
o
o
o
case WM_GETTEXTLENGTH:
//Hypothetical function
psz=PszGetButtonTextPointer(hWnd);
cch=lstrlen(psz);
return (LONG)cch;
case WM_GETTEXT:
o
o
o
}
o
o
o
}
Since the lstrlen function is independent of a control, an application can use it to determine the length of any string.
It is your choice whether to export your control functions from a DLL. While creating and documenting these functions takes considerable time, it prevents you from having to create a hidden control just to use its functions. This wastes system resources and is far less convenient than directly calling functions. Creating these types of functions helps isolate the control’s visual and message implementation from the actual data manipulation.
Messages originating in the system, like WM_KEYDOWN or WM_LBUTTONDOWN, inform the control of an event or of a state change, such as WM_SIZE or WM_MOVE. These messages inform the control that something happened in the system that affects the control, whether it was direct user input or the result of some other user action.
When a control receives a system event message it usually needs to change its appearance, especially for messages generated as a result of mouse clicks and text entry. The easiest way to process such events is to follow a change-state-and-repaint strategy. Any code that affects the visual state of a control should change the state, invalidate the affected portion of the control, and force a paint on that area. A control’s WM_PAINT handler should therefore be written to check the control’s internal state and paint as appropriate.
In the MicroScroll control, look at the WM_ENABLE message case in MicroScrollWndProc:
case WM_ENABLE:
if (wParam)
StateClear(pMS, MUSTATE_GRAYED);
else
StateSet(pMS, MUSTATE_GRAYED);
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
break;
When the control receives WM_ENABLE, wParam is 1 if the control is enabled or 0 if disabled. On this message the control either clears or sets the state flag MUSTATE_GRAYED (StateClear and StateSet are macros to change flags), then forces an immediate repaint of the entire control through InvalidateRect and UpdateWindow. The paint procedure, LMicroScrollPaint, tests the MUSTATE_GRAYED flag and changes the arrow image of the control (see Figure 7). This strategy relies completely on WM_PAINT processing to handle all possible states, and thus avoids having paint code scattered around the control.
One easily forgotten but important state-change message is WM_CANCELMODE. This message is sent to a control’s window proc whenever the control is disabled or destroyed, when a message box or dialog box is displayed, when the user presses a system (Alt) key or otherwise moves, sizes, or changes a window. When a control receives this message, it should cancel states like mouse capture, and kill timers that are not used in the control’s quiescent state. By default, DefWindowProc calls ReleaseCapture if any window has the focus. The MicroScroll control uses this message to clear any states, kill two timers that might be operating, and release the capture.
Figure 7 Graying Out the Control
//Draw top and bottom buttons borders.
Draw3DButtonRect(hDC, rgHPen[MSCOLOR_HIGHLIGHT],
rgHPen[MSCOLOR_SHADOW],
0, 0, cx-1, cy2, xAdd1);
Draw3DButtonRect(hDC, rgHPen[MSCOLOR_HIGHLIGHT],
rgHPen[MSCOLOR_SHADOW],
0, cy2, cx-1, cy-1, xAdd2);
//Select default line color.
SelectObject(hDC, rgHPen[MSCOLOR_ARROW]);
//Draw the arrows depending on the enable state.
if (StateTest(pMS, MUSTATE_GRAYED))
{
/* Draw arrow color lines in the upper left of the
* top arrow and on the top of the bottom arrow.
* Pen was already selected as a default.
*/
MoveTo(hDC, cx2, cy4-2); //Top arrow
LineTo(hDC, cx2-3, cy4+1);
MoveTo(hDC, cx2-3, cy2+cy4-2); //Bottom arrow
LineTo(hDC, cx2+3, cy2+cy4-2);
/* Draw highlight color lines in the bottom of the
* top arrow and on the lower right of the bottom arrow.
*/
SelectObject(hDC, rgHPen[MSCOLOR_HIGHLIGHT]);
MoveTo(hDC, cx2-3, cy4+1); //Top arrow
LineTo(hDC, cx2+3, cy4+1);
MoveTo(hDC, cx2+3, cy2+cy4-2); //Bottom arrow
LineTo(hDC, cx2, cy2+cy4+1);
SetPixel(hDC, cx2, cy2+cy4+1, rgCr[MSCOLOR_HIGHLIGHT]);
}
Function messages cause controls to perform some action or manipulate data. There are two types of function messages: standard Windows messages and control-specific (sometimes referred to as user-defined) messages. Standard Windows messages have a meaning for all windows and controls, whereas control-specific messages apply only to a particular control (see Figure 8).
Figure 8 Windows and Control-Specific Messages
Message | Type | Meaning |
WM_SETTEXT | Windows | Changes the window’s text that is stored internally in Windows. |
WM_GETTEXTLENGTH | Windows | Retrieves the window text’s length. |
WM_SETFONT | Windows | Changes the font used by the window. A window must retain the font handle passed in this message since Windows does not automatically store it nor does it apply the font to default painting. |
WM_CUT | Windows | Instructs the window to delete any selection and store it in the Clipboard. |
WM_VSCROLL | Windows | Instructs the window to move the contents of its client area up or down. |
EM_SETSEL | Control | Changes the selection in an edit control. |
LB_DIR | Control | Fills a list box with the contents of a disk directory. |
MSM_HWNDASSOCIATESET | Control | In MicroScroll, this message changes the associated window stored in a control-specific data structure. MicroScroll sends all notifications and scroll messages to the associate, which may be any window. |
MSM_DWRANGESET | Control | In MicroScroll, this message changes the effective scroll range. |
All control-specific messages must be defined as WM_USER plus a positive number and must be placed in an include file accessible by the control and an application. The control-specific messages for the MicroScroll control are defined as WM_USER+1 through WM_USER+12 in MUSCROLL.H. Control-specific messages should also follow a naming convention where the first few characters in the name are similar to the class of the control, such as EM for edit and MSM for MicroScroll, where the last M stands for message.
When designing the set of function messages, try to create a GET for every SET; in other words, if you create a message to modify the control’s data (SET), implement a message to retrieve that data (GET). The MicroScroll control defines MSM_HWNDASSOCIATESET (makes the association between the control and the application window) with MSM_HWNDASSOCIATEGET, and MSM_DWRANGESET (sets the effective scroll range) with MSM_DWRANGEGET.
If an application can change the data it must be able to retrieve it. However, when the data is read-only, a SET message is not appropriate. For example, a matching SET message to LB_GETCOUNT is pointless since the count can only be changed by adding or deleting items in the list box.
All messages include two parameters that contain extra data for the function accessed by the message: a WORD wParam and a LONG lParam. The wParam generally contains an identification, index, or other strictly numeric data. The lParam may be a numeric value or a reference to a data structure. The reference could be a far pointer or a handle to global memory in the HIWORD of lParam and an offset into that memory in the LOWORD of lParam.
In general, applications should not post messages to a control, since you want to ensure that any data passed through a far pointer in lParam is valid when the control finally receives it. For example, freeing memory immediately after a PostMessage could cause a GP Fault:
//BAD CODE
hMem=GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, 100);
psz=GlobalLock(hMem);
PostMessage(hWndControl, WM_GETTEXT, 0, (LONG)psz);
GlobalUnlock(hMem);
GlobalFree(hMem);
In this example, the WM_GETTEXT message will not be received by the control until the application’s message loop retrieves it from the message queue and dispatches it to the control. However, the application already unlocked and freed the memory, rendering the pointer invalid. Replacing PostMessage with SendMessage solves the problem.
Posting messages can also cause problems with visual synchronization. You can never be sure when the control will process a posted message, but a sent message is processed immediately, before the application can continue.Then you can predict exactly what state the control is in when SendMessage returns.
Standard controls communicate control status changes to an application through a limited set of notification messages: WM_COMMAND, WM_HSCROLL, and WM_VSCROLL. When a control sends a notification, it informs the application of a state change. A state change can be modification to data or selections, changes in the control’s characteristics, or warnings that the control has encountered a possible fatal problem.
The WM_COMMAND message is used for most notification messages. Controls must follow the standard notification message format, where the wParam contains the control’s ID, the LOWORD of lParam contains the control’s window handle, and the HIWORD of lParam contains the notification code.
Notification codes are not related to message codes, so their defined values can start at any number. Be sure to give each code a symbolic name that accurately describes the notification and to define these codes in the same include file where you define control-specific messages. The accepted practice is to name these codes like you would name a control message but with an n for notification instead of m for message.
The MicroScroll control defines three notifications: MSN_ASSOCIATEGAIN, MSN_ASSOCIATELOSS, and MSN_RANGECHANGE, defined as the values 1, 2, and 3. The first two notifications are sent in response to an MSM_HWNDASSOCIATESET message; the newassociated window receives MSN_ASSOCIATEGAIN and the previously associated window receives MSN_ASSOCIATELOSS. The MSN_RANGECHANGE notification is sent to the associate window when the control receives MSM_DWRANGESET.
When you document the message interface to your control, be sure to explain when the control sends notification messages. When the application receives a notification code it might immediately send another message to the control, perhaps triggering the same notification back again, locking the application in an infinite loop. Complete message documentation helps prevent misuse and programming errors.
The notification message WM_PARENTNOTIFY is generated from within Windows when the control window is created, destroyed, or clicked with the mouse. Controls themselves do not generate this message--Windows sends the message from CreateWindow, DestroyWindow, and GetMessage when a mouse click is detected.
Message API functions are equivalents of the control-specific messages supported by the control. Functions, unlike messages, are not limited to single WORD and LONG parameters. If you want to pass a data structure through a message, you must fill that data structure and pass a pointer to it. This may be a serious inconvenience requiring one line of code to fill each field of the data structure.
Message API functions that match the control-specific messages eliminate this requirement. Where a message takes a far pointer to a structure, the function takes each field of the structure as a separate parameter.
TableCreateTable(hWnd, 36, 24, 28, 50,
WOODTYPE_MAHOGANY);
To the application, the coding effort is the same as sending a message. One line of code may be used where otherwise it would have to fill a structure. The code is also more readable, because the description of the function can be seen inline. With messages, the function that the message is expected to perform is buried deeper in the SendMessage call. Of course, if the application already has a pointer to the structure it uses SendMessage much more efficiently.
If there are too many fields in the data structure to pass as individual parameters (over 15 is a good cutoff point), you should reconsider the design of the control and the data structure. Can the data structure be broken up? Is it too complex for a single control to represent? Can users easily manipulate that data through the control’s user interface? Most controls will not have such large structures, but when they do you should reevaluate your design.
Using both messages and message API functions is pointless, since both effect the exact same actions in the control. There are two simple ways to prevent duplicating code.
The first is to call the equivalent message control API function when the control receives the message (see Figure 1). The control window should dereference each field of the data structure pointed to by lParam and pass each one as a parameter to the function. The second choice is to have the application call a message API function directly. When the message API function is called, it needs to allocate a temporary data structure, fill its fields with the parameters to the function, and send the equivalent message to the control’s window procedure.
The first method is better if the control manipulates large data structures, since processing each message consists of a few pointer dereferences and a single function call. Dereferencing a few fields from a pointer is usually very fast compared to the execution of the API function. The second method, applied in the MicroScroll control, centralizes the code in the control’s window procedure. It also allows the message API functions to be defined as macros if the functions can pass all their parameters in the WORD and LONG parameters of a message. However, macros are generally harder to debug, and they prevent an application from getting pointers to the functions.
A style is a control variation that is not distinct enough to be a new class. Styles generally fall into several categories where all styles in a particular category are mutually exclusive, but styles from different categories may be combined. Some categories are slight variations of the control class, such as CBS_SIMPLE for combo boxes. Other examples include variations in orientation (horizontal, vertical, diagonal), scroll direction (horizontal, vertical, diagonal, inverted), dimensions (fixed sizes, single or multiple lines or columns), selection response (as with LBS_MULTIPLESEL and LBS_EXTENDEDSEL), text alignment (left, right, centered, justified),and text alignment modifiers (wordwrap, rectangle, hyphenate, like SS_LEFTNOWORDWRAP in static controls. This list is by no means exhaustive.
Styles that cannot be classified are action modifiers. These are styles that affect the behavior (and possibly the display) of the control and that can be used with any other style, including other action modifiers (see Figure 9).
Figure 9 Action Modifier Styles
Style | Control | Action modification |
ES_PASSWORD | Edit | Causes the control to display a password character instead of the actual character typed. This style causes the control to modify the display but not the actual data. |
ES_UPPERCASE | Edit | Causes the control to convert all characters to uppercase as they are typed. This style causes the control to modify the data before it is displayed. |
LBS_SORT | List box | Causes all items in the list box to be sorted alphabetically. Modifies the control’s display but not the data. |
MSS_NOPEGSCROLL | MicroScroll | Prevents the control from sending repeated scroll messages when the position is at a minimum or maximum. Modifies the control’s behavior but not the display or the data. |
You should restrict an application’s ability to modify control-specific styles by copying the style bits into private data (such as the DLL’s local heap), and requiring applications to use messages and message API functions to modify those styles. This prevents applications from arbitrarily modifying style bits through SetWindowLong. The problem with that is the control is not notified of such a style change and may never notice.
In the MicroScroll control, all control-specific styles are copied into private data when the control is created. The only styles that the control allows an application to change are MSS_NOPEGSCROLL and MSS_INVERTRANGE, and so the control supports the MSM_FNOPEGSCROLLSET and MSM_FINVERTRANGESET messages. Furthermore, since there should be a Get for every Set, the control also allows an application to retrieve the state of the style through the MSM_FNOPEGSCROLLGET and MSM_FINVERTRANGEGET message.
Since visuals are the only components of a control that use graphics, they must also be sensitive to system colors and track changes in those colors.
Windows operates on a variety of display adapters, so your visuals must be device-independent. This means that you should avoid fixed-size bitmaps, assumptions about aspect ratios, and assumptions about the number of colors available. The MicroScroll control draws its visuals with lines, rectangles, and polygons in proportion to the size of the control, so if the display driver changes and the size of the control changes, the control still looks the same.
You should design the control to allow for change, for as the system changes you will want to improve your control as well. A great way to design for change, as applied in the MicroScroll control, is to put any graphical code in the WM_PAINT case, using the change-state-and-repaint strategy discussed earlier.
Before writing code to produce visuals, you must take some time to determine how the control will look in various states. Some general states are normal (quiescent); with focus or without focus; disabled (grayed); clicked (depending where the click occurred); key press (or character entry); dragging (such as a scroll bar thumb) or when the control has mouse capture, selection (single or multiple items); and control-specific states, like "pending query" on a control that handles SQL database queries.
Draw the control and avoid the temptation to figure out the implementation immediately. During this creative phase you should not consider any Windows API limitations (like BitBlt and LineTo) just yet. Concentrate on the colors and visual features of the control. You will usually find a way to implement even the most complex control visual with the available API later on. Figure 10 shows the visual representations of the MicroScroll control in its normal, clicked, and disabled states.
Figure 10 MicroScroll Visual States
Consider how the control will use both system colors and application-supplied colors. Always assume that your personal color preferences are unique and that no one else on this or any other planet will be using the same color scheme. Hardcoding your preferences into a control (or any application for that matter) is a very poor practice.
Not all system colors affect a control, but those that do should be monitored (see Figure 11). When designing the control’s visuals, specify where it will use the important system colors, as shown in Figure 10.
Figure 11
COLOR_WINDOW | Generally used for the background of the control to match dialog boxes and standard controls (with the exception of push buttons). |
COLOR_WINDOWTEXT | The default text color used by standard controls (with the exception of push buttons). |
COLOR_WINDOWFRAME | Used to draw separator lines (such as the border of a scroll bar thumb) and control borders. Although most users set this color to black, you must assume that it can be anything. |
COLOR_BTNTEXT | Used for the text (foreground) color in controls similar to push buttons. |
COLOR_BTNFACE | Used for the non-highlighted and non-shadowed parts of controls similar to push buttons, and as the background color for text. |
COLOR_BTNSHADOW | Used for the shadow lines on controls similar to push buttons with a 3-D beveled appearance. The highlight portion on a 3-D button control is generally white. |
COLOR_GRAYTEXT | Used to draw gray text in a disabled control. |
COLOR_HIGHLIGHT | Used as the background color of a selected item in a control such as the selected item in a list box or the highlighted text in an edit control. |
COLOR_HIGHLIGHTTEXT | Used as the foreground color of a selected item. COLOR_HIGHLIGHT is the background color. |
Always remember that these colors will not stay constant during the lifetime of the control; do not save system colors only once when the control is created and use those colors until the control is destroyed. The control’s painting function should call GetSysColor to retrieve a color when it is needed. GetSysColor is an extremely fast function, so the overhead of calling it is minimal compared to actually drawing graphics.
Finally, consider adding control-specific function messages to manipulate all the colors used in the control, as the messages will make the color configurable for applications using the control. Configurable color is a frequent reason why applications subclass standard controls--since standard controls lack support for configurable colors--and why developers write custom controls. By allowing any application to affect your control’s color use, you may relieve others from having to write custom controls. The MicroScroll control supports messages to govern its use of color as shown in Figure 12.
The MUSTEST test application demonstrates how to use the MSM_CRCOLORSET message to make MicroScroll blue instead of the usual gray.
Figure 12 Color Messages in MicroScroll
Message | Affected Color |
MSM_CRCOLORSET | Changes a specific color use by the control, returning the previous value. |
MSM_CRCOLORGET | Retrieves a specific color. |
In both messages wParam is an index that can be one of the following: |
MSCOLOR_FACE | Button face color, COLOR_BTNFACE by default. | |
MSCOLOR_ARROW | Arrow color, COLOR_BTNTEXT by default. | |
MSCOLOR_SHADOW | Dark shadow line color, COLOR_BTNSHADOW by default. | |
MSCOLOR_HIGHLIGHT | Bright highlight line color, white by default. |
In MSM_CRCOLORSET lParam contains the new value for the indexed color. |
The MicroScroll control is built from a number of source files, each isolating particular functions (see Figure 13).
Figure 13 MicroScroll Components
MAKEFILE | Make file |
MUSCROLL.LNK | Linker response file, referenced in MAKEFILE |
MUSCROLL.DEF | Module definitions file |
MUSCROLL.H | Public definitions for the control |
MUSCRDLL.H | Private definitions for the control DLL |
LIBENTRY.ASM | LibEntry, the DLL’s entry-point function |
INIT.C | Initialization code |
MUSCROLL.C | MicroScrollWndProc, the main message procedure |
PAINT.C | LMicroScrollPaint, the WM_PAINT handler for the control |
MSAPI.C | LMicroScrollAPI, handler for control-specific messages |
CTLDLG.C | Dialog Editor interface functions |
WEP.C | WEP, required exit procedure for a DLL |
MUSCROLL.RC | Resources for the control DLL |
CTLSTYLE.DLG | Style dialog definition, referenced by MUSCROLL.RC |
All these files are available on any MSJ bulletin board. |
MAKEFILE builds the MicroScroll DLL (MUSCROLL.DLL). MUSCROLL.DEF, referenced in MUSCROLL.LNK, defines segments, heapsize, and the exports from the DLL. The exports in the DEF file that deal with the Dialog Editor interface must have the ordinals 2through 6.
MUSCROLL.H contains everything that an application needs when using the MicroScroll control: the control messages, notification codes, API function prototypes, styles, and indexes for use with the messages and an API to effect color changes. MUSCRDLL.H contains everything internal to the control, such as its private data structures, state flags, macros to manipulate state flags, miscellaneous ID values, and internal function prototypes.
The actual source code for MicroScroll is distributed among a number of files, both to allow precise control of segment flags (in MUSCROLL.DEF) and to provide some organization to the code, making it easier to find specific pieces. LIBENTRY.ASM is the standard Windows SDK DLL entry code, defining the LibEntry function. LibEntry calls LibMain, which resides in INIT.C. INIT.C also contains four other functions. FRegisterControl, called from LibMain, registers the MicroScroll window class. LMicroScrollCreate is called from MicroScrollWndProc (see below) to handle the WM_NCCREATE and WM_CREATE messages, using the functions FTextParse and WTranslateUpToChar to parse the control’s text to determine the initial scroll range and position.
MUSCROLL.C contains MicroScrollWndProc, the main message-handling procedure for the MicroScroll control. It also contains two helper functions, ClickedRectCalc (for calculating regions to repaint) and PositionChange (to change the current position and send scroll messages as necessary). The control’s logic is contained in MicroScrollWndProc. Any WM_PAINT message to the control is passed to LMicroScrollPaint in PAINT.C, which also contains the helper function Draw3DButtonRect. All visual changes and color changes are handled in PAINT.C.
The MSAPI.C file contains LMicroScrollAPI, the handler for all control-specific messages, and all message API functions, such as MSCrColorSet. CTLDLG.C contains the Dialog Editor interface functions, including a Style dialog box invoked when the user double-clicks on the control in the Dialog Editor. WEP.C simply contains the required exit procedure for a Windows DLL.
MUSCROLL.RC defines strings for the control, such as text for each supported style that the Dialog Editor places in a DLG file. MUSCROLL.RC references CTLSTYLE.DLG, which defines the dialog box invoked from within the SDK Dialog Editor when the user wishes to change the control’s style.
When the MicroScroll control receives WM_NCCREATE or WM_CREATE messages, they are passed to LMicroScrollCreate in INIT.C. On WM_NCCREATE the control allocates space for a MUSCROLL data structure and stores its handle in the control’s window extra bytes. If the allocation fails, it can return 0 from WM_NCCREATE to stop creation before other actions take place.
On WM_CREATE, LMicroScrollCreate initializes the contents of the MUSCROLL structure, since by the time the control receives this message the structure has been allocated. If the control has the MSS_TEXTHASRANGE style, then the initial window text is parsed using FTextParse and the initial range and position is set. During WM_CREATE the control also copies its initial style, clears all special states (such as a clicked state), and sets all colors to default.
MicroScrollWndProc, the message-handling procedure found in MUSCROLL.C, handles only a few messages. Any control-specific message is passed to LMicroScrollAPI (in MSAPI.C). The messages that MicroScrollWndProc processes are listed in Figure 14.
Whenever the control needs to invalidate half of the control, it calls CalcClickedRect. This function fills a RECT structure with the coordinates of the half of the control that is affected, based on the current state of the control.
PositionChange is called to update the control’s current position and sends WM_VSCROLL and WM_HSCROLL messages if necessary. No messages are sent if the MSS_NOPEGSCROLL is set and the actual position did not change.
Figure 14 Messages Handled by MicroScrollWndProc
Message | Processing |
WM_NCCREATE | Passed to LMicroScrollCreate to allocate memory. |
WM_CREATE | Passed to LMicroScrollCreate to initialize control-specific data. |
WM_NCDESTROY | Frees memory containing control-specific data. |
WM_PAINT | Calls LMicroScrollPaint to draw any visuals. |
WM_ENABLE | Changes the MUSTATE_GRAYED bit in the control’s state flags and forces a repaint. |
WM_SHOWWINDOW | Changes the MUSTATE_HIDDEN bit in the control’s state flags. No repaint is necessary since hiding or showing a window automatically causes a total repaint. |
WM_CANCELMODE | Clears all states, destroys timers, and releases the mouse capture. |
WM_TIMER | On the first long timer, which is an initial delay before scrolling repeats, the first timer is destroyed and a second, shorter timer created. On any timer message where the mouse is still over the control, another scroll message is sent to the associate window. |
WM_LBUTTONDOWN | Handles hit-testing of the control to determine which half was clicked, then changes the state of the control to reflect that click and forces a repaint. Also sets the initial delay timer. |
WM_LBUTTONDBLCLK | Same as WM_LBUTTONDOWN. |
WM_MOUSEMOVE | Determines if the mouse is still within the control. If the mouse moves out of the control’s window, the MUSTATE_MOUSEOUT bit is set; otherwise that bit is cleared. If the bit changes, then the control is repainted to reflect that state. |
WM_LBUTTONUP | Destroys any timers, clears all states, and forces a repaint, bringing the control back to its normal state. |
The MSAPI.C file contains LMicroScrollAPI and all message API functions, such as MSHwndAssociateSet. Each message API function simply translates to a SendMessage that eventually returns to invoke LMicroScrollAPI. If these functions called LMicroScrollAPI directly, they would each need to retrieve a pointer to the control’s MUSCROLL structure to pass on. This may seem like a reasonable alternative, but it saves little time: SendMessage to another function in your task is quite fast, and allows MicroScrollWndProc to handle the details of getting a pointer to the control’s data. The messages and API functions are documented individually in Figure 15.
Figure 15 MicroScroll Messages and Message API Functions
If the wParam or lParam message parameters are not mentioned, they are not used. All message API functions take a window handle to identify the control to be affected.
MSM_HWNDASSOCIATESET
HWND FAR PASCAL MSHAssociateSet(HWND hWnd, HWND hWndAssociate);
Changes the current associate window of the control. The control always sends a WM_COMMAND message with the MSN_ASSOCIATELOSS to the previous associate window and a MSN_ASSOCIATEGAIN to the new associate window.
Parameters | Message | Function | Definition |
wParam | hWndAssociate | New associate window handle |
Return Value | Previous associate window handle; contained in low-order word if returned from SendMessage |
MSM_HWNDASSOCIATEGET
HWND FAR PASCAL MSHAssociateGet(HWND hWnd);
Returns the current associate window of the control
Parameters | None |
Return Value | Current associate window handle; contained in low-order word if returned from SendMessage |
MSM_DWRANGESET
DWORD FAR PASCAL MSDwRangeSet(HWND hWnd, WORD iMin, WORD iMax);
Changes the current range of the control, moving the position if the current position is outside of the new range. The control always sends a WM_COMMAND message with the MSN_RANGECHANGE notification to the associate window. If the current position is outside the new range, the position is set to the middle of the range and the control sends a scroll message with SB_THUMBTRACK.
Parameters | Message | Function | Definition |
LOWORD(lParam) | iMin | New minimum of the range | |
HIWORD(lParam) | iMax | New maximum of the range |
Return Value | Previous range minimum is in the low-order word, maximum in the high-order word |
MSM_DWRANGEGET
DWORD FAR PASCAL MSDwRangeGet(HWND hWnd);
Returns the current range of the control
Parameters | None |
Return Value | Current range minimum is in the low-order word, maximum in the high-order word |
MSM_WCURRENTPOSSET
WORD FAR PASCAL MSWCurrentPosSet(HWND hWnd, WORD iPos);
Changes the current position of the control if the new position is within the current range. If the new position is in the current range, the control always sends a scroll message with the SB_THUMTRACK code notification to the associate window.
Parameters | Message | Function | Definition |
wParam | iPos | New position |
Return Value | Previous position; contained in low-order word if returned from SendMessage; -1 is returned if the new position is not in the current range |
MSM_WCURRENTPOSGET
WORD FAR PASCAL MSWCurrentPosGet(HWND hWnd);
Returns the current position of the control
Parameters | None |
Return Value | Current position; contained in low-order word if returned from SendMessage |
MSM_FNOPEGSCROLLSET
BOOL FAR PASCAL MSFNoPegScrollSet(HWND hWnd, BOOL fNoPegScroll);
Changes the MSS_NOPEGSCROLL style bit in the control
Parameters | Message | Function | Definition |
wParam | fNoPegScroll to clear it | TRUE to set the MSS_NOPEGSCROLL style or FALSE |
Return Value | Previous state of the MSS_NOPEGSCROLL style bit; contained in the low-order word if returned from SendMessage |
MSM_FNOPEGSCROLLGET
BOOL FAR PASCAL MSFNoPegScrollGet(HWND hWnd);
Returns the current state of the the MSS_NOPEGSCROLL style bit in the control
Parameters | None |
Return Value | Current state of the MSS_NOPEGSCROLL style bit; contained in low-order word if returned from SendMessage |
MSM_FINVERTRANGESET
BOOL FAR PASCAL MSFInvertRangeSet(HWND hWnd, BOOL fInvertRange);
Changes the MSS_INVERTRANGE style bit in the control
Parameters | Message | Function | Definition |
wParam | fInvertRange to clear it | TRUE to set the MSS_INVERTRANGE style or FALSE |
Return Value | Previous state of the MSS_INVERTRANGE style bit; contained in the low-order word if returned from SendMessage |
MSM_FINVERTRANGEGET
BOOL FAR PASCAL MSFInvertRangeGet(HWND hWnd);
Returns the current state of the the MSS_INVERTRANGE style bit in the control
Parameters | None |
Return Value | Current state of the MSS_INVERTRANGE style bit; contained in low-order word if returned from SendMessage |
MSM_CRCOLORSET
COLORREF FAR PASCAL MSCrColorSet(HWND hWnd, WORD iColor, COLORREF cr);
Changes a specified color used in the control
Parameters | Message | Function | Definition |
wParam | iColor | Index of the color to change. Can be one of the following: |
MSCOLOR_FACE | Face color | ||||
MSCOLOR_ARROW | Arrow color | ||||
MSCOLOR_SHADOW | Dark shadowing color | ||||
MSCOLOR_HIGHLIGHT | Bright highlight color | ||||
MSCOLOR_FRAME | Outer frame color |
lParam | cr | New COLORREF value for the color |
Return Value | Previous color for the given index; 0L if the color index is out of range |
MSM_CRCOLORGET
COLORREF FAR PASCAL MSCrColorGet(HWND hWnd, WORD iColor);
Returns a specified color used in the control
Parameters | Message | Function | Definition |
wParam | iColor | Index of the color to change. Can be one of the following: |
MSCOLOR_FACE | Face color | ||||
MSCOLOR_ARROW | Arrow color | ||||
MSCOLOR_SHADOW | Dark shadowing color | ||||
MSCOLOR_HIGHLIGHT | Bright highlight color | ||||
MSCOLOR_FRAME | Outer frame color |
Return Value | Current color for the given index; -1 if the control is using a default color, 0L if the color index is out of range |
A control often needs extra memory to track its internal states and hold private data. An obvious place for this memory is the control’s window extra bytes. However, extra bytes are inconvenient and a scarce system resource. Creating a large number of windows that each use a large number of window extra bytes quickly runs available system resources down to zero.
A much better method, applied in the MicroScroll control, is to declare only enough extra bytes to store a memory handle to a control-specific data structure. The FRegisterControl function in INIT.C specifies CBWINDOWEXTRA (defined as sizeof(HANDLE)) as the cbWndExtra field of the WNDCLASS structure (see Figure 16). The LMicroScrollCreate function, also in INIT.C, allocates memory for a MUSCROLL structure on the WM_NCCREATE message, since it can return 0L from this message to abort the window creation if the allocation fails. The memory is freed in the WM_NCDESTROY message case of MicroScrollWndProc in MUSCROLL.C.
Since the memory is allocated as LMEM_FIXED, the handle is also a local pointer. MicroScrollWndProc simply does a single GetWindowWord call on entry to get a pointer to the MUSCROLL structure.
Allocating a separate data structure is not only better for the system, but has many advantages over using window extra bytes and functions like GetWindowWord.
First, retrieving a structure pointer once and dereferencing fields in that structure is much faster than repeated calls to GetWindowWord. This is especially important for accessing data for very frequent messages like WM_MOUSEMOVE.
Second, GetWindowWord and GetWindowLong restrict the size of data you can manipulate in extra bytes to a single word and a single long. A private structure lets you easily manipulate arrays or floating-point values. For example, the MUSCROLL data structure containsan array of four COLORREFs for storing application-supplied colors.
Third, fields in a structure are much easier to use in expressions, resulting in cleaner code and fewer local variables to hold values retrieved from window extra bytes.
Fourth, you can easily get a pointer to a field in a structure, but you cannot get a pointer to window extra bytes, necessitating a local variable.
And finally, all changes made to fields in the structure take effect immediately. With the alternative approach, you must call SetWindowWord or SetWindowLong to change window extra bytes.
If an application requires additional data for the control, it should allocate memory for a separate data structure and attach the handle to the control as a property (with the SetProp function). Storing a memory handle as a single property gives the application these same advantages for manipulating this data.
1For ease of reading, "Windows" refers to the Microsoft Windows operating system. Windows is a trademark that refers only to this Microsoft product and is not intended to refer to such products generally.