Creating a Dialog-based App

Home
Back To Tips Page

This is one of several essays dealing with dialogs. Another one discusses the issues of control management, and one discusses issues of menu management, printing from a dialog, and resizable dialogs.

This essay covers

Avoiding Enter/Escape termination

There are a large number of recurring questions on creating dialog-based apps. This is my method of creating dialog-based apps, and in addition, illustrates how to handle the Enter key in an edit control.

The most common problems people ask about are "How do I trap the ESC key so it doesn't terminate my application?" and "How do I trap the Enter key so it doesn't terminate my application?". These are easy.

First, I do not believe in handling this in PreTranslateMessage. The result of this is distributed knowledge which makes it difficult to maintain the code, particularly when controls are added or deleted. My technique uses subclassing to put the intelligence where it belongs, in the control, and puts the handlers in a known and easily understood place, a message handler.

First, create your dialog-based application. You will end up with something that resembles the dialog below (which I've shrunk so it doesn't take up too much space on the page):

Enter the ClassWizard. Select IDOK and its BN_CLICKED handler, and click Add Function. Accept the name it gives. Do the same for IDCANCEL. You should end up with something that looks like the illustration shown below. I show the IDCANCEL handler clicked, since that is the last one I added (note that two lines below, the ON_IDOK::BN_CLICKED handler is already present).

Next, select the dialog class, and find the WM_CLOSE message handler. Click Add Function. You should now have something like I show below.

Go to your source code and examine it. You can either exit ClassWizard via the OK button, or click the Edit Code button. Your code should look like this:

void CDialogappDlg::OnOK() 
{
	// TODO: Add extra validation here
	
	CDialog::OnOK();
}

void CDialogappDlg::OnCancel() 
{
	// TODO: Add extra cleanup here
	
	CDialog::OnCancel();
}

void CDialogappDlg::OnClose() 
{
	// TODO: Add your message handler code here and/or call default
	
	CDialog::OnClose();
}

Change it to be as shown below. Delete the bodies of OnOK and OnCancel. Put the CDialog::OnOK call in the OnClose handler.

void CDialogappDlg::OnOK() 
{
}

void CDialogappDlg::OnCancel() 
{
}

void CDialogappDlg::OnClose() 
{
	CDialog::OnOK();
}

Go back to the dialog. Delete the OK and Cancel buttons. Add in your controls. For example, I want to add an edit control that reacts only when you either leave it or hit Enter, and prior to that it can be changed without any effect. The other control reacts immediately.

Creating reactive controls

To create an edit control that reacts immediately, put in an EN_CHANGE handler

In VS.NET, this is done by right-clicking on the control and selecting Add Event Handler.

Also, create a member variable for the control; I describe how to do this in a companion essay. 

In order to access the status control which will show the effect, you must assign it an ID other than IDC_STATIC. Create a control variable to represent it.

I end up with variables as shown below (note that I have not yet created a variable for IDC_DELAYED)

The code for immediate response is simple:

void CDialogappDlg::OnChangeImmediate() 
{
 CString s;
 c_Immediate.GetWindowText(s);
 c_Status.SetWindowText(s);
}

When running, it produces output such as

As each character is typed in the "Immediate Reaction" edit control, it appears in the box labelled "I see". 

However, I might not want a reaction to take place until I hit Enter, or leave focus, or both. To do this, I create a subclass of CEdit:

 

Create a class derived from CEdit, which I have called CEnterEdit.

In VS.NET, you will go to the ClassView, right click on the project, and select the Add Ø item, then Add Class...  (this example is from a different application, but the idea should come across)

You will then have to select what kind of class, and the selection should be "MFC Class"

You can then supply the name of the class and select the base class.

Now create a member variable for IDC_DELAYED of that type.  In VS6, you will go to the ClassWizard, select the Members tab, and ask to add a member variable.

Which should leave you with the following definitions

In VS.NET you will right-click on the control, and select Add Variable...  In VS.NET, you don't even have the option of having the known immediately-derived classes available to you in the dropdown; you have to type in the name of the class.  Like a lot of the VS.NET IDE interface, this change was apparently made to make it as difficult as possible to actually get work done.  It doesn't help the usability that after each variable addition, you are switched to the source code of the module, as if this makes any sense whatsoever, since there is absolutely nothing you can do in the source at this point that is interesting.  Typically, you want to add all the variables, and then go edit the source.

Note that you should change the Access of the variable to "protected" because it makes no sense whatsoever to have a control variable be public

Notifying the Parent

In ClassWizard select the new class you defined. (Note that if you are continuing from the previous step you will be prompted to save your changes; select "Yes").

Add handlers for =EN_KILLFOCUS, WM_CHAR, and WM_GETDLGCODE:

You must remember to add the header file for your new class to your compilations before any file that uses it. For a dialog-based app, this means the app source and the dialog source must both have it, for example

#include "stdafx.h"
#include "dialogapp.h"
#include "EnterEdit.h"
#include "dialogappDlg.h"

Otherwise, you will get compilation errors whenever your ...Dlg.h file is processed.

If you use a custom message, you must define it. I prefer to use Registered Window Messages, as outlined in my companion essay, so I added to EnterEdit.h the following declaration. Note that it is critical that when you add a user-defined message, you document its parameters, effect, and return result! Doing anything less will lead to unintelligible and unmaintainable code.

/****************************************************************************
*                              UWM_EDIT_COMPLETE
* Inputs:
*       WPARAM: Control ID of the control whose edit completed
*	LPARAM: CWnd * of the control whose edit completed
* Result: LRESULT
*       Logically void, 0, always
* Effect: 
*       Posted/Sent to the parent of this control to indicate that the
*	edit has completed, either by the user typing <Enter> or focus leaving
****************************************************************************/

#define UWM_EDIT_COMPLETE_MSG _T("UWM_EDIT_COMPLETE-{165BBEA0-C1A8-11d5-A04D-006067718D04}")

I declare it in EnterEdit.cpp as shown below, or you could use the more convenient DECLARE_MESSAGE macro I define in my companion essay.

static UINT UWM_EDIT_COMPLETE = ::RegisterWindowMessage(UWM_EDIT_COMPLETE_MSG);

This allows me to write the handlers I need.

First, in order to bypass the tendency of the dialog superclass to intercept and handle keyboard input, you should make the indicated change in the OnGetDlgCode handler:

UINT CEnterEdit::OnGetDlgCode() 
{
	return CEdit::OnGetDlgCode() | DLGC_WANTALLKEYS;
}

This tells the dialog superclass that when focus is in this control it should deliver to it all the keys pressed.

Then, in your OnChar handler, you can do

void CEnterEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
  switch(nChar)
      { /* nChar */
       case VK_RETURN:
	    GetParent()->SendMessage(UWM_EDIT_COMPLETE, GetDlgCtrlID(), (LPARAM)this);
	    return;
      } /* nChar */
  CEdit::OnChar(nChar, nRepCnt, nFlags);
}

Why a switch with only one case? Why not? It makes it easy to add other cases, and captures the fact that you are concerned at the moment with some finite set of characters. I prefer switch statements in such contexts. They eliminate the temptation to add complex if-then-else structures, resulting in cleaner and easier-to-maintain code.

When an Enter key is seen, the message is sent to the parent which can then react to it.

To handle processing when focus is lost, modify the reflected =EN_KILLFOCUS handler as shown below

void CEnterEdit::OnKillfocus() 
{
 GetParent()->SendMessage(UWM_EDIT_COMPLETE, GetDlgCtrlID(), (LPARAM)this);	
}

Now you need to add a handler to the parent. This means you must declare a UINT for the Registered Window Message just like you did in the child edit control, and add the indicated message to the MESSAGE_MAP:

BEGIN_MESSAGE_MAP(CDialogappDlg, CDialog)
        ON_REGISTERED_MESSAGE(UWM_EDIT_COMPLETE, OnEditComplete)
	//{{AFX_MSG_MAP(CDialogappDlg)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_WM_CLOSE()
	ON_EN_CHANGE(IDC_IMMEDIATE, OnChangeImmediate)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

IMPORTANT: In VS6, this should be outside (above or below; I show above) the magic AFX_MSG_MAP comments! Otherwise you can confuse the ClassWizard.  in VS.NET, there is no such requirement.

You must add a handler to the declarations in your header file: 

        afx_msg LRESULT OnEditComplete(WPARAM, LPARAM);
	// Generated message map functions
	//{{AFX_MSG(CDialogappDlg)
	virtual BOOL OnInitDialog();

IMPORTANT: In VS6, this must be outside (above or below, I show above) the magic AFX_MSG comments. Otherwise you can confuse the ClassWizard.  In VS.NET, there is no such requirement.

Note that in VS.NET the handler methods are all declared as public.  There is no possible way it could ever make sense to have these methods be declared as public.  Edit the header file and make them all protected.  Good programming practice in C++ is to minimize the number of public entities in a class.  Only those variables and methods which would be accessed from outside the class should be public.  Control variables and handler methods would never be accessed from outside the class (and if you are doing this, you have committed a grievous error.  Rewrite your code).

The handler could look like the one I show here:

LRESULT CDialogappDlg::OnEditComplete(WPARAM, LPARAM lParam)
    {
     CEnterEdit * edit = (CEnterEdit *)lParam;
     CString s;
     edit->GetWindowText(s);
     c_Status.SetWindowText(s);
     return 0;
    } // CDialogappDlg::OnEditComplete

In this simple example, I don't care which window generated the message, so I ignore WPARAM, and whatever control was activated, I simply set its text into the status window. In a dialog with many controls, you would probably want to switch on the WPARAM value.

Now when I run the app, and type text into the "Delayed Reaction" box, I see the following. Note that the contents of the "I see" status box is whatever was left from the previous typing.

but if I hit Enter or change focus, I will get the following result. The new text is in the "I See" box, and the focus (as shown by the caret) is still in the Delayed Reaction" box.

Or, if I switch focus, I will also see the effect. Note that by doing this on a kill focus, I will also see the text activate if I switch to another application. If this is not desired, don't use the =EN_KILLFOCUS reflector, or you must do a more complex test. Note that starting from the first picture, I get the result shown below when I change focus to the upper window.

Many people have commented on this technique, "You are creating another subclass. Why do you feel you need to do this?" Never be afraid to create a custom subclass. I have been known to create a dozen in one day. It is a natural paradigm for MFC programming. These are the techniques I use to build real software that is used by thousands of people and maintained by someone other than myself.

download.gif (1234 bytes)Download the code sample I used to create all those windows.

 


A Tray-Based Dialog App

Sometimes you want your app to come up as a tray icon app, with no actual visibility when it starts up. There are two issues involved here: preventing the initial "flash", and creating the tray icon and its associated menu.

Preventing the "flash" 

When a dialog-based application comes up, the dialog manager normally forces it to be visible. The usual solution I used was to put up with this, and do a PostMessage of a user-defined message in OnInitDialog which, when finally processed, would hide the application. This meant the dialog would initially "flash" on the screen. This is visually disturbing.

Preventing the flash was shown in a very clever method found in a nice article

http://www.codeproject.com/dialog/dlgboxtricks.asp

which has several other dialog box tricks. I suggest you go look at it as well. I was given permission by the author to include this in my essay, but there are lots of other useful techniques there as well. These include

All of these are useful techniques, and I highly recommend this article.

The trick (one I would probably have never discovered on my own) is to use the WM_WINDOWPOSCHANGING message. 

First, add a BOOL variable to your dialog class, and initialize it as FALSE:

class CWhateverDialog {
    ... usual stuff here
    protected:
       BOOL visible;
    ...probably lots of other stuff here
   };

To the .cpp file, add the initialization in the constructor:

CWhateverDialog::CWhateverDialog()
   {
    // ... usual initialization of constructor here
    visible = FALSE;
   }

Then add a handler for WM_WINDOWPOSCHANGING:

void CWhateverDialog::OnWindowPosChanging(WINDOWPOS * pos)
   {
    if(!visible)
      pos->flags &= ~SWP_SHOWWINDOW;
    CDialog::OnWindowPosChanging(pos); 
   }

To actually make the window available, you need to do

visible = TRUE;
ShowWindow(SW_SHOW);

Creating the Tray Icon

Create an icon of your choice for the tray icon. You should create a 16×16 version of the icon. You will also need to create a user-defined message code; I strongly recommend using a Registered Window Message, as I describe in my accompanying essay. You must add the prototypes for the following functions to the class definition of your class, in the .h file:

BOOL registerTray();
void unregisterTray();

You then add code to the .cpp file:

#define TRAY_ICON_ID 1 // see below

BOOL CWhateverDialog::registerTray()
    {
     NOTIFYICONDATA tray;
     // IDI_TRAY is the image of the tray icon
     HICON icon = AfxGetApp()->LoadIcon(IDI_TRAY);
     CString tip;

     // This is the string that is displayed to describe 
     // the application when the mouse hovers over the
     // tray icon
     tip.LoadString(IDS_TRAY_TIP);

     // initialize the NOTIFYICONDATA structure
     tray.cbSize = sizeof(tray);

     // Indicate that the notification messages should come to 
     // this window
     tray.hWnd   = m_hWnd;

     // This ID is an integer you assign so you can distinguish,
     // on a callback, which of several icons may have been involved
     // in the notification. An app could register more than one
     // tray icon if it provides for multiple functions. For this
     // app there is only one ID, which I've defined as the constant 1
     tray.uID    = TRAY_ICON_ID;

     // Set the code for the message to be sent to this window when
     // a notification occurs
     tray.uCallbackMessage = UWM_TRAY_CALLBACK;

     // Set the icon to the icon handle of the icon we loaded
     tray.hIcon  = icon;

     // Set the string which is the text of the string to display
     // when the mouse hovers over the icon.
     // This is limited to 64 characters in Windows NT and
     // 128 characters in Windows 2000/XP
     lstrcpy(tray.szTip, tip);

     // Set the flags. These flags indicate certain fields are defined
     // in the NOTIFYICONDATA structure:
     //  NIF_MESSAGE: The uCallbackMessage field is valid
     //  NIF_ICON:    The hIcon field is valid
     //  NIF_TIP:     The szTip field is valid
     tray.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;

     // Set the notification icon
     BOOL result = Shell_NotifyIcon(NIM_ADD, &tray);
     if(result)
        { /* success */
         //...you may want to do something to record that it succeeded
	} /* success */
     else
        { /* failure */
	 DWORD err = ::GetLastError();
         //...you may want to do something to record the error
         // In particular, you may want to disable the ability to 
         // hide the app because if there is no tray icon it
         // will be impossible to make the app visible
	} /* failure */

     // We no longer need the icon handle, so the icon can be destroyed
     ::DestroyIcon(icon);
     return result;
    }

A corresponding routine is required to unregister the tray icon. If you don't unregister the icon, you will end up with multiple copies in the tray, and they will appear to spontaneously disappear when the tray is activated (if you normally keep it hidden), which is an ugly effect. Expect this to happen naturally if you are debugging and simply terminate the program in the middle of debugging.

void CWhateverDialog::unregisterTray()
    {
     NOTIFYICONDATA tray;

     // Initialize the tray icon structure. To unregister, you must
     // use the same hWnd and uID values as you used to register
     tray.cbSize = sizeof(tray);
     tray.hWnd = m_hWnd;
     tray.uID = TRAY_ICON_ID;

     // Unregister the icon
     BOOL result = Shell_NotifyIcon(NIM_DELETE, &tray);
     if(result)
        { /* success */
	 //... you may want to record in some way the fact that the
         //... icon is unregistered
	} /* success */
     else
        { /* failure */
	 DWORD err = ::GetLastError();
         //... you may want to record in some way the fact that the
         //... icon failed to unregister
	} /* failure */
    }

Handling the Tray Icon menu

You need to register the tray callback message ID:

static UINT UWM_TRAY_CALLBACK =                                     
                   ::RegisterWindowMessage(_T("UWM_TRAY_CALLBACK-") 
                     _T("{6b8c4821-e6a4-11D1-8370-00aa005c0507}")); 

You must add an entry to your MESSAGE_MAP, outside the magic AFX_ comments:

ON_REGISTERED_MESSAGE(UWM_TRAY_CALLBACK, OnTrayCallback)

You must declare the handling function in your header file, by creating an entry

    protected:
       afx_msg LRESULT OnTrayCallback(WPARAM, LPARAM);

You then add the code to the .cpp file. Note that the wParam value is not needed. In this code, a double-click is designed to pop the window up, and a right-button click will pop up a menu.

LRESULT CWhateverDialog::OnTrayCallback(WPARAM, LPARAM lParam)
    {
     switch(lParam)
        { /* lParam */
	 case WM_LBUTTONDBLCLK:
                 visible = TRUE; // see previous section
		 ShowWindow(SW_SHOW);
		 SetForegroundWindow();
		 break;
	 case WM_RBUTTONDOWN:
		 createPopupMenu();
		 break;
	} /* lParam */
     return 0;
    }

The popup menu handler requires a couple tricks, which are documented in KB article Q135788. Generally, it looks like any other way you would invoke TrackPopupMenu on a right-click, except for the special actions required by the KB article. You will simply add ordinary menu-handling methods using the ClassWizard, and create the menu using the ordinary menu-editing capabilities of the resource editor.

void CWhateverDialog::createPopupMenu()
    {
     CMenu top;
     top.LoadMenu(IDR_TRAY_MENU);
     CMenu * popup = top.GetSubMenu(0);

     SetForegroundWindow(); // See KB article Q135788!

     CPoint pt;
     GetCursorPos(&pt);

     if(popup != NULL)
	popup->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, pt.x, pt.y, this);

     PostMessage(WM_NULL); // See KB article Q135788!
    }

Adding a menu to a dialog

A menu can be easily added to any dialog.  Simply go to the Properties of the dialog and add the menu ID.  Also, see below the erroneous advice that a "thin" border is mandatory for a menu to work.  This advice is nonsense.  A dialog can have a menu independent of its border size (thin, dialog, or resizing). 

There is a problem with adding a menu to a dialog box, if you are trying to use CalcWindowRect to compute the size of the dialog box to hold a specific size client area.  CalcWindowRect fails to take into account the presence of the menu bar.  See my essay on MSDN Errors and Omissions for the solution to this problem.

Getting Accelerator Keys to work

If you dialog has a menu, you'd like to use accelerator keys to activate some of the menu items.  For example, Ctrl+S to save a file, Alt+F4 to exit the program, etc.  The problem in dialog-based apps is that the MFC framework necessary to process these is missing.

There is a KB article, #100770, which is almost right, but it introduces a lot of fundamentally-not-best-practice mechanisms.  So this essay discusses how to add accelerator keys without violating good practice.  It is loosely based on KB100770, and I'll show where it differs.

First, note that in step 3, it says that the border style must be specified as "Thin".  It says "This step is required for a dialog box that contains a menu".  It suggests that other border styles will not work.  This is simply not true.  I use this technique with a resizing border and it works just fine.  What it apparently meant to say was to not use a Dialog border.

I will skip over the steps where you create a menu, the individual menu items, and the accelerator table.  Presumably anyone sophisticated enough to care about this issue already knows how to do that.

The first major difference with KB 100770 is the use of global variables.  They are not necessary.  The article advises two points which constitute something fairly distant from "best practice": the use of global variables, and the editing of stdafx.h to hold project definitions instead of just stable library definitions.  Neither of these steps are necessary, and appear to be the result of an amateur translation from a pure Win32 program to MFC by someone who did not really understand MFC.

In addition, this violates various hook requirements.  If the code is < 0, then no further processing should be done.  So the return statement is required where indicated.

Having created your dialog project, your menu, and your accelerator table, proceed as follows:

Edit the class definition of your CWinApp-derived class and add a member variable for the accelerator table.  For example:

class CMyApp : public CWinApp {
    ...whatever is already there
   protected:
       HACCEL accelerators;
};

In the constructor of the class, set this member to NULL:

CMyApp::CMyApp()
   {
     accelerators = NULL;
   }

In the InitInstance handler for your application, add the following line:

accelerators = ::LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_ACCELERATOR_TABLE_NAME_HERE));

Add a new virtual method using the ClassWizard; overriding the CWinApp::ProcessMessageFilter method:

BOOL CMyApp::ProcessMessageFilter(int code, LPMSG msg)
   {
    if(code < 0)
       return CWinApp::ProcessMessageFilter(code, msg);  // [add return]

    if(accelerators != NULL)
       if(::TranslateAccelerator(m_pMainWnd->m_hWnd, accelerators, msg))
           return TRUE;
    
    return CWinApp::ProcessMessageFilter(code, msg);
   }

Note that VS.NET erroneously declares this method as public in the header file.  There is no possible way it could ever make sense to declare this method public.  Edit the header file and make it protected.

When the shortcuts don't work

You may be dismayed to discover that in spite of this, your shortcuts don't work.  For example, supposed you use Alt+F to activate the File menu.  But the file menu doesn't drop down?  What went wrong?  The glitch here is that you have some control on the dialog which has an underlined F as its activation, e.g., Find or Fixup or something like that. If you have a control which has the same shortcut as a menu item, the control will take precedence.

Change log

[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-2008 FlounderCraft, Ltd.,  All Rights Reserved
Last modified: May 14, 2011