Marc Adler
Learning Windows is a series of articles that explores programming the Microsoft WindowsÔ graphical environment Version 3.0. In the previous two articles I discussed the event-driven, message-passing nature of Windows1, initialization of an application, resource files, menu systems, and accelerators. In addition, I designed the skeleton of the sample stock-charting application. This article explains the types of control windows that Windows supports. I will also continue developing the stock-charting application by adding Multiple Document Interface (MDI) support.
Let's begin by reviewing the term window. A window is a data object that has certain properties associated with it. It may occupy an area on the screen, but it doesn't have to. It can be invisible during the entire course of an application. (One use for a hidden window is to act like a "traffic cop," monitoring the activity in the application and processing messages. In OS/2 Presentation Manager, these windows are known as object windows.)
A window's appearance and behavior is determined mostly by its window procedure (WinProc). Windows sends messages to a WinProc whenever something "interesting" happens to the window it is associated with. A default window procedure supplied by Windows, DefWindowProc, has a default action for every message sent to a window. DefWindowProc defines the behavior of a generic window. To give a different behavior or appearance to a window or an object within it, some or all of these messages must be handled by code in your WinProc, instead of DefWindowProc.
For instance, you can process mouse messages to tell an on-screen control how to react when the control is clicked. Imagine implementing a custom light switch control, where a switch is toggled up and down when clicked on. When the mouse is clicked, the program should determine whether the switch is in the up or down position. Then the program should redraw the switch in the opposite position. This means intercepting the WM_LBUTTONDOWN message, negating the value of the state variable that is internal to this control, and redrawing the control, possibly using a different bitmap to represent the on and off positions. By writing varying amounts of message-processing code yourself, you can make a window act in any conceivable way.
Controls
Control windows are objects that assist a user in interacting with an application. Predefined Windows controls include scroll bars, radio buttons, check boxes, push buttons, list boxes, combo boxes, edit fields, and static text objects. You can design your own custom control windows, or purchase others from several third-party manufacturers. As Windows becomes more popular, you will see more custom control libraries appear on the marketplace, especially because Windows 3.0 introduces owner-draw controls. (For more about custom controls, see "Extending the WindowsÔ 3.0 Interface with Installable Custom Controls," MSJ, Vol. 5, No. 4.)
There are two ways to create controls. You can use the CreateWindow function (and, in Windows Version 3.0, the function CreateWindowEx) or you can use the dialog editor and define controls as part of a dialog box. Although the latter method is more common, let's begin by discussing CreateWindow.
The first article ("Learning WindowsÔ Part I: The Message-based Paradigm," MSJ, Vol. 5, No. 4) showed you how to use CreateWindow to create the stock-charting application's main window. Two parameters of the CreateWindow function are of particular interest: the name of the window class and the handle to the parent window.
In the Windows environment, windows can have a parent-child relationship. Child windows appear inside the window that owns them, known as the parent. Child windows are clipped to the parent window's client area (the main, initially empty space of the window-the part that is not the caption bar, window border, menu bar, and scroll bars), meaning that any part of the child window extending past its parent window's borders will not be displayed. Certain behavioral aspects are inherited by the child from the parent. If a parent window is hidden or destroyed, its children are hidden or destroyed as well. The same thing goes for the children of the child windows, and so on. When a window is moved, any children are moved along with it. For control windows, the most important aspect of the parent-child relationship is that when certain events happen to a control window, it notifies its parent window of the event by sending it a WM_COMMAND message. (A scroll bar control sends a WM_VSCROLL or WM_HSCROLL message.)
In the stock-charting application, the main window did not have a parent. You can see this by examining the hParent parameter of the CreateWindow function that created it. If the hParent parameter is NULL, the window has no parent. A window without a parent is called a top-level window. (Actually, the top-level window is a child of the desktop window, but this does not ever affect the Windows programmer.)
Control windows are almost always children of another window. So, when you use CreateWindow to create a control, you must specify the handle of a window as its parent.
As far as the class name of a control goes, Windows predefines a number of control window classes. A list of the control class names and the types of controls that you can create in each class is found in Figure 1.
Using CreateWindow
When you use CreateWindow to create a control, you must give one of the strings in Figure 1 as the class name if you are using a predefined control class. Often you must specify one or more flags for the third argument of the CreateWindow call. The following call creates a push button control.
hButton = CreateWindow("Button",
"&OK",
BS_PUSHBUTTON | WS_CHILD | WS_TABSTOP,
100, 75, 32, 16,
hWndMain,
IDOK,
hInstance,
(LPSTR) NULL);
This call has several interesting features. First, note the ampersand in the button's text, which indicates an accelerator. Second, the BS_PUSHBUTTON flag is one of of the style bits, which informs Windows that you wish to create a push button. Figure 2 contains a complete list of the flags you can use for each window class. The WS_CHILD flag tells Windows that this is a child window and to use hWndMain as the parent.
Finally, note that the value of the hMenu parameter is IDOK. IDOK, a value defined in WINDOWS.H, is usually used for OK buttons. When you create a child window, the value in the hMenu parameter must be the control identifier of the window, not a handle to a menu. Windows overloads the menu handle and the control identifier fields, so that when you create a top-level window, the value of this parameter is considered the menu handle; when you create a child window, this value is considered the control identifier.
The control identifier is a number that differentiates a child/control window from any other at that moment in your application. Because it is used in many of the messages passed between the parent and the control and in some Windows functions (such as CheckRadioButton), a control identifier should be unique.
Dialog Boxes and Controls
Dialog boxes will really be covered in the next article. However, control windows and dialog boxes are inextricable, so I am forced to digress a little.
A dialog box is a window containing a number of control windows. It can be created on-the-fly, or, more commonly, defined in a resource file and loaded as needed. A dialog box can be modal or modeless. When a modal dialog box is displayed, the user cannot switch to another window in the same program. He or she must respond to the dialog box in some way, often by clicking an OK or Cancel push button. The user can switch to another program, unless the dialog box is a system modal dialog box, which must be responded to before the user does anything else in Windows. When the user brings up a modal dialog box, its parent window is disabled and the dialog box manager (the Windows code that creates the dialog box window and processes its messages) goes into its own internal message loop. The user must fill in the controls and accept or cancel the dialog box before the application continues with its own message loop. Modeless dialog boxes impose no restrictions on the user, who can move the
focus on and off the modeless dialog box freely before responding to it.
Two of the more important functions of the dialog box manager are processing keystrokes and assigning the input focus to the appropriate control. For example, when you press Tab or Backspace, the input focus should move to the next or previous control. When you press Enter, the default push button control should be activated. When you press Alt with another key, the dialog box manager must see if that keystroke corresponds to the accelerator associated with a particular control.
If you use CreateWindow to create control windows instead of going through the dialog box manager, your application must handle keystrokes itself and decide if they cause a change of focus. This is a non-trivial task better left to the dialog box manager.
Static Controls
In this article, assume that all control windows (except scroll bars) are associated with dialog boxes.
A static control differs from other types of controls in two respects. A static control cannot have the input focus, so you cannot give it keyboard or mouse input. Also, a static control does not notify its parent when an interesting event occurs.
Static controls are used mainly to display text strings and rectangular frames. For typical static controls, see any dialog box containing an edit control: the text labeling the edit control is a static control window. It may seem strange to have an entire window structure (and therefore, a WinProc) behind simple text strings, but Windows offers maximum flexibility and extensibility for even the simplest objects.
A style flag is associated with each type of static control. For hollow frames, you can specify SS_BLACKFRAME, SS_GRAYFRAME, or SS_WHITEFRAME. For solid rectangles, you can specify SS_BLACKRECT, SS_GRAYRECT, or SS_WHITERECT. Text can be displayed left-, center-, or right-justified by specifying SS_LEFT, SS_CENTER, or SS_RIGHT. The SS_SIMPLE style is used to display a simple rectangle with a single line of left-justified text. To disable the ampersand processing that associates an accelerator with a static control, specify the SS_NOPREFIX style. Finally, you can draw your own static controls using the SS_USERITEM style.
Edit Controls
Windows supports both single-line and multiline data entry controls. Actually, you can fashion a simple text editor just by creating a standard Windows multiline edit control-it's that powerful. The default capabilities give you rudimentary cursor motion (left, right, up, down, home/end of line, home/end of buffer, next/previous word) and cutting and pasting. In Windows 3.0, edit controls interpret tab stops correctly. Of course, the mouse is fully supported. A multiline edit control will also automatically perform wordwrapping.
A multiline edit control has the ES_MULTILINE style. You can attach vertical and/or horizontal scroll bars
by specifying the ES_AUTOVSCROLL and ES_AUTOHSCROLL styles. If you attach scroll bars to an edit control, the edit control processes its own scroll
bar messages.
In Windows 3.0, the ES_LEFT, ES_CENTER, and ES_RIGHT styles function properly. These flags tell the edit control to left-, center-, or right-justify the text within the edit window (see Figure 3).
Windows is still weak in its data verification capabilities. Subclassing edit controls is a popular pastime among developers who need this kind of power. Windows 3.0 implements three new edit styles that ameliorate this somewhat. The ES_LOWERCASE and ES_UPPERCASE styles transform keyboard input into all lowercase or all uppercase characters. The ES_PASSWORD style directs an edit control to display a user-defined masking character instead of the normal text. The default masking character is an asterisk, but you can change this by using the EM_SETPASSWORDCHAR message. Hopefully the need for formatted data entry fields will be addressed in future versions of Windows.
Edit Control Messages
The edit control supports a great number of messages. Some of these messages direct the edit control to perform an editing operation on the text, while others query the state of the control.
Most edit control messages begin with the prefix EN_. The four messages without this prefix direct an edit control to perform a cut-and-paste action. All text cut, copied, or pasted from an edit control is sent to the Windows Clipboard.
The WM_CUT message copies the highlighted text to the clipboard and then deletes the text from the edit control. The WM_CLEAR message simple deletes the text. The WM_PASTE message inserts the text in the clipboard at the current editing position. The WM_UNDO control reverses the last editing operation. An edit control is limited to only one undo. In these messages, the wParam and lParam are not used, so you can use 0 as their values.
Two of the more frequently used edit control messages deal with querying and setting the selected area of text. The EM_GETSEL message returns the starting position of the selected text in the LOWORD of the return value and the position of the end of the selection plus 1 in the HIWORD of the return value. If no text is selected, the current cursor is returned in the LOWORD.
DWORD ulRet = SendMessage(hEdit, EM_GETSEL, 0, 0L);
WORD iStart = LOWORD(ulRet);
WORD iEnd = HIWORD(ulRet);
The EM_SETSEL message sets the area of the selected text. The starting and ending positions of the selection are contained in lParam. If the LOWORD of lParam is 0 and the HIWORD is 7FFFH, the entire contents of the edit field will be selected.
SendMessage(hEdit, EM_SETSEL, 0, MAKELONG(0, 0x7FFF));
You can replace the selected text with another piece of text using the EM_REPLACESEL message. In this message, lParam is a far pointer to the text string that will be inserted in place of the selected text.
If the edit control is in a dialog box, you can use two functions to retrieve and set the contents of an edit control. All you need to know is the handle of the dialog box and the control identifier of the edit control. GetDlgItemText copies a specified number of characters from the edit control into a user-defined buffer. SetDlgItemText sets the contents of an edit control a certain string. Here is a function that changes the contents of an edit control to uppercase:
GetDlgItemText(hDlg, ID_EDIT,(LPSTR) szBuf,
sizeof(szBuf)-1);
strupr(szBuf);
SetDlgItemText(hDlg, ID_EDIT,szBuf);
There are many more edit messages that may be of use with a multiline edit control. Refer to the Microsoft Windows Software Development Kit (SDK) for a complete description.
The notification messages in Figure 4 are generated by an edit control when certain events occur and are sent to the edit control's parent window.
Button Controls
A button is a generic term for a control that generates a message and possibly changes its appearance when a user clicks on it (see Figure 5). Button controls include check boxes, radio buttons, push buttons, and group boxes. A group box doesn't respond when you click on it; it is used only to contain a group of one of the other kinds of buttons.
When a button is clicked, it sends one or more WM_COMMAND messages to its parent window. The WM_COMMAND message notifies the parent that an interesting event has happened involving a button. The wParam of the message is set to the control identifier of the window. The LOWORD of lParam is set to the window handle of the control, and the HIWORD is set to the notification code. The notification code tells exactly which event occurred, and depending on this information, you might have to dismiss a dialog box or simply tell Windows to alter the state of a button.
In addition, Windows supports owner-draw buttons. These are buttons the programmer draws. Windows sends owner-draw buttons a message when it needs to be drawn in both its pressed and unpressed states. You can have a lot of fun drawing buttons that resemble real objects. For instance, a musical Windows application could implement buttons shaped like the ones on stereo equipment.
Push Buttons
Push buttons are rectangular objects containing descriptive text in the center. Unlike check boxes and radio buttons, which are mainly used to show the state of an object visually, push buttons invoke action. When the user clicks a push button, it usually means that the user wants to perform an activity, such as dismissing a dialog box or activating a help window.
There is only one style of push button, indicated by the BS_PUSHBUTTON flag. A push button can have a special property. If a user presses Enter in a dialog box, the dialog box manager will determine if there is a default push button control in the dialog box. If there is, the dialog box manager will simulate its pressing; in other words, the dialog box manager will generate a WM_COMMAND message with the identifier of the default push button in the wParam.
Two Windows messages deal with default push buttons. The DM_GETDEFID message asks a dialog box if there is a default push button in it. The DM_SETDEFID message lets you make any push button control the default. The following code determines if the OK button is the current default button in the dialog box. If it is, it makes the CANCEL button the default push button.
DWORD ulRet = SendMessage(hDlg, DM_GETDEFID, 0, 0L);
if (HIWORD(ulRet) = = DC_HASDEFID && LOWORD(ulRet) = = IDOK)
SendMessage(hDlg, DM_SETDEFID, IDCANCEL, 0L);
Check Boxes
A check box is a button control with two parts: a small rectangular frame that is either empty or contains a check mark, and a text string describing the button positioned on the right side of the frame. Check boxes indicate properties that are either on or off. For example, in a word processing program, a check box allows the user to turn wordwrap on or off.
A standard check box has either the BS_CHECKBOX or the BS_AUTOCHECKBOX style. Windows automatically toggles the check state of a check box with the auto style when the user clicks on it. If it does not have the auto style, Windows sends a WM_COMMAND message to the check box's parent when the check box is clicked. Then CheckDlgButton must be called to check or uncheck the check box.
A variation of the standard check box control is the three-state check box. In addition to a checked/unchecked state, a three-state check box has a grayed state when disabled. A three-state box has either the BS_3STATE or the BS_AUTO3STATE style. Other than that, they are the same as standard check boxes.
Two API functions exist for setting and querying the state of a check box. CheckDlgButton sets the state of a standard check box or a three-state check box. Its format is shown below.
CheckDlgButton(HWND hDlg, int idButton, WORD wCheck);
The handle of the dialog box that contains this check box is hDlg, and idButton is the control identifier of the check box. wCheck is 0 if the button is unchecked, 1 if the button is checked, and for three-state buttons, 2 if it is grayed. The CheckDlgButton function simply determines the window handle of the check box and sends a BM_SETCHECK message to the check box's WinProc.
The state of a check box can be determined by using the IsDlgButtonChecked function.
IsDlgButtonChecked(HWND hDlg, int idButton);
This function returns 0 if the check box is not checked, 1 if it is, and 2 for grayed three-state buttons.
Radio Buttons
Picture a car radio from the 1970s, before they went digital. There were usually five button presets. When you pressed one button, all the others would pop out. The Windows radio button class works the same way. Radio buttons always come in groups of two or more. When the user clicks one, the button is on, and all the other radio buttons in that group are off.
A standard radio button is defined with the BS_RADIOBUTTON style. Like check boxes, there is a BS_AUTORADIOBUTTON style where Windows takes care of the checking and unchecking for you.
The CheckDlgButton and the IsDlgButtonChecked functions set and test the state of a radio button. You can also use the CheckRadioButton function to check one button in the group.
CheckRadioButton(HWND hDlg, int idFirst, int idLast,
int idCheckButton);
The second and third arguments of this function specify the control identifiers of the first and last radio buttons in a group. The fourth argument gives the control identifier of the radio button that you want to check. For this function to work effectively, all the radio buttons in the group should be numbered consecutively from idFirst to idLast. CheckRadioButton enumerates all the controls with identifiers in the range of idFirst to idLast, and sends BM_SETCHECK messages to them with wParam set to 0. Then it sends a BM_SETCHECK message with wParam set to 1 to the radio button passed in the last argument.
Group Boxes
Group boxes aren't really buttons, as I said previously. Simple rectangles used to enclose a group of buttons, group boxes are more like static text frames than buttons. They cannot accept input, nor can they generate WM_COMMAND messages, so they are used mainly for visual aesthetics or as a place marker for the end of a button group.
List Boxes
A list box is a control window containing a list of strings (in Windows 3.0, it can be a list of anything able to be displayed, but I will concentrate on strings for the time being). The user selects one or more strings from the list box by highlighting a string or by clicking on it. If all the strings cannot fit in the window, vertical or horizontal scroll bars can be used to scroll through the strings.
Windows 3.0 beefed up the capabilities of the list box control. A list box can now grow horizontally instead of vertically so it can have more than one column. Any kind of item can be displayed in a list box, not just strings. List boxes now can be tabbed to align columns. An extended list box can be created, in which certain key combinations select multiple items. In fact, the Windows SDK contains a program called OWNCOMBO that demonstrates some of these new capabilities. You should take a few minutes to compile and run it. The available list box styles are shown in Figure 6.
By default, a list box supports a single string selection and the strings are unsorted. To sort the strings, you must specify the LBS_SORT style. You should also specify the LBS_NOTIFY style if you want the list box to send WM_COMMAND messages to its parent when something interesting happens. The WINDOWS.H file defines the LBS_STANDARD style; it's simply a combination of LBS_SORT and LBS_NOTIFY.
Now that I have defined a list box, I want to be able to add strings to it. To add a string, send a message to the list box with the lParam set to a far pointer to the string. There are two messages used to add strings, LB_ADDSTRING and LB_INSERTSTRING. LB_ADDSTRING appends a string to the end of the list box. If the list box is sorted, LB_ADDSTRING inserts the string in the sorted position.
SendMessage(hListBox, LB_ADDSTRING, 0,
(DWORD) (LPSTR) "Hello");
LB_INSERTSTRING inserts a string at any specified position in the list box. The zero-based position passed in the wParam will be adhered to, even if the list box is sorted. If you want to put the string at the end of the list box, you must pass 1 in wParam.
SendMessage(hListBox, LB_INSERTSTRING, 12,
(DWORD) (LPSTR) "Hello");
Windows allocates a 64Kb global memory block for each list box. This means that the sum of the lengths of all
of the strings in a list box cannot be greater than 64Kb.
If you exceed this limit, the LB_INSERTSTRING or LB_ADDSTRING message returns the value of LB_ERRSPACE to your application.
To delete a string from the list box, use the LB_DELETESTRING message and pass the zero-based index of the string in wParam.
SendMessage(hListBox, LB_DELETESTRING, 5, 0L);
Unfortunately, no single message in Windows allows you to change the contents of a string. You must delete the string and then reinsert it.
If you append or insert strings in a visible list box, an annoying phenomenon will take place. The list box refreshes itself after each string is added-not an aesthetically pleasing sight for the user. To avoid this, you have to tell the list box not to refresh itself until all of the strings have been added. The WM_SETREDRAW message sets or clears the list box's auto-refresh flag. Before you start inserting strings, call SendMessage with the third parameter set to FALSE.
SendMessage(hListBox, WM_SETREDRAW, FALSE, 0L);
It is now safe to insert strings. After the last string
has been added, reenable the refresh flag by calling SendMessage with the third parameter TRUE.
SendMessage(hListBox, WM_SETREDRAW, TRUE, 0L);
Once the strings are in the list box and the user interacts with it, a rich set of messages is available to query the current state of the list box. The LB_GETCURSEL message returns the index of the currently selected item in a single-selection list box. If you would like to retrieve the text associated with that item, use the LB_GETTEXT message.
int iSel;
char szBuf[128];
if ((iSel = SendMessage(hListBox, LB_GETCURSEL, 0,
0L)) != LB_ERR)
SendMessage(hListBox, LB_GETTEXT, iSel, (DWORD)
(LPSTR) szBuf);
To change the current selection to a different string, send the LB_SETCURSEL message with wParam set to the zero-based index of the string to select. To search for
a string containing a certain prefix, you can use the LB_FINDSTRING message. To do both of these things, use LB_SELECTSTRING, which finds a string with a given prefix and makes it the current selection.
Multiple-Selection List Boxes
In a multiple-selection list box, more than one string can be selected at a time. Suppose you want to present the user with a list of files to delete. It would be a hassle to select one file, press delete, select another, press delete again, and so on. Instead, using a multiple-selection list box, the user could mark several files, and press delete once.
Although most of the list box messages can be used for both single- and multiple-selection list boxes, several do not overlap. The LB_GETCURSEL and LB_SETCURSEL messages can only be used with single-selection list boxes, since there cannot be a single currently selected item in a multiple-selection list box. Instead, you use the LB_GETSEL and LB_SETSEL messages to query and set the selection state of an item.
To use these messages effectively, you may need to know how many items are in the list box. You can determine the number of entries in the list box by sending the LB_GETCOUNT message. Once you have the number of items, you can march down the list box and query or set the selection state of each item. The following fragment of code reverses the selection state of every item in the list box.
int nItems, i;
BOOL bisSelected;
nItems = (WORD) SendMessage(hListBox, LB_GETCOUNT, 0,
0L);
for (i = 0; i < nItems; i++)
{
bIsSelected = (BOOL) SendMessage(hListBox,
LB_GETSEL, i, 0L);
SendMessage(hListBox, LB_SETSEL, !bIsSelected,
(DWORD) i);
}
Two new Windows 3.0 messages help in the programming of multiple-selection list boxes. LB_GETSELCOUNT returns the number of items selected within a multiple-selection list box. LB_GETSELITEMS fills a buffer with an array of integers that are the indices of all of the selected items. The next fragment of code demonstrates these two messages.
int nItems;
nItems = (WORD) SendMessage(hListBox, LB_GETSELCOUNT,
0, 0L);
if (nItems != LB_ERR)
{
HANDLE haItems = LocalAlloc(LMEM_MOVEABLE,
nItems * sizeof(int));
LPINT lpaItems = (LPINT) LocalLock(haItems);
SendMessage(hListBox, LB_GETSELITEMS, nItems,
(DWORD) lpaItems);
o
o
o
}
The new proportionally spaced system font in Windows 3.0 may disarray Windows 2.x applications that relied on the old monospace font to line up the contents of a list box. However, the new tab stop support can be used to line up columnar data. To do so, you must give the list box the LBS_USETABSTOPS style. The default position for tab stops is one every 32 horizontal dialog box units. (A horizontal dialog box unit is 1/4 of the dialog base width unit, a figure based on the width of the current system font.) To set your own tabs, you can use the new LB_SETTABSTOPS message.
Multiple-Column List boxes
Windows 3.0 supports list boxes that scroll horizontally or vertically, but not both. A list box that scrolls horizontally is called a multiple-column list box and is defined using the LBS_MULTICOLUMN style. Each column in the list box is given a default width of 15 characters. To change the column width, use the LB_SETCOLUMNWIDTH message. The new width is passed in wParam. Unfortunately, you cannot have variable width columns-but then again, if you did, there would be nothing to wish for in future versions of Windows.
List Box Notification Codes
Like any control, a list box notifies its parent window when certain things happen to it. Remember that the notification process occurs only if the list box has the LBS_NOTIFY or LBS_STANDARD styles. A list of these notification codes is found in Figure 7.
The LBN_SETFOCUS and LBN_KILLFOCUS messages, new to Windows 3.0, inform the parent when the focus is gained or lost by a list box. The LBN_SETFOCUS message can be used, say, to highlight a default selection when the user tabs into a list box.
The LBN_SELCHANGE message is used frequently when another control tracks the selected text in a list box. For example, almost all Windows file-directory dialog boxes have both a list box containing the file, directory, and drive names, and an edit control in which you enter the name of a file. In most cases, if the focus is set to the list box, the text of the currently selected item is displayed in the edit control. To do this, you need to know when the current selection in the list box changes. The LBN_SELCHANGE message is just the thing you need.
Each time the list box selection changes, a WM_COMMAND message is sent to the dialog box with the LBN_SELCHANGE notification code in the HIWORD of the lParam. You can then retrieve the text of the currently selected item using the code shown earlier, and set the contents of the edit control to that text with the SetDlgItemText function.
The LBN_DBLCLK message is generated when the user double clicks on a list box entry. Double-clicking usually means that the user wants to select a string and terminate the dialog box.
Owner-Draw List Boxes
The previous article discussed owner-draw menu items, in which Windows gives total responsibility to the application for displaying a menu item. This concept is carried over to list box controls, so that the user can choose anything in a list box control, not just alphanumeric text strings. This enables more powerful Windows user-interfaces-you can have a picture of an object rather than a descriptive text string. If the stock-charting application supported different types of graphs, it could prompt the user for the type of graph to display by showing a picture of the type of graph within a list box, not just a text string describing the graph. A miniature picture of a scatter graph would be much more useful than the words "Scatter Graph."
To create an owner-draw list box, you must give your
list box the LBS_OWNERDRAWFIXED or the LBS_OWNERDRAWVARIABLE styles. A list box created with the LBS_OWNERDRAWFIXED style assumes that all items in the list box will be of uniform height, while a list box created with the LBS_OWNERDRAWVARIABLE must know the height of every item. If you wanted to fill a list box with strings, with each string drawn in a different font (maybe to query the user for a font style selection), you would give the list box the LBS_HASSTRINGS style in addition to one of the two styles mentioned above.
Assume that you created an owner-draw list box and want to add items to it. You can use the regular LB_ADDSTRING or LB_INSERTSTRING functions. However, the lParam does not have to be a far pointer to a string, unless the list box has the LBS_HASSTRINGS style. It can be any user-defined 32-bit value. This value can be a handle to a bitmap, an index into an array, or anything that can be used to map a list box item to a graphical object.
Consider the stock-charting application. I would like to give the user a choice of grid styles to use when drawing the grid lines of a graph. To draw the grid lines, I use lines created with different Windows pens. (A pen is a bitmap that is used to draw a line.) Windows has a solid pen and several dashed pens. An owner-draw list box answers my needs perfectly. Instead of trying to describe the pen styles with a text string, I can create a list box in which each item is a line drawn with a different pen.
The first thing I do is create a list box using the LBS_OWNERDRAWFIXED style, since the size of each line will be the same. Then I add the items to it. Each item has a user-defined value associated with it, which are the Windows-defined values for each pen style.
/* Fill the list box with pen IDs */
int iPen;
for (iPen = 0; iPen < PS_NULL; iPen++)
SendMessage(hPenListBox, LB_ADDSTRING, 0, (LONG) iPen);
What if an owner-draw list box had to be in some kind of order (that is, it has the LBS_SORT style)? The list box control does not know how to sort graphical objects. Fortunately, Windows 3.0 has a new message, WM_COMPAREITEM, which is sent to the parent window of the list box whenever the list
box needs to sort two objects. When this message is
sent, the value of lParam is a far pointer to a COMPAREITEMSTRUCT data structure. Among the elements of this structure are the indices and the user-defined values of the two items being compared. One of three possible values will be returned: 0 if the objects are equal, 1 if item 1 sorts before item 2, and 1 if item 1 sorts after item 2.
Windows needs to know the size of each item in an owner-draw list box. To determine this, the WM_MEASUREITEM message is sent to the parent of the list box. If a list box has the LBS_OWNERDRAWFIXED style, this message is sent only once, since all items
will be the same size. However, if it has the LBS_OWNERDRAWVARIABLE style, this message is sent once for each item in the list box. The value of lParam for the WM_MEASUREITEM message is a far pointer to a MEASUREITEMSTRUCT data structure. The previous article showed the details of the MEASUREITEMSTRUCT data structure, so I won't go into it here.
Assume that each item in the pen-style list box has a width of 42 pixels and a height of 20 pixels. The following code processes the WM_MEASUREITEM message.
case WM_MEASUREITEM :
{
LPMEASUREITEMSTRUCT lpMI = (LPMEASUREITEMSTRUCT) (LPSTR) lParam;
lpMI->itemWidth = 42;
lpMI->itemHeight = 20;
break;
}
Finally, to draw each item, Windows sends the list box's parent the WM_DRAWITEM message. I'm borrowing a little code from the OWNCOMBO SDK demo program to show how to process this message (see Figure 8). I need to
be able to draw a list box item in its normal, unselected state. I also have to draw an item in its selected state, and if the item is in a multiple-selection list box, I need to draw the item in its focus state. (When a standard list box text string has the focus, Windows draws a dashed frame around the string.)
When an item is deleted from an owner-draw list
box, Windows sends the parent window the WM_DELETEITEM message. In this message, lParam points to a DELETEITEMSTRUCT data structure. This structure contains the index and the user-defined value corresponding to that item. WM_DELETEITEM gives the user a chance to free resources associated with that item (such as a bitmap or a brush).
Stock-ChartingApplication
The stock-charting application's controls will really come into play once I have covered dialog boxes in the next installment. But since I do want to add functionality to the application in this article, and since "A New Multiple Document Interface API Simplifies MDI Application Development," MSJ (Vol. 5, No. 4) is probably still fresh in your mind, I put an MDI shell over the application. MDI really suits this application well. Each stock graph can be considered a separate MDI document, and the application as a whole seems to fall nicely into the user-interface scheme supported by the MDI specification (see Figure 9).
I tied the File New menu item to the MDI child creation routine. When this menu item is chosen, a new MDI child window is created and placed at a default position within the client window. There is nothing displayed in the child windows yet; as I develop the application further, I will fill these windows with the stock graphs.
The Window pull-down menu allows you to position the MDI children in certain ways on the screen (see Figure 10). You can automatically tile or cascade the child windows by choosing the Window Tile and the Window Cascade menu selections. Any window can be iconized; the icons are automatically arranged nicely at the bottom of the MDI client window. You can close all the children at once by choosing the Window Close All item.
Some of the MDI code was shamelessly borrowed from the MULTIPAD demo program that comes with the SDK. There are many good demos and programming techniques available for examination in the SDK. I strongly suggest that if you are new to Windows, or if you want to catch up on some of the new features in Windows 3.0, you study some of this code.
The first change in the new version of the stock-
charting application is new logic in the main loop to recognize the MDI accelerator keys. So, before you call
the TranslateAccelerator function, you need to call
TranslateMDISysAccel. If any of these functions return a nonzero value, the keystroke was translated.
if (!TranslateMDISysAccel(hwndMDIClient, &msg) &&
!TranslateAccelerator(hWndMain, hAccelTable,
&msg))
The second change was registering a window class for the MDI child windows. Nothing in this code reveals that these child windows will be MDI children.
/* Register MDI chld cls */
wc.lpfnWndProc = GraphWndProc;
wc.hIcon = LoadIcon(
hThisInstance,
MAKEINTRESOURCE(ID_GRAPH));
wc.lpszMenuName = NULL;
wc.cbWndExtra = CBWNDEXTRA;
wc.lpszClassName = "GraphWindow";
if (!RegisterClass(&wc))
{
return FALSE;
}
The third task was creating the MDI client window. This needs to be done when the main window is created. An easy way to do this is to create the MDI client window in response to the WM_CREATE message sent to the main window. The WM_CREATE message informs the WinProc that a window of a particular class was just created and informs the programmer that initialization of that window should be done. You fill in the two elements that comprise the MDI CLIENTCREATESTRUCT data structure and call CreateWindow to create the MDI client.
case WM_CREATE:
{
CLIENTCREATESTRUCT ccs;
/* Find window menu where children will be listed */
ccs.hWindowMenu = GetSubMenu(GetMenu(hWnd), 3);
ccs.idFirstChild = ID_WINDOW_CHILDREN;
/* Create the MDI client filling the client area */
hwndMDIClient = CreateWindow("mdiclient",
NULL,
WS_CHILD |
WS_CLIPCHILDREN |
WS_VSCROLL|WS_HSCROLL,
0,0,0,0,
hWnd,
0xCAC,
hThisInstance,
(LPSTR) &ccs);
ShowWindow(hwndMDIClient, SW_SHOW);
You also need to add logic to the main window's WinProc to process some commands that deal with MDI. This code tells the MDI client to tile, cascade, and close the MDI children and to arrange the iconized windows.
Be aware that the default WinProc for a window containing an MDI client window is not DefWindowProc. Instead you must call DefFrameProc to handle all of the default message processing.
default :
/*
We might have chosen to change the focus to
one of the MDI children.
*/
return DefFrameProc(hWnd, hwndMDIClient, msg,
wParam, lParam);
The final change needed for MDI support is to create an MDI child window in response to the user choosing the File New menu item. Process the WM_COMMAND message in the WinProc for the main window; if wParam is the identifier ID_NEW, call GraphCreateWindow.
All that GraphCreateWindow does is fill out an MDICREATESTRUCT data structure and send a WM_MDICREATE message to the MDI client window (see Figure 11). The MDICREATESTRUCT contains information about the MDI child's class, caption title, dimensions, instance handle, and style.
The WinProc for an MDI child window has no special processing associated with it, except that you must call DefMDIChildProc instead of DefWindowProc when you do not want it to process a message.
default:
/* Again, since the MDI default behavior is a
* little different,call DefMDIChildProc instead
* of DefWindowProc()
*/
return DefMDIChildProc(hWnd, msg, wParam, lParam);
That's all there is to it! You just added MDI to the stock-charting application with a few statements. In previous versions of Windows, MDI was extremely complicated, but Windows 3.0 makes it much easier.
The next article concludes the discussion of control windows, exploring combo boxes and scroll bars. The main topic will be the use of dialog boxes, with a brief discussion of some of the third-party tools that can assist you in creating a dialog box. I will integrate our knowledge of controls with dialog boxes and add these to the stock-charting application.
1For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally.
Figure 1. Windows Control Styles
Scrollbar Horizontal or vertical scroll bar
Edit Single- or multiline edit field
Listbox Single- or multiple-selection list box
Combobox Simple, drop-down, or drop-down list combo box
Static Left-, centered, or right-justified text strings, frames
Button Push button, radio button, check box, group box
MDIClient Multiple Document Interface client window (this is not a control window)
Figure 2. Window Styles
Style Meaning DS_LOCALEDIT Specifies that edit controls in the dialog box will use memory in the application's data segment. By default, all edit controls in dialog boxes use memory outside the application's data segment. This feature may be suppressed by adding the DS_LOCALEDIT flag to the STYLE command for the dialog box. If this flag is not used, EM_GETHANDLE and EM_ SETHANDLE messages must not be used since the storage for the control is not in the application's data segment. This feature does not affect edit controls created outside of dialog boxes. DS_MODALFRAME Creates a dialog box with a modal dialog-box frame that can be combined with a title bar and system menu by specifying the WS_CAPTION and WS_SYSMENU styles. DS_NOIDLEMSG Suppresses WM_ENTERIDLE messages that Windows would otherwise send to the owner of the dialog box while the dialog box is displayed. DS_SYSMODAL Creates a system-modal dialog box. WS_BORDER Creates a window that has a border. WS_CAPTION Creates a window that has a title bar (implies the WS_BORDER style). This style cannot be used with the WS_DLGFRAME style. WS_CHILD Creates a child window. Cannot be used with the WS_POPUP style. WS_CHILDWINDOW Creates a child window that has the WS_CHILD style. WS_CLIPCHILDREN Excludes the area occupied by child windows when drawing within the parent window. Used when creating the parent window. WS_CLIPSIBLINGS Clips child windows relative to each other; that is, when a particular child window receives a paint message, the WS_CLIPSIBLINGS style clips all other overlapped child windows out of the region of the child window to be updated. (If WS_CLIPSIBLINGS is not given and child windows overlap, it is possible, when drawing within the client area of a child window, to draw within the client area of a neighboring child window.) For use with the WS_CHILD style only. WS_DISABLED Creates a window that is initially disabled. WS_DLGFRAME Creates a window with a double border but no title. WS_GROUP Specifies the first control of a group of controls in which the user can move from one control to the next by using the arrow keys. All controls defined with the WS_GROUP style after the first control belong to the same group. The next control with the WS_GROUP style ends the style group and starts the next group (that is, one group ends where the next begins). Only dialog boxes use this style. WS_HSCROLL Creates a window that has a horizontal scroll bar. WS_ICONIC Creates a window that is initially iconic. For use with the WS_OVERLAPPED style only. WS_MAXIMIZE Creates a window of maximum size. WS_MAXIMIZEBOX Creates a window that has a maximize box. WS_MINIMIZE Creates a window of minimum size. WS_MINIMIZEBOX Creates a window that has a minimize box. WS_OVERLAPPED Creates an overlapped window. An overlapped window has a caption and a border. WS_OVERLAPPEDWINDOW Creates an overlapped window having the WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, and WS_MAXIMIZEBOX styles. WS_POPUP Creates a pop-up window. Cannot be used with the WS_ CHILD style. WS_POPUPWINDOW Creates a pop-up window that has the WS_BORDER, WS_POPUP, and WS_SYSMENU styles. The WS_CAPTION style must be combined with the WS_POPUPWINDOW style to make the system menu visible. WS_SYSMENU Creates a window that has a system-menu box in its title bar. Used only for windows with title bars. WS_TABSTOP Specifies one of any number of controls through which the user can move by using the Tab key. The Tab key moves the user to the next control specified by the WS_TABSTOP style. Only dialog boxes use this style. WS_THICKFRAME Creates a window with a thick frame that can be used to size the window. WS_VISIBLE Creates a window that is initially visible. This applies to overlapped and pop-up windows. For overlapped windows, the Y parameter is used as a ShowWindow function parameter. WS_VSCROLL Creates a window that has a vertical scroll bar.
Figure 4. Edit Notification Messages
EN_CHANGE Indicates that the user has taken some action that may have changed the content of the text. EN_ERRSPACE Indicates that the edit control is out of space. EN_HSCROLL Indicates that the user has clicked the edit control's horizontal scroll bar with the mouse; the parent window is notified before the screen is updated. EN_KILLFOCUS Indicates that the edit control has lost the input focus. EN_MAXTEXT Specifies that the current insertion has exceeded a specified number of characters for the edit control. EN_SETFOCUS Indicates that the edit control has obtained the input focus. EN_UPDATE Specifies that the edit control will display altered text. EN_VSCROLL Indicates that the user has clicked the edit control's vertical scroll bar with the mouse; the parent window is notified before the screen is updated. Figure 7. List Box Notification Codes
LBN_DBLCLK Sent when the user double-clicks a string with the mouse LBN_ERRSPACE Sent when the system is out of memory LBN_KILLFOCUS Indicates that a list box has lost input focus LBN_SELCHANGE Sent when the selection has been changed LBN_SETFOCUS Indicates that the list box has received input focus
Figure 8. The WM_DRAWITEM Message
case WM_DRAWITEM :
{
LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) (LPSTR) lParam;
switch (lpdis->itemAction)
{
case ODA_DRAWENTIRE:
DrawEntireItem(lpdis, -4);
break;
case ODA_SELECT:
HandleSelectionState(lpdis, 0);
break;
case ODA_FOCUS:
HandleFocusState(lpdis, -2);
break;
}
break;
}
/********************************************************************
*
* FUNCTION: HandleSelectionState(LPDRAWITEMSTRUCT, int)
*
* PURPOSE: Handles a change in an item selection state. If an item
* is selected, a black rectangular frame is drawn around
* that item; if an item is de-selected, the frame is
* removed.
*
* COMMENT: The black selection frame is slightly larger than the
* gray focus frame so they won't paint over each other.
*
*******************************************************************/
void FAR PASCAL HandleSelectionState(lpdis, inflate)
LPDRAWITEMSTRUCT lpdis;
int inflate;
{
RECT rc;
HBRUSH hbr;
/* Resize rectangle to place selection frame outside of the focus
* frame and the item.
*/
CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem);
InflateRect ((LPRECT)&rc, inflate, inflate);
if (lpdis->itemState & ODS_SELECTED)
{
/* selecting item -- paint a black frame */
hbr = GetStockObject(BLACK_BRUSH);
}
else
{
/* de-selecting item -- remove frame */
hbr = CreateSolidBrush(GetSysColor(COLOR_WINDOW));
}
FrameRect(lpdis->hDC, (LPRECT)&rc, hbr);
DeleteObject (hbr);
}
/*******************************************************************
*
* FUNCTION: HandleFocusState(LPDRAWITEMSTRUCT, int)
*
* PURPOSE: Handle a change in item focus state. If an item gains
* the input focus, a gray rectangular frame is drawn
* around that item; if an item loses the input focus,
* the gray frame is removed.
*
* COMMENT: The gray focus frame is slightly smaller than the black
* selection frame so they won't paint over each other.
*
*******************************************************************/
void FAR PASCAL HandleFocusState(lpdis, inflate)
LPDRAWITEMSTRUCT lpdis;
int inflate;
{
RECT rc;
HBRUSH hbr;
/* Resize rectangle to place focus frame between the selection
* frame and the item.
*/
CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem);
InflateRect ((LPRECT)&rc, inflate, inflate);
if (lpdis->itemState & ODS_FOCUS)
{
/* gaining input focus -- paint a gray frame */
hbr = GetStockObject(GRAY_BRUSH);
}
else
{
/* losing input focus -- remove (paint over) frame */
hbr = CreateSolidBrush(GetSysColor(COLOR_WINDOW));
}
FrameRect(lpdis->hDC, (LPRECT)&rc, hbr);
DeleteObject (hbr);
}
/********************************************************************
*
* FUNCTION : DrawEntireItem(LPDRAWITEMSTRUCT, int)
*
* PURPOSE : Draws an item and frames it with a selection frame
* and/or a focus frame when appropriate.
*
*******************************************************************/
void FAR PASCAL DrawEntireItem(lpdis, inflate)
LPDRAWITEMSTRUCT lpdis;
int inflate;
{
RECT rc;
HANDLE hOldPen;
HPEN hPen;
/* Resize rectangle to leave space for frames */
CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem);
InflateRect ((LPRECT)&rc, inflate, inflate);
hPen = CreatePen((int) lpdis->itemData, 1, RGB(0, 0, 0));
hOldPen = SelectObject(lpdis->hDC, hPen);
MoveTo(lpdis->hDC, rc.left, rc.top + (rc.bottom - rc.top) / 2);
LineTo(lpdis->hDC, rc.right, rc.top + (rc.bottom - rc.top) / 2);
SelectObject(lpdis->hDC, hOldPen);
DeleteObject(hPen);
/* Draw or erase appropriate frames */
HandleSelectionState(lpdis, inflate + 4);
HandleFocusState(lpdis, inflate + 2);
}
Figure 11. MIDICREATESTRUCT
MDICREATESTRUCT mcs;
static int nChildren = 1;
if (!lpName)
{
/* If the lpName parameter is NULL, load the "Untitled" string
* from STRINGTABLE and set the title field of the MDI
* CreateStruct.
*/
sprintf(sz, "(Untitled - %d)", nChildren++);
mcs.szTitle = (LPSTR) sz;
}
else
{
/* Title the window with the supplied filename */
AnsiUpper(lpName);
mcs.szTitle = lpName;
}
mcs.szClass = "GraphWindow";
mcs.hOwner = hThisInstance;
/* Use the default size for the window */
mcs.x = mcs.cx = CW_USEDEFAULT;
mcs.y = mcs.cy = CW_USEDEFAULT;
/* Set the style DWORD of the window to default */
mcs.style = 0L;
/* tell the MDI Client to create the child */
hWnd = (WORD) SendMessage(hwndMDIClient,
WM_MDICREATE,
0,
(LONG) (LPMDICREATESTRUCT) &mcs);
return hWnd;