Click here to Skip to main content
15,879,239 members
Articles / Desktop Programming / MFC
Article

Developing Windows Forms Control using MFC and Managed C++

Rate me:
Please Sign up or sign in to vote.
4.92/5 (21 votes)
12 Mar 2002CPOL10 min read 378.9K   2.6K   90   71
Demonstrates different ways to move MFC based controls to .NET Windows Forms

Introduction

Windows Forms is the framework for developing rich client GUI applications under the .NET framework. There are many cool features in Windows Forms which greatly simplify development. The problem is that all the cool controls available at codeproject are written in MFC and cannot be used on Windows Forms Application directly. I know of at least three different ways by which existing controls can be migrated to .NET :-

  1. Rewriting the controls completely in managed code
  2. Making them ActiveX controls and using the ActiveX control on windows forms
  3. Using managed C++.

The purpose of this article is to demonstrate the last of the above techniques.

Demo Control

In order to demonstrate these techniques I select one of my favorite controls in codeproject Mark C. Malburg's Analog Meter Control. The aim is to be able to develop a windows forms control by wrapping the existing MFC control (with little or no changes to the original MFC code).  The final Windows Forms control developed can be placed on windows forms designer as shown in the image.

How the control looks like in the designer

The control has following properties :-

Property NameProperty TypeDescription
UnitsSystem::StringThe text of units shown in the meter
ValuedoubleThe value which determines the needle position and also shown at the bottom of the control
NeedleColorSystem::Drawing::ColorThe color of the needle

In addition the control supports a managed event called  OnValueChanged which is fired whenever the Value property is changed.

To start with we need to create a managed C++ class library with MFC support. I wrote a wizard that does it. The wizard is downloadable from http://www.codeproject.com/useritems/ManagedMFCDLL.asp. Download and install the wizard and also download the source for Analog Meter Control.

Using Managed C++ With MFC

Our aim is to retain as much existing MFC code as possible because all this code has been tested and runs well (assuming). VC++.NET allows to mix managed (a code that is compiled to IL) and unmanaged (native) code. It can even compile existing code to IL. But you cannot use the classes so compiled in other .NET languages unless they are marked with <code>__gc prefix. e.g <code>__gc class MyControl . A class that is marked __gc cannot derive from any class not marked __gc. This rules out use of CObject, CWnd classes as base class as thet are not marked __gc. The other restriction is that __gc classes cannot contain members of any (practically) other classes. But a __gc class can contain a pointer to MFC class. 

In order to develop a windows forms control we need to create a class that extends the managed class (marked with __gc) System::Windows::Forms::Control class just like an MFC custom control extends CWnd.  Within this managed object we will contain an instance of the MFC class. We will make the implementation of properties and methods of the managed control delegate to the MFC class and let the MFC code do all the hard work. The problem is that we need to associate the same window handle (all controls are windows) to both the MFC object and the System::Windows::Forms::Control object. This can be done in two ways :-

  1. Let the windows form control create its window an MFC subclass it.
  2. Let the windows form control superclass the window used by the MFC control.

Both the methods are covered in the article.

Subclassing Windows Forms control through MFC

We start with creating a blank VS.NET solution with any name.

Image 2

Add a Managed MFC DLL project to the solution using the wizard named Control. This project will be used for any managed classes.

Image 3

Next add to the solution a Win32 static library project with support  for MFC and precompiled headers. Call this ControlS (S stands for static). This will contain the MFC code for the control. Separation of managed and unmanaged code makes maintenance a bit easier. We will  make the project "Control" dependent on "ControlS" to link them together.

Place the files 3DMeterCtrl.cpp, 3DMeterCtrl.h and MemDC.h in the "ControlS" project. Modify the 3DMeterCtrl.cpp file to remove #include "MeterTestForm.h" as shown

MC++
#include "stdafx.h"
#include "math.h"
#include "3DMeterCtrl.h"
<FONT color=#3333ff>//#include "MeterTestForm.h" This line is to be removed</FONT>
#include "MemDC.h"

After this the project ControlS would build successfully.

We need to write a managed wrapper for the control. We call this class ThreeDMeter and it should derive from System::Windows::Forms::Control. In order to do this we need to import the required assemblies add required #using's to stdafx.h as shown. Any code that needs to be added is shown in blue

MC++
#include <afxwin.h>         // MFC core and standard components
<FONT color=#3333ff>#include "..\Controls\3DMeterCtrl.h"</FONT>
#using <mscorlib.dll>
<FONT color=#6633ff>#using <system.drawing.dll>
#using <system.dll>
#using <system.design.dll>
#using <system.windows.forms.dll></FONT>

Now we can create the main control class. Replace the default class generated by the wizard with class ThreeDMeter.

MC++
// 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;
    };
}        

Compile and build the DLL. At this time we have got a windows forms control that can be used in a C# or a VB application. To test the control, launch another instance of VS.NET and create a VB or a VC# Windows Application. Add the ThreeDMeter control to the toolbox (Click here if you want to know how to do it). Double clicking on the ThreeDMeter toolbox item adds the control to the form as shown

Image 4

If you get any errors try modifying the copy local property of the reference to Control assembly to false as shown below (I am not sure why this works, I would be glad if anyone can explain me why setting Copy Local to false makes fixes the problem)

Image 5

So we have succeeded in wrapping an MFC control around a managed class and using it in the windows forms designer. How exactly does it work?

A windows forms control is a window with style of WS_CHILD by default. When a control is added to the form the window handle is created. When this happens System::Windows::Forms::Control 's protected method OnHandleCreated is called. We overload this method and subclass it with the C3DMeterCtrl object we create, using SubclassWindow method. If you are not familiar with subclassing refer to Chris Maunder's tutorial on Subclassing.

Even though our control paints successfully and can be used in the designer there is not much user can do to modify the behavior of the control. (like changing the needle color or changing the text of units etc.) In the next step we would add properties in the control that would allow us to do this.

Lets add code add the properties using managed extensions to C++ keyword __property. After adding the properties the code looks like this

MC++
__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);
}

These properties can be called at any time even when the handle is not created. So we need to make sure that none of the MFC methods that make use of Windows handle get called before the window is created this would lead MFC to fire assertions. We fix this in the MFC code in 3DMeterCtrl.cpp as shown

MC++
void C3DMeterCtrl::ReconstructControl() 
{
<font color="#0000ff">    if (!GetSafeHwnd())
        return;
</font>
    // 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 () ;
}

The above code demostrates a few points

  1. Using properties in C++ (refer to Chris Maunders tutorial on managed C++ properties for more details).
  2. Changing System::Drawing::Color to COLORREF using ColorTranslator
  3. Changing System::String type to CString using the new CString constructor for System::String. (Thanks to Anson Tsao for pointing that to me)
  4. Use of AFX_MANAGE_STATE to ensure proper MFC state. Those of you who developed COM objects using MFC and ATL must be familiar with this.

What we have actually done is to delegate the calls C3DMeterCtrl class. After you build the project it would be possible to set or change these properties from the designer and instantly see the change in the rendering of the control on the VB/VC# form.

It would be really cool if the properties can be organized in the property grid. This can be done simply by using managed C++ attributes e.g.

MC++
[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);
}

In the above example we have applied CategoryAttribute to the property NeedleColor. The effect of this attribute can be seen in the property grid after you build the project with these changes.

The thing which is missing from the Control is that it doesn't fire any events. An example event could be OnValueChanged fired when the value changes. The following declaration indicates that the control supports OnValueChanged event.

MC++
__event EventHandler * OnValueChanged; 

In order to fire the event the set_Value method can be changed as following

MC++
__property void set_Value(double d)
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
            
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
        
    m_pCtrl->UpdateNeedle(d);

    <FONT color=#6633ff>OnValueChanged(this, EventArgs::Empty);</FONT>
}

Thus we have a completely functional control based on an MFC control. The problem with this implementation is that MFC control gets a first shot at the events and not the managed control. This would be a major problem if the control needs to be enhanced later on in managed code for example by deriving another control written C# from this control. This brings us to the next technique - allowing windows forms to superclass our MFC control so that windows forms control gets a first shot at the messages.

Allowing Windows Forms control to superclass existing MFC controls

The System::Windows::Forms::Control class can use an existing Window Class to create its window. This can be done by overloading

get_CreateParams
method and specifying a new class name. The only restriction is that Window class should be registered with CS_GLOBALCLASS. So we register a new window class in InitInstance and unregister it in ExitInstance as shown

MC++
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();
}        

Now we can overload get_CreateParams method. For demonstration purposes we create a totally new class called ThreeDMeter2 with much of the code same as

ThreeDMeter
except for the protected methods.

MC++
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;
    }

Specifying class name as Analog3dMeter makes the window forms control use this window class to create the window for the control. Now the problem is how do we associate m_pCtrl with the window handle so created. This is done at two places. First the CreateHandle method of a control which is actually responsible for creating the window.

MC++
void CreateHandle()
{
    __try
    {
        CallContext::SetData(S"Controls.CurrentControl", 
                             __box(IntPtr(m_pCtrl)));
        
        Control::CreateHandle();
    }
    __finally
    {
        CallContext::SetData(S"Controls.CurrentControl", NULL);
    }
}    

CallContext
is an equivalent (kind of) of ThreadLocal storage here. In the above code we have set a named property for the thread called "Controls.CurrentControl" to the pointer value of  m_pCtrl. Observe __box operator which is used to convert a value type to Object* which is the type of
SetData
's second parameter. After setting this property we call base classes implementation which actually calls CreateWindowEx and finally we clear the thread property.

Now the question is how to associate the window handle with the object. The place we need to do is in the Analog3dMeterWindowProc which is the window procedure. The code of the window procedure looks like this

MC++
//Retrieves the pointer set earlier in CreateHandle
CWnd* GetPointerFromCallContext()
{
    IntPtr ip = *dynamic_cast<IntPtr*>(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;
}

In the window procedure we first check to see if the window is already in the permanent handle map. If not (which is the case when the control gets the

WM_NCCREATE
message), obtain the object pointer from call context, which we set originally in the CreateHandle method, and attach the window handle to it. Once we have done that we call AfxCallWndProc to do all the hard work of window message handling. Finally, we delete the object pointer when we get
WM_NCCDESTROY
message.

Looking at the window through spy++ shows certain interesting things :-

Image 6

Observe that the name of the class in WindowForms10.Analog3dMeter.xxx This is because windows forms framework superclasses our window class and uses the superclass to create our control.

There is one small implementation detail which I in the constructor of the ThreeDMeter2

MC++
ThreeDMeter2()
{
    m_pCtrl = new C3DMeterCtrl();
    SetStyle(ControlStyles::UserPaint, false);
}

The SetStyle(ControlStyles::UserPaint, false) makes it possible for the WM_PAINT messages to be forwarded to the original window proc Analog3dMeterWindowProc instead of them being handled in the managed code.

The class ThreeDMeter2 demonstrates that way windows forms superclasses an existing window class. We could have managed to do the something without creating a superclassed window. The control ThreeDMeter3 demonstrates this.

MC++
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;
    }
}

In ThreeDMeter3 we don't create any separate window class. We don't overload

CreateHandle
or get_CreateParams. We let Control do it's default creation. Instead we load DefWndProc and OnHandleDestroyed. DefWndProc is the function responsible for forwarding the call to the superclassed window procedure (if the control superclassed any window or else to the DefWindowProc function). We overload that and forward the calls to m_pCtrl using AfxCallWndProc. Finally we detach and delete m_pCtrl in OnHandleDestroyed function. Even though this method looks simpler this is not very clean (in my opinion) as there are two potential routes to DefWindowProc one through MFC implementation and other through System::Windows::Forms::Control 's implementation.

Thus, in short I have covered certain ways by which existing MFC controls can be moved to Windows Forms without completely rewriting them.

My special thanks to Mark C. Malburg for making his code available. My special thanks to Essam Ahmed for proof reading the article.

Updates

3/13/2002

  1. Modified string properties to use CString constructor for System::String*
  2. Added ThreeDMeter3

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralAnother problem Pin
konstc15-May-02 17:17
konstc15-May-02 17:17 
GeneralRe: Another problem Pin
Rama Krishna Vavilala15-May-02 18:19
Rama Krishna Vavilala15-May-02 18:19 
GeneralUsing Grid Control within .NET: Problem with AfxGetInstanceHandle() Pin
23-Apr-02 22:21
suss23-Apr-02 22:21 
GeneralRe: Using Grid Control within .NET: Problem with AfxGetInstanceHandle() Pin
Rama Krishna Vavilala24-Apr-02 11:20
Rama Krishna Vavilala24-Apr-02 11:20 
Generalcannot compile test app in visual studio.net beta 2 Pin
19-Mar-02 11:38
suss19-Mar-02 11:38 
GeneralRe: cannot compile test app in visual studio.net beta 2 Pin
20-Mar-02 15:00
suss20-Mar-02 15:00 
GeneralCan't download sources Pin
13-Mar-02 3:37
suss13-Mar-02 3:37 
GeneralRe: Can't download sources Pin
Rama Krishna Vavilala13-Mar-02 3:48
Rama Krishna Vavilala13-Mar-02 3:48 
GeneralRe: Can't download sources Pin
17-Mar-02 22:08
suss17-Mar-02 22:08 
GeneralCString has String* as a constructor parameter Pin
Anson Tsao8-Mar-02 7:26
Anson Tsao8-Mar-02 7:26 
GeneralRe: CString has String* as a constructor parameter Pin
Rama Krishna Vavilala8-Mar-02 11:03
Rama Krishna Vavilala8-Mar-02 11:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.