Introduction
Recently we came across a problem where we needed to have multiple dialogs within a single property page or dialog. This article will explain how we did it.
From the internal thread that went around the company, this feature was the source of endless confusion and rewrites. The various sample on the Internet did not help matters much as they required custom derivations for both the CPropertyPage
and CDialog
classes as well as not providing a good keyboard user experience.
This article documents the steps required to provide an embedded dialog(s) within a dialog using MFC. There are no extension classes provided as this provides the technical insight to modify your code to support embedded dialogs.
Background
Embedding a dialog within a property sheet or another dialog appears to be the source of much confusion and various attempts over the years. Sadly I was unable to find a solution that worked transparently and cleanly. There are various flavour of solutions on the net, but not one that had the look and feel of a correct solution. I then came across article �Q131283 - PRB: Cannot Use TAB to Move from Standard Controls to Custom� on Microsoft�s website and it all became clear.
The solution we required was to be simple (it was only on one dialog), easy to use (For end users), simple to maintain (not everyone in the company is a guru). The solution chosen was to have a tab strip and then CDialog
s for each page. As the current page was selected, the previous page was hidden and the new page shown.
Whilst this appears to be simple enough to document, implementing it proved to be more of a challenge, especially with regards to using the keyboard to tab which is an integral feature of the product. Users need to be able to tab in and out of the dialog and the dialog should at the same time look integral to the main window.
This type of design also allows the developer to set the tab control at compile time and runtime by specifying options such as images and tab name, using the common controls API.
Using the code
In the code section I have given an example that shows how a programmer would go about implementing the two embedded dialogs within a new dialog. These dialogs are selectable by clicking or navigating onto the tab control. Note that you can set the focus to the tab control and then use the left and right arrows to select the appropriate tab, then using the tab key, tab into and out of the dialog.
The first thing to do is to create the dialogs that will be embedded in the main dialog. These are just two straight forward CDialog
classes. You can put what ever controls you like in them. The only requirements are that the dialog style is set to Child and Control, on the "More styles" tab on the dialog properties in the resource editor
It is recommended that the Frame type be set to Dialog Frame. Additionally you can set the X and Y positions of the dialog relative to its parent. The dialog to be embedded should be offset so that the dialog frame top appears as the tab frame bottom. This is of course a matter of personal preference and aesthetics.
In the sample there are two dialogs that have been created. CDialog1
ion files Dialog1.h and Dialog1.cpp and CDialog2
in Dialog2.h and Dialog2.cpp.
Now we have built the two dialogs to be embedded in the main dialog, we can create the main dialog.
We create a new Dialog class called CEmbeddedDialogDlg
implemented in two files, EmbeddedDialogDlg.h and EmbeddedDialogDlg.cpp. This is where the implementation of the embedded dialogs will go.
Having created the dialogs, we need to add includes for the two CDialog
classes we created earlier and add them as members to the CEmbeddedDialogDlg
class. These are the modeless dialogs that will be inserted into the CEmbeddedDialogDlg.h class.
public:
CDialog2 m_dlg2;
CDialog1 m_dlg1;
Next, we insert a new tab control resource into the dialog resource for the CEmbeddedDialog
class and subclass this control, just to make it easier to work with. This is added as a member m_ctlTab1
of the CEmbeddedDialogDlg
class.
The only function we need in the CEmbeddedDialogDlg
class is the UpdateVisibleWindow
method which shows the dialog associated with the current tab and hides all the other dialogs from being shown. When a user changes the tab in the tab control, because it is a windows common control this generates a WM_NOTIFY
message with a submessage of TCN_SELCHANGE
, indicating the current tab selection has changed. We can get the current tab selection using the CTabCtrl::GetCurSel()
. This is equivalent to the TCM_GETCURSEL
message that would be sent to a native control. MFC just makes it easier.
The final piece of the jigsaw is the CEmbeddedDialogDlg::OnInitDialog
member. This is the method that is first called after the dialog is first constructed but before it is displayed. This is where the major portion of the changes occur.
The first step in this method is to create the two tabs that we will be using.
m_ctlTab1.InsertItem(TCIF_TEXT, 0, _T("Dialog1"), 0,0,0,0);
m_ctlTab1.InsertItem(TCIF_TEXT, 1, _T("Dialog2"), 0,0,0,0);
Next, we create modeless instances of the two dialogs as indicated below.
m_dlg1.Create(CDialog1::IDD, this);
m_dlg2.Create(CDialog2::IDD, this);
Note that we make the two dialog instances' parents the CEmbeddedDialogDlg
dialog so all the offsets will be relative to the CEmbeddedDialogDlg
instance.
Now, if you call UpdateVisibleWindow()
and run the project, you will see the project works and you can select different tabs as well as use the tab control to switch between dialogs. However you will see that the tab order appears to be incorrect because tabbing will take you to the tab control, then to the ok and cancel buttons and then to the embedded controls. Obviously this is incorrect and confusing, so we need to alter the tab order of the newly inserted controls so they appear to correctly tab after the tab control.
m_dlg2.SetWindowPos(GetDlgItem(IDC_TAB1), 0, 0, 0, 0,
SWP_SHOWWINDOW|SWP_NOSIZE|SWP_NOMOVE);
m_dlg1.SetWindowPos(GetDlgItem(IDC_TAB1), 0, 0, 0, 0,
SWP_SHOWWINDOW|SWP_NOSIZE|SWP_NOMOVE);
To do this we use the SetWindowPos function and tell the window where it should be in the tab order. Note that we have reversed the order of the SetWindowPos
calls, the first call is CDialog2
and then the next call is to CDialog1
. This make the code easier to understand, there is no reason why other implementations could not be followed.
One final point to note that even though it is not in the code, from within CEmbeddedDialogDlg::DoDataExchange
you should call both the m_dlg1
and m_dlg2
DoDataExchange
methods.
Points of Interest
I was amazed at how easy it was to implement embedded dialogs once the DS_CONTROL
style is understood. The whole model integrates simply and easily within the Windows framework, giving the look and feel that shows its Microsoft's intended route to embedded dialogs.
History
Following the discussion on the thread I have shown how to add support for XP Themes.
To add theme support is a two step operation. The first step is to create a manifest file either embedded as a resource or a separate file with a yourapp.exe.manifest filename. If either are present and you are running XP then the application will take on the theme of the current XP theme.
The second step is to make the tabs appear the same color as the background. This is achieved by calling EnableThemeDialogTexture
with a flag of ETDT_ENABLETAB
. Obviously this executable will now only work on XP.
I have added two additional configurations in the .dsp file, one called Win32 XP Debug and Win32 XP Release. For a good article on theming see Using Windows XP Visual Styles on the Microsoft web site.