C/C++ Users Journal January, 2005
When you create dialogs at runtime, you specifydirectly or indirectlya resource ID and the dialog loads itself from that resource, which is embodied into your executable. That dialog canand usually doescontain child controls. These child controls and some of their attributes (text, position, font, and so on) are contained in that resource and are automatically created at runtime. Thus, C++ and GUI can slowly get out of syncsimply using the Resource Editor and removing a control from a dialog can have disastrous effects if in your code you still assume that control exists.
Wizards won't help you, something MFC/ATL aficionados know all too well. You have to make changes by handan error-prone process since you never know for sure if you've updated everything. That the code compiles does not let you off the hook. Here's an easy mistake to make: On the dialog you just finished, your boss wants a listbox instead of a combobox. You go to the Resource Editor, remove the listbox, and insert a combobox with the same ID. However, you've been listening for the "Kill Focus" event from this control. Looking at the code, it is easy to overlook the fact that you should change LBN_KILLFOCUS into CBN_KILLFOCUSwhich in fact have different values [1].
Clearly, what you want is a tool that goes through your code and warns you about semantic errors such as "misusing controls" (for instance, treating an edit control as a button, or using a control that does not exist on the dialog) and "misusing events" (listening for the wrong events). And it would be great if this tool could also correct the misused events in your favor, automatically turning LBN_KILLFOCUS into CBN_KILLFOCUS.
First of all, let me make a clear distinction. A dialog is an on-screen window. A dialog class is a C++ class that handles an on-screen window. Say you have a dialog that has nontrivial logic and you create a corresponding C++ class to handle this dialog. You might want to keep your children controls as data members of your class. After all, that's what MFC advocates. This isn't necessarily wrong (though it's one more thing that could get out of sync), but this could lead to compilation issues. Every small GUI change (adding/removing controls) means modifying the header file, which then means recompiling all other files that depend on your dialog header. This can soon become a lot of compilation time.
The good news is that all information about dialogs and their controls is kept in your project .rc file in more-or-less human-readable form [2]. By parsing the .rc file, you can always find which controls are on each of your dialogs.
But what if you want to parse this .rc file and, for each dialog, create a C++ header file with enough information about the controls in it? To do this, you need a tool like Resource Splitter, the program I present here; see Figure 1. (The source code for Resource Splitter is available at http://www.cuj.com/code/ and http://www.torjo.com/win32gui/.) When you run the program, all you do is drag-and-drop the .rc file(s) you want to monitor. Resource Splitter then looks for changes in these .rc files, and as they happen, it updates the corresponding dialog header files. It is also persistent between sessionsif you close and restart the program, it remembers the last monitored files.
Once Resource Splitter is running, it creates the win32gui_res subdirectory for each monitored .rc file and places the header files in the appropriate subdirectory. It then creates menus.hpp, which contains the IDs of the menus in the menu_ namespace [3], and a corresponding .hpp file, which contains controls for each dialog.
For each such control, it contains information about its:
Each control with a certain ID is lowercased, the ID*_ prefix is removed, and it becomes a class. Say you have a login dialog with two edit box controls (IDC_USER_NAME and IDC_PASSW) and two buttons (IDOK and IDCANCEL). Thus, four C++ classes are created: m_user_name, m_passw, m_ok, and m_cancel. The m_ prefix suggests that conceptually, these are children of your dialog class. However, they are not member variables. Had they been members, you'd run into the problems previously described. If you're not comfortable with the m_ prefix, you can use the ctl:: prefix instead (ctl::user_name, ctl::passw, and so on).
This looks good, but what are its benefits? First of all, addressing your children is much easier:
child<m_passw>()->whatever()
instead of the old:
child<edit>(IDC_PASSW)->whatever()
This is more than syntactic sugarit's type-safety, too. The m_user_name class knows it is an edit control, so that only edit operations will work:
child<m_passw>()->sel(0, 3); // ok // compile-time error child<m_passw>()->click();
Compare this to:
// oops! child<button>(IDC_passw)->click();
Now imagine that user_name becomes a combobox (remembering the names of the last 10 logged on users). As you recompile, the compiler automatically shows you which operations are not valid for the new control:
// compile-time error child<m_user_name>()->sel(0,-1);
This solves the "misusing controls" problem previously introduced. But what about the "misusing events" problem? The C++ classes corresponding to the controls also know what events you can listen to. While the old syntax still works, the new syntax for listening to events is simpler:
event_ex<control_name::ev::some_event_name>
For a few examples, see Listing 1(a) (new style) and Listing 1(b) (old style).
You no longer need to worry about whether you're listening for an event, command, or notification. The event_ex class itself knows this and does what's appropriate.
Again, imagine that the m_name control from Listing 1(a) changes from an edit box into a combobox. As you recompile, you get a compile error (m_name does not have an event called change). Thus, we've taken care of type-safety for misusing events, hitting two birds with one shot.
Remember the boss who asked you to change the listbox into a combobox? Assume you're listening for:
m_food::ev::kill_focus
As m_food becomes a combobox, the kill_focus nicely turns from LBN_KILLFOCUS (kill focus for a listbox) into CBN_KILLFOCUS (kill focus for a combobox). All without any extra work.
So far, we've handled all events coming from dialog controls. This is all well and good, but what about those WM_* messages? event_ex can listen for them, too. Instead of saying control_name::ev::some_event, say wm::some_event; for instance, wm::size or wm::move from Listing 1(a). I've started implementing this for all known WM_ messages, and hopefully, by the time you read this I will have finishedthere are just about 1000 of them!
But we can still do better. So far, you can still say something like Listing 2. That's the "beauty" of raw Win32 GUI programming: You can twist WPARAM and LPARAM in any way you like. That said, there are cases when you actually need this. But for ordinary programming, you seldom need this much flexibility. When handling events, wouldn't it be great if the argument you get matches the event you're handling? This might sound like a fairytale, but with help from templates, you can do it.
Listing 3 is a straightforward solution: Just append ::arg to the event you're watching. The best part is that all mistakes are caught at compile time: If you're listening for event A and get an argument from event B, you get a compile-time error (see the last two functions in Listing 3).
This mechanism only works for the first argument you pass to the event-handler function. Remember that you can receive multiple arguments if you wish. For instance, your function might want to return a resultthere are a few event functions that do return a result to the caller. Such an event is CB_GETLBTEXT; it returns a string from a combobox. Listing 4 shows how you can handle multiple arguments.
Now that you have this new, cool way of dealing with controls and events, you might wonder why you should keep the old way (of dealing with controls/events)? Besides the obvious reason of backwards compatibility, there are plenty of others.
The new mechanism lets you immediately handle one event. You might want to create a function to handle an event range, command range, or for that matter, all eventsyou'll use the old mechanism for that. Also, there are times when you need to deal with "raw" windowsthe splitter control knows the windows it needs to split by their ID only, not their type. When showing tooltips, the dialog only matches the ID of a control with a function that returns a string (the tooltip itself). The save_dlg also references controls by their ID. Thus, the old and new styles complement each other.
Commands that come from a menu, accelerator, or a toolbar are easy to handle. They were simply put in the menu_:: namespace, in the menus.hpp header [3].
If you want to handle a menu command, see Listing 5. When you want to create menus, the group menus are put in the menu_::group:: namespace (Listing 6).
When I implemented Resource Splitter, I wanted to make things as simple as possible. Code completion usually pops up when you need to type some member (after ".", "->","::") and makes the learning curve of a new library easier. Thus, you'd expect it to pop up after any of child<m_name>()->m_name::ev::. The unfortunate thing is that existing IDEsVisual Studio 7.1, 8.0, Dev-Cppdon't go that far. I have also tested the Visual Assist Add-in (for Visual Studio) and the results are promising. By the time you read this, this add-in might allow all of the aforementioned and maybe morefor instance, when you have an argument such as m_name::ev::sel_change::arg a, and write a. The good thing is that VS does provide code completion for wm::, menu_::, and menu_::group::.
If you have a Visual C++ background, you're accustomed to this line when dealing with resources from code:
#include "resource.h"
Avoid this as much as possible. All you need is already carefully split into several files. When you're implementing widget_dlg dialog, instead of the aforementioned, use the following in your source file:
#include "win32gui_res/widget.hpp"
In case you need only menu commands, include win32gui_res/ menus.hpp (by default, this is included by all other generated files).
The rationale for avoiding resource.h is that VC does a bad job at handling the IDs [4]. But most important, you'd have one more chance for mistakes. By not including resource.h, you'll be forced not to make any mistakes. Code like Listing 7 will generate a compile error (the ID is not defined). You'll be forced to do it right (by the way, did you notice the bug in Listing 7 [5])?
Every now and then, when implementing dialog A, you'll need to reference the children of dialog B. It's simple: besides #including win32gui_res/A.hpp, you'll need to #include "win32gui_res/B.hpp". However, you should always do it after the inclusion of A.hpp (the same applies if you want more dialogs: C, D, and the like). Those children will have to be prefixed by the name of the dialog header plus an underscore; for example, m_user_name from the employee dialog should be referenced as employee_::m_user_name [3].
There's a good reason for requiring the extra prefix. You might have two different controls with the same name (for instance, m_user_name could be a combobox in your dialog, and an edit box in the employee dialog).
This month, I've shown you that GUI programming can be type-safe. In the upcoming installments, I'll tackle advanced subclassing and show you yet another way to spy on your programs.
[1] LBN_KILLFOCUS is the notification code for a listbox, while CBN_KILLFOCUS is the notification code for a combobox.
[2] When you compile and link your project, the .rc file is compiled as well. A .res file is output (the binary correspondent of the .rc file). This is then linked together with the rest of the .obj files.
[3] I have appended an underscore to avoid name collisions with other class/namespace names.
[4] More to the point, it maintains a lot of duplicate IDs. For example, when you add an edit box, it will have the ID IDC_EDIT1. Once you rename it something meaningful to you (such as ID_user_name), the resource.h contains both the IDC_EDIT1 and ID_user_name, which both have the same ID.
[5] EN_CHANGE is a command, not a notification.