Dialog Box Control Management

Home
Back To Tips Page

Dialog box control management is a perpetual problem. You want to have certain controls enabled or disabled, visible or invisible, based on the values of other controls. Unlike windows, there is no ON_UPDATE_COMMAND_UI mechanism for doing this. Most books that show examples of how to do it get it wrong. That's because most of them illustrate fairly simple examples, where the simple methods are easy to write and easy to maintain. Unfortunately, in the real world, dialog boxes are usually fairly complex, and the enabling conditions are nontrivial. The simple methods illustrated in the introductory textbooks are considerably less than adequate in this case. A decade of Windows programming has allowed me to develop a methodology for handling this.

Paul DiLascia has published a technique in his C++ Q&A column that suggests a technique for causing ON_UPDATE_COMMAND_UI messages to be handled in a dialog. Unfortunately, I've tried this a couple times and could not get it to work. It appears on the MSDN CD-ROM; find it by searching for WM_KICKIDLE and looking at the C++ Q&A columns that come up. However, the only difference between the technique I'm about to describe and the technique he describes is how the code is partitioned. In his solution, each control has an ON_UPDATE_COMMAND_UI handler, and the code to enable the control goes in that handler. In my solution, all the code is in one function. But the important feature of both these techniques is that the control manipulation code exists in precisely one place in the program.

The usual examples given in introductory textbooks (with one exception I am certain of: see Win32 Programming, by Brent Rector and Joseph M. Newcomer) show dialogs as enabling controls in response to button presses, ListBox selections, and the like, in the code that responds to the event. This is the wrong place to do it.  

Why is this the wrong place? Well, a good example is a piece of code I inherited (I'm a freelance consultant). It had twenty-three separate places where it enabled one control. As usual, this led to the case where each change (such as adding a new control) created a new condition for enabling the control in question, and not every site got updated consistently. In fact, I found five separate algorithms for determining enabling. And the order in which they were executed produced very odd results. In addition, at least two other places where the state changed so the enabling condition changed did not bother to do the computations at all. Maintaining code like this is a nightmare. Getting it right is impossible.

Of course, you might think "The obviously correct thing to do would have been to make a subroutine that did the enabling and call it from the right places". This assumes that you know the right places. While my solution is not perfect in this regard, it is a whole lot better than the normal distributed update mechanism.

What I do is treat the enabling condition as a set of constraint equations. Each control has at most one instance of "EnableWindow" and at most one instance of "ShowWindow". Or it has at most two instances, which are in opposite branches of a simple if-statement. That's all. 

Every dialog of mine that has any interesting things happening to controls has a method called "updateControls". All state changes on all controls are computed in this method. The defect in this scheme is that I have to call updateControls on all state changes, which often means that I have control handlers (for example, for checkboxes) that do nothing but call updateControls. An alternative is to simply call the method from the OnIdle handler, which I choose not to do.

Furthermore, when the state of one control depends on one or more other controls, the state of the controls affecting it are directly accessed at the time the computation is done. No Boolean variables set magically from some other function. Every variable that can affect the state is computed when needed, and not an instant before. Don't do something clever like precompute the state based on some control state combination and store this in a Boolean for use by updateControls. Always compute from first principles, every time (well, if a database access is required, you can cache the database state, but in this case, the conditions should be computed directly from the fields of the record(s), and not from precomputed information that has been stored).

A potential objection to this method is that the distributed method computes only the state of the controls affected by the state change being processed, while my method requires recomputing the state of all the controls. This is "inefficient". This argument is fundamentally meaningless. It is based on early training of programmers that emphasizes that executing the fewest number of instructions possible to achieve a goal is the metric for a good program. This is the wrong training. If you believe this, rethink the problem. Efficiency matters only when it matters. The rest of the time, simplicity, correctness, and maintainability dominate. Efficient code is almost always harder to write, harder to debug, and harder to maintain than simple code. 

Unless you are doing a computation-intensive algorithm, efficiency is a third- or fourth-order effect. Remember that many of these rules were developed in the era when machines were very slow. 30 MIPS? That's a midrange Pentium. The first machine I programmed executed at approximately 0.003 MIPS. That's a factor of 10,000 improvement in performance in 36 years. Back then, every instruction counted. Today, the only criterion is responsiveness, and the cost of developing a program (the largest program that machine could hold was about 1800 assembly-code instructions; compare that with a medium-sized Windows app which may run 60,000 to 120,000 lines of C code).

Why doesn't efficiency matter when you're updating controls? Look at the human factors. A mouse is held approximately 2 feet from the ear. Sound travels at approximately 1100 ft/sec. This means that it takes approximately 2ms for the sound of the mouse click to reach the ear. The neural path from the fingertip to the brain of an adult is approximately 3 feet. Propagation of nerve impulses is approximately 300 ft/sec, meaning the sensation of the mouse click takes approximately 10ms to reach the brain. Perceptual delay in the brain can add between 50 and 250ms more.

Now, how many Pentium instructions can you execute in 2ms, 10ms, or 100ms? In 2ms, on a 500MHz machine that's 1,000,000 clock cycles, so you can execute a lot of instructions in that time. Even on a now-clunky 120MHz Pentium there is no noticeable delay in handling the controls. 

Therefore, the key idea is to make life as simple as possible, both for coding and for maintenance. Here's a sample that handles the updating of the OK button based on the emptiness of the c_Text control.

Here's the formal spec:

void CMyDialog::OnChangeText()
    {
     updateControls();
    }
 
void CMyDialog::updateControls()
    {
     BOOL enable;
 
     // c_OK =========================================
     CString s;
     c_Text.GetWindowText(s);
     s.TrimLeft(); // ignore leading spaces
     enable = s.GetLength() != 0 &&
                 (c_Option.GetCheck == BST_UNCHECKED ||
                  c_Count != 0);
     c_OK.EnableWindow(enable);
 
     // c_Count =======================================
     enable = c_Option.GetCheck();
     c_Count.EnableWindow(enable);
     x_Count.EnableWindow(enable);
     //================================================
    }

Sometimes, complex Boolean expressions get hard to write and debug. Another alternative is to rewrite the enable test for OK as:

     enable = s.GetLength() != 0;
     if(c_Option.GetCheck == BST_CHECKED &&
        c_Count == 0)
             enable = FALSE;

The technique is to establish the value of 'enable', and then all subsequent modifications to the variable use either '&=' or simply set the value FALSE. The monotonicity of the changes must always favor moving towards FALSE. Once the value is set, at no point is any computation done that can explicitly set it TRUE. This discipline ensures that you won't accidentally set it TRUE after some other condition has set it (correctly) FALSE. You can set it explicitly to FALSE, or use &=, but never do anything that might possibly increase it from FALSE to TRUE. A similar rule is used to compute visibility. I tend to use a Boolean variable for visibility, applying the same technique of monotonically decreasing computation, then execute a statement of the form

     c_Control.ShowWindow(visible ? SW_SHOW : SW_HIDE);

Often you want to enable/disable a group of controls based on a condition. I tend to call a function for such a group by first computing the desired state, and then calling a method such as shown below. The code implements controls that are complementary, and all based on the same condition:

void CMyDialog::changeGroup(BOOL mode)
    {
     c_Something.EnableWindow(mode);
     c_OtherThing.EnableWindow(!mode);
     c_ThisThing.ShowWindow(mode ? SW_SHOW : SW_HIDE);
     c_ThatThing.ShowWindow(mode ? SW_HIDE : SW_SHOW);
    }

So although not all the code is necessarily inline in updateControls, the changeGroup method is not called from any place other than updateControls.

No, this isn't perfect. Perfect would be that the ClassWizard provided ON_UPDATE_COMMAND_UI handlers for every control and Microsoft did the work necessary to see they were called. The persistent defect in my method is that you must remember to call updateControls whenever the state changes. The good news is that if you call it at the end of each control handler, and nothing changes, nothing needs to be done. Note that you will typically call updateControls at the end of OnInitDialog.

Yes, there is one additional problem. If what is being computed is the caption for a control, you'll find this technique produces an undesirable flicker. This is easy to fix. Consider the following, which changes a button caption from "Stop" to "Run":

BOOL isrunning;  // protected class member; set FALSE in constructor
 
void CMyDialog::OnStopRun( )
     {
      isrunning = !isrunning;
      updateControls( );
    }
 
void CMyDialog::UpdateControls()
    {
     CString caption;
     caption.LoadString(isrunning ? IDS_STOP : IDS_RUN);
     CString oldcaption;
     c_StopRun.GetWindowText(oldcaption);
     if(oldcaption != caption)
        c_StopRun.SetWindowText(caption);
    }

It should be obvious that if you have more than one button you are doing this to, there is a high payoff in defining a new CDCButton class (Dynamic Caption Button) which overrides SetWindowText:

void CDCButton::SetWindowText(CString caption)
    {
     CString old;
     CButton::GetWindowText(old);
     if(old == caption)
       return;
     CButton::SetWindowText(caption);
    }

When Controls Aren't There Yet: ASSERT failures

There is a problem during dialog startup. For example, if you have an OnChange or OnUpdate handler for an edit control, and you wish to manipulate another control. Even if you don't believe my methodology about always doing this in a single routine, you will still have serious problems. Consider the case where you want to enable a button when the text becomes nonempty. The test looks like

CString s;
c_Edit.GetWindowText(s);
s.TrimLeft();
c_DoSomething.EnableWindow(s.GetLength() > 0);

You find yourself in the middle of an ASSERT statement which, if you trace back, came from the EnableWindow call. You look at the m_hWnd member of the c_DoSomething button, you find that it is 0. If you are using GetDlgItem, it is worse, because you would have written something like

CButton * doSomething = (CButton *)GetDlgItem(IDC_DO_SOMETHING);

and your attempt to use it would take an access fault because the pointer was NULL. (Don't use GetDlgItem; see my essay on the right way to do this).

This happens because of a mismatch between MFC and the underlying Windows mechanisms. What has happened is that the DDX mechanism has created the edit control, which means that it can start generating messages that the MESSAGE_MAP will start dispatching, but has not yet assigned the IDC_DO_SOMETHING control to its corresponding CButton. Hence the ASSERT failure when you try to use it, even though the control already exists.

The most common cause of the GetDlgItem failure is that in the tab order the sequence is to create the edit control, create its spin-control buddy, which then generates a message to the dialog, which intercepts it, but the dialog creation code has not yet created the IDC_DO_SOMETHING button. So GetDlgItem necessarily returns a NULL pointer.

The way I get around this is to create a member variable of my dialog class, 

BOOL initialized;

In the class constructor, I simply set

initialized = FALSE;

At the end of OnInitDialog, I do the following:

initialized = TRUE;
updateControls();

and I modify updateControls() to have, as its first executable code, the statement

if(!initialized)
     return;

There are occasional other places I need to perform this test. Often you find these empirically.

You can sometimes replace the test with the implicit test,

if(!::IsWindow(c_Button.m_hWnd))
    return;

for example, which tests the control variable c_Button to see if the window has been created. Since I usually have to introduce the initialized variable, I find this variant less common in my code.

Summary

Of the variety of techniques for control management in dialogs, a decade of Windows programming has convinced me that the core philosophy of this method is the only philosophy that can ever make sense. Whether it is done by explicit call, as I do, or from OnIdle, or distributed to ON_UPDATE_COMMAND_UI handlers, there must be no more than one ShowWindow and one EnableWindow per control. To do otherwise is simply insane. It produces code that is difficult to write, difficult-to-impossible to debug, and utterly impossible to maintain. That is unacceptable.

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 1999-2003 The Joseph M. Newcomer Co. All Rights Reserved.
Last modified: May 14, 2011