使用MFC和托管C++开发Windows Forms控件






4.92/5 (21投票s)
演示将基于 MFC 的控件迁移到 .NET Windows Forms 的不同方法
引言
Windows Forms 是用于在 .NET 框架下开发富客户端 GUI 应用程序的框架。Windows Forms 中有许多很棒的功能,可以极大地简化开发。问题是,codeproject 上提供的所有很棒的控件都是用 MFC 编写的,无法直接在 Windows Forms 应用程序中使用。我知道至少有三种方法可以将现有控件迁移到 .NET:
- 完全用托管代码重写控件
- 将它们制作成 ActiveX 控件,并在 Windows Forms 上使用 ActiveX 控件
- 使用托管 C++。
本文的目的是演示上述技术的最后一种。
演示控件
为了演示这些技术,我选择了我在 codeproject 上最喜欢的控件之一,Mark C. Malburg 的 模拟仪表控件。目标是能够通过包装现有的 MFC 控件(对原始 MFC 代码进行很少或不进行更改)来开发 Windows Forms 控件。开发的最终 Windows Forms 控件可以像图像中所示那样放置在 Windows Forms 设计器上。
该控件具有以下属性:
属性名称 | 属性类型 | 描述 |
单位 | System::String | 仪表上显示的单位的文本 |
值 | double | 决定指针位置并显示在控件底部的数值 |
NeedleColor | System::Drawing::Color | 指针的颜色 |
此外,该控件支持一个名为 OnValueChanged
的托管事件,当 Value
属性更改时会触发该事件。
首先,我们需要创建一个支持 MFC 的托管 C++ 类库。我编写了一个执行此操作的向导。该向导可从 https://codeproject.org.cn/useritems/ManagedMFCDLL.asp 下载。下载并安装向导,同时下载 模拟仪表控件 的源代码。
将托管 C++ 与 MFC 结合使用
我们的目标是尽可能多地保留现有的 MFC 代码,因为所有这些代码都经过测试并且(假定)运行良好。VC++.NET 允许混合托管代码(编译为 IL 的代码)和非托管代码(原生代码)。它甚至可以将现有代码编译为 IL。但是,除非标记了 <code>__gc
前缀,否则不能在其他 .NET 语言中使用这样编译的类。例如 <code>__gc
class MyControl。标记为 __gc
的类不能派生自任何未标记为 __gc
的类。这排除了将 CObject
、CWnd
类作为基类使用的可能性,因为它们没有标记为 __gc
。另一个限制是 __gc
类不能包含任何(实际上)其他类的成员。但是 __gc
类可以包含 MFC 类的指针。
为了开发 Windows Forms 控件,我们需要创建一个类,该类继承自托管类(标记为 __gc
)System::Windows::Forms::Control
,就像 MFC 自定义控件继承自 CWnd
一样。在此托管对象中,我们将包含一个 MFC 类的实例。我们将使托管控件的属性和方法的实现委托给 MFC 类,并让 MFC 代码完成所有繁重的工作。问题在于,我们需要将相同的窗口句柄(所有控件都是窗口)关联到 MFC 对象和 System::Windows::Forms::Control
对象。这可以通过两种方式完成:
- 让 Windows Forms 控件创建其窗口,然后 MFC 对其进行子类化。
- 让 Windows Forms 控件对 MFC 控件使用的窗口进行超级类化。
文章中涵盖了这两种方法。
通过 MFC 子类化 Windows Forms 控件
我们首先创建一个名为任何名称的空白 VS.NET 解决方案。
使用名为 Control 的向导向解决方案添加一个托管 MFC DLL 项目。该项目将用于任何托管类。
接下来,向解决方案添加一个支持 MFC 和预编译头文件的 Win32 静态库项目。称其为 ControlS(S 代表静态)。它将包含控件的 MFC 代码。分离托管代码和非托管代码可以使维护稍微容易一些。我们将使项目“Control”依赖于“ControlS”以将它们链接在一起。
将文件 3DMeterCtrl.cpp、3DMeterCtrl.h 和 MemDC.h 放在“ControlS”项目中。修改 3DMeterCtrl.cpp 文件以删除 #include "MeterTestForm.h",如下所示
#include "stdafx.h"
#include "math.h"
#include "3DMeterCtrl.h"
//#include "MeterTestForm.h" This line is to be removed
#include "MemDC.h"
此后,项目 ControlS 将成功生成。
我们需要为该控件编写一个托管包装器。我们称此类为 ThreeDMeter,它应继承自 System::Windows::Forms::Control
。为此,我们需要导入所需的程序集,并在 stdafx.h 中添加所需的 #using,如下所示。需要添加的任何代码都显示为蓝色。
#include <afxwin.h> // MFC core and standard components
#include "..\Controls\3DMeterCtrl.h"
#using <mscorlib.dll>
#using <system.drawing.dll>
#using <system.dll>
#using <system.design.dll>
#using <system.windows.forms.dll>
现在我们可以创建主控件类了。用 class ThreeDMeter 替换向导生成的默认类。
// Control.h
#pragma once
#include "resource.h" // main symbols
using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Runtime::InteropServices;
using namespace System::Runtime::Remoting::Messaging;
namespace ControlDemo
{
public __gc class ThreeDMeter : public Control
{
public:
ThreeDMeter()
{
m_pCtrl = new C3DMeterCtrl();
}
protected:
void Dispose(bool b)
{
Control::Dispose(b);
if (m_pCtrl != NULL)
{
delete m_pCtrl;
m_pCtrl = NULL;
}
}
void OnHandleCreated(EventArgs* e)
{
System::Diagnostics::Debug::Assert(m_pCtrl->GetSafeHwnd() == NULL);
m_pCtrl->SubclassWindow((HWND)get_Handle().ToPointer());
Control::OnHandleCreated(e);
}
private:
C3DMeterCtrl* m_pCtrl;
};
}
编译并生成 DLL。此时,我们已经有了一个可以在 C# 或 VB 应用程序中使用的 Windows Forms 控件。要测试该控件,请启动另一个 VS.NET 实例并创建一个 VB 或 VC# Windows 应用程序。将 ThreeDMeter 控件添加到工具箱(如果您想知道如何操作,请单击此处)。双击 ThreeDMeter 工具箱项会将控件添加到窗体,如下所示
如果您遇到任何错误,请尝试将对 Control 程序集的引用的“复制本地”属性修改为 false,如下所示(我不确定为什么这样做有效,如果有人能向我解释为什么将“复制本地”设置为 false 可以解决问题,我将不胜感激)。
因此,我们已成功将 MFC 控件包装到托管类中并在 Windows Forms 设计器中使用。它究竟是如何工作的?
Windows Forms 控件是一个默认样式为 WS_CHILD 的窗口。当控件添加到窗体时,窗口句柄就会被创建。发生这种情况时,会调用 System::Windows::Forms::Control
的受保护方法 OnHandleCreated。我们重载此方法,并使用 SubclassWindow 方法将其与我们创建的 C3DMeterCtrl 对象进行子类化。如果您不熟悉子类化,请参阅 Chris Maunder 关于子类化的教程。
尽管我们的控件可以成功绘制并在设计器中使用,但用户能做的修改控件行为(例如更改指针颜色或更改单位文本等)的事情并不多。在下一步,我们将为控件添加属性,以便我们能够做到这一点。
让我们使用 C++ 关键字 __property 的托管扩展添加代码来添加属性。添加属性后,代码如下所示
__property Color get_NeedleColor()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
}
__property void set_NeedleColor(Color clr)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_pCtrl->SetNeedleColor(ColorTranslator::ToWin32(clr));
}
__property void set_Units(String* units)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CString strUnits(units);
m_pCtrl->SetUnits(strUnits);
}
__property String* get_Units()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
LPCTSTR szUnits = (m_pCtrl->m_strUnits);
return new String(szUnits);
}
__property double get_Value()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
return m_pCtrl->m_dCurrentValue;
}
__property void set_Value(double d)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_pCtrl->UpdateNeedle(d);
OnValueChanged(this, EventArgs::Empty);
}
这些属性可以在任何时候调用,即使在句柄尚未创建时也是如此。因此,我们需要确保在使用窗口句柄的任何 MFC 方法在窗口创建之前不会被调用,否则会导致 MFC 触发断言。我们在 3DMeterCtrl.cpp 中的 MFC 代码中进行了修复,如下所示
void C3DMeterCtrl::ReconstructControl()
{
if (!GetSafeHwnd())
return;
// if we've got a stored background - remove it!
if ((m_pBitmapOldBackground) &&
(m_bitmapBackground.GetSafeHandle()) &&
(m_dcBackground.GetSafeHdc()))
{
m_dcBackground.SelectObject(m_pBitmapOldBackground);
m_dcBackground.DeleteDC() ;
m_bitmapBackground.DeleteObject();
}
Invalidate () ;
}
上面的代码演示了几个要点
- C++ 中属性的使用(有关更多详细信息,请参阅 Chris Maunder 关于托管 C++ 属性的 教程)。
- 使用 ColorTranslator 将 System::Drawing::Color 更改为 COLORREF
- 使用 System::String 的新 CString 构造函数将 System::String 类型更改为 CString。(感谢 Anson Tsao 指出这一点)
- AFX_MANAGE_STATE 的使用,以确保正确的 MFC 状态。那些使用 MFC 和 ATL 开发 COM 对象的人应该对此很熟悉。
我们实际上所做的是将调用委托给 C3DMeterCtrl 类。在您构建项目后,就可以从设计器设置或更改这些属性,并立即在 VB/VC# 窗体上的控件渲染中看到更改。
如果属性可以组织在属性网格中,那将非常酷。这可以通过使用托管 C++ 属性(例如)来轻松完成
[property: System::ComponentModel::CategoryAttribute("Meter")]
__property Color get_NeedleColor()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
}
在上面的示例中,我们将 CategoryAttribute
应用于 NeedleColor
属性。在您使用这些更改构建项目后,您可以在属性网格中看到此属性的效果。
控件中缺少的一点是它不触发任何事件。一个示例事件是当值更改时触发的 OnValueChanged
。以下声明表明控件支持 OnValueChanged
事件。
__event EventHandler * OnValueChanged;
为了触发事件,可以按以下方式更改 set_Value
方法
__property void set_Value(double d)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_pCtrl->UpdateNeedle(d);
OnValueChanged(this, EventArgs::Empty);
}
因此,我们拥有一个完全基于 MFC 控件的功能性控件。此实现的问题在于 MFC 控件优先处理事件,而托管控件则没有。如果以后需要在托管代码中增强该控件,例如通过从该控件派生一个用 C# 编写的另一个控件,这将是一个主要问题。这就引出了下一项技术——允许 Windows Forms 超类化我们的 MFC 控件,以便 Windows Forms 控件优先处理消息。
允许 Windows Forms 控件超类化现有的 MFC 控件
System::Windows::Forms::Control
类可以使用现有窗口类来创建其窗口。这可以通过重载 get_CreateParams
方法并指定新的类名来完成。唯一的限制是窗口类应使用 CS_GLOBALCLASS
注册。因此,我们在 InitInstance
中注册一个新的窗口类,并在 ExitInstance
中取消注册它,如下所示
BOOL CControlApp::InitInstance()
{
CWinApp::InitInstance();
WNDCLASS wc;
memset(&wc, 0, sizeof(wc));
wc.lpszClassName = "Analog3dMeter";
wc.hInstance = m_hInstance;
wc.lpfnWndProc = Analog3dMeterWindowProc;
wc.style = CS_DBLCLKS | CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW;
return RegisterClass(&wc);
}
int CControlApp::ExitInstance()
{
UnregisterClass("Analog3dMeter", m_hInstance);
return CWinApp::ExitInstance();
}
现在我们可以重载 get_CreateParams
方法了。为了演示目的,我们创建了一个名为 ThreeDMeter2 的全新类,其大部分代码与 ThreeDMeter
相同,但受保护的方法除外。
protected:
void Dispose(bool b)
{
Control::Dispose(b);
m_pCtrl = NULL;
}
__property System::Windows::Forms::CreateParams * get_CreateParams()
{
System::Windows::Forms::CreateParams * pParams =
Control::get_CreateParams();
pParams->ClassName = S"Analog3dMeter";
return pParams;
}
将类名指定为 Analog3dMeter 使 Windows Forms 控件使用该窗口类来创建控件的窗口。现在的问题是如何将 m_pCtrl
与创建的窗口句柄关联起来。这在两个地方完成。首先是控件的 CreateHandle
方法,该方法实际负责创建窗口。
void CreateHandle()
{
__try
{
CallContext::SetData(S"Controls.CurrentControl",
__box(IntPtr(m_pCtrl)));
Control::CreateHandle();
}
__finally
{
CallContext::SetData(S"Controls.CurrentControl", NULL);
}
}
CallContext
在这里相当于(某种程度上)ThreadLocal
存储。在上面的代码中,我们将线程的命名属性“Controls.CurrentControl
”设置为 m_pCtrl
的指针值。观察 __box 运算符,它用于将值类型转换为 Object*,这是 SetData
第二个参数的类型。设置此属性后,我们调用基类的实现,该实现实际调用 CreateWindowEx
,最后我们清除线程属性。
现在的问题是如何将窗口句柄与对象关联起来。我们需要这样做的地方是 Analog3dMeterWindowProc,即窗口过程。窗口过程的代码如下所示
//Retrieves the pointer set earlier in CreateHandle
CWnd* GetPointerFromCallContext()
{
IntPtr ip = *dynamic_cast(CallContext::GetData(S"Controls.CurrentControl"));
return (CWnd*)ip.ToPointer();
}
#pragma unmanaged
LRESULT CALLBACK Analog3dMeterWindowProc(HWND hwnd, UINT msg,
WPARAM wp, LPARAM lp)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CWnd* pWnd = CWnd::FromHandlePermanent(hwnd);
if (pWnd == NULL)
{
pWnd = GetPointerFromCallContext();
ASSERT(pWnd != NULL);
pWnd->Attach(hwnd);
}
LRESULT ret = AfxCallWndProc(pWnd, hwnd, msg, wp, lp);
if (msg == WM_NCDESTROY)
delete pWnd;
return ret;
}
在窗口过程中,我们首先检查窗口是否已在永久句柄映射中。如果不在(当控件收到 WM_NCCREATE
消息时会发生这种情况),则从调用上下文获取对象指针,我们最初在 CreateHandle
方法中设置了它,并将窗口句柄与其关联。一旦完成,我们就调用 AfxCallWndProc
来完成窗口消息处理的所有繁重工作。最后,在我们收到 WM_NCCDESTROY
消息时删除对象指针。
通过 spy++ 查看窗口会显示一些有趣的事情:
观察到类名是 WindowForms10.Analog3dMeter.xxx。这是因为 Windows Forms 框架超类化了我们的窗口类,并使用超类来创建我们的控件。
ThreeDMeter2 的构造函数中有一个小的实现细节,我在此处省略了。
ThreeDMeter2()
{
m_pCtrl = new C3DMeterCtrl();
SetStyle(ControlStyles::UserPaint, false);
}
SetStyle(ControlStyles::UserPaint, false)
使 WM_PAINT
消息能够转发到原始窗口过程 Analog3dMeterWindowProc,而不是在托管代码中处理它们。
ThreeDMeter2 类演示了 Windows Forms 如何超类化现有的窗口类。我们也可以在不创建超类窗口的情况下完成类似的事情。ThreeDMeter3 控件演示了这一点。
void DefWndProc(Message* m)
{
if (m_pCtrl)
{
if (!m_pCtrl->GetSafeHwnd())
{
m_pCtrl->Attach((HWND)m->HWnd.ToPointer());
}
m->Result = AfxCallWndProc(m_pCtrl, (HWND)m->HWnd.ToPointer(),
m->Msg, (WPARAM)m->WParam.ToPointer(),
(LPARAM)m->LParam.ToPointer());
}
else
//This woul happen after destroy messages
Control::DefWndProc(m);
}
void OnHandleDestroyed(EventArgs* e)
{
if (m_pCtrl)
{
m_pCtrl->Detach();
delete m_pCtrl;
m_pCtrl = NULL;
}
}
在 ThreeDMeter3 中,我们不创建任何单独的窗口类。我们不重载 CreateHandle
或 get_CreateParams
。我们让 Control 进行默认创建。相反,我们加载 DefWndProc
和 OnHandleDestroyed
。DefWndProc
是负责将调用转发到超类化窗口过程(如果控件超类化了任何窗口,或者转发到 DefWindowProc
函数)的函数。我们重载该函数,并通过 AfxCallWndProc
将调用转发到 m_pCtrl。最后,我们在 OnHandleDestroyed
函数中分离并删除 m_pCtrl
。尽管这种方法看起来更简单,但在我的看法中,它并不太干净,因为存在两条可能通往 DefWindowProc
的路径:一条通过 MFC 实现,另一条通过 System::Windows::Forms::Control
的实现。
因此,简而言之,我涵盖了一些方法,通过这些方法,现有的 MFC 控件可以在不完全重写它们的情况下迁移到 Windows Forms。
特别感谢 Mark C. Malburg 提供他的代码。特别感谢 Essam Ahmed 校对本文。
更新
3/13/2002
- 已修改字符串属性以使用 System::String* 的 CString 构造函数
- 已添加 ThreeDMeter3