C++ Q & A

Paul DiLascia

Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992).

Q. I have an MFC doc/view MDI application that currently displays a DIB. I would like the ability to view other bitmap types such as PCX or TIFF. Do I need to create and add to the app class a separate document template for each bitmap type, or is there a way to add the type’s extension to the File | Open dialog and then instantiate the respective graphic object such as CDib, CTiff, and so on, from the document object, based on the type selected in the File | Open dialog?

Richard D. Shane

A. Yes, there’s a way. In fact, adding additional file extensions to the File | Open dialog is easy. The place to do it is in the resource string for your document template (see Figure 1). IDR_MYDOCTYPE is the ID used to create the document template. MFC uses the fourth and fifth newline-delimited substrings in the File | Open dialog, passing in the OPENFILENAME struct to ::GetOpenFileName. The former is the human-readable list of file types; the latter specifies the actual file extensions.

Figure 1 File Extensions in a Resource Template
"STRINGTABLE PRELOAD DISCARDABLE 
BEGIN
 IDR_MYDOCTYPE "MyApp\nMyApp\nMyApp Document\nMyApp Files (*.dib,*.tif,*.pcx)\n.dib;*.tif;*.pcx\n\n"
END

The exact syntax for the strings is a little confusing because of the peculiar way MFC munges the strings before sending them to GetOpenFileName. MFC prepends an asterisk before passing the fifth substring to ::Get­OpenFileName. In a vanilla doc/view app with just one file extension per document template, you’d specify the string as .dib. MFC would prepend the asterisk to make it *.dib, which it passes along to GetOpenFileName as the filter in OPENFILENAME::lpstrFilter. If you want to add more extensions, you can just add them, separated with semicolons, remembering to include the asterisk for all but the first. In the example, the filter is

".dib;*.tif;*.pcx

which MFC converts to

"*.dib;*.tif;*.pcx

before sending to the File | Open dialog. MFC also appends the all filter, *.*, to the extension list, as well as the text “All Files (*.*)” to the human-readable version. If you don’t want this, you’ll have to override CWinApp::OnFileOpen to run your own File | Open dialog without adding *.*. It is, however, considered standard UI to give the user the option of looking at all files.

Once you make the changes to the resource string, you’ll get a dialog like the one in Figure 2. When the user opens a file, your OnOpen­Document function could get a DIB, TIFF, or PCX image, and your code is responsible for determining which type it is and loading it appropriately.

Figure 2 An open dialog

When you do, don’t rely on the filename extension to tell you what kind of file it is, because the user may have mistakenly saved a PCX file with a TIF extension or vice versa. Also, some apps may use the same extension. For example, Microsoft® Word and WordPerfect both use .doc, and many software programs come with a readme.doc file that’s plain ASCII. Nevertheless, if you try to open a WordPerfect or ASCII file in Word, it opens correctly, even if the extension is .doc. The only sure way to tell what kind of file the user selected is to open it and read the first few bytes of the file, which contain the magic number or other identifying information. For example, Windows bitmaps begin with the letters BM whereas TIFF files begin with II.

If I were writing a system like yours, I’d write a function with a name like CDocument::DetermineFileType to determine the file type. Once you’ve done that, you can either rewind to the start of the file and call the appropriate serialization function (assuming you’re using MFC’s Serialize mechanism), or just continue reading the file using your own homebrew load function.

Q. I’m writing a Web browser in MFC 4.0 and would like to set up a CDialogBar with a CComboBox similar to the interface used by Internet Explorer. However, I can’t get the CComboBox to notify my parent window that the user pressed the Return key. Is there a way to do this?

Dominic Milioto

Q. How can I change the tab order of controls in a dialog box? I’m writing a program that creates dialog boxes on-the-fly rather than from the RC files. To do this I create the dialog box template in memory and call the CreateIndirect (or InitModalIndirect) function. Everything is OK (the order of controls in memory gives me the tab order) until I add more controls at runtime. In some cases, I need to insert more controls from case to case. I add them in OnInitDialog, but all the controls are added to the end of the tab order list. I tried to catch the Tab key in CDialog and play with OnSetFocus but that doesn’t seem to work. I’m using Visual C++® with MFC 4.2.

Sturm Dorel

A. Both of these problems can be solved easily by using accelerators. In general, whenever you want to trap a single key for an entire dialog or window, you’ll go bonkers trying to do it with WM_CHAR/OnChar handlers or WM_KEYUP/WM_KEYDOWN messages in the individual controls—this is what accelerators are for. In the case of Return and Tab, all you have to do is invent commands like ID_ENTER and ID_TAB, and then bind them to the Enter and Tab keys to achieve total key-handling harmony. How you implement the commands is up to you. In the case of changing the Tab order of controls in a dialog, you don’t have to physically alter the order of DLGITEMTEMPLATEs; all you have to do is intercept the Tab and Shift-Tab keys to ignore the natural Tab order and march to the beat of a different drum.

Of course, the details are a little more complex than first appears. Figure 3 shows a program I wrote, TABDLG, that handles the Return and Tab keys and changes the tab order dynamically. Figure 4 shows the dialog running. When you press the Scramble TAB Order button, navigation becomes random; pressing Tab or Shift-Tab passes focus to a random control in the dialog. If you press Unscramble TAB Order (the button is renamed), the tab order returns to normal as defined by the order of controls in the resource file.

Figure 3 File Open Dialog
"////////////////////////////////////////////////////////////////
// TabDlg 1997 Microsoft Systems Journal. 
// If this program works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
//
// TabDlg illustrates how to make a dialog with dynamic TAB order
// and that handles the RETURN key.
//

#include <afxwin.h>         // MFC core and standard components
#include <afxext.h>         // MFC extensions
#include <afxpriv.h>        // for WM_KICKIDLE
#include <stdlib.h>         // for random number generator fns
#include "resource.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

//////////////////
// Dialog class that alters the TAB sequence and handles
// the RETURN key.
//
class CTabDialog : public CDialog {
   HACCEL m_hAccel;                     // dialog accelerators
   BOOL   m_bRandomTabOrder;            // do random TAB order?
public:
   CTabDialog();
   virtual BOOL OnInitDialog();
   virtual BOOL PreTranslateMessage(MSG* pMsg);

   // Message/command handlers
   afx_msg LRESULT OnKickIdle(WPARAM wp, LPARAM lp);
   afx_msg void OnChangeTabs();
   afx_msg void OnReturn();
   afx_msg void OnUpdateChangeTabs(CCmdUI* pCmdUI);
   afx_msg BOOL OnNextPrevField(UINT nCmdID);
   DECLARE_MESSAGE_MAP()
};

////////////////////////////////////////////////////////////////
// Application class
//
class CApp : public CWinApp {
public:
   CApp() { }
   virtual BOOL InitInstance();
} theApp;

/////////////////
// Initialize: just run the dialog and quit.
//
BOOL CApp::InitInstance()
{
   srand(time(NULL)); // seed random number generator
   
   CTabDialog dlg;
   int nResponse = dlg.DoModal();
   if (nResponse == IDOK) {
      // TODO: handle case when the dialog is dismissed with OK
   } else if (nResponse == IDCANCEL) {
      // TODO: handle case when the dialog is dismissed with Cancel
   }

   // Since the dialog has been closed, return FALSE so that we exit the
   // application, rather than start the application's message pump.
   return FALSE;
}


////////////////////////////////////////////////////////////////
// CTabDialog
//
BEGIN_MESSAGE_MAP(CTabDialog, CDialog)
   ON_MESSAGE(WM_KICKIDLE,       OnKickIdle)
   ON_COMMAND(ID_CHANGE_TABS,    OnChangeTabs)
   ON_COMMAND(ID_RETURN,         OnReturn)
   ON_COMMAND_EX(ID_NEXT_FIELD,  OnNextPrevField)
   ON_COMMAND_EX(ID_PREV_FIELD,  OnNextPrevField)
   ON_UPDATE_COMMAND_UI(ID_CHANGE_TABS, OnUpdateChangeTabs)
END_MESSAGE_MAP()

//////////////////
// Construct dialog: set everything to zero or NULL.
//
CTabDialog::CTabDialog() : CDialog(IDD_DIALOG1)
{
   m_hAccel = NULL;
   m_bRandomTabOrder = FALSE;
}

//////////////////
// Possibly translate accelerator key.
//
BOOL CTabDialog::PreTranslateMessage(MSG* pMsg)
{
   if (pMsg->message >= WM_KEYFIRST && pMsg->message <= WM_KEYLAST) {
      // Translate the message using accelerator table
      ASSERT(m_hAccel);
      return ::TranslateAccelerator(m_hWnd, m_hAccel, pMsg);
   }
   return CDialog::PreTranslateMessage(pMsg);
}

//////////////////
// Handle idle message: update dialog controls. This is required to make
// the idle update command UI mechanism work for a dialog.
//
LRESULT CTabDialog::OnKickIdle(WPARAM wp, LPARAM lCount)
{
   UpdateDialogControls(this, TRUE);
   return 0;
}

/////////////////
// Initialize dialog: load accelerators and set initial focus.
//
BOOL CTabDialog::OnInitDialog()
{
   m_hAccel = ::LoadAccelerators(AfxGetInstanceHandle(),
      MAKEINTRESOURCE(IDD_DIALOG1));
   ASSERT(m_hAccel);
   GotoDlgCtrl(GetDlgItem(IDC_EDIT1));
   return FALSE; // because I have set the focus
}

//////////////////
// Scramble/unscramble tab order
//
void CTabDialog::OnChangeTabs()
{
   m_bRandomTabOrder = !m_bRandomTabOrder;
}

//////////////////
// Update name of Scramble/unscramble button
//
void CTabDialog::OnUpdateChangeTabs(CCmdUI* pCmdUI)
{
   pCmdUI->SetText(m_bRandomTabOrder ?
      "Unscramble &TAB Order" : "Scramble &TAB Order");
}

/////////////////
// Handle Return (Enter) key
//
void CTabDialog::OnReturn()
{
   OnNextPrevField(ID_NEXT_FIELD);
}

/////////////////
// Go to next or previous field
//
BOOL CTabDialog::OnNextPrevField(UINT nCmdID)
{
   CWnd* pWnd = CWnd::GetFocus();
   if (m_bRandomTabOrder) {
      static int controls[] = { IDC_EDIT1,
         IDC_EDIT2, IDC_EDIT3, IDC_COMBO1, ID_CHANGE_TABS, IDOK, IDCANCEL };
      const NCONTROLS = sizeof(controls)/sizeof(int);

      // Select a random control that's different from the current one.
      int nIDCtrl;
      while ((nIDCtrl = controls[rand() % NCONTROLS]) == pWnd->GetDlgCtrlID())
         ; // same as current, try another
      pWnd = GetDlgItem(nIDCtrl);

   } else 
      // Normal next/prev using dialog's TAB order.
      pWnd = GetNextDlgTabItem(pWnd, nCmdID==ID_PREV_FIELD);

   ASSERT(pWnd);
   GotoDlgCtrl(pWnd);
   return TRUE;
}

Figure 4 TABDLG dialog running

To make TABDLG work, the first step is to invent the commands and bind them to keys. This part is straightforward. Here are the relevant lines from my RC file:

"IDD_DIALOG1 ACCELERATORS PRELOAD MOVEABLE PURE 
BEGIN
  VK_TAB,    ID_NEXT_FIELD, VIRTKEY 
  VK_TAB,    ID_PREV_FIELD, VIRTKEY, SHIFT
  VK_RETURN, ID_RETURN,     VIRTKEY 
END

I used the same ID (IDD_DIALOG1) for the accelerators as for the dialog to suggest that the accelerators go with the dialog. ID_NEXT_FIELD, ID_PREV_FIELD, and ID_RETURN are commands I made up, just like ID_OPEN_FILE or ID_SOME_OTHER_COMMAND or any other command ID. In doc/view apps, if you use IDR_MAIN­FRAME or whatever ID defines your doc template, MFC will automatically find and use the accelerator table with the same ID. For dialogs, however, you have to manually hook up the accelerator table.

Fortunately, it’s not too difficult. There are two steps. First, you have to load the accelerators. The most convenient place is in the dialog’s InitDialog function.

"// In CTabDialog::InitDialog
m_hAccel = ::LoadAccelerators(AfxGetInstanceHandle(),
                       MAKEINTRESOURCE(IDD_DIALOG1));
ASSERT(m_hAccel);

Pretty easy, eh?

Second, you have to use the accelerators. This requires calling a special::TranslateAccelerator function from your dialog’s PreTranslateMessage virtual function:

"BOOL CTabDialog::PreTranslateMessage(MSG* pMsg)
{
  if (pMsg->message >= WM_KEYFIRST && 
    pMsg->message <= WM_KEYLAST) {
    // translate using accelerator table
     ASSERT(m_hAccel);
     return ::TranslateAccelerator(m_hWnd, m_hAccel, 
                                   pMsg);
  }
  return CDialog::PreTranslateMessage(pMsg);
}

Don’t have a conniption; it’s just Windows voodoo. TranslateAccelerator does all the magic to translate WM_KEYUP and WM_KEYDOWN messages into WM_
COMMAND messages when the keys pressed match commands in the accelerator table. Figure 5 shows how it works.

Figure 5 TranslateAccelerator

TranslateAccelerator is a Windows thing, and PreTrans­lateMessage is an MFC thing. MFC calls CWnd::Pre­­TranslateMessage—deep in the dialog’s message loop—in CWnd::RunModalLoop, which is called from CDialog::DoModal. MFC gurus and trivia experts know that this behavior is new as of MFC version 4.0, in which the modal dialog became a modeless dialog that runs with its own MFC-supplied message loop and with its main window disabled to simulate modalness. If you’re using an earlier version of Visual C++ (such as version 1.52 for Windows® 3.1), the easiest way to get accelerators for a modal dialog is to mimic the newer MFC: implement your dialog as modeless and disable your main window while the dialog is running. Then you can call TranslateAccelerator from your app’s PreTranslateMessage function.

Once you have the accelerators hooked up, all that remains is to implement the commands. This works like any other command: just add a handler function with an ON_COMMAND entry to your dialog’s message map. For TABDLG, ID_RETURN is handled by OnReturn, which treats the Return key just like a Tab, by navigating to the next field—instead of pressing the default OK button as is normally the case.

ID_NEXT_FIELD and ID_PREV_FIELD are handled by the same function, OnNextPrevField, which determines the next field in the tab order based on the value of a member variable m_bRandomTabOrder. If this variable is TRUE, OnNextPrevField chooses a random control as the next control; otherwise it calls CDialog::GetNextDlgTabItem to get the next control in the dialog’s natural tab order. Whichever method it uses to determine the new control, OnNextPrevField calls CDialog::GotoDlgCtrl to navigate there. This is important; you don’t want to call CWnd::SetFocus, because that won’t highlight the contents of the control the way dialogs normally do. If an edit control or combo box contains some text like foo, and the user navigates there, the text foo should be highlighted. Calling pWnd->SetFocus will not highlight the contents of the control; calling GotoDlgCtrl(pWnd) will. Figure 6 shows some other useful dialog-navigation functions you can use.

Figure 6 Functions for Navigating Dialog Controls

CWnd Function
CWnd* GetNextDlgTabItem(CWnd* pWndCtl,
BOOL bPrevious = FALSE) Gets the next child control in the dialog’s tab order.

CDialog Functions(These take care of highlkighting edit text, and so on.)

void CDialog::NextDlgCtrl() Move to next control
void CDialog::PrevDlgCtrl() Move to previous control
void CDialog::GotoDlgCtrl(CWnd* pWndCtrl) Move to specified control

Before leaving, I should point out that TABDLG illustrates another useful dialog technique: how to make ON_COM­MAND_UPDATE_UI handlers work in a dialog. In a normal MFC doc/view app, MFC updates user interface items such as menu items, toolbar buttons, and status bar panes using an internal WM_IDLEUPDATECMDUI message, which it broadcasts to all the windows in your app as part of its idle processing. The handlers for tool, status, and dialog bars in turn call UpdateDialogControls to broadcast another command, CN_UPDATE_COMMAND_UI, to all the controls in the window. From your perspective as programmer, these mes­sages are invisible. All you have to do is implement ON_UPDATE_COMMAND_UI handlers for your menu items and buttons and, voilà, they get updated magically. (For more information, see my article “Meandering Through the Maze of MFC Message and Command Routing” in the June 1995 issue of MSJ.)

Unfortunately, this wonderful UI update mechanism doesn’t work for dialogs—at least not automatically. You have to hook the pieces up yourself. Fortunately, it’s easy. All you have to do is handle WM_KICKIDLE, a private MFC message that MFC sends whenever the dialog is idle (analogous to the app’s OnIdle handler) to call Update­DialogControls yourself.
"LRESULT CTabDialog::OnKickIdle(WPARAM wp,
                               LPARAM lCount)
{
    UpdateDialogControls(this, TRUE);
    return 0;
}

CWnd::UpdateDialogControls sends the magic CN_UPDATE_COMMAND_UI message to all the dialog controls, the upshot being that now ON_COM­MAND_UP­DATE_UI handlers suddenly work for dialogs. I used a COMMAND_UPDATE_UI handler in TABDLG to change the name of the Scramble TAB Order button to Unscramble TAB Order if the tab order is already scrambled.

Quick Anti-Flicker Update

In my May 1997 column, I answered a question about how to get rid of annoying screen flicker in MFC apps. I showed various techniques, but forgot to mention the all-important CS_HREDRAW and CS_VREDRAW window class styles, as a few readers pointed out. Here’s the quick lowdown, in case you don’t already know.

Whenever you register a window class, you can specify style bits. These style bits are not the same as the WS_XXX style bits you use when you create a window. The class style applies to all windows of a particular class, not just one window. In the old days, you would specify the class style when you called ::RegisterClass in your WinMain function. In MFC-land, you don’t normally call RegisterClass because MFC registers several window classes for you, such as AfxFrameOrView and others with weird names like Afx:400000:3:1:14fe:10:0. Among the class style flags are CS_HREDRAW and CS_VREDRAW, which tell Windows to erase the entire window if a movement or size adjustment changes the width (CS_HREDRAW) or height (CS_VREDRAW) of the client area.

By default, MFC registers frame classes with these bits turned on, which is the safest thing to do for error-free painting. However, it generates excessive flicker when the user resizes the window, as the client area is continually erased and redrawn. Fortunately, it’s easy to override MFC’s default class style: all you have to do is override your main frame’s PreCreateWindow function to register a class without CS_HREDRAW or CS_VREDRAW. This can be done by calling AfxRegisterWndClass, which registers a new class and returns its name (or just returns the name if a class with the desired properties is already registered). Figure 7 shows the details.

Figure 7 AfxRegisterWndClass
"//////////////////
// How to register your own window class without CS_HREDRAW and CS_VREDRAW
//
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
   cs.lpszClass = AfxRegisterWndClass(
      CS_DBLCLKS,                       // if you need double-clicks
      NULL,                             // no cursor (use default)
      NULL,                             // no background brush
      AfxGetApp()->LoadIcon(IDR_MAINFRAME)); // app icon
   ASSERT(cs.lpszClass);

   return CFrameWnd::PreCreateWindow(cs);
}

Window class registration is one area where MFC actually makes life more difficult by attempting to be clever. By hiding window class registration, MFC does indeed make it easier to write apps—but at the cost of producing ones that all have the dreaded sizing flicker. Also, it’s a little weird to be registering the window class in CWnd::PreCreate­Window, since registration is a meta-window operation and you only need to do it the first time a window of that particular class is created. This is why AfxRegisterWndClass only registers the class if it isn’t already registered. A more logical way would be to do it as in the old days—register all your window classes up front in your app’s InitInstance function. You can also change the style by calling SetClassLong(GCL_STYLE), but you might confuse MFC because it encodes the style as part of the class name (the 400000 in the example above). u

To obtain complete source code listings, see page 5.

Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com.