Introduction
One of the questions that often confronts a programmer is the question of "who owns the GUI?" There are several answers to this, of varying degrees of certainty, but after a decade of Windows programming, I am absolutely convinced there is one, simple, overriding rule that you must follow: you, the programmer, do not own the GUI. Any decisions that are based on a violation of this simple rule will lead eventually to pain, chaos, and unmaintainable code. This essay explores the notion of who does own the GUI, and how you protect yourself from them.
One owner of the GUI is Microsoft. The design guide for the GUI specifies certain properties of the menus and toolbar icons. If you have an icon in your program that looks like this: then it had better be coupled to an open-file operation, and not to, say, paste. The leftmost menu shall be called File. The rightmost menu shall be called Help. In an MDI application, the next-to-rightmost menu shall be called Window. The second-to-leftmost item shall be Edit in any application for which editing is an operation. In between, you have some negotiating room, but if you deliver a product with Edit on the right and Help on the left, your users are going to get more than a little annoyed. If you put Exit on the Edit menu, you will look like a fool.
So Microsoft owns the GUI.
But not really. I'm a consultant. For me, the client owns the GUI. When they make a suggestion that would violate the Microsoft standards, I have to warn them, but ultimately I may have to do what they suggest. When they are doing something really stupid, I may dig in my heels harder than if it is a simple matter of taste. When they ask for something that is not readily possible, such as 512 check boxes on a dialog, I have to explain why this won't work and create a palatable solution.
So the client owns the GUI.
Of course, Carl von Clausewitz (1780-1831), a 19th century programming theorist, said it quite succinctly: "No GUI design survives first contact with its users" (I may have misremembered some minor details of this quote, but it is embodies all the key ideas). You may deliver a GUI, but if the users don't like it, you will have to make changes. As a consultant, I get this via one level of indirection; the end users complain to my client, who complains to me, and we make changes. (And sometimes I'm vindicated when I tell the client what will and won't work and they insist that I do it their way -- and the users tell them exactly what they think of that idea).
So the end users own the GUI.
But that's not all. I've had to make changes because of how a competing product arranged its menus. When you are trying to sell into that market and want to capture the established customer base, you can be more effective if your GUI doesn't introduce a learning effort on the part of the end users.
You can read this two ways: the marketing department owns the GUI, or the competitors own the GUI.
But you don't own the GUI.
In fact, you don't even own it for programs you are writing for yourself! Your own opinion of the right way to do something will evolve as you use the program. As the end user of your own program, you own more of the GUI than you do as the programmer of the same application. Just because you are the same person doesn't excuse you from obeying the first rule.
The illusion that you own the GUI can lead you down a garden path to unmaintainable code. It can be worse: it can render persistent state not just meaningless, but blatantly incorrect. This means that any values you retain in the Registry, in data files, or other places, must not in any way be coupled to the GUI.
Mapped Encodings
Don't use clever encodings is the first rule. If you want to store black in the persistent state, store it as RGB(0,0,0). That is language-independent. Store a bit vector in a DWORD
if that is the best way to store the value. Store an integer that makes sense with respect to the program, particularly for enumerated representations. When you need to transfer it to the GUI, use a mapping that is specific to the GUI representation.
When you go international, the problems become compounded. For example, take a simple case where you have a dropdown list of possible colors. Black, Green, Red, and Yellow. Or Schwartz, Grün, Rot and Gelb. What if the requirement is that the list be in alphabetical order of color? You can't couple the offset in the list to the color value. Don't even try.
Take the case where the customer suddenly wants to add Blue and Orange. If you've encoded black as 0 in the persistent state, and suddenly offset 0 of the color is 1 or 7, exactly what color have you encoded? If you couple your internal representation to your GUI based on completely incidental details such as which class of control you have chosen this week to represent it, and trivial and incidental details of how that control encodes the value, you are in deep, deep trouble. Of course, you won't know it until you have to actually change the control, or the set of values in the control, or go international, but believe me, you are already in trouble. It is only a question of how soon you discover you are in trouble, and exactly how much trouble you find yourself in.
List Box, ListCtrl, and Combo Box Issues
A common failing of programming technique is to couple the integer value which is the list box or combo box index with some interpretation other than the completely accidental property that the value is stored in a certain position in the list box or combo box. As soon as you do this, you've lost maintainability.
One common solution is to do something like:
static COLORREF colors[] = {
RGB(0, 0, 0),
RGB(255, 0, 0),
RGB(0, 255, 0),
RGB(0, 0, 255),
...
};
so that you can preload the combo box (for example, from the resource editor) with "Black", "Red", "Green" and "Blue". This is just silly. Why some set of string
s in some part of the program would necessarily have a 1:1 positional correspondence with some other hard-coded table somewhere else in the program is only marginally fathomable. Furthermore, it would require that someone changing the order of the values (say in the resource editor) would have to know the correspondence to the table. I've found only one sensible way to handle this, which makes it insensitive to how the control is sorted (or unsorted), the number of values, and their textual representation. I discuss this in my essay on Combo Box initialization, so I won't go into it in great detail here except to summarize the design issues.
If you want to decouple the values from the index, it means you have to figure out which combo box or list box entry corresponds to a particular color. Fortunately, this is easy. You attach to the ItemData
component of the list box or combo box the color value, and you don't do SetCurSel(n)
(for n, the offset value stored in the persistent state) or SelectStringExact(name)
for a text name, but simply enumerate all the members of the control, comparing each ItemData
to the desired value. Since we're in C++, it is trivial to create a subclass that does this for you. I've even done so in my CIDCombo
class.
Never, ever, under any conditions you can possibly imagine, should you couple a value in persistent storage to some incidental characteristic of something as transient as a GUI layout. It is very marginal to consider doing this even for transient state of data maintained inside your program, and it makes no sense at all to couple the representation of a control in a dialog box to any other component of the program. Create a canonical representation for the information. When you need to present it to the user, map it at that point (and only at that point) to the GUI. When the value is specified in the GUI by the user, map it back to the canonical representation. Note that the mappings generally provided by Microsoft are fairly weak. For example, for list boxes and combo boxes in dialogs, using MFC, the only mapping is from an integer value to the index of the control. Why any application should know or care that the representation of the value in a particular control escapes me.
You can get around this by writing your own DDX handler. I've found it just as expedient to do the initialization in the OnInitDialog
handler. Writing an extra line or two of code to create a highly-maintainable program is an activity I don't mind at all.
Check Box Techniques
Check boxes often have problems, in spite of the fact that DDX is often useful for translating values of type BOOL
from the application to a dialog. Not everything you want to do is necessarily represented by a simple BOOL
value. Often, the most sensible representation from the application viewpoint is a set of bits in a single WORD
or DWORD
. And while you could write a custom DDX for this, it only helps you going into and coming out of the dialog. You still need a way to manipulate values inside the dialog. And no, don't use UpdateData
inside your dialog. That way madness lies, and I have another essay just on that topic.
For example, I sometimes have a value which is encoded as a bitmap which is reflected to the GUI as check boxes. I write two mapping functions: bitsToControls
and controlsToBits
. If you've read my essay about not using GetDlgItem
, you may recall the principle I stated that "If you write more than one GetDlgItem
per year, you are probably not using MFC correctly". The bitsToControls
and controlsToBits
functions are the kind of functions where using GetDlgItem
makes sense, and indeed, I write these about once a year.
typedef struct {
UINT ctl;
DWORD value;
} controlMap;
#define MASK_ACTIVE 0x80000000
#define MASK_REVERSE 0x00000001
#define MASK_AUTO 0x00000002
#define MASK_MALLEABLE 0x00008000
static controlMap mapping[] = {
{ IDC_ACTIVE, MASK_ACTIVE},
{ IDC_REVERSE, MASK_REVERSE},
{ IDC_AUTO, MASK_AUTO},
{ IDC_MALLEABLE, MASK_MALLEABLE},
{ 0, 0}
};
The implementations of bitsToControls
and controlToBits
should now be obvious:
void CMyDialog::bitsToControls(DWORD bits)
{
for(int i = 0; mapping[i].ctl != 0; i++)
{
CButton * button = (CButton *)GetDlgItem(mapping[i].ctl);
button->SetCheck( bits & mapping[i].value ? BST_CHECKED : BST_UNCHECKED);
}
}
DWORD CMyDialog::controlsToBits()
{
DWORD result = 0;
for(int i = 0; mapping[i].ctl != 0; i++)
{
CButton * button = (CButton *)GetDlgItem(mapping[i].ctl);
if(button->GetCheck() == BST_CHECKED)
result |= mapping[i].value;
}
return result;
}
OK, be pedantic--there's two instances of GetDlgItem here, so I should only write this code once every two years if I want to adhere to my own rule. Let's not get carried away here...
So the advantage is that if I have to change one of the options to a set of mutually exclusive radio buttons and encode it as multiple bits, I can feel free to do so. I don't have to rely on the external representation in any way conforming to the GUI representation. Use the representation that makes sense for your program inside your program. Use the representation that makes sense in persistent storage for persistent storage. And map it to and from the GUI representation as needed. Thus these two functions would form part of your custom DDX implementation.
Radio Button Issues
Radio buttons are a particularly obnoxious object when you use the Microsoft tooling. For reasons that escape me, they feel that you should represent radio button values as integers. The integer you see in your program maps directly to an offset into a radio button set on your dialog. I cannot fathom why this would ever make sense. For example, say that I have a set of options, Big, Medium and Small. I have an encoding of 0
, 1
or 2
by the Microsoft method. Now say that I have discovered that nobody ever buys the medium size, so I discontinue it. By the Microsoft method, I now have two integers, 0
and 1
, representing Big and Small. 1
is now Small, not Medium. Dumbest idea I've seen in decades (I've been at this for 35 years). The only sensible and correct representation is to have Big be one constant, and Small be another, always. The value representing medium is simply no longer a legal value. But if you use the Microsoft method, old values of "Medium" now become "Small", and old values encoding "Small" are not legal at all. There is also no reason that those values should in any way represent offsets into a radio button set; if I add six new sizes, I might want to represent the values in a dropdown list, and furthermore, what if I found that I started with only Big and Small, and wanted to add Medium in between? Following the Microsoft strategy, I'd find that my values coupled to the GUI would always select the wrong size! Furthermore, the mapping of the values to controls must be done inside the dialog, not outside the dialog; the abstract
interface to the dialog is the persistent storage representation, and it is up to the dialog to map that to its control representation. Anyone who understands modular abstraction would know this immediately.
The way I handle the problem is to create an enum
, or a collection of #defines
, to represent the values. The set of values is application-centric. The values are assigned in such a way as to represent a persistent-storage-stable and code-stable representation. If I need these in a dialog, I map them in OnInitDialog
. Forget the "convenience" of stock DDX in this case; it is utterly and completely useless for this purpose. It is worse than useless. It is destructive to long-term maintainability. And in this business, maintainability is everything. In fact, stock DDX should never be used to handle list boxes, combo boxes, and radio buttons. It couples GUI representation to application representations in a way that cannot possibly be maintainable.
It is not an advantage to be able to quickly write an unmaintainable program.
It is not well-known, but the DDX mechanism is extensible. You can, if you want, write your own DDX method to initialize your list box or combo box, and this custom DDX will embody the necessary mapping. A really smart custom DDX would know how to handle a variety of control types.
What I do with radio buttons is either use a switch
statement or a decode table to map an application value to a radio button control ID. Not an index. A control ID. An index implies that a reorganization of the controls (adding or deleting radio buttons) will maintain the invariance of the index, which cannot be true
. It even allows for schema migration; if you have an invalid value, you can take specific action to deal with it.
UINT id = IDC_WHATEVER;
switch(applicationValue)
{
case value1:
id = IDC_VALUE1;
break;
case value2:
id = IDC_VALUE2;
break;
case whatever:
id = IDC_WHATEVER;
break;
}
CheckRadioButton(IDC_FIRST, IDC_LAST, id);
A serious deficiency of this is that you have to actually know the ID of the first control in the group, and the last control of the group, so if you change the radio buttons in a way that affects the first and last values of the group, you have to change the CheckRadioButton
call. A bit more work, particularly if you use a table instead of a switch
statement, would not be overly difficult.
Another serious deficiency of the Microsoft method is that they only allow you to create a control variable for the first radio button of a group, the radio button with the WS_GROUP
flag set. You can't query the value of an individual button. I find this equally silly. The workaround is tedious, but straightforward.
First, set the WS_GROUP
flag on every radio button in the group. Then go to the ClassWizard
, select the Member Variables tab, and for each radio button, create a Control
variable, for example (and note that I use a different prefix for this: c_
means "control" and "m_
" means "a variable which is passed in as a parameter from the caller"):
CButton c_Value1;
CButton c_Value2;
CButton c_Whatever;
Then you can go back to the dialog editor and remove the WS_GROUP
flag from all but the first radio button (I always forget to do this step, until I run the dialog and find out that my radio buttons are not disjoint!)
Now, if you use my technique for dialog control management, you can write constraint equations such as:
BOOL enable = c_Whatever.GetCheck() == BST_CHECKED;
c_WhateverText.EnableWindow(enable);
(This causes the text input box to the right of the radio button to be enabled if the radio button is checked).
Summary
I hope I've pointed out some major problems in relying too heavily on the built-in mechanisms of MFC with respect to control management. Most particularly, I hope I've made clear exactly why you, the programmer, have the least claim on the GUI, and shown techniques you can use to effectively decouple the GUI from application-centric representations.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.
You can leave comments below with questions or comments about this article.
Copyright © 1999 The Joseph M. Newcomer Co. All Rights Reserved.
www.flounder.com/mvp_tips.htm
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.