在 WinForms 和 WPF 应用程序中托管 MFC MDI 应用程序






4.97/5 (48投票s)
在 .NET 应用程序中构建旧的 MFC 应用程序,用现代 WinForms 或 WPF 框架替换 MFC MDI 框架,并将其中的 MFC 视图和对话框集成
包含用于托管简单向导 MFC/对话框应用程序的源代码的编程指南
- 下载演示 - 3.2 MB
包含托管UMLEditor
的MfcAdapter
源代码(免费源代码仅支持 VS2003\VS2005!!!在 VS2010 (MFC 10) 中,某些功能出现问题。) - 下载 MfcAdapter VS2003\VS2005
尝试我新文章中的 MfcAdapter 3.0 (VS 2019):“MfcAdapter - 非 OLE .Net/WPF 的 MFC 应用程序容器” - 下载 MfcAdapter 3.0 演示 VS2019
(请参阅“部署和安装”以仔细安装)
目录
- 引言
- MFC 与派生自 CWinApp 的类和托管 C++ DLL 混合
- MfcAdapter 的架构
- 在 WinForms 应用程序中托管 MFC 视图和对话框
- WPF 在托管 WinForms 控件中自动布局
- .NET 2.0 控件布局适配器到 .NET 1.x 控件
- 演示框架
- 部署和安装
- 关注点
- 历史
引言
就 .NET 的开发方式而言,对于一组基于 MFC MDI 架构的应用程序,存在一个将它们迁移到 .NET 的新问题,而且成本高昂。我只找到一个迁移工具 - DudeLabs 的 RC Converter。但它只支持 MFC MDI GUI 的一小部分转换 - 仅限于 MFC 资源。GUI 的大部分和所有业务逻辑,您都必须自己转换。
作为迁移方案的替代方案,Microsoft 提供了一些互操作解决方案(在互操作方案中,您不需要自动将 MFC 代码转换为 .NET,您只需要确保它们可以协同工作)
- 基于
HWnd
的 Win32 和 WPF 互操作(WPF 和 Win32 互操作概述 - Microsoft MSDN) - 基于 COM 的 MFC Windows 窗体支持类(在 MFC 中使用 Windows 窗体用户控件 - Microsoft MSDN)
第一种技术只允许托管 MFC 控件。(“如果您的 Win32 逻辑已经很好地封装为控件……”)您可以付出一些努力将对话框转换为控件 - 但不能是更大的 MFC MDI 应用程序。
MFC Windows 窗体支持类允许将 .NET 代码反向集成到 MFC 应用程序中。它允许您将 .NET 窗体布置为视图或对话框。在这种情况下,存在一些重要的限制:.NET 框架无法集成(必须使用旧的 MFC 框架),集成窗体功能的一部分不可用(例如,布局),并且只能将单独的控件作为窗体集成(例如,.NET MenuStrip
无法在 MFC 框架中使用)。我发现这些类型的互操作解决方案是迁移方案的纯粹替代方案,而且我在互联网上没有找到这些解决方案的真实示例。
针对特定应用程序(Johan Rosengren 的UMLEditor - 重新审视矢量编辑器),本文演示了如何在现有 .NET 应用程序中构建旧的 MFC MDI 应用程序,或为其快速创建新的 WinForms/WPF GUI 框架。在此之后,您可以逐步将旧的 MFC 对话框和视图迁移到新的 WinForms,然后迁移到 WPF UI 元素。与完全迁移到 .NET 相比,这种 MFC 和 WinForms 的中间集成对于大型 MFC MDI 项目可能更为便捷。
从技术角度来看,本文使用的混合 MFC DLL 带有 CWinApp
派生类、在 WinForms 控件中托管 MFC 视图以及在托管 .NET 1.x 和 .NET 2.0 WinForms 控件中支持 WPF 自动布局,这些在互联网上都没有讨论。
这是一个如何通过使用 MfcAdapter
包装器(50 KB!WinForms 和 WPF 版本的源代码)来演进应用程序 GUI 框架的示例
作为原始 MFC MDI 应用程序的 UMLEditor
使用 WinForms 框架构建的 UMLEditor
- 带有停靠和皮肤(stardock)的窗体
在 WPF 页面中构建的 UMLEditor
(带有两个 UML 图的 FlowDocument
)
MFC 与 CWinApp 派生类和托管 C++ DLL 混合
通常,基于 MFC 类的应用程序架构是强类型化的。许多类,例如 MDI 类 CDocument
、CView
、CFrameWnd
等,如果没有 CWinApp
(应用程序)类和复杂的初始化就无法使用。在这种情况下,简单的解决方案是将整个应用程序放入 DLL 中,并与托管 C++ 类混合。我们可以将这些类用作 WinForms 框架和 MFC 应用程序之间的托管接口。在这种情况下,我们必须解决“第二个”MFC 应用程序主窗口的问题。它必须是不可见的,并且所有 MFC 窗口都必须托管在 WinForms MainForm 窗口中。但是,在这种情况下,MFC 应用程序的所有功能都可以在托管框架中使用。首先,让我们讨论混合 DLL 的创建。我们为 VS2003 和 VS2005 提供了不同的解决方案,因为混合 DLL 加载过程在每种情况下都不同(混合程序集的初始化)。
VS2003
让我们使用 MSDN 中的标准解决方案(将 C++ 托管扩展项目从纯中间语言转换为混合模式)。请参阅“修改包含使用托管代码和 DLL 导出或托管入口点的使用者的 DLL”这一段。
为了防止在 DLL 初始化期间调用不安全代码(加载器锁定问题),Microsoft 建议使用 _noentry 链接器选项。在这种情况下,混合 DLL 没有入口点,_CRT_INIT 不调用静态 MFC 构造函数,并且 _DllMainCRTStartup 不调用 MFC DllMain(它调用 CWinApp::InitInstance)。只创建和初始化托管对象。
要创建静态 MFC 对象并初始化 MFC 对象,您必须从托管代码中调用 __crt_dll_initialize()
静态函数。您必须小心调用 __crt_dll_initialize()
的位置,并小心您的 MFC 构造函数和 InitInstance
方法代码 - 不要忘记加载器锁定问题,并且不要使用未初始化的 MFC 本机代码!!!
除此之外,您必须在 InitInstance
的开头显式调用 InitApplication
,而不是调用 CWinApp::InitInstance()
。否则,这将导致 CDocManager
、PreTranslateMessage
(将短键转换为命令消息)、内存泄漏等问题。
关于混合 MFC 和托管 C++ DLL 的初始化有一些一般性说明。我不会描述 MFC 对象初始化的特殊情况(例如,扩展 DLL 资源链的初始化),因为这不是本文的主题。最后一点是关于终止。您必须小心先关闭 CWinApp
对象(请参阅 MfcAppAdapter.cpp 中的示例代码),然后调用 __crt_dll_terminate()
。在这种情况下,WinForms 框架将正确关闭。
现在,我们可以用 UMLEditor
(或简单的向导 MFC/对话框应用程序)检查这个解决方案。对于 UMLEditor
,进行了以下项目配置更改:
配置类型 | 动态库 (.dll) |
字符集 | 使用 Unicode 字符集 |
使用托管扩展 | 是 |
输出文件 | .dll |
附加选项 | /noentry |
更新与项目配置文件和其他源文件的编译器选项 /clr 冲突的选项。
基本运行时检查 | 默认值 |
创建 MfcAppAdapter.cpp 文件,如下图所示,其中包含一个简单的托管类。使用“单例”模式允许我们封装此类的初始化和从 WinForms 代码中处置。它使 MFC 应用程序能够在首次使用前初始化,并在 WinForms 应用程序退出前终止 MFC 应用程序。此外,MfcAppAdapter
能够封装 MFC 命令消息接口。我们仅将此类用于 DLL 测试(更多安全代码在下载中)
#include "stdafx.h"
#include "_vcclrit.h"
using namespace System;
using namespace System::Windows::Forms;
public __gc class MfcAppAdapter
{
private:
static MfcAppAdapter* m_Instance;
MfcAppAdapter()
{
__crt_dll_initialize();
Application::add_ApplicationExit(
new EventHandler(this,
&MfcAppAdapter::OnApplicationExit));
}
void OnApplicationExit(Object* sender, EventArgs* e)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CMDIFrameWnd* pMainFrame =
dynamic_cast <CMDIFrameWnd*>(AfxGetMainWnd());
::SendMessage(pMainFrame->GetSafeHwnd(),WM_CLOSE,0,0);
//change DLL flag in AFX_MODULE_STATE to careful call
//ExitInstance as for application context
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
pModuleState->m_bDLL = NULL;
__crt_dll_terminate();
m_Instance=NULL;
}
public:
static MfcAppAdapter* GetInstance()
{
if(m_Instance==NULL)
{
m_Instance = new MfcAppAdapter();
}
return m_Instance;
}
bool OnCmdMsg( int nID)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CMDIFrameWnd* pMainFrame =
dynamic_cast <CMDIFrameWnd*>(AfxGetMainWnd());
return pMainFrame->OnCmdMsg(nID, 0, NULL, NULL) !=
FALSE;
}
};
UMLEditor
不需要特殊初始化,所以我们只在 UMLEditorDemo.cpp 中的 UMLEditorDemo::InitInstance
中添加 InitApplication()
。(对于其他应用程序,需要一些小的更改。)
构建 DLL 后,我们可以在一个简单的 C# WinForms 应用程序中托管并测试它(见图)
public Form1()
{
...
private void button1_Click(object sender,
System.EventArgs e)
{
//ID_APP_ABOUT
MfcAppAdapter.GetInstance().OnCmdMsg(0xE140);
}
}
我们使用按钮单击将 ID_APP_ABOUT
命令消息发送到 UMLEditor
VS2005
如上所述,VS2003 中只执行托管初始化。我们在托管初始化之后自己进行非托管初始化。在 VS2005 中,非托管和托管初始化都执行 - 非托管初始化首先发生,之后发生托管初始化。加载器锁定仍然可能发生(例如,如果 DllMain
中的代码访问 CLR),但现在它确定性地发生并被检测到。不要试图在 VS2005 中使用 /noentry 链接器选项跳过非托管初始化并调用 __crt_dll_initialize()
!!!
为了确保 `DllMain` 中的代码和静态变量构造函数不访问 CLR,新的混合 DLL 加载过程需要特殊的项目配置。在以 `DllMain` 为根的调用树中,任何函数都不能编译为 MSIL,因为这意味着访问未初始化的 CLR。我们可以在这里解决问题,包含这些函数的目标文件应该在没有 `/clr` 的情况下编译。在我们的例子中,我们在项目属性中设置“**无公共语言运行时支持**”,并在每个包含托管类的文件的属性中设置“**公共语言运行时支持 (`/clr`)**”。如果 `DllMain` 尝试直接执行 MSIL,将导致编译器警告(级别 1)C4747。但是,编译器无法检测 `DllMain` 调用另一个模块中的函数,而该函数又尝试执行 MSIL 的情况。初始化代码中包含 MSIL 指令的静态变量必须放置在托管模块中,并且不应由非托管初始化进行初始化和使用。
然后,我们必须解决链接器问题 - LNK2005 错误。Microsoft 不支持 MFC 扩展 DLL 中的 CWinApp
派生类 - 所以可能这不是一个链接器错误。无论如何,如果 CRT 库中的 DllMain
在 MFC 库中的 DllMain
之前链接,则会出现链接错误:“mfcs80ud.lib(dllmodul.obj) : error LNK2005: _DllMain@12 already defined in msvcrtd.lib(dllmain.obj)”。我通过一个小技巧解决了这个问题:在静态构造函数或 InitInstance
的开头添加宏 AFX_MANAGE_STATE (AfxGetStaticModuleState())
。
InitApplication
的问题与 VS2003 中的解决方案相同。
我们不需要调用特殊函数来创建和初始化静态 MFC 对象,也不需要调用终止函数!但我们必须关闭 MFC 应用程序(使用与 VS2003 中相同的代码)。
与 VS2003 不同,如果 MFC 应用程序在 WinForms 主窗体激活后激活 MainFrame 窗口,则托管调试助手会抛出 LoaderLock
异常。但正如稍后将讨论的,我们不需要两个激活的主窗口,因此我们不激活 MFC MainFrame 窗口。
我们用新的 C++/CLI 编写混合 DLL 中的所有托管类。使用“C++ 托管扩展语法升级清单”来迁移托管 VC++ 到 C++/CLI,或者使用 VS2003 MC++。在这种情况下,只需设置项目选项 /clr:oldSyntax 而不是 /clr。所有其他项目设置将与 C++/CLI 相同。
现在,我们可以用 UMLEditor
(或简单的向导 MFC/对话框应用程序)检查这个解决方案。
在 VS2005 中自动更新 UMLEditor
后,对项目配置进行以下更改:
配置类型 | 动态库 (.dll) |
字符集 | 使用 Unicode 字符集 |
使用托管扩展 | 否 |
输出文件 | .dll |
更新与项目配置文件和其他源文件的编译器选项 /clr 冲突的选项
基本运行时检查 | 默认值 |
启用最小重新生成 | 否 |
启用 C++ 异常 | 使用 SHE 异常 (/EHa) |
调试信息格式 | 程序数据库 (/Zi) |
创建/使用预编译头 | 不使用预编译头 |
入口点 |
创建 MfcAppAdapter.cpp 文件,如下图所示,其中包含一个简单的托管类。我们从 VS2003 版本中移除了 __crt_dll_initialize()
、__crt_dll_terminate()
,并使用了新的 C++/CLI 语法。在 MfcAppAdapter.cpp 文件属性中设置 CLR 支持
使用公共语言运行时支持编译 | 公共语言运行时支持 (/clr) |
#include "stdafx.h"
using namespace System;
using namespace System::Windows::Forms;
public ref class MfcAppAdapter
{
private:
static MfcAppAdapter^ m_Instance;
MfcAppAdapter()
{
Application::ApplicationExit +=
gcnew EventHandler(this,
&MfcAppAdapter::OnApplicationExit);
}
void OnApplicationExit(Object^ sender, EventArgs^ e)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CMDIFrameWnd* pMainFrame =
dynamic_cast <CMDIFrameWnd*>(AfxGetMainWnd());
::SendMessage(pMainFrame->GetSafeHwnd(),WM_CLOSE,0,0);
//change DLL flag in AFX_MODULE_STATE to careful call
//ExitInstance as for application context
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
pModuleState->m_bDLL = NULL;
delete m_Instance;
m_Instance=nullptr;
}
public:
static MfcAppAdapter^ GetInstance()
{
if(m_Instance==nullptr)
{
m_Instance = gcnew MfcAppAdapter();
}
return m_Instance;
}
bool OnCmdMsg( int nID)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CMDIFrameWnd* pMainFrame =
dynamic_cast <CMDIFrameWnd*>(AfxGetMainWnd());
return pMainFrame->OnCmdMsg(nID, 0, NULL, NULL) != FALSE;
}
};
在 UMLEditorDemo.cpp 的 UMLEditorDemo::InitInstance
中添加 InitApplication()
。构建 DLL 并在简单的 WinForms 2005 应用程序中测试。
MfcAdapter 的架构
我们到哪里了?到目前为止,我们只讨论了创建混合 MFC 和具有 CWinApp
派生类及托管 C++ DLL 的技术问题。但是,我们如何在 WinForms 或 WPF 框架窗口中托管 MFC 窗口呢?让我们讨论 MfcAdapter
包装器的架构
MfcAdapter
是一个托管组件,由 MfcAppAdapter
和合适的 WndManager
组成
MfcAppAdapter
(C++/CLI) 是一个非托管传统 MFC 应用程序的包装器。它是MfcHost
的核心。其主要任务是封装 MFC 应用程序的初始化、该应用程序的命令接口及其窗口托管。命令接口是指传统 MFC 应用程序中命令处理程序的接口。为了访问框架,首先,为了将 MFC 窗口附加到框架,MfcAppAdapter
使用外部IWndManager
接口。这个解决方案允许我们只为任何框架类型使用一个MfcAppAdapter
。MfcAppAdapter
类只使用传统 MFC 应用程序中基于 MFC 的类,因此它独立于UMLEditor
的特定类,可用于包装其他应用程序。WndManager
是框架的一个外观,它允许我们将MfcAppAdapter
实现为所用框架的一个组件 - MDI 窗体演示的 WinForms 组件,或 WPF 页面演示的 WPFFrameworkElement
。对于其他托管框架,可以实现合适的WndManager
。WndManager
的共同部分是IWndManager
接口。WndManager
为特定框架实现此接口。WndManager
的其余部分取决于框架以及我们需要的 MFC 应用程序功能。例如,MdiFormsWndManager
实现了 MFC 应用程序中框架的ToolStripMenuItems
和ToolStripButtons
(VS2003 的MenuItem
和ToolBarButton
)的处理和更新,并支持设计时的 Visual Studio 设计器。WpfPageWndManager
实现了OpenDocument
方法,并支持 WPF 自动布局管理器。在本文中,我们将讨论MfcAppAdapter
和WpfPageWndManager
。MdiFormsWndManager
在我们的案例中是辅助性的,本文将不讨论,但您可以使用源代码。
在 WinForms 应用程序中托管 MFC 视图和对话框
我们在这里讨论 MfcAppAdapter
实现中最重要的问题。请使用源代码进行详细理解。
封装的 MFC 代码的更改
在使用创建的混合 DLL 之前,我们必须解决一些问题。
Unicode 集
Unicode 字符集,以简化字符串消息编组。
托管对话框、MFC 主窗口的创建和激活 (UmlEditorDemo.cpp)
MFC 对话框窗口使用传统 MFC 应用程序的主窗口作为默认父窗口。如果我们将 WinForms 框架 MainForm 窗口设置为传统 MFC 应用程序主窗口的父级,我们可以保留托管对话框的模态属性。我们可以通过以下解决方案解决此问题:
- 在我们的应用程序类中添加
m_ExtFramework
成员、AttachApplication
方法和AttachApplication
静态函数。m_ExtFramework
保存了 WinForms 框架的接口 (IFramework
)。AttachApplication
方法是m_ExtFramework
的设置器,而AttachApplication
静态函数是访问AttachApplication
方法的简单接口(我们使包装器类MfcAppAdapter
和ViewCtrl
独立于 MFC 应用程序)。WinForms 框架在混合 DLL 初始化后但在使用任何传统 MFC 应用程序之前调用AttachApplication
方法。 - 将主窗口的创建从
InitInstance
移到AttachApplication
方法。 - 覆盖
CMainFrame::PreCreateWindow
,并在cs.hwndParent
中设置来自IFramework
的 MainForm 窗口句柄。 - 通过关闭 MainForm 窗口,子窗口被销毁。传统的主窗口接收
WM_DESTROY
消息。在这种情况下,我们必须小心关闭传统的 MFC 应用程序。但正常的关闭场景发生在客户端对MfcAppAdapter
调用 dispose 并在 WinForms Framework MainForm 关闭之前关闭传统的 MFC 应用程序时。因此,如果客户端忘记了这一点,我们会在输出窗口中写入警告。 - .NET 1.x 和 .NET 2.0 的消息循环实现不同。在 .NET 1.x 中,消息循环首先在
MainForm
中预处理来自无模态对话框控件的一些常见控件消息(WM_NOTIFY
、WM_COMMAND
)。我们通过在传统 MFC 应用程序中显式预翻译这些消息来解决此问题。
我们还使用 CNotifyHook
作为 COleFrameHook
接口实现,以使 WinForms 框架窗口与传统 MFC 应用程序窗口同步。
我们保持传统 MFC 应用程序主窗口不可见。因此,必须从应用程序 InitInstance
中删除一些源代码。
//// The main window has been initialized, so show and update it.
//pMainFrame->ShowWindow( m_nCmdShow );
//pMainFrame->UpdateWindow();
命令行处理现在在 WinForms 框架中。所以,也删除此代码
//// Dispatch commands specified on the command line
// if (!ProcessShellCommand( cmdInfo ))
// return FALSE;
托管视图
为了在 WinForms 控件中托管 MFC 视图,我们必须更改视图类和相关类
- 首先,视图内容在创建和初始化时必须不可见。它只会在附加到 WinForms 控件后激活。在源代码中,我们在重写的
MultiDocTemplate::OpenDocumentFile
、CDocManagerEx::OpenDocumentFile
和CMainFrame::OnWindowNew
中将bMakeVisibleparameter
设置为false
。 - 我们必须保存指向最后一个打开文档的指针,我们将在 WinForms 控件中使用它。这是通过我们的
IFramework
接口的SetOpenedDocument
方法实现的。 - 不要使用
CView::OnCreate
。创建和初始化继承自CWnd
、Control
、DropTarget.Register
、ToolTip
等的视图类成员应从OnCreate
方法移至OnInitialUpdate
。在这种情况下,这些成员将在视图附加到 WinForms 控件后创建。 - 视图由
WndManager
附加到 WinForms 控件后,CView::GetParent
方法将返回 WinForms 控件的父hWnd
。因此,所有使用CView::GetParent
的代码,例如SetTitle
,都必须更改为我们IFramework
的GetNativeParent
方法。我们为CDocManagerEx::OpenDocumentFile
、CUMLEditorDemoDoc::CanCloseFrame
和CUMLEditorDemoDoc::UpdateFrameCounts
做了这些更改。通过关闭控件容器,WndManager
必须释放带有附加视图的控件!!!在这种情况下,视图将被分离,视图的 MFCCFrame
父级将被恢复,然后视图将在 MFC 应用程序中关闭。我们必须检查所有视图是否已从 WinForms 控件分离,然后才能关闭 MFC 应用程序。UMLEditorDemo::DetachApplication
方法执行此检查。 - 为了支持自动布局,我们必须在托管视图中实现一些方法:
AutoSize
、GetPreferredSize
和ScaleControl
。我们使用带有这些方法的基类CLayoutView
作为托管视图中MfcAppAdapter
的接口。视图必须继承此基类。在控件中使用的视图的所有自定义属性都必须以这种方式实现。 - 在本文中,我们不描述继承自控件和带有
m_dropTarget
(支持拖放)的视图的视图类的更改,因为UmlEditor
没有这些视图。
包装器类
MfcAppAdapter
传统 MFC 应用程序的命令接口由两个方法包装:OnCmdMsg
(CCmdTarget::OnCmdMsg
) 和 OnUpdateCmdMsg
。我们还使用 CCmdUIHandler
类进行用户界面更新。该类为框架菜单项和工具栏按钮实现了 CCmdUI
接口。GetMfcMenuString
方法实现了对 MFC CMenu
的访问,以支持 WinForms 菜单中的“Recent”菜单项。
在 VS2003 版本中,TranslateMsg
方法支持通用控件的消息调度。
MfcAppAdapter
是一个单例。CreateInstance
方法可用于访问 private static
实例。MfcAppAdapter
构造函数调用 UMLEditorDemo::AttachApplication
来初始化传统的 MFC 应用程序。MfcAppAdapter
的 Dispose
检查所有托管视图是否已关闭,然后使用 UMLEditorDemo::DetachApplication
方法关闭传统应用程序。
ViewCtrl
ViewCtrl
支持在 WinForms 控件中托管 MFC 视图。它是 MfcAppAdapter
的辅助类。
- 作为 WinForms 控件的子控件,
ViewCtrl
继承了Control
接口。为了托管视图,我们必须将CView
对象从“CView
”窗口分离,并通过创建“ViewCtrl
”窗口将CView
对象附加到“ViewCtrl
”窗口。 - 我们通过用
StubWndProc
替换CViewWndProc
来保持从“CView
”窗口到CView
对象的调度消息。StubWndProc
将这些消息从“CView
”窗口调度到“ViewCtrl
”窗口,然后调度到CView
对象。 - 为了保持“
CView
”窗口的CWnd
接口,我们必须将StubWnd
(基于CWnd
)附加到“CView
”窗口。现在,我们的CView
对象连接到“ViewCtrl
”窗口和“CView
”窗口!这并不糟糕。见图 - 通过更改重写的
ViewCtrl::CreateParams
中的“Windows 类”,我们可以在创建“ViewCtrl
”窗口时执行这些操作。然后,我们使用WM_CREATE
消息在ViewCtrlWindowProc
中调用ViewCtrl::AttachView
方法。当CView
对象附加到“ViewCtrl
”窗口时,我们调用CView::OnInitialUpdate
;并且视图内容呈现在 WinForms 窗体中,而不是 MFCCFrame
中。 - 我们必须将
ViewCtrl
框架(WinForms 窗体)与 MFCCFrame
同步,以支持传统 MFC 应用程序的 MDI 逻辑。我们通过以下方法实现此目的:OnGotFocus
ProcessDialogKey
SetText
GetParent
- 重写
OnHandleDestroyed
方法将视图从控件分离到 MFCCFrame
,并使用 dispose 关闭视图。 - 为了支持自动布局,基控件(VS2005)或特殊基类
LayoutControl
(VS2003)的AutoSize
、GetPreferredSize
和ScaleControl
方法被重写。通过CLaoutView
接口连接的托管视图实现了这些方法所需的功能。 MfcAppAdapter
使用IWndManager
接口将创建的ViewCtrl
控件附加到外部框架,并在控件收到GotFocus
事件时激活主窗口。否则,外部框架会获取MfcAppAdapter
的OnClosing
取消事件处理程序。框架可以使用此处理程序来支持传统应用程序的“CanClose
”功能。在 WinForms 框架中,我们只需将OnClosing
取消事件处理程序与 MainForm 的Closing
事件连接起来。
UmlEditorCommand
UmlEditorCommand
枚举包含所有可在传统 MFC 应用程序中处理的命令 ID。
WPF 托管 WinForms 控件中的自动布局
WPF 类 WindowsFormsHost
允许我们将 MfcAppAdapter
实现为 WPF FrameworkElement
。此外,VS2005 Visual Designer 不支持 WPF,这使得 MfcAdapter
和 WFP 框架的集成更加容易!唯一需要解决的问题是当前版本的 WindowsFormsHost
不支持 WPF 自动布局管理器。在这个简单的 WPF WndManager
版本中,我们展示了如何实现它。
UmlEditorHost
UmlEditorHost
是一个扩展的 WindowsFormsHost
,具有对托管控件的自动布局支持。标准解决方案涉及重写 WindowsFormsHost
基方法:MeasureOverride
和 ArrangeOverride
。
在 MeasureOverride
中,我们从托管控件获取首选大小(Control.GetPreferredSize
方法),并在必要时保持宽度/高度比不变地将其减小到所需大小。
在 ArrangeOverride
中,我们将托管控件缩放到首选大小,如果需要则将其缩小到最终大小,并乘以缩放因子。缩放因子保存在添加的依赖属性 Zoom
中,该属性可以与外部布局管理器绑定。
WpfPageWndManager
WpfPageWndManager
是 IWndManager
接口的 WPF 实现。在 AttachCtrl
方法中,我们只需将由 MfcAppAdapter
创建的控件添加到 UmlEditorHost
。在我们的案例中,我们不编辑托管的 UML 图,因此我们禁用托管控件并且不使用其 OnClosing
事件处理程序。
WpfPageWndManager
负责创建和处置 MfcAppAdapter
以及由 MfcAppAdapter
创建的其他控件。它是一个单例,并在其自己的构造函数中创建 MfcAppAdapter
实例。在 DisposeContainer
中,它保存所有带有创建控件的主机。使用 Application.Current.MainWindow.Unloaded
事件,我们处置 DisposeContainer
,然后处置 MfcAppAdapter
。
.NET 2.0 控件布局适配器到 .NET 1.x 控件
如果 MfcAppAdapter
是 .NET 1.x 组件,则我们需要在 WpfPageWndManager
和 MfcAppAdapter
之间使用 MfcHost1Adapter
。此解决方案的原因是 .NET 1.x WinForms 控件没有新的布局方法 AutoSize
、GetPreferredSize
和 ScaleControl
,并且不能直接支持 WPF 自动布局。通过使用复合控件 .NET 2.0 LayoutControlAdapter
作为容器和 .NET 1.x LayoutControl
作为构成控件,MfcHost1Adapter
允许我们将 .NET 1.x WinForms 控件作为 WpfPageWndManager
的 .NET 2.0 WinForms 控件实现。LayoutControlAdapter
的标准方法:AutoSize
、GetPreferredSize
和 ScaleControl
将被包含的 LayoutControl
的自定义方法覆盖。LayoutControlAdapter
可以在其他项目中用于在 .NET 2.0 框架中托管 .NET 1.x 控件。
扩展的 MfcAppAdapter
和 WndManagerAdapter
允许我们将附加的 LayoutControl
替换为复合 LayoutControlAdapter
。
演示框架
您可以自己创建简单的演示应用程序,也可以使用下载的演示。尝试使用其他托管框架而不是这些演示(这更有趣)。在这里,我将描述两个演示应用程序:MDI 窗体演示和 WPF 页面演示。
MDI 窗体演示
创建 C# Windows 应用程序项目,并在主窗体中添加 ToolStripMenuItem
和 ToolStripButton
(VS2003 的 MenuItem
和 ToolBarButton
)的集合。将 MdiFormsWndManager.dll 添加到工具箱。然后,将 UmlEditorComponent
从工具箱拖到主窗体。现在,ToolStripMenuItems
和 ToolStripButtons
具有 CommandName
扩展属性,您可以在其中为连接的 UmlEditorComponent
从列表框中设置合适的命令,如下图所示。只需编译并运行应用程序
WPF 页面演示
创建一个 WinFX Windows 应用程序。添加这些引用 - MfcAppAdapter.dll、WpfPageWndManager.dll 和 WindowsFormsIntegration.dll。如果您使用 VS2003 版本的 MfcAppAdapter.dll,也添加 MfcHost1Adapter.dll。
在图中所示的窗口中创建一个页面。我们有一个简单的 FlowDocument
,其中包含 AS:UMLEditorHost
和 FrameworkElement
。AS:UMLEditorHost
的 FileSource
属性定义了要添加到文档中的 UML 图的路径。为了支持 SinglePageViewer
缩放,AS:UMLEditorHost
的 Zoom
属性与 SinglePageViewer
资源中的 SinglePageViewer.Zoom
绑定。只需编译并运行应用程序
<!--WPF Page Demo-->
<?Mapping XmlNamespace="AS" ClrNamespace="AS.MfcHost2"
Assembly="WpfPageWndManager"?>
<Page xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
xmlns:AS="AS" Width="800" Height="540">
<SinglePageViewer Name="ZoomSource">
<!--Bind AS:UMLEditorHost.Zoom and SinglePageViewer.Zoom
for the Zoom support-->
<SinglePageViewer.Resources>
<Style TargetType="{x:Type AS:UMLEditorHost}">
<Setter Property="Zoom"
Value="{Binding ElementName=ZoomSource,
Path=Zoom, Mode=OneWay}"/>
</Style>
</SinglePageViewer.Resources>
<FlowDocument TextAlignment="Left" Background="AliceBlue">
<Paragraph>
<Bold>Hosting MFC application in WPF Page</Bold>
</Paragraph>
<Paragraph KeepTogether="True">
Document - <Italic>UMLEDI1.uml</Italic>
<!--use hosted MFC application as FrameworkElement-->
<AS:UMLEditorHost FileSource="../../UMLEDI1.uml"/>
</Paragraph>
<Paragraph KeepTogether="True">
Document - <Italic>UMLEDI2.uml</Italic>
<!--use hosted MFC application as FrameworkElement-->
<AS:UMLEditorHost FileSource="../../UMLEDI2.uml"/>
</Paragraph>
</FlowDocument>
</SinglePageViewer>
</Page>
部署和安装
请参阅“MfcAdapter - 不是 OLE .Net/WPF 的 MFC 应用程序容器”中的“部署和安装”。
- 托管的简单向导 MFC/对话框应用程序
VS 2005 和 VS2010 编程指南和源代码
我提供了一个简单的向导 MFC/对话框应用程序
DialogWiz
的编程指南和源代码,作为您托管 MFC 应用程序的起点。为简单起见,我从MfcAppAdapter
组件中删除了托管 MFC 视图支持。只需按照编程指南中的说明更改您的 MFC 应用程序即可。使用
MfcAppAdapter.OnCmdMsg()
,您可以将命令从 .NET 窗体发送到您的 MFC 命令处理程序并显示您的对话框。代码兼容 VS 2003-VS 2012,您只需更新项目设置即可。
- MfcAdapter VS2003\VS2005 托管
UMLEditor
源代码
解压后,我们有两个解决方案 -
UMLEditor2003
(VS2003) 和UMLEditor2005
(VS2005)。传统的 MFC 源代码 (Legacy 目录) 对这两个解决方案都是通用的。请不要将UMLEditor2003
解决方案转换为 VS2005 或将UMLEditor2005
解决方案转换为 VS2010 - 在这种情况下您需要更改一些源代码。UMLEditor2003
解决方案包含MfcAppAdapter
、MdiFormsWndManager
和MdiFormsDemo
项目。将MdiFormsDemo
设置为启动项目,然后构建并运行。UMLEditor2005
解决方案包含额外的WpfPageWndManager
、WpfPageDemo
和MfcHost1Adapter
项目。WpfPageDemo
和WpfPageWndManager
与 WinFX Runtime Components 3.0 - Beta 2 (Jan CTP 程序集,版本 6.0.5070.0) 兼容。检查对 WindowsFormsIntegration.dll 的引用(通常是 ..\Program Files\Reference Assemblies\Microsoft\WPF\v3.0\WindowsFormsIntegration.dll)。将
MdiFormsDemo
或WpfPageDemo
设置为启动项目,然后构建并运行。如果您想将
MfcAppAdapter
的 VS2003 版本与 WPF 一起使用,请在MfcHost1Adapter
中添加对 VS2003 MfcAppAdapter.dll 的引用。构建MfcHost1Adapter
,并在WpfPageWndManager
和WpfPageDemo
中添加对 VS2003 MfcAppAdapter.dll 和 MfcHost1Adapter.dll 的引用,而不是 VS2005 MfcAppAdapter.dll。重新构建并运行。我试图使封装的 MFC 代码尽可能简洁,因此我没有修复此代码的一些原生问题,例如,打开不存在的最近文件时未处理的异常。
- MfcAdapter 3.0。演示 VS2019
MfcAdapter
3.0 不需要特殊安装。从存档中提取后,您将获得 5 个 MFC 应用程序的源代码和可执行程序
- “
DialogWiz
” - 使用 VS 2005 向导创建的没有视图(只有对话框)的简单 MFC 应用程序 - “
DialogEditor
” - Johan Rosengren 的 DIY 矢量和对话框编辑器 (https://codeproject.org.cn/) - “
UMLEditor
” - Johan Rosengren 重新审视矢量编辑器 (https://codeproject.org.cn/) - “
Mesh
” - 使用 OpenGL 和 MFC 的小型 VRML 查看器 (https://codeproject.org.cn/) - “
SimpleCtrl
” -CLayoutView
、CView
的 MFC 简单示例
托管在 WinForms 中
- FormsDemo_UmlEditor.exe - 托管 MFC 应用程序“
UmlEditor
”和“DialogEditor
”的容器 - MdiFormsDemo.exe - 托管“
UmlEditor
”的完整版本 - SdiFormsDemo.exe - 托管“
DialogEditor
”的完整版本 - FormDemo_SimpleCtrl.exe -
CScrollView
、CListCtrl
的简单测试 - FormsDemo_DialogWiz.exe - 托管“
DialogWiz
”
托管在 WPF 中
- WpfDemo_UmlEditor.exe - 托管 MFC 应用程序“
UmlEditor
”和“DialogEditor
”的容器 - WpfPageDemo_UmlEditor.exe - WPF
FlowDocument
。托管 MFC 应用程序“UmlEditor
”和“DialogEditor
”的容器 - WpfPageDemo_WrlViewer.exe - 托管 VRML 查看器
只需运行必要的应用程序即可。
源代码
注意:MfcAdapter
3.0 演示仅支持一种配置:x86 Debug, Unicode, .NET 4.0-4.7
解压后,我们有一个 VS 2019 解决方案 - \Samples\Src\Samples.sln,其中包含所有演示项目。所有项目的构建输出都将连同必要的 MfcAdapter DLLs 一起复制到公共执行目录 \Samples\Src\x86\Debug 中。
如果出现引用错误,只需重复构建即可。
我试图使封装的 MFC 代码尽可能简洁,因此我没有修复此代码的一些原生问题,例如,编译器警告、打开不存在的最近文件时未处理的异常等等。
托管您的 MFC 应用程序
要托管您的 MFC 应用程序,请使用 MfcAdapter
的 x86 Debug Unicode .NET 4.0-4.7 版本
\Debug:
- MfcAppAdapter.dll, ViewControl.dll, ViewFrameworkElement.dll
\Include:
- HostApp.h, LayoutView.h
帮助:
- MfcAdapterAPI.chm, ProgrammingGuide.htm
关注点
我认为本文的解决方案——带有 CWinApp
派生类的 MFC 扩展 DLL——允许您通过更改 MDI 类并使用合适的包装器,将 MFC 应用程序托管在任何(不限于托管 WinForms/WPF)Win32 无 MFC 框架中。否则,这可能是一个有用的示例,用于在 WinForms 组件 (UmlEditorComponent
) 中进行 Visual Studio 设计器编程,因为它支持在设计器生成的代码中设置 Host。
历史
- 2005年12月30日:首次发布
- 2022年5月31日:文章更新