This Chapter describes
a windows application based on a dialogue box which allows to
enter a list of names and sort the list in ascending or descending
order.
Contents of this chapter
To compile and modify the program you need the following files:
DIALOG.C - the main code file
DIALOG.DEF - the module definition file, required only for Win16
DIALOG.H - Header file
DIALOG.RC - the main resource
file
VERSINFO.RC - Information about program version, Included by Example.RC
DIALOG.ICO - Icon file containing program icon in binary
form
DLGW16.MAK - Project file for Visual C++ 1.x
DLGW32.MAK - Project file for Visual C++ 2.x
DIALOG.IDE - Project file for Borland C++ 4.x for both
Win16 and Win32
DIALOG16.EXE - 16-bit executable file for Windows 3.x
DIALOG32.EXE - 32-bit executable file for Windows 95/
Windows NT
Click here to download the archive to your computer.
As I mentioned before, with this approach writing code only comes second. What you start with here, once you've created the project in the IDE is to call the ResourceWorkschop (Application Studio for the Microsofties) by double clicking on the .RC file. The RC files is created automatically by the resource editor, but since it is a plain ASCII file you can modify afterwards like any other text file. In this case we want to create a menu and a dialogue template for our main window. Most dialogues do not require menus, but I've implemented one here just to show you how it can be done.
In order to design our application window now, we first create a new resource of type Dialog (oooh, they speak American English...). You will be provided with an dialogue window that is empty except for the three default buttons OK, Cancel and Help that you will find in most dialogues. As we don't need them here they can be deleted. Now you add controls to this dialogue and place and size them as you want. There are five types of standard controls that you can use in Windows 3.1 and some more if you've got a resource editor for Windows95. Each control also has properties with determine the style and behaviour of the control. To access the properties double click on a control. If you want to find our more about a particular option in the property dialogue press the help button (I keep on saying that because most people don't use help files very much). At runtime i.e. during program execution you can send messages to controls in order to set them up or check their state and you will receive so called "Notification Messages" if the user changed data or the state of a control.
Here's a brief description for the six standard controls:
Now you've got the basic information you need and you can design something fancy. In all my years of Windows programming experience the design of dialogue boxes is still the most difficult and time consuming task. It sometimes takes me up to a couple of days to make a dialogue box look nice and intuitive to use. But if you are less of a perfectionist you can of course just throw the controls into the dialogue window. However it is not a very good idea to do it quick now, and perfectionise it later, because if you change the design after you've done the coding you'll face a lot more work than doing the design properly in the first place. The position and size of controls is not a problem since you can always change that without changing your code, but if you later on decide you need radio buttons instead of check boxes or you need to turn this list box into a combo box say, your up shit creek.
Another issue is the creation of control identifiers. In order to send and receive message from controls you need to give each of them a unique identifier which is simply an integer number. Both ResourceWorkshop and Application Studio help you by creating identifiers automatically although I prefer doing it myself. It's a bit more work, but you know exactly what's going on. Let me give you an example on this can be done:
Suppose you've got a dialogue which looks somewhat like this:
Then the resource script created by the ResourceWorkshop for this dialogue looks like this:
DIALOG_1 DIALOG 64, 99, 207, 46 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Authorisation" FONT 8, "MS Sans Serif" { CONTROL "Enter your name:", -1, "STATIC", SS_RIGHT | WS_CHILD | WS_VISIBLE | WS_GROUP, -1, 18, 60, 8 CONTROL "", IDC_EDIT1, "EDIT", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 64, 16, 79, 12 CONTROL "&OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 148, 6, 50, 14 CONTROL "&Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 148, 24, 50, 14 }
The first line gives the dialogue name DIALOG_1 followed by the type of resource which is DIALOG and the position and size of it. Now change DIALOG_1 into something more meaningful like DLG_AUTHORISE and add a define statement in a header file which you include in both your code and the resource file. For example call the header file DIALOG.H and the follwoing line:
#define DLG_AUTHORISE 100
Then do the same thing with the identifier for the edit control which is set to IDC_EDIT1. If you change that to IDC_NAME say, you have to add a define for that as well in the header file like e.g.
#define IDC_NAME 100
Note that both values can be 100 since the first one is a resource and the second one only a identifier within this resource. By the way, IDC_ stands for IDentifier Control as you might have guessed. But you don't need to stick to it. I normally start with the name of the dialogue the control belongs to so I would call it DLGAUT_NAME (for DiaLoGAUTorise_NAME). You do not need control identifiers for the OK and Cancel buttons since IDOK and IDCANCEL are predefined constants in the windows.h file and so is IDHELP. And you can give all your static controls the value of -1 unless you want to set the text of the control at runtime.
Another thing you might want to do in the end is to put your controls in order. This can either be done in the resource editor or by editing the .RC file directly. The order is important for two things:
All right, that was the visual part, now lets go and implement the code for it.
The procedure WinMain is the entry point of the application and it normally only features a few lines of code to create window classes and create the main application window. In this example the WinMain features as many as three lines of code. If you want more information about the WinMain and the parameters read the section about the procedure WinMain in the previous chapter.
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
The first parameter hInst is the handle to the program instance which we will need later on for various other functions. Since it is constant and valid for the whole lifetime of this instance of the program we store it in the global variable hInstance.
hInstance=hInst;
Now we create our application window which is a dialogue box in this case. The function will not return before the dialogue is closed. This type of dialogue is called a "modal" dialogue. There is another type of dialogue called "modeless" which will not wait till the dialogue is closed and return immediately. If you want to find out more about those see the windows SDK help file for CreateDialog.
DialogBox(hInstance,MAKEINTRESOURCE(DLG_MAIN),NULL,MainDlgProc);
In its second parameter DialogBox requires the name of the template that is used for the dialogue. This is the one we defined earlier with the ResourceWorkshop. You can here use either a string or better use a numeric identifier. In the latter case you have to use MAKEINTRESOURCE to pass the value to the function. I used DLG_MAIN as a numeric identifier which I defined in the dialog.h header file. This file must then also be included by the resource file (dialog.rc).
Sometimes it can happen, that you call dialogue box, but nothing happens and DialogBox returns immediately with a return value of zero. In most cases this is due to an invalid dialogue template identifier. Check first, whether both the .C file and the .RC file include the header file where the numeric identifier is defined and that it is also used as the template name in the .RC file. If this is correct and there is still no dialogue box, check the dialogue box procedure (remember to return FALSE if you do not process a message) and if you use custom controls, whether all window classes are registered.
The third parameter of DialogBox is the handle to the parent window. In this case we do not have one, that's why we pass it NULL. The last parameter is a pointer to the dialogue box procedure that will handle all events. In the early days of windows this procedure address could not be given to the function directly and you had to a function called MakeProcInstance before you called DialogBox and FreeProcInstance afterwards. This was a major pain especially for modeless dialogues but luckily all modern windows compilers automatically deal with this problem. Under Win32 this is obsolete anyway and so there is no reason to bother you with the reason for that.
Well that was it really. If the dialogue is closed, we terminate by returning TRUE. It's as easy as that!
return TRUE;
The main difference between the previous approach described in the previous chapter and this one is, that the main window of the application is based on a dialogue box rather than a custom window. Of cause a dialogue box is also a window but the difference is, that the window procedure for a dialogue window is defined in Windows's itself. This window procedure will then call a dialogue procedure which we have to provide in our program in order to handle events.
A dialogue procedure gives you exactly the same parameters as a window procedure but the return value is here of type BOOL rather than of type LONG. Whereas in a window procedure we return zero (0L), if we have processed a message and call DefWindowProc if we didn't. In a dialogue procedure we return TRUE if we processed it and FALSE otherwise. Here is a general outline for a dialogue procedure:
BOOL FAR PASCAL DlgProc(HWND hDlg,UINT msg,WPARAM wParam,LPARAM lParam) { switch (msg) { case WM_... /* process a message */ break; default: /* let Windows handle the message */ return FALSE; } return TRUE; }
The most important messages to process for a dialogue procured are WM_INITDIALOG, WM_COMMAND and may be WM_DESTROY. WM_INITDIALOG is sent to the dialogue procedure instead of a WM_CREATE message (which is only sent to window procedures). It can be used to initialise dialogue controls and allocate required resources. Use WM_DESTROY to clean up afterwards if necessary. In a window procedure one of the most important messages to handle is the WM_PAINT message and you might wonder where that is here. Well, of course you get this message for a dialogue window well, but normally there is no reason to handle it.
Let's now look at communication with controls and the initialisation of the dialogue with the WM_INITDIALOG message. In my example a good thing to start with, is to disable all controls that are useless at the beginning. These are the buttons for adding a name to the list and sorting the list. Note: All controls are enabled by default unless you specify otherwise in the dialogue template.
case WM_INITDIALOG: EnableWindow(GetDlgItem(hDlg,IDC_ADDNAME),FALSE); EnableWindow(GetDlgItem(hDlg,IDC_SORTLIST),FALSE);
Since EnableWindow requires a window handle we have to get the handle of the control first by calling GetDlgItem, which we give a handle to the dialogue window and the numeric control identifier. Set the second parameter of EnableWindow to TRUE if you want to enable the control or to FALSE to disable it. Disabled controls are displayed with grey text and are unavailable to the user. You can enable or disable all types of controls just like any other window.
Next we set the default options for the radio and check buttons. CheckRadioButton takes the identifier of the first and last radio button in the group and the one you want to set the check mark to. It is important that the identifiers for radio buttons all have consecutive numbers assigned to them i.e. if the numeric identifier for the first button is defined with a value of 100 then the control identifiers for the other radio buttons of that group have to be 101, 102, 103 and so on. CheckDlgButton is used for the check box.
CheckRadioButton(hDlg,IDC_ASCENDING,IDC_DESCENDING,IDC_ASCENDING); CheckDlgButton(hDlg,IDC_CASEINSENSITIVE,TRUE);
And last not least, we set the maximum length of text the user can type into the edit control. There is no API function for that, but we can send a message and the message for this kind of thing is EM_LIMITTEXT. The maximum is then MAXNAMELEN which I've defined earlier on as 50 characters.
SendDlgItemMessage(hDlg,IDC_NAME,EM_LIMITTEXT,MAXNAMELEN,0L); break;
Now that we've got that sorted, we can go on the WM_COMMAND message which.
The WM_COMMAND is the heart of a dialogue box procedure since it handles both menu and control events. Unfortunately there is a slight difference here between Win16 and Win32. In Win16 where wParam is 16 bit and lParam 32 bit wide, wParam contains the identifier of the control and lParam contains both the handle of the control window (in the low order word) and a notification code (in the high order word). In Win32 wParam and lParam are 32 bit, wParam and contains both the control identifier (in the low order word) and a notification code (in the high order word) and lParam contains the handle to the control window only. Are you with me? Don't worry, here is how to keep everything nice, easy and independent:
First we define two macros, one for each API version:
#ifdef WIN32 // Win32 #define CTLID LOWORD(wParam) // Control ID for WM_COMMAND #define CTLMSG HIWORD(wParam) // Notification Message of Control #define HCTL (HWND)lParam // window handle of control #else // Win16 #define CTLID wParam // Control ID for WM_COMMAND #define CTLMSG HIWORD(lParam) // Notification Message of Control #define HCTL (HWND)LOWORD(lParam) // window handle of control #endif
It is best to put that in your main header file, so it is available in every source code module. Then you can use it by processing the WM_COMMAND message in the following way:
case WM_COMMAND: switch(CTLID) // determine the control the message came from { case IDC_NAME: // Notification message from edit control if (CTLMSG==EN_UPDATE) { ..... // something changed in the edit field } break; case IDC_.... // add case statements for other controls ...... // and some code to handle the event break; } break; // end of WM_COMMAND
The first thing I am doing in my example, is to check whether there is any text at all in the edit window called IDC_NAME and enable or disable the "Add name to list" push button accordingly. So we say:
case IDC_NAME: if (CTLMSG==EN_UPDATE) { int len=SendDlgItemMessage(hDlg,IDC_NAME,EM_LINELENGTH,0,0L); EnableWindow(GetDlgItem(hDlg,IDC_ADDNAME),len); } break;
This reads in natural language as follows: If text has been altered in the edit window [if (CTLMSG==EN_UPDATE)] then get the number of characters in the edit control [len=SendDlgItemMessage(hDlg,IDC_NAME,EM_LINELENGTH,0,0L)] and enable the button IDC_ADDNAME according to number of characters [EnableWindow(GetDlgItem(hDlg,IDC_ADDNAME),len)]. If you've got that, then you've understood the basic principle and the rest is just a variation.
The next thing the user is likely to do is to press the "Add name to list"-button or press Return, which is the same thing as this is our default push button. Normally the OK button is the default push button but we haven't go one here. You can make any button your default push button and in the resource editor you can specify which on it should be. For buttons we do not need to check the notification message since buttons have only one for the event that the button is pressed and hence CTLMSG will always be zero. What we need to do in this case now, is to get the text in the edit control and add it to the list of names. Therefore we allocate a buffer for the text first and fill it by calling GetDlgItemText, giving that the control identifier of the edit control, a pointer to the text buffer and the maximum number of characters we want to read.
case IDC_ADDNAME: // Button Add name was clicked { char szName[MAXNAMELEN]; GetDlgItemText(hDlg,IDC_NAME,szName,MAXNAMELEN);
To add the text to the list, we send the message LB_ADDSTRING to the list control IDC_NAMELIST. To avoid compiler warnings we cast szName to the correct type.
SendDlgItemMessage(hDlg,IDC_NAMELIST,LB_ADDSTRING,0,(LPARAM)(LPSTR)szName);
If you want to insert the name at a particular position then you can use LB_INSERTSTRING instead, giving it the list index where you want to insert it in the fourth parameter. Again there is lots of information about this in the SDK help file.
To make our program convenient to use, we clear the text in the edit control and set the input focus to it again so the user can type in another name immediately.
SetDlgItemText(hDlg,IDC_NAME,NULL); SetFocus(GetDlgItem(hDlg,IDC_NAME));
Done that all that is left to do for this event is to enable the "Sort" button since there are now strings in the list box.
EnableWindow(GetDlgItem(hDlg,IDC_SORTLIST),TRUE); } break; // end of IDC_ADDNAME
As it does not make sense sorting the list with only one item in, this could actually be improved by checking how many list items are in the list already. and enabling the button only if there is more than one. But you can add that easily yourself by sending a LB_GETCOUNT message to the list box and evaluating the return value.
The user can now type in names and add them to the list box. The next thing we've got to consider is the user pressing the "Sort" button to sort the list. This task is performed by the procedure SortList which is coded later on in the source file. But before we call this procedure we've got to get the sort options from the radio buttons and the checkbox control. Therefore we call IsDlgButtonChecked which will return TRUE if it is and FALSE if the button is not checked.
case IDC_SORTLIST: { BOOL CaseInsensitive=IsDlgButtonChecked(hDlg,IDC_CASEINSENSITIVE); BOOL bDescendingOrder=IsDlgButtonChecked(hDlg,IDC_DESCENDING); SortList(hDlg,IDC_NAMELIST,bCaseInsensitive,bDescendingOrder); } break;
Now our application is fully operational. However there is one little serious problem left: the user won't be able to close the dialogue which will in this case also terminate the program. For that we have got the Quit command in the menu and in dialogues you will also get a IDCANCEL event though the WM_COMMAND if the user presses the escape key or closes the window with the close command in the system menu (in Windows95 there is also a close button in the title bar).
Let's do one after the other and deal with the Quit command in the menu first. As with control notifications we receive a WM_COMMAND message when the user selected the menu command. In this case we close the dialogue by calling EndDialog. The first parameter for this function is again the handle of the dialogue window and the second one is the return value for the calling function. You might have forgotten by now, but we called this dialogue in the WinMain with the DialogBox command. Since we do not evaluate the return value there we can return basically anything, but in an ordinary dialogue box with a OK and Cancel button you would return TRUE (or another positive value) if the user pressed OK and FALSE if he/ she cancelled. Anyway, this is what it looks like:
case IDM_QUIT: EndDialog(hDlg,TRUE); break;
After you've called EndDialog you will receive some more messages like the WM_DESTROY for example. Use this to free resources like memory that you have allocated if any.
To deal with the other ways to close the dialogue mentioned above you need to add some code for IDCANCEL. In this case I ask the user whether he/ she really wants to close the program with a MessageBox. It will feature a little question mark icon (MB_ICONQUESTION) and a Yes and No button (MB_YESNO). If termination is confirmed then EndDialog is called.
case IDCANCEL: { int answer=MessageBox(hDlg,"Do you really want to quit?","Confirm",MB_ICONQUESTION|MB_YESNO); if (answer==IDYES) EndDialog(hDlg,FALSE); } break;
Finally let's have a brief look at the procedure used for sorting the list which uses a simple bubble sort algorithm. Again this is just to demonstrate how to communicate with controls and it is not a very efficient way of doing it. And after all in most cases there is not need to sort list boxes because you get the sort option for free if you specify this property for the list box.
Here's the procedure in which we first allocate a number of integers and two buffers to store both strings that we want to compare
void SortList(HWND hDlg,int idList,BOOL bCaseInsensitive,BOOL bDescendingOrder) { int nItems,i,j; BOOL bSwap; char buffer1[MAXNAMELEN],buffer2[MAXNAMELEN];
Now we need to know how many items are in the list. Therefore we send a message LB_GETCOUNT message to the list box which will return the number of list items.
nItems=SendDlgItemMessage(hDlg,idList,LB_GETCOUNT,0,0L);
In this case it is no problem if there are no entries in the list i.e. nItems is zero, since the following two for loops will handle it correctly. But how about other cases where you need a minimum of on list entry say. Well, normally it should not be necessary to check for that here. In this program for example we can be sure that there is at least one item in the list box otherwise the "Sort List" push button is disabled. This is an important matter! It is always better to prevent the user from making invalid input than to tell him/ her later with an error message that the input was invalid. And with windows you have the opportunity to do that easily. Take the editing of names as another example. Theoretically one could input an empty string to the list box, but since we only enable the "Add name to list"-button when at least one character is in the edit field, this can never happen. This rule applies not just to dialogue boxes. Menu command and toolbar buttons can and should be treated in the same manner.
Now we enter the first loop and copy the text of the first list item into our buffer. Again we send a message to the list box to do that. If you're not sure how long the string is and whether your buffer is big enough, you can send LB_GETTEXTLEN first and allocate sufficient space.
for (i=nItems;i>0;i--) { SendDlgItemMessage(hDlg,idList,LB_GETTEXT,0,(LPARAM)(LPSTR)buffer1);
In the inner loop we get the next string in the list, compare the strings and if neccessary we swap them.
for (j=1;j<i;j++) { // Get the text of the next item in the list SendDlgItemMessage(hDlg,idList,LB_GETTEXT,j,(LPARAM)(LPSTR)buffer2); // compare the two strings if (bCaseInsensitive) bSwap=(lstrcmpi(buffer1,buffer2)>0); else bSwap=(strcmp(buffer1,buffer2)>0); if (bDescendingOrder) bSwap=!bSwap; // Swap the items if necessary if (bSwap) { // swap the strings SendDlgItemMessage(hDlg,idList,LB_DELETESTRING,j,0L); SendDlgItemMessage(hDlg,idList,LB_INSERTSTRING,j-1,(LPARAM)(LPSTR)buffer2); } else lstrcpy(buffer1,buffer2); } } }
Unfortunately there is no message to set the text of a list box item. But what we can do is delete the current string by sending LB_DELETESTRING and then insert it again at the previous position with LB_INSERTSTRING.