将旧版 MFC 应用程序移植到 MFC Feature Pack






4.89/5 (41投票s)
我在完善一个现有 200K LOC 应用程序的 GUI 时遇到的问题。
引言
我需要完善一个现有的 MFC 应用程序的 GUI,并决定使用 Visual Studio 2008 MFC Feature Pack。
起初,我想遵循 Marius Bancila 描述的步骤。然而,许多步骤并未按预期进行;反而出现大量编译错误和断言。由于我(以及其他人)发现文档不足,许多教程只关注演示应用程序,因此我决定让您参与我的奋斗过程。
对于谷歌搜索的用户,我包含了大多数断言消息。所有截图(出于法律原因)并非来自我正在处理的原始应用程序,而是来自 VS 2008 随附的标准 MFC WordPad 示例;因此,有时它们可能与文本不完全匹配。
移除 Win98 风格
在开始之前,实现现代 GUI 的第一步是 启用 Visual Styles
CMyApp::InitInstance()
{
...
INITCOMMONCONTROLSEX CommonControls;
CommonControls.dwSize = sizeof (INITCOMMONCONTROLSEX);
// I could not see any effect of the specific value here
CommonControls.dwICC = ICC_STANDARD_CLASSES;
InitCommonControlsEx (&CommonControls);
确保使用 ComCtl32.dll 的 6.0 版本
#pragma comment(linker, "\"/manifestdependency:type='win32'\
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
重构以预测编译错误
在新的 MFC 类中,一些接口已更改,因此如果您直接切换到新类,您将收到编译错误,并且必须盲目工作一段时间。因此,提前执行一些最小化的重构是有益的。
GetToolBarCtrl
新的 CMFCToolBar
中不提供 GetToolBarCtrl()
方法,因此必须修改一些用法。(为了更容易检测到这些问题,我引入了我自己的 MyToolBar
,它派生自 CToolBar
,但将 GetToolBarCtrl()
标记为已弃用。)
一些方法可以直接在 CToolBar
上调用(我猜想在早期版本的 MFC 中这是不可能的)。这些调用
CEdit* pEdit = (CEdit*) this->GetToolBarCtrl().GetDlgItem(ID_EDIT);
this->GetToolBarCtrl().GetItemRect();
this->GetToolBarCtrl().GetButtonCount();
可以替换为
CEdit* pEdit = (CEdit*) this->GetDlgItem(ID_EDIT);
this->GetItemRect();
this->GetCount();
HideButton
我找不到一种直接的方法来删除对 HideButton
的多个调用。(根据我们应用程序的配置,一些工具栏根本不可用,另一些则包含更少的项目。)
我决定为以后使用 SetNonPermittedCommands
做准备;因此,我必须一次性更改所有按钮的可见性,而不是通过单独的调用。
我的尝试是替换
m_wndToolBar->GetToolBarCtrl().HideButton(ID_FILE_PRINT,false);
m_wndToolBar->GetToolBarCtrl().HideButton(ID_FILE_OPEN,true);
用类似这样的东西
MyToolBarBase::ItemVisibility_t visibility;
visibility[ID_FILE_PRINT] = true;
visibility[ID_FILE_OPEN] = false;
myToolBar->SetItemVisiblity(visibility);
转换本身
对于以下所有内容,我建议运行应用程序验证器以立即检测新断言。我从 Marius Bancila 描述的步骤开始。
在您的 stdafx.h 中添加新的头文件
#include <afxcontrolbars.h>
开始使用新类
将 CWinApp
更改为 CWinAppEx
。
在 InitInstance
中添加以下行
InitContextMenuManager();
InitShellManager();
InitKeyboardManager();
InitTooltipManager();
CMFCToolTipInfo ttParams;
ttParams.m_bVislManagerTheme = TRUE;
theApp.GetTooltipManager()->
SetTooltipParams(AFX_TOOLTIP_TYPE_ALL,
RUNTIME_CLASS(CMFCToolTipCtrl), &ttParams);
将 CMDIFrameWnd
更改为 CMDIFrameWndEx
。
这会在 CWinAppEx::GetRegSectionPath
中产生一个断言
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\afxregpath.cpp(33) : Assertion failed!
在 InitInstance()
中添加以下内容
SetRegistryKey(_T("MyCompany"));
现在应用程序运行得更远一些,但在 CMDIClientAreaWnd::CreateTabGroup
中崩溃。
"CMDIClientAreaWnd::OnCreate: can't create tabs window\n"
在此崩溃之前,已经出现了许多更多的断言和错误消息。特别是这个
Can't load bitmap: 42b8. GetLastError() = 716
(请勿点击 此链接,它对您没有帮助。)还有这个
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\afxtabctrl.cpp(1395) : Assertion failed!
相应的代码行是
ENSURE(str.LoadString(IDS_AFXBARRES_CLOSEBAR));
因此,又缺少了一些资源。解决方案在 MSDN 中给出
"it's a known problem for statically linked projects using the feature pack"
=> 将应用程序更改为使用 MFC 作为共享 DLL。在我的情况下,这并不是什么大问题,否则请按照上面的链接中的说明进行操作。
断言仍在继续
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!
出现在
CFrameWnd::DockControlBar(...)
pDockBar = (CDockBar*)GetControlBar(dwDockBarMap[i][0]);
ASSERT(pDockBar != NULL);
原因是 CControlBars
和 CToolBars
不能停靠在 CFrameWindow
中。(请参阅 social.msdn。)
=> 目前,注释掉所有形式的语句
DockControlBar(&m_myToolBar,AFX_IDW_DOCKBAR_TOP);
DockControlBar(&m_myDialogBar,AFX_IDW_DOCKBAR_LEFT);
=> 是的!应用程序已启动!
好的,所有的对话框和菜单都丢失了,但目前我们先不用管它。
相反,让我们做些积极的事情,启用样式
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (MyBaseClass::OnCreate(lpCreateStruct) == -1)
return -1;
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerOffice2007));
CMFCVisualManagerOffice2007::SetStyle(
CMFCVisualManagerOffice2007::Office2007_ObsidianBlack);
重新启动应用程序,瞧!应用程序显示了一个酷炫的深色框架和修改过的系统菜单按钮。
重新显示菜单栏
除了所有工具栏和控件栏之外,现在菜单也消失了。嗯,实际上,它并没有完全消失;按下 Alt-F,例如,它会再次可见,但位于窗口顶部某处。
要解决这个问题,请在 CMainFrame
中添加一个 CMFCMenuBar
。
if (!m_wndMenuBar.Create(this))
{
TRACE0("Failed to create menubar\n");
return -1; // fail to create
}
m_wndMenuBar.SetPaneStyle(m_wndMenuBar.GetPaneStyle() |
CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY);
...
m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
DockPane(&m_wndMenuBar);
现在菜单是可见的(并显示新样式);系统菜单也出现在应有的位置。
将所有 CDialogBar 替换为 CPaneDialog
避免 GetControlBar
当我们替换 ToolBars 和 DialogBars 时,对 GetControlBar(IDD_MY_TOOLBAR)
的调用将开始返回 NULL
并可能导致应用程序崩溃。
因此,避免使用 GetControlBar
,并可能替换
ShowControlBar(GetControlBar(IDD_MY_TOOLBAR, ...))
用
ShowControlBar(&m_myToolBar, ...)
重载的 ShowControlBar
,它可以处理 CBasePane
。在一种情况下,我也处理了 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;
}
(但后来发现该特定函数现在已过时,并由框架自动处理;见下文。)
将 CDialogBar 更改为 CPaneDialog
确保资源的 WS_VISIBLE
标志已设置(请参阅 此帖子)。
GetWindowRect
中的一个崩溃是由窗口层次结构的变化引起的。因此,我替换了
GetParent()-> GetParent()->GetWindowRect(&m_window_rect);
用
GetParent()->GetWindowRect(&m_window_rect);
直到发现整个块(超过 100 行)是以前程序员(试图)修复的错误,由于没有成功,它已在其他地方重做,现在是多余的。
确保对接时的最小尺寸
当对话框被拖动时,大小会变得太小。原因是 CalcDynamicLayout
方法现在不再被调用。相反,在对话框的构造函数中,调用
SetMinSize(CSize(190, 480));
重要提示:最小对话框尺寸的处理必须显式启用,例如,在 CWinApp::InitInstance
调用中
CPane::m_bHandleMinSize = true;
就这样!对话框栏现在可以正常工作了!
将 CStatusBar 更改为 CMFCStatusBar
这里,Mainfrm.h 中只需要更改一行;工作起来没有问题。
将 CToolBar 更改为 CMFCToolBar
虽然 CMFCToolBar
已经 可以容纳许多不同的控件,但对于
CToolBar
,此功能必须单独编程。因此,无法仅用 CMFCToolBar
(可能需要大量单独编程)替换 CToolBar
。
在我的案例中,对于单个工具栏,我遇到了大量断言,因为所有组合框、编辑字段等现在都丢失了。因此,每个 GetDlgItem(IDD_MY_FANCY_CONTROL)
现在都会失败。
总之,如果您运气好,您的工具栏只显示一些按钮,那么就这样做
将 CToolBar
更改为 CMFCToolBar
。
一切编译,没有出现断言,但是——工具栏不可见。在菜单中点击它们也无济于事。对于状态栏,一切都如预期工作。
在我的案例中,原因有两方面
- 存储工具栏状态的注册表路径已更改,并且尚未包含工具栏应显示的信息
- 菜单尚未为(取消)显示工具栏而功能化
从菜单显示和隐藏工具栏
在原始应用程序中,为每个工具栏创建了一个菜单项。然后,此条目基本上链接到以下方法
CFrameWnd::OnBarCheck(UINT nID);
CFrameWnd::OnUpdateControlBarMenu(CCmdUI* pCmdUI);
此方法(以及其中提到的 OnToggleBar
方法)现在已过时。显示工具栏的相应菜单现在可以动态创建。您只需要一个(子)菜单,例如,ID 为 ID_VIEW_TOOLBAR
,并在
// Enable toolbar and docking window menu replacement
EnablePaneMenu(TRUE, ID_VIEW_CUSTOMIZE, strCustomize, ID_VIEW_TOOLBAR);
在 OnCreate()
方法中。您可以使用 0 代替 ID_VIEW_CUSTOMIZE
,如果您不允许自定义工具栏。strCustomize
是为启用的“自定义”功能显示的文本。
这里出现了一些问题,是我由于菜单本地化引起的。菜单最初只包含占位符文本,该占位符文本用作获取本地化文本的资源密钥。不幸的是,代码后台对于子菜单(“//递归尚未实现”)和动态文本似乎不起作用。作为一种变通方法,我从菜单中删除了工具栏条目;它们现在只能通过右键单击工具栏窗格来启用/禁用。
此外,工具栏和对话框现在需要有意义的(和本地化的)标题,因为菜单条目将显示相应的文本。
m_wndToolBar.SetWindowText("Fancy Toolbar");
移植现有的 CToolBars
如果您的应用程序的外观和感觉得到了更新,那么很可能许多工具栏也会被重新设计(例如,被可停靠的多行控件替换),因此以下内容可能根本不需要。
无论如何,让我们试试看,并将现有的、高级的 CToolBar
移植到 CMFCToolBar
。如前所述,对于简单的工具栏,这可以立即完成。
而且,新的工具提示现在也显示了
不幸的是,对于自定义工具栏,情况并非如此容易。WordPad 示例中的格式工具栏现在完全搞砸了。
将单个元素替换为 CMFC 类
注意:由于您的自定义工具栏实现不同,以下内容看起来会有所不同,但它应该会给您一个关于转换是否可行的提示。此外,我描述了我应用程序中进行的操作;我没有尝试移植 WordPad 工具栏。
MSDN 上还有一篇关于 在工具栏上放置控件的文章。
要在工具栏中插入一个编辑控件,现有的代码看起来像这样
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;
修改后的代码要短得多
case ToolBar_Value::tbs_Edit :
{
CMFCToolBarEditBoxButton editButton(lCurrID, 0);
m_pWinToolbar->ReplaceButton(lCurrID, editButton);
break;
}
继承层次结构
有趣的是,以下调用仍然有效
CEdit* pEdtPosition = (CEdit*)this->GetDlgItem(ID_EDT_FOO);
原因是返回的值是 CMFCToolBarEditCtrl
类型,它继承自 CEdit
。其他控件类型也存在类似的层次结构。
正确的元素数量
出现了一个问题,因为在我的案例中,自定义元素是添加到工具栏的,而在新语义中,现有按钮被替换,即必须添加占位符。我不得不更新包含 SetButtons
语句的代码。
=> 确保存在足够的占位符(具有正确的 ID)。
正确的按钮图像
对于某些按钮,图标是按程序确定的。有趣的是
myToolBar.SetButtonInfo(buttonId, ID_BTN_TO_CHANGE, state, 3);
现在不使用 myToolbar
的按钮 #3,而是第一个工具栏的图标 #3。
现在正确的方法是通过其所属的命令动态确定图像,例如
CMainFrame::OnUpdateFooUI(...)
{
int imageIndex = GetCmdMgr()->GetCmdImage(ID_BTN_WITH_IMAGE_I_NEED, FALSE);
...
myToolBar.SetButtonInfo(buttonId, ID_BTN_TO_CHANGE, state, imageIndex);
事件路由
在一个工具栏中,一旦从组合框中选择了一个项目,就会触发一个事件。事件处理程序位于工具栏类中
BEGIN_MESSAGE_MAP( MyToolBar, MyToolBarBase )
ON_CBN_SELENDOK(ID_LB_FOO ,OnSelChangeCmbFoo)
此事件现在不再被捕获。有人可能会说,上面的做法(我不同意)是一种不良实践,您应该将消息处理程序放在例如 CMainFrame
中。(我不太确定在 CMainFrame
中拥有 200 个消息处理程序的实际优势是什么,但请随意告诉我。)
一种变通方法是明确告知工具栏事件(请参阅 BCG 论坛)。
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;
}
请注意,如果 m_wndToolbar
可以处理命令,我在这里不会提前返回。原因是(出于任何原因)部分处理在工具栏类中进行(例如 OnCommand
(..)),而其他部分在 CMainFrame
中进行(例如 OnUpdateCommandUI
(..)。在统一之前,必须按上述方式进行路由。
关注点
MFC Feature Pack 示例
如前所述,MSDN 中的文档确实很缺乏。您知道,如果有什么东西比缺失的文档更糟糕,那就是错误的文档。我最喜欢的例子是 CMFCToolBarMenuButton
。
该页面显示了一个代码片段,我根本无法在硬盘上的示例中找到它。因此,我决定重新下载示例。原则上,我在正确的轨道上,但当您遵循 MSDN 中提供的链接(CMFCToolBarMenuButton -> WordPad-Sample -> Visual Studio 2008 Samples Page -> Visual C++ samples)时,您将到达一个过时的网站(日期为 2007 年 11 月),其中包含我已安装的相同示例。更好的是,Microsoft 知道这些过时的链接,请参阅 此帖子。
这是 MFC Feature Pack Samples 的正确下载(相关示例在此:C:\Program Files\Microsoft Visual Studio 9.0\Samples\1033\AllVCLanguageSamples\C++\MFC\Visual C++ 2008 Feature Pack)。
其他断言
实际上,我分几次执行了转换的一部分。当我发现一些错误可以在切换到新库之前就可以预料到时,我撤消了所有操作,并在原始的、可编译的、可立即测试的应用程序中实现了修复。
在这些尝试中,出现了一些其他断言,我不想让您错过。
f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\winfrm.cpp(1712) : Assertion failed!
现在由...引起
this->LoadBarState("TOOLBARSTATES");
原因是转换前某些工具栏可见,并且这些工具栏本身尚未转换。尽管如此,配置仍然提到了那些工具栏。
=> 删除配置(.ini 文件或注册表项)以避免配置未转换的工具栏。
结论
- 文档缺失:文档远不完整。MSDN 中的许多页面建议查找源代码(RTFC)。许多答案只能在 BCGSoft 论坛中找到。因此:如果您发现其他有益的信息,请随时在此分享!
- 自定义控件引起问题:许多问题由自定义控件引起,尤其是那些最初是为了弥补可用 MFC 类中的不足而编写的。在我的案例中,大多数问题是由我们的本地化、工具栏和工具提示类引起的。
- 值得付出努力:尽管我还没有触及应用程序的布局(所有框、工具栏……仍然在它们原来的位置),但外观和感觉已经有了显著的改善。同时,引入的所有不稳定性都可以立即检测到(至少我希望我做到了)。
最后,感谢您阅读本文,祝您的转换项目好运!
历史
- 2011 年 7 月 6 日:初始版本。应用程序编译。菜单、对接对话框正常工作。工具栏损坏。
- 2011 年 7 月 18 日:应用程序运行,没有已知缺陷。