Introduction
I had to polish the GUI of an existing MFC application and decided to use the Visual Studio 2008 MFC Feature Pack.
At first, I wanted to follow the procedure described by Marius Bancila. Nevertheless, many steps did not work out directly; instead I got tons of compilation errors and assertions. Since I (and others) have found that documentation is lacking and many tutorials focus on demo applications only, I decided to let you participate in my struggle.
For the Googlers, I included most of the assertion messages as well. All screenshots are (for legal reasons) not taken from the original application I am working with, but from the (standard MFC) WordPad sample, distributed with VS 2008; so sometimes they might not fit 100% to the text.
Remove Win98 Style
Before we start, the first step to a modern GUI is to enable Visual Styles:
CMyApp::InitInstance()
{
...
INITCOMMONCONTROLSEX CommonControls;
CommonControls.dwSize = sizeof (INITCOMMONCONTROLSEX);
CommonControls.dwICC = ICC_STANDARD_CLASSES;
InitCommonControlsEx (&CommonControls);
Ensure that version 6 of ComCtl32.dll is used:
#pragma comment(linker, "\"/manifestdependency:type='win32'\
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
Win98 style:
WinXP style:
Refactoring to Anticipate Compilation Errors
In the new MFC-classes, some interfaces have changed, so if you directly switch to the new classes, you will get compilation errors and will have to work blindly for a while. Therefore, it is advantageous to perform some minimal refactoring beforehand.
GetToolBarCtrl
The method GetToolBarCtrl()
is not available in the new CMFCToolBar
, so some usages must be modified. (To more easily detect these issues, I introduced my own MyToolBar
which derives from CToolBar
, but has GetToolBarCtrl()
marked as deprecated.)
Some methods can be called directly on CToolBar
(I suppose this was not possible in earlier versions of MFC). These calls:
CEdit* pEdit = (CEdit*) this->GetToolBarCtrl().GetDlgItem(ID_EDIT);
this->GetToolBarCtrl().GetItemRect();
this->GetToolBarCtrl().GetButtonCount();
can be replaced by:
CEdit* pEdit = (CEdit*) this->GetDlgItem(ID_EDIT);
this->GetItemRect();
this->GetCount();
HideButton
I could not find a straightforward way to remove several calls to HideButton
. (Depending on the configuration in our app, some toolbars will be not available at all, others will contain fewer items.)
I decided to prepare for later use of SetNonPermittedCommands
; therefore I have to change the visibility of all buttons at once, not via individual calls.
My attempt is to replace:
m_wndToolBar->GetToolBarCtrl().HideButton(ID_FILE_PRINT,false);
m_wndToolBar->GetToolBarCtrl().HideButton(ID_FILE_OPEN,true);
with something like this:
MyToolBarBase::ItemVisibility_t visibility;
visibility[ID_FILE_PRINT] = true;
visibility[ID_FILE_OPEN] = false;
myToolBar->SetItemVisiblity(visibility);
The Conversion Itself
For all of the following, I recommend having application verifier running to immediately detect new assertions. I started with the procedure described by Marius Bancila:
Add a new header to your stdafx.h:
#include <afxcontrolbars.h>
Start Using the New Classes
Change CWinApp
to CWinAppEx
.
Add the following lines to InitInstance
:
InitContextMenuManager();
InitShellManager();
InitKeyboardManager();
InitTooltipManager();
CMFCToolTipInfo ttParams;
ttParams.m_bVislManagerTheme = TRUE;
theApp.GetTooltipManager()->
SetTooltipParams(AFX_TOOLTIP_TYPE_ALL,
RUNTIME_CLASS(CMFCToolTipCtrl), &ttParams);
Change CMDIFrameWnd
to CMDIFrameWndEx
.
This gives an assertion in CWinAppEx::GetRegSectionPath
:
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\afxregpath.cpp(33) : Assertion failed!
Add the following to InitInstance()
:
SetRegistryKey(_T("MyCompany"));
Now the app runs a bit further, but crashes in CMDIClientAreaWnd::CreateTabGroup
.
"CMDIClientAreaWnd::OnCreate: can't create tabs window\n"
Before this crash, already many more asserts and error messages have appeared. In particular, this one:
Can't load bitmap: 42b8. GetLastError() = 716
(Don't click this link, it won't help you.) And this one:
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\afxtabctrl.cpp(1395) : Assertion failed!
The corresponding line of code is:
ENSURE(str.LoadString(IDS_AFXBARRES_CLOSEBAR));
So again some resource is missing. The solution is given in MSDN:
"it's a known problem for statically linked projects using the feature pack"
=> Change application to use MFC as a shared DLL. In my case, this was not a big deal, otherwise follow the instructions in the link above.
The assertions continue:
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\winfrm2.cpp(92) : Assertion failed!
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\winfrm2.cpp(99) : Assertion failed!
occurs in:
CFrameWnd::DockControlBar(...)
pDockBar = (CDockBar*)GetControlBar(dwDockBarMap[i][0]);
ASSERT(pDockBar != NULL);
The reason is that CControlBars
and CToolBars
cannot be docked in a CFrameWindow
. (See social.msdn.)
=> For the moment, comment out all statements of the form:
DockControlBar(&m_myToolBar,AFX_IDW_DOCKBAR_TOP);
DockControlBar(&m_myDialogBar,AFX_IDW_DOCKBAR_LEFT);
=> Yes! The application starts!
OK, all the dialogs and the menu are missing, but for the moment, let's not bother with that.
Instead, let's go for something positive and enable styles:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (MyBaseClass::OnCreate(lpCreateStruct) == -1)
return -1;
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerOffice2007));
CMFCVisualManagerOffice2007::SetStyle(
CMFCVisualManagerOffice2007::Office2007_ObsidianBlack);
Restart the application and - voilĂ ! - the application shows a cool dark frame and modified system menu buttons.
Re-Show the Menu Bar
Besides all the toolbars and control bars, now the menu has also disappeared. Well, actually, it did not disappear completely; after pressing, e.g., Alt-F, it becomes visible again, but somewhere on top of the window.
To fix this, add a CMFCMenuBar
to CMainFrame
.
if (!m_wndMenuBar.Create(this))
{
TRACE0("Failed to create menubar\n");
return -1; }
m_wndMenuBar.SetPaneStyle(m_wndMenuBar.GetPaneStyle() |
CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY);
...
m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
DockPane(&m_wndMenuBar);
Now the menu is visible (and shows the new style); also now the system menu appears where it should.
Replace Every CDialogBar with a CPaneDialog
Avoid GetControlBar
When we replace ToolBars and DialogBars, calls to GetControlBar(IDD_MY_TOOLBAR)
will start to return NULL
and possibly crash the application.
Therefore, avoid GetControlBar
and maybe replace:
ShowControlBar(GetControlBar(IDD_MY_TOOLBAR, ...))
with:
ShowControlBar(&m_myToolBar, ...)
with an overloaded ShowControlBar
that can handle CBasePane
as well. In one case, I could also handle the case of an ID:
BOOL CMainFrame::OnToggleBar(UINT nID)
{
MyControlBarBase* pBar = GetControlBar(nID);
bool bfReturn = false;
if(pBar != NULL)
{
ShowControlBar(pBar, !pBar->IsWindowVisible(),false);
return true;
}
CBasePane * pPane = GetPane(nID);
if (pPane != NULL) {
ShowControlBar(pPane, !pPane->IsWindowVisible(), false);
return true;
}
return false;
}
(But later it turned out that this particular function is now obsolete and handled automatically by the framework; see below.)
Change CDialogBar to CPaneDialog
Ensure that the WS_VISIBLE
flag for the resource is set (see this post).
One crash in GetWindowRect
was caused by a change in the window hierarchy. So I replaced:
GetParent()-> GetParent()->GetWindowRect(&m_window_rect);
with:
GetParent()->GetWindowRect(&m_window_rect);
until it turned out that the whole block (more than 100 lines) was an (attempted) bug-fix by a former programmer that - since it didn't work out - had been redone elsewhere and was now superfluous.
Ensure Minimal Size While Docking
When dialogs are now dragged around, the size becomes much too small. The reason is that the method CalcDynamicLayout
is now not called anymore. Instead, in the constructor of the dialog, call:
SetMinSize(CSize(190, 480));
Important: The handling of the minimal dialog size must be explicitly enabled, e.g., in the CWinApp::InitInstance
call:
CPane::m_bHandleMinSize = true;
That was all! Dialog bars are working now!
Change CStatusBar to CMFCStatusBar
Here, just a single line in Mainfrm.h requires a change; worked without problems:
Change CToolBar to CMFCToolBar
While CMFCToolBar
already
can host many different controls, for CToolBar
, this functionality had to be programmed individually. Therefore it is not really possible to just replace a CToolBar
(with possibly lots of individual programming) by a CMFCToolBar
.
In my case, for the individual toolbars, I got tons of asserts because all the combo boxes, edit fields, etc., are missing now. So every GetDlgItem(IDD_MY_FANCY_CONTROL)
fails now.
Anyway, if you are lucky and your toolbar just shows some buttons, here we go:
Change CToolBar
to CMFCToolBar
.
Everything compiles, no asserts occur but - no toolbar is visible. Clicking them in the menu also doesn't help. For the status bar, everything works as expected.
In my case, the reason was twofold:
- the Registry path where the toolbar states are stored has changed, and contains no information yet that the toolbars should be shown
- the menu is not yet functional for (un-)displaying toolbars
Show and Hide Toolbars from the Menu
In the original application, for each toolbar, a menu entry was created. This entry was then essentially linked to these methods:
CFrameWnd::OnBarCheck(UINT nID);
CFrameWnd::OnUpdateControlBarMenu(CCmdUI* pCmdUI);
This approach (and with it the method OnToggleBar
mentioned above) is now obsolete. The corresponding menu for showing toolbars can instead be dynamically created. All you need is a (sub)menu with, e.g., the ID ID_VIEW_TOOLBAR
and:
EnablePaneMenu(TRUE, ID_VIEW_CUSTOMIZE, strCustomize, ID_VIEW_TOOLBAR);
in the OnCreate()
method. Instead of ID_VIEW_CUSTOMIZE
, you can use 0, if you do not allow the customization of toolbars. strCustomize
is the text to be displayed for the "Customize" functionality if enabled.
Some problems appear here for me, caused by the localization of the menu. The menu originally contained only placeholder text, which is used as the resource key to obtain the localized text. Unfortunately, the code-behind does not work for sub-menus ("//recursion not implemented yet") and seemingly also not for dynamic texts. As a workaround, I removed the toolbar entries from the menu; they can now only be enabled / disabled via right click on the toolbar-pane.
Furthermore, the toolbars and dialogs now need a meaningful (and localized) caption, since the menu entry will display the corresponding text.
m_wndToolBar.SetWindowText("Fancy Toolbar");
Porting the Existing CToolBars
If the look and feel of your application is refreshed, then probably many toolbars will be redesigned as well (and e.g., be replaced by dockable multi-lined controls), so probably the following will not be necessary at all.
Anyways, let's give it a try and port the existing, advanced CToolBar
s to CMFCToolBar
s. As mentioned, for simple toolbars, this works immediately.
Also, the new tooltips show up now:
Unfortunately, for custom toolbars, life is not so easy. The format toolbar in the WordPad example is totally screwed up now:
Replace Individual Elements by CMFC Classes
Note: the following will look different for you, depending on your custom toolbar implementation, but it should give a hint if conversion is possible for you. Furthermore, I describe what I did in my application; I did not try to port the WordPad toolbars.
There is also an MSDN-article on putting controls on toolbars.
To insert an Edit-control into the toolbar, the existing code looked like this:
case ToolBar_Value::tbs_Edit :
{
rect.DeflateRect(0,2);
pWin = new CEdit();
tbvLocal.setControlWindow(pWin);
if (!((CEdit*)pWin)->Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP |
ES_AUTOHSCROLL |
WS_BORDER | tbvLocal.getCtlAlignStyle() ,
rect, getToolbar(), lCurrID))
{
TRACE0("Failed to create Editfield\n");
return ;
}
((CEdit*)pWin)->SetLimitText(50);
}
break;
The modified code is quite a bit shorter:
case ToolBar_Value::tbs_Edit :
{
CMFCToolBarEditBoxButton editButton(lCurrID, 0);
m_pWinToolbar->ReplaceButton(lCurrID, editButton);
break;
}
Inheritance Hierarchy
Interestingly, the following call will still work:
CEdit* pEdtPosition = (CEdit*)this->GetDlgItem(ID_EDT_FOO);
The reason is that the returned value is of type CMFCToolBarEditCtrl
which inherits from CEdit
. Similar hierarchies exist for other control types.
Correct Amount of Elements
A problem occurs because in my case, the custom elements were added to the toolbar, while in the new semantics, existing buttons are replaced, i.e., placeholders must be added. I had to update the code containing SetButtons
-statements.
=> Ensure that enough placeholders (with correct IDs) are present.
Correct Button Images
For some buttons, the icons are determined programmatically. Interestingly:
myToolBar.SetButtonInfo(buttonId, ID_BTN_TO_CHANGE, state, 3);
does not now use button #3 of myToolbar
, but icon #3 of the first toolbar.
The correct way now is to dynamically determine the image, via the command it belongs to, e.g.:
CMainFrame::OnUpdateFooUI(...)
{
int imageIndex = GetCmdMgr()->GetCmdImage(ID_BTN_WITH_IMAGE_I_NEED, FALSE);
...
myToolBar.SetButtonInfo(buttonId, ID_BTN_TO_CHANGE, state, imageIndex);
Event Routing
In one toolbar, an event was fired as soon as an item was picked from a combo box. The event handler was placed in the toolbar class:
BEGIN_MESSAGE_MAP( MyToolBar, MyToolBarBase )
ON_CBN_SELENDOK(ID_LB_FOO ,OnSelChangeCmbFoo)
This event is now not caught any more. One could argue that the above is bad practice anyway (I wouldn't agree with that), and you should instead put the message handler in, e.g., CMainFrame
. (I am not really sure what the advantage of having 200 message handlers within CMainFrame
actually is, but feel free to tell me.)
One workaround is to explicitly inform the toolbar about the event (see the BCG-forum).
BOOL CMainFrame::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
{
BOOL Handled = FALSE;
if (m_wndToolBar->CommandToIndex(nID) >= 0)
Handled |= m_wndToolBar->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
Handled |= MyMDIFrameWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
return Handled;
}
Note that I am not returning early here, if m_wndToolbar
could handle the command. The reason is that (for whatever reasons) part of the handling is done in the toolbar class (e.g., OnCommand
(..)) whereas other parts are done in CMainFrame
(e.g., OnUpdateCommandUI
(..)). Until this is unified, routing must be done as above.
Points of Interest
MFC Feature Pack Samples
As already mentioned, the documentation in MSDN is really lacking. As you know, if there is one thing that is worse than missing documentation, it is a wrong one. My favorite example is CMFCToolBarMenuButton
.
The page shows a code snippet that I just could not find in the samples on my hard disk. So I decided to download the samples again. In principle, I was on the right track, but when you follow the links given in MSDN (CMFCToolBarMenuButton -> WordPad-Sample -> Visual Studio 2008 Samples Page -> Visual C++ samples), you will arrive at an outdated site (dating 11/2007) featuring the same samples I already had installed. Even better, Microsoft knows about these outdated links, see this post.
Here is the correct download for the MFC Feature Pack Samples (the relevant samples are here: C:\Program Files\Microsoft Visual Studio 9.0\Samples\1033\AllVCLanguageSamples\C++\MFC\Visual C++ 2008 Feature Pack).
Other Assertions
Actually, I performed parts of the conversion several times. When I found that some errors could already be anticipated before switching to the new library, I rolled back everything and implemented the fix in the original, compilable and immediately testable application.
During these attempts, some other assertions occurred that I don't want you to miss.
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\winfrm.cpp(1712) : Assertion failed!
now caused by:
this->LoadBarState("TOOLBARSTATES");
The reason is that some toolbars were visible before conversion and were themselves not converted yet. Still, the configuration states something about those toolbars.
=> Remove the configuration (either the .ini file or the Registry entry) to avoid configuration of unconverted toolbars.
Conclusion
- Documentation is lacking: The documentation is by far not complete. Many pages in MSDN suggest to look up the source code (RTFC). Many answers could only be found in the BCGSoft Forum. Therefore: if you find out other beneficial information, don't hesitate to share it here!
- Custom controls cause problems: Many problems occur with custom controls, in particular such that were originally written to work around what was lacking in the available MFC classes. In my case, most problems were caused by our localization, toolbar, and tooltip classes.
- It is worth the trouble: Although I did not yet touch the layout of the application (all boxes, toolbars, ... are still where they used to be), the look and feel has noticeably improved. At the same time, all instabilities introduced could be immediately detected (at least I hope I did so).
Finally, let me say thank you for reading this article, and good luck for your own conversion project!
History
- July 6, 2011: Initial version. Application compiles. Menu, docking dialogs working. Toolbars broken.
- July 18, 2011: Application runs with no known deficits.