Nigel Thompson
Microsoft Developer Network Technology Group
Created: September 27, 1994
Click to open or copy the files in the FWTEST sample application for this technical article.
Click to open or copy the files in the ANIMATE sample application.
This article describes how the CDialogBar class can be used to create Control Panel-like windows in a multiple document interface (MDI) application. Applications built using the techniques described in this article can be shipped as a single .EXE file without additional DLLs for the controls.
FWTEST, the sample application that accompanies the articles, includes examples of two different Control Panel-like windows. One window uses standard Microsoft® Windows® controls and is discussed in this article. The window that uses custom controls also uses a 256-color palette. The FWTEST samples use the ANIMATE library.
Multiple document interface (MDI) applications often need to do a little more than just show one or more views of a document. Often they need to be able to show other windows not related to the documents at all. I refer to these windows as "control panels" because that’s generally what I use them for—controlling some aspect of the application’s behavior. There are other cases when it’s useful to be able to create a simple window showing some data and containing a few controls. Again, these windows may have little or nothing to do with a particular document. Such a window might be showing the current weather forecast, for example, while the main part of the application is concerned with scheduling rock-climbing trips to Smith Rock (a common requirement <grin>). I like my applications to be colorful, and so I also wanted my control panels to be able to use a 256-color palette.
The Microsoft® Foundation Class Library (MFC) provides the CFormView class as a way to use a dialog template to design a form. This is pretty close to what I want in a control panel, except that CFormView is derived from CView and is very closely tied to the whole template-document-view model. I did try using CFormView for my control panel windows. I generated a dummy document and template but got into all sorts of problems because this dummy document then becomes one of the options presented when the user chooses the File New command. I quickly decided that CFormView is for creating forms to edit documents and not for Nigel’s control panels.
MFC also provides the CDialogBar class, which can be used effectively to add a modeless dialog bar to some part of a window. The dialog bar occupies some part of the window’s client area. Using CDialogBar looked very promising; if I could make the dialog bar occupy the entire client area, I’d have what I wanted. But life isn’t that easy. It turns out that CDialogBar is a little challenged (to be politically correct, for once) when it comes to scroll bars. If you add a scroll bar to a CDialogBar window, the scroll bar doesn’t work, although the other one or two problems with using CDialogBar to create a control panel window are solvable.
What I'm looking for is a simple way to design and build windows that act like MDI child windows but are not related to the application’s documents, views, or templates. This technical article describes how the CDialogBar class can be used (with some help) to create control panel windows and how custom controls can be used to make them more appealing.
The CDialogBar class is derived from CControlBar, which is derived from CWnd. Figure 1 shows the full derivation of CDialogBar.
Figure 1. The derivation of CDialogBar
Notice that CDialogBar is not derived from CDialog. CDialog’s derivation is shown in Figure 2.
Figure 2. The derivation of CDialog
The fact that CDialogBar is not derived from CDialog causes several problems when we try to create CDialogBar windows using App Studio and ClassWizard. ClassWizard can, for example, add a handler function for the OK button being clicked. Unfortunately, ClassWizard thinks we are creating a dialog box and adds the virtual function OnOK to the application source code. The problem is that OnOK is a member of CDialog and not CDialogBar, so when the user clicks the OK button, your code doesn’t get called. Another irritating problem with CDialogBar is that it doesn’t pass scroll bar messages to its parent, so if you add a scroll bar to the template, the scroll bar won’t work.
In order to make implementing a panel full of controls a lot simpler, we need a new class or two. We need a new class derived from CMDIChildWnd to fix problems related to the handling of frame messages and one derived from CDialogBar to fix the problems with scroll bars.
Naming new classes is always tough: The ideal name is usually already taken or means too many different things. I wanted to replace CDialogBar with some other meaningful name, yet retain its roots. Because the things I’m trying to create with the first new class are like instrument control panels, I called them dialog panels and called the class CDlgPanel. Because I needed a new frame to go around my new dialog panels, I called the second class CDlgPanelFrame.
Most of the problems with CDialogBar can be cured by deriving a new class from CDialogBar and overriding a few of CDialogBar’s functions. In the FWTEST sample application, the CDlgPanel class fixes the scroll bar problem and adds some other useful functionality, such as the ability to determine the size in pixels of the dialog template. The problems with the OnOK virtual function can be fixed by deriving a new frame window class from CMDIChildWnd. In FWTEST, this new class is called CDlgPanelFrame. We’ll look at the implementation of CDlgPanel and CDlgPanelFrame next. Figure 3 shows a test window from the FWTEST sample that includes a variety of Windows controls.
Figure 3. A test control panel from the FWTEST sample
The CDlgPanel class is derived publicly from CDialogBar and its primary function is to pass scroll bar messages to its parent window so that scroll bar controls in the dialog template will work correctly. As secondary functions, the CDlgPanel class also provides a convenient Create function, a mechanism for handling a palette, and a way to retrieve the dimensions of the dialog template in pixels. Here’s the class definition from DLGPANEL.H:
class CDlgPanel : public CDialogBar
{
public:
CDlgPanel();
BOOL Create(CWnd* pParentWnd, UINT nIDTemplate,
UINT nStyle, UINT nID, CPalette* pPal);
void GetPanelRect(RECT* pRect);
virtual ~CDlgPanel();
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
private:
CPalette* m_pPal;
UINT m_uiIDTemplate;
//{{AFX_MSG(CDlgPanel)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Let’s see how the Create function works first:
BOOL CDlgPanel::Create(CWnd* pParentWnd, UINT nIDTemplate,
UINT nStyle, UINT nID, CPalette* pPal)
{
m_pPal = pPal;
m_uiIDTemplate = nIDTemplate;
BOOL b = CDialogBar::Create(pParentWnd, nIDTemplate,
nStyle, nID);
if (!b) return FALSE;
// Send all child windows and their children a message
// containing the palette.
SendMessageToDescendants(DLGPANELMSG_SETPALETTE,
0, (LPARAM) m_pPal, TRUE);
return TRUE;
}
The Create function takes the parent window, template, and child window ID arguments and calls CDialogBar::Create to create the window. Having done that, it sends a message to all the child windows (the controls in the dialog) to let them know the palette that is being used. The DLGPANELMSG_SETPALETTE message is defined in DLGPANEL.H as:
#define DLGPANELMSG_SETPALETTE (WM_USER+100)
Obviously, the standard Windows controls don’t know a palette from a Book of the Month Club subscription (an equally dangerous and misunderstood object), but our own custom controls can make good use of this, as we’ll see shortly.
I mentioned earlier that CDialogBar doesn’t support scroll bars. You can verify this by experiment or by looking in the MFC source code. The MFC\SRC\BARCORE.CPP file contains this code:
LRESULT CControlBar::WindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
ASSERT_VALID(this);
// Parent notification messages are just passed to parent of control bar.
switch (nMsg)
{
case WM_COMMAND:
case WM_DRAWITEM:
case WM_MEASUREITEM:
case WM_DELETEITEM:
case WM_COMPAREITEM:
case WM_VKEYTOITEM:
case WM_CHARTOITEM:
return ::SendMessage(::GetParent(m_hWnd), nMsg, wParam, lParam);
}
return CWnd::WindowProc(nMsg, wParam, lParam);
}
This code is used to send control window notification messages to the parent (frame) window. As you can see, WM_HSCROLL and WM_VSCROLL are missing. I assume these were omitted because of problems with dialog bar frame windows that might have scroll bars of their own. In creating simple control panel windows, I don't want scroll bars in the frame window; therefore, the WM_HSCROLL and WM_VSCROLL messages must be coming from controls within the window rather than from scroll bars on the outside of the window. CDlgPanel overrides CControlBar::WindowProc to fix the problem:
LRESULT CDlgPanel::WindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
// Parent notification messages are just passed to parent of control bar.
switch (nMsg)
{
case WM_COMMAND:
case WM_DRAWITEM:
case WM_MEASUREITEM:
case WM_DELETEITEM:
case WM_COMPAREITEM:
case WM_VKEYTOITEM:
case WM_CHARTOITEM:
case WM_HSCROLL: // new
case WM_VSCROLL: // new
return ::SendMessage(::GetParent(m_hWnd), nMsg, wParam, lParam);
}
return CDialogBar::WindowProc(nMsg, wParam, lParam);
}
That’s all it takes. Now you can add scroll bar controls to the dialog template, and they’ll work just fine.
The final function that CDlgPanel provides is to return the dimensions of the dialog template in pixels. Dialog templates are created in dialog box units. Dialog box units are related to the size of the font used to create the dialog box. This relationship generally makes a lot of sense because if the user changes the system font, the dialogs and their controls are all resized to ensure that the text in the dialog box, on its buttons, and so on, still fits correctly. CDlgPanel includes the GetPanelRect function to return the dimensions of the dialog template:
void CDlgPanel::GetPanelRect(RECT* pRect)
{
// Load the dialog template.
CDlgTemplate tpl(m_uiIDTemplate);
ASSERT(pRect);
tpl.GetDlgRect(pRect);
}
Wow, that looks easy! But wait! What’s that CDlgTemplate thing?
The CDlgTemplate class was written by Paul Oka, a consultant with Microsoft Consulting Services, and provides a vast number of dialog template functions. I’m using only one of those functions (GetDlgRect) here to get the template dimensions. The source code for the CDlgTemplate class is in the DLGTEMPL.H and DLGTEMPL.CPP files of the FWTEST sample. The class was written so that dialog templates could be used to design forms for an application. Each form consists of a number of Windows controls. The application itself builds the forms rather than calling the Windows dialog manager to create them so that the application can have more complete control.
The constructor for the CDlgTemplate class takes the ID of the dialog-template resource. It opens the template and extracts from it various bits of information, such as the dialog base unit dimension, which is what I wanted in order to be able to compute the size of the window required to contain a given dialog template. The GetDLUBaseUnits function computes the value of the base units:
void CDlgTemplate::GetDLUBaseUnits()
{
HDC hDC = ::GetDC(NULL);
int nWeight;
// Round up to get better results (NigelT).
// Note that this is still wrong for Lucida Console 8pt
// for some unknown reason.
int nHeight = (m_pDialogHeader->GetPointSize() *
::GetDeviceCaps(hDC, LOGPIXELSY) + 36) / 72;
TRACE("Font size: %d pt. Height: %d pixels",
m_pDialogHeader->GetPointSize(), nHeight);
CFont font;
nWeight = FW_BOLD; // All dialogs use bold fonts (NigelT).
font.CreateFont(-nHeight, 0, 0, 0, nWeight, 0, 0, 0, 0, 0, 0, 0, 0,
m_pDialogHeader->GetFaceName());
HGDIOBJ hOldFont = ::SelectObject(hDC, font.m_hObject);
TEXTMETRIC tm;
::GetTextMetrics(hDC, &tm);
m_cyBase = tm.tmHeight;
TRACE("Font pitch and family: %4.4XH", tm.tmPitchAndFamily);
if (tm.tmPitchAndFamily & 0x01)
{
// Proportional font
TRACE("Dlg has prop font");
char szAveCharWidth[52]; // Array to hold A-Z,a-z
for (int i = 0; i < 26; i++)
{
szAveCharWidth[i] = (char)(i + 'a');
szAveCharWidth[i + 26] = (char)(i + 'A');
}
SIZE sizeExtent;
::GetTextExtentPoint(hDC, szAveCharWidth, 52, &sizeExtent);
m_cxBase = (sizeExtent.cx + 26) / 52;
}
else
{
TRACE("Dlg has fixed pitch font");
m_cxBase = tm.tmAveCharWidth;
}
TRACE("Dlg base units: %d x %d", m_cxBase, m_cyBase);
::SelectObject(hDC, hOldFont);
::ReleaseDC(NULL, hDC);
}
Note This code is very similar to code in the Windows 3.1 dialog manager and works for almost all fonts. However, you should be aware than some fonts (Fixedsys, for example) give incorrect results. I have been unable to track down the cause of this despite digging in the Windows source code for some hours. The problem seems to be the way the size of the average character is rounded up. All the errors I have found corresponded to either the computed height or width being off by one pixel. Should the solution to this bug be found, I’ll add it to the MSDN Library in a subsequent article.
Now that we have a fixed version of CDialogBar, we need to see how to create a new MDI child window that has a CDlgTemplate window filling its entire client area. Once we’ve done that, we’ll have all that’s required to create a control panel like the one shown in Figure 3.
Recall that I mentioned earlier that the default OK and Cancel button handlers, OnOK and OnCancel, are not members of CDialogBar. We can fix this problem in the frame window we will use to handle the notification messages from the dialog panels we create. To do this, we derive a new class from CMDIChildWnd. In FWTEST this new class is called CDlgPanelFrame. CDlgPanelFrame’s primary function is to handle the OnOK and OnCancel functions. It also provides a convenient Create function and an OnSize handler to conveniently set the size of the frame window to fit the size of the dialog panel within it. Doing this in an OnSize function might seem a bit bizarre, but I find it works well and is easy to find in the source code later when you’ve forgotten how it all works. Here’s the definition of the class from DLGPANFM.H:
class CDlgPanelFrame : public CMDIChildWnd
{
DECLARE_DYNCREATE(CDlgPanelFrame)
public:
CDlgPanelFrame();
BOOL Create(LPCSTR lpszWindowName,
const RECT& rect,
CMDIFrameWnd* pParentWnd,
UINT uiTemplateID,
UINT uiResourceID,
CPalette* pPal = NULL);
// Implementation
protected:
CPalette* m_pPal;
CDlgPanel m_wndPanel;
UINT m_uiTemplateID;
virtual ~CDlgPanelFrame();
// Generated message map functions
//{{AFX_MSG(CDlgPanelFrame)
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize(UINT nType, int cx, int cy);
virtual void OnOK();
virtual void OnCancel();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Notice the member variables for keeping track of the current palette and the CDlgPanel object, which will occupy the entire client area. Let’s begin with a look at the Create function from DLGPANFM.CPP:
BOOL CDlgPanelFrame::Create(LPCSTR lpszWindowName,
const RECT& rc,
CMDIFrameWnd* pParentWnd,
UINT uiTemplateID,
UINT uiResourceID,
CPalette* pPal/*= NULL*/)
{
m_pPal = pPal;
m_uiTemplateID = uiTemplateID;
// Create the window with a caption and just a thin border.
return CMDIChildWnd::Create(GetIconWndClass(WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW,
uiResourceID),
lpszWindowName,
WS_CHILD|WS_VISIBLE|WS_OVERLAPPED
|WS_CAPTION|WS_BORDER,
rc,
pParentWnd);
}
The Create function stores the palette and template ID for use later and then calls the base class to create the window. The uiResourceID parameter is used to load an appropriate icon for this window. The undocumented MFC function GetIconWndClass is used to obtain the name of a suitable window class based on the style of the window and the supplied icon. Note that I have chosen to omit a system menu and the Maximize and Minimize buttons. This is simply because I happened to want the windows to look that way in my application (see Figure 3). You can alter this to your own taste or include a dwStyle parameter for more flexibility.
CDlgPanelFrame includes a handler for WM_CREATE messages that is called as a part of the window creation process:
int CDlgPanelFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CMDIChildWnd::OnCreate(lpCreateStruct) == -1)
return -1;
// Create the dialog panel.
// Note that we send them the palette we are using.
ASSERT(m_uiTemplateID);
if (!m_wndPanel.Create(this,
m_uiTemplateID,
CBRS_TOP,
1, // Child ID
m_pPal)) {
TRACE("Failed to create panel from template");
return -1;
}
return 0;
}
OnCreate creates the panel window from the dialog template ID supplied to the Create function. Note that I have used the CBRS_TOP flag here, but that since the panel will occupy the entire client area, it is not relevant. Also note that the palette is passed down to the panel window (which passes it to its control windows).
CDlgPanelFrame also includes a handler for WM_SIZE messages:
void CDlgPanelFrame::OnSize(UINT nType, int cx, int cy)
{
CMDIChildWnd::OnSize(nType, cx, cy);
// Get the size of the panel and resize the parent frame to fit.
CRect rcPanel;
m_wndPanel.GetPanelRect(&rcPanel);
if ((cx == rcPanel.right) && (cy == rcPanel.bottom)) {
return;
}
CRect rcWnd;
GetWindowRect(&rcWnd);
rcWnd.right -= cx - rcPanel.right;
rcWnd.bottom -= cy - rcPanel.bottom;
SetWindowPos(NULL,
0, 0,
rcWnd.right - rcWnd.left,
rcWnd.bottom - rcWnd.top,
SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOMOVE);
}
Using OnSize in this way is a little strange in as much as the window will resize itself to fit the size of the dialog template. This works well in practice, so I have left it this way. It helps me keep track of all the sizing code having it here, but you are welcome to do your own thing if you find this too weird.
The final functions in CDlgPanelFrame serve simply as reminders that the OnOK and OnCancel functions need to be implemented in any class you derive from CDlgPanelFrame if you have OK and Cancel buttons.
void CDlgPanelFrame::OnOK()
{
TRACE("No OnOK handler in your derived class");
}
void CDlgPanelFrame::OnCancel()
{
TRACE("No OnCancel handler in your derived class");
}
The FWTEST sample includes the files PANELFRA.H and PANELFRA.CPP as an example of how to use the CDlgPanelFrame class. The code in these files implements the panel shown in Figure 3. Note that the panel includes OK and Cancel buttons, scroll bars, and a combo box. We’ve discussed what it takes to make the OK and Cancel buttons and the scroll bars work, but the combo box needs a little help, too.
I like to use combo boxes with the CBS_DROPDOWNLIST style for many purposes. However, when used as controls in a dialog box, they have one major failing: When the user clicks a new selection from the list, the application gets a CBN_SELCHANGE message before the text of the static part of the control has changed, and no CBN_EDITCHANGE message is sent at all. This means that you have to treat this type of combo box as a special case. Normally when the edit window of a combo box is altered, the application gets a CBN_EDITCHANGE message, and the application can simply call GetWindowText to retrieve the new text. Fortunately, there is a very simple cure for this problem: On receiving a CBN__SELCHANGE message, simply post a CBN_EDITCHANGE message back to the same window. Note that we post the message, not send it, so that it will be placed in the message queue and processed after the combo box has actually updated its window text. Here’s the code from PANELFRA.CPP that does this:
void CPanelFrame::OnSelchangeCombo()
{
// Handle the CBN_SELCHANGE messages by converting them to
// CBN_EDITCHANGE messages.
// CBN_SELCHANGE occurs before the edit window text has changed, and
// combo boxes of the drop-down-list style don't send CBN_EDITCHANGE
// messages, so we post the CBN_EDITCHANGE message to ourselves.
PostMessage(WM_COMMAND,
MAKEWPARAM(IDC_COMBO, CBN_EDITCHANGE),
(LPARAM)(m_wndPanel.GetDlgItem(IDC_COMBO)->m_hWnd));
}
Unfortunately, we can’t handle this message translation transparently in the CDlgPanelFrame class because we can’t tell if it’s a combo box or a list box that is sending the message, and the SELCHANGE and EDITCHANGE messages have different values for combo and list boxes. So you simply have to add this simple function to the class you derive from CDlgPanelFrame.
As a finishing touch, I thought I’d pass on a few other tips that relate to using the dialog panels.
Watch out for CWnd::GetDlgItem, which returns a temporary CWnd object pointer. Don’t store it for use later; you must get it every time you want it. Because you often want to get a pointer to a control object, you might want to add some helper functions to CDlgPanelFrame, such as GetDlgItem(UINT uiCtlID). This saves you from having to use m_wndPanel.GetDlgItem(id) in your frame code.
More information on handling user-defined messages, such as the one I used to send palette information to the control windows, can be found in the "User Defined Windows Messages" section of "Technical Note 6: Message Maps" [Technical Articles, Visual C++ 1.0 (32-bit), MFC 2.0 (32-bit) Technical Notes].