65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (21投票s)

2002年3月8日

CPOL

10分钟阅读

viewsIcon

403143

downloadIcon

2604

演示将基于 MFC 的控件迁移到 .NET Windows Forms 的不同方法

引言

Windows Forms 是用于在 .NET 框架下开发富客户端 GUI 应用程序的框架。Windows Forms 中有许多很棒的功能,可以极大地简化开发。问题是,codeproject 上提供的所有很棒的控件都是用 MFC 编写的,无法直接在 Windows Forms 应用程序中使用。我知道至少有三种方法可以将现有控件迁移到 .NET:

  1. 完全用托管代码重写控件
  2. 将它们制作成 ActiveX 控件,并在 Windows Forms 上使用 ActiveX 控件
  3. 使用托管 C++。

本文的目的是演示上述技术的最后一种。

演示控件

为了演示这些技术,我选择了我在 codeproject 上最喜欢的控件之一,Mark C. Malburg 的 模拟仪表控件。目标是能够通过包装现有的 MFC 控件(对原始 MFC 代码进行很少或不进行更改)来开发 Windows Forms 控件。开发的最终 Windows Forms 控件可以像图像中所示那样放置在 Windows Forms 设计器上。

How the control looks like in the designer

该控件具有以下属性:

属性名称 属性类型 描述
单位 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 的类。这排除了将 CObjectCWnd 类作为基类使用的可能性,因为它们没有标记为 __gc。另一个限制是 __gc 类不能包含任何(实际上)其他类的成员。但是 __gc 类可以包含 MFC 类的指针。

为了开发 Windows Forms 控件,我们需要创建一个类,该类继承自托管类(标记为 __gcSystem::Windows::Forms::Control,就像 MFC 自定义控件继承自 CWnd 一样。在此托管对象中,我们将包含一个 MFC 类的实例。我们将使托管控件的属性和方法的实现委托给 MFC 类,并让 MFC 代码完成所有繁重的工作。问题在于,我们需要将相同的窗口句柄(所有控件都是窗口)关联到 MFC 对象和 System::Windows::Forms::Control 对象。这可以通过两种方式完成:

  1. 让 Windows Forms 控件创建其窗口,然后 MFC 对其进行子类化。
  2. 让 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 () ;
}

上面的代码演示了几个要点

  1. C++ 中属性的使用(有关更多详细信息,请参阅 Chris Maunder 关于托管 C++ 属性的 教程)。
  2. 使用 ColorTranslator 将 System::Drawing::Color 更改为 COLORREF
  3. 使用 System::String 的新 CString 构造函数将 System::String 类型更改为 CString。(感谢 Anson Tsao 指出这一点)
  4. 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 中,我们不创建任何单独的窗口类。我们不重载 CreateHandleget_CreateParams。我们让 Control 进行默认创建。相反,我们加载 DefWndProcOnHandleDestroyedDefWndProc 是负责将调用转发到超类化窗口过程(如果控件超类化了任何窗口,或者转发到 DefWindowProc 函数)的函数。我们重载该函数,并通过 AfxCallWndProc 将调用转发到 m_pCtrl。最后,我们在 OnHandleDestroyed 函数中分离并删除 m_pCtrl。尽管这种方法看起来更简单,但在我的看法中,它并不太干净,因为存在两条可能通往 DefWindowProc 的路径:一条通过 MFC 实现,另一条通过 System::Windows::Forms::Control 的实现。

因此,简而言之,我涵盖了一些方法,通过这些方法,现有的 MFC 控件可以在不完全重写它们的情况下迁移到 Windows Forms。

特别感谢 Mark C. Malburg 提供他的代码。特别感谢 Essam Ahmed 校对本文。

更新

3/13/2002

  1. 已修改字符串属性以使用 System::String* 的 CString 构造函数
  2. 已添加 ThreeDMeter3
© . All rights reserved.