Steve Welstead is a staff scientist with COLSA Corporation in Huntsville, Alabama, and an adjunct associate professor of mathematics at the University of Alabama in Huntsville. He holds a Ph.D. degree in applied mathematics from Purdue University. He is the author of Neural Networks and Fuzzy Logic Applications in C/C++, published by John Wiley & Sons, Inc., in 1994, and he contributed the article "Scrolling List Dialog for Scientific Programming" to the April, 1994, issue of C/C++ Users Journal. He is currently working on a book on fractal imaging techniques in C/C++ for Windows. The author can be reached at COLSA Corporation, 6726 Odyssey Drive, Huntsville, AL 35806, or at 71773.2253@compuserve.com.
Introduction
If you have tried writing your own code for Windows dialog boxes, then chances are that you have dealt with the issues of designing resources, identifying dialog controls with program variables, and writing callback procedures to define dialog box functionality. Even with today's "expert" and "wizard" application-building tools this process can be tedious. This article describes how to build a type of dialog, the data-object list dialog, that can replace large complex input dialogs. This allows you to rapidly develop prototype Windows programs without getting bogged down in the details of dialog design.The data-object list dialog is based on a small C++ class library of dialogs that includes simple dialogs for commonly used data types as well as a list dialog class. The list dialog manages a list of data objects. Each data object knows how to update its own value through an appropriate input dialog. For example, string data objects call up a text input dialog, while objects representing RGB color values summon the standard Windows dialog for choosing colors. Once you have made the initial investment in designing this list dialog, you can provide your programs with a user input interface simply by inserting objects into the list. You don't need to design separate resources or callback procedures for each new application. The C++ class structure hides the issues of resources, control identifiers, and callback procedures. In the final section of this article, I will show how to implement the data-object list dialog in a simple WinMain application example.
The C++ code presented is compatible for compilation as both 16-bit and 32-bit Windows executables. You should be able to produce executables for Windows 3.1, Win32, Windows NT, and hopefully Windows 95. This code does not use any proprietary C++ Windows class libraries such as Borland's OWL library or Microsoft's MFC library, though you can implement the concepts in those libraries. I have compiled and run this code using both Borland C++ v3.1 and v4.5, as well as Symantec's new C++ v7.0 compiler.
The Object List
The object list is a simple C++ class that manages an array of pointers to objects. It is similar to a collection class in a container class library. This is the class I use to manage the items in the data-object list box. Listing 1 shows the header file CWOBJ.H, which defines this class. The code for the object-list class is straightforward. Due to space limitations, it is not listed here, but is available on this month's code disk and CUJ online sources.The array the_list stores the actual array of pointers. This array uses indexes one through max_count for its items rather than the usual C indexing starting with zero. The member function insert_item inserts a new object at the designated index using this convention, while the member function at returns the object at a given index value. It is important to note that the calling program supplies allocated objects for each item in the_list.
Since object_list does not allocate these objects, it does not destroy them either. (The calling program may, and probably will, want to use these objects after the object_list is destroyed.) Thus, delete_item does not actually delete the object at the given index, it only rearranges the list to skip over that item. Similarly, the destructor for object_list does not destroy items in the list, it only deletes the allocated array the_list.
Data Objects
File CWOBJ.H also defines the object types that the data-object list dialog displays and manipulates. The class tdata_obj defines the basic data-object type. Each data object contains a pointer to a value (value_addr), a description string (descr_addr), and an item number that indicates its position in an object list.From tdata_obj, I derive the class ttyped_data_obj, which includes information about what type of data the object represents and how to update that data. Listing 2 shows the file CWDATOBJ.CPP with the code for this class.
The job of a ttyped_data_obj object is to allow the user to update the value of the data that the object represents, and to provide a string display of a description of this data and its current value. The function value_str formats the data value into a string, and the function build_display_str appends the value string to the description string for that item. The resulting string is what appears in the data-object list box. For example, suppose in a graphics program you had a COLOR_DATA item called "Line Color," and the current RGB values for this item were 255, 0, and 0 (pure red). The function build_display_str would produce the string:
"Line Color: RGB(255,0,0)"for this item, and this is what would appear in the list box.The typed data object obtains updates for its data value through the member function get_new_value, which presents to the user a dialog that is appropriate for that particular data type. The following sections discuss these dialogs in more detail.
Dialog Classes
At this point, I need to build some C++ classes that will define basic dialog behavior. If you are working with one of the Windows C++ libraries, such as Borland's OWL or Microsoft's MFC, you may want to substitute the basic dialog classes from one of those libraries. Listing 3 shows the header file CWDLG.H for these classes, and Listing 4 shows the file CWDLG.CPP, which contains the source code for the base class tdialog and the derived class tinput_dialog.C++ classes provide a convenient means for dealing with resource scripts and data-handling issues. The base class tdialog does not have a resource script associated with it since this dialog does not have any real functionality. However, it does provide a member variable which descendant classes can use to store their resource identifiers.
Member variables can also store the data that the dialog box obtains from the user. This allows you to associate the data with the dialog and avoid the use of global variables. You also avoid having to use that mysterious "extra" Windows parameter with its requirements for locking and unlocking local data. You can pass data to the dialog class through its constructor, and obtain updated values from the class object after dialog execution.
It is somewhat more difficult to elegantly hide the issue of callback functions. The callback function defines the behavior of the dialog box, that is, how it responds to user input and system messages. Ideally, this function would be a member function of the dialog class. The problem is that when the dialog box is created, you must pass the address of the callback function to the Windows function DialogBox. C++ does not allow the address of a member function to be passed as a parameter, since this address is not known until a particular class object is instantiated at run time. Even using the this pointer in front of a class member function to indicate a particular instantiation of the class is not sufficient for the C++ police. The callback function needs to be a stand-alone function whose definition is external to any C++ class.
To address this problem, I define a callback function for each dialog class which is external to that class. I also define a global pointer to that class that represents a particular object instance of that class. The callback function uses this global pointer to call a specific instance of a member function which defines the behavior of this dialog class. Listing 4 illustrates this approach in file CWDLG.CPP. The callback function for the base class tdialog is called tdialog_proc, and there is a global tdialog pointer called this_dialog. Member function handle_message determines the behavior of the dialog by defining how it responds to Windows messages. This is what should be the callback function. Thus, tdialog_proc calls a specific instantiation of handle_message, namely the one belonging to the global tdialog object that this_tdialog points to.
It is important to consider when to set the value of the global pointer used in the callback function. Member function exec_dialog makes the call to Windows that actually executes the dialog. It is here that the global pointer this_tdialog is set to the this pointer for the current instantiation of the dialog class object. Note that you don't want to set this pointer in the constructor for the class since any derived type will override the initial setting and reset the value to its own this value. This will cause problems if you have more than one dialog executing at one time. Also, exec_dialog saves the current value of the global pointer before resetting it and restores this value when it is done, in case there are two dialogs of the same class executing at one time.
The first descendant class of tdialog is tinput_dialog, which takes text input from the user. Note that this dialog, as well as all other derived dialog classes, has its own global pointer (this_input_dialog in this case) and its own callback procedure (tinput_dialog_proc) defined external to the C++ class. The member function exec_dialog calls this procedure in the same way as was the case for the tdialog class.
The class tinput_dialog uses the resource script named INPUT_DIALOG, which defines a simple dialog box with a single edit control with the identifier ID_DLG_INPUT, a default "OK" button, and a "Cancel" button. Resource scripts are not listed here, but are included with this month's code disk and from CUJ on-line sources.
Dialogs
To use a C++ class, you need to create a particular instance of the class. For our dialog classes, this can be accomplished with a function that calls the class constructor for the particular dialog class, then executes the dialog. This function also initializes the data presented to the user, and brings input from the user back to the calling program. When the dialog terminates, this function obtains the data from the dialog class and then deletes the dialog class instance. The calling program never has to be aware of the existence of the C++ class.Listing 5 shows the header file CWDIALGS.H for the three dialog functions used in the example discussed here. These are the dialog functions which the ttyped_data_obj member function get_new_value calls when updating the object data values. The function string_dialog instantiates the tinput_dialog class, and obtains string input from the user. Listing 6, file CWINPDLG.CPP, shows the code for this function. Recall that class tinput_dialog has the capability to override the dialog-window caption and edit-control caption. The function string_dialog supplies these captions, using string parameters dlg_title and descr.
The two other dialog functions whose definitions appear in Listing 5, get_file_name_dlg and get_rgb_color, are actually not based upon C++ classes. Rather, they call Windows common dialogs. The function get_file_name_dlg fills the Windows-defined structure OPENFILENAME and calls the Windows common dialog function GetOpenFileName. The function get_rgb_color fills the Windows structure CHOOSECOLOR and calls the Windows common dialog ChooseColor. The use of these structures and dialogs is fairly standard. You should be able to find examples of their use in the documentation for your Windows software development environment. For that reason, the code for these dialogs is not listed here, but is included with the other code in this month's disk, in the file CWCOMDLG.CPP.
List Dialog
The list dialog class tlist_dialog displays an object list by implementing a Windows list-box control in a dialog box. Listing 7, file CWLSTDLG.CPP, shows the code for the class tlist_dialog and its descendant tdata_list_dialog. The items in the object_list may be strings, as in a standard Windows list box, or they may be more complex structure or class types. The list box displays string descriptions of these more general items. The tlist_box_data parameter sent to the tlist_dialog constructor contains the object list pointer item_collection as well as an integer indicating the currently selected item in the list. Note that this means that tlist_dialog does not instantiate, nor does it destroy, the object items in the list. This is because in many cases, such as the data-object list dialog discussed below, you want the list items to have a life beyond that of the list dialog.The member functions of tlist_dialog implement list-box functionality by sending messages to Windows. Member function set_data fills the list box with strings representing each item in the object list, using strings obtained from get_item_string. The key to modifying the basic behavior of tlist_dialog to handle objects more general than strings is to override get_item_string.
Data-Object List Dialog
The descendant class tdata_list_dialog (CWLSTDLG.CPP, Listing 7) manipulates a list of objects of type ttyped_data_obj (CWOBJ.H, Listing 1) , rather than simple strings. In this case, the derived version of get_item_string calls the ttyped_data_obj member function get_display_str to obtain the string that will appear in the list box. Figure 1 shows a data-object list dialog with some sample strings obtained from get_display_str. The tlist_dialog member function, without any modification, takes care of inserting these strings in the list box. This is a good example of how C++ classes can ease the job of Windows programming.The data-object list dialog responds to a double-click on an item in the list by providing the user with an opportunity to update the value of that item. Objects of the class ttyped_data_obj know how to update their own values through the member function get_new_value. This function presents the user with a dialog box appropriate for updating the value of the data object. Thus, for example, items of type STR_DATA will call the dialog function string_dialog to obtain an updated text string from the user, while items of type PATH_DATA will summon the standard Windows file-opening dialog. After the user enters the new value, the list box shows that new value in the display string for that item.
All of this is accomplished by having the list box respond to a LBN_DBLCLK message in respond_wm_command with a call to the ttyped_data_obj member function get_new_value, and then calling its own member function set_data, which resets the list box contents using the newly derived version of get_item_string. Notice the use of a macro to handle the LBN_DBLCLK message. This is one of the messages that is handled differently in 16- and 32-bit Windows.
An Example
To implement a data-object list dialog in a Windows application, you need to define the variables you wish to obtain from the user and associate these variables with an object list. It is convenient, though not necessary, to collect these variables into a single structure. Listing 8 shows file TSSETLST.H, which defines the structure setup_record containing three variables representing the different data types that the list dialog developed here can address. The function init_setup initializes this structure, and setup_to_collection inserts the structure variables into an object list. The code for these functions is contained in the file TSSETLST.CPP, Listing 9.Note that setup_to_collection instantiates a new ttyped_data_obj object for each variable in the setup_record structure, then inserts these objects into the object list. This object list is the item_collection that is inserted in the list dialog. To use a data object list dialog in a different application, all you need to do is change the definitions of setup_record and setup_to_collection appropriately.
File TSTMAIN.CPP (Listing 10) contains the WinMain function code for this example, which produces the application window shown in Figure 2. The menus are defined in the resource file DLGTEST.RC, which also includes resource scripts for the dialogs. There are two menu choices: a "Dialog" submenu with a single option tied to the identifier IDM_DIALOG which summons the list dialog, and a "File" submenu whose single option "Exit" terminates the program.
WinMain takes care of registering and creating the main application window, as well as the message loop code that runs the program. In addition, this example contains function calls to initialize the list-box data, as well as code for deleting allocations associated with the list box when the program terminates. You should be particularly careful about cleaning up your allocations in a Windows program, since they will live on beyond the end of your program.
The Window callback function WndProc creates and executes a data-object list dialog in response to the menu command identified by IDM_DIALOG. When the dialog terminates, the latest information is displayed in the window, including a rectangle filled with the color designated by the latest value of g_setup.rect_color.
Table 1 lists the files needed to build the executable for this example. For 16-bit compilations, use the large memory model. For 32-bit executables, be sure to define the constant WIN_32 at the top of the file CWDLG.H (Listing 3) .
As you can see, the data-object list dialog gives you a convenient way of getting information from the user into your Windows program with a minimum of development effort. This is handy for small programs you write for your own use, and for prototype versions of larger programs. When you're ready to go to a full production version, you can replace the list dialog with a fancy dialog built with one of those "app wizard" resource workshops.