Monday, November 24, 2008

Making MFC controls available for WinForms through MFC subclassing

When trying to figure out how to use some of my MFC controls in a WinForms application, I came across this article by Rama Krishna Vavilala. As his article was targetting the .NET 1.1 framework, I decided to rework it for .NET 2.0. The main difference is the switch from using Managed Extension for C++ to using C++/CLI.

Static Win32 library for C3DMeterCtrl

As is the case in Rama's article, I will also use Mark C. Malburg's Analog Meter Control. So first, create a Win32 static library project with support for MFC and precompiled headers called "ControlS" (S stands for static). This will contain the MFC code for the existing 3DMeterCtrl control. Place the files 3DMeterCtrl.cpp, 3DMeterCtrl.h and MemDC.h in the "ControlS" project. Modify the 3DMeterCtrl.cpp file to remove the line #include "MeterTestForm.h".

The .NET designer and runtime will call functions that try to talk to your MFC control even before its window handle is created. In case of the C3DMeterCtrl, I needed to add this function call at the beginning of the "UpdateNeedle" and "ReconstructControl" functions:

if (!GetSafeHwnd())
  return;

MFC library for the managed 'ThreeDMeter' control

To bridge the gap between MFC and .NET I'm going to use C++/CLI. This allows me to create a managed wrapper object around the MFC control.

Add an "MFC DLL" project, called "control". Go to the project properties and enable the common language runtime support (/clr). Using the "Add Class" wizard add a new control and call it "ThreeDMeter". Make these changes to the ThreeDMeter.h file:

  • #include the header file of the MFC control "..\ControlS\3DMeterCtrl.h"
  • Change the inheritance of the control to public System::Windows::Forms::Control
  • Add a private instance of C3DMeterCtrl to the class. Create it in the constructor and delete it in the finalizer. In "OnHandleCreated", call its "SubclassWindow" method using the .NET controls window handle.
The file "ThreeDMeter.h" should now look like this:
#pragma once

using namespace System;
using namespace System::ComponentModel;
using namespace System::Collections;
using namespace System::Windows::Forms;
using namespace System::Data;
using namespace System::Drawing;

#include "..\ControlS\3DMeterCtrl.h"

namespace Control {

public ref class ThreeDMeter : public System::Windows::Forms::Control
{
public:
ThreeDMeter(void)
{
  InitializeComponent();
  m_pCtrl = new C3DMeterCtrl();
}

protected:
//Finalizer
!ThreeDMeter()
{
  if (m_pCtrl != NULL)
  {
    delete m_pCtrl;
    m_pCtrl = NULL;
  }
}

//Destructor
~ThreeDMeter()
{
  if (components)
  {
    delete components;
  }

  //call finalizer to release unmanaged resources.
  this->!ThreeDMeter();
}

virtual void OnHandleCreated(EventArgs^ e) override
{
  System::Diagnostics::Debug::Assert(m_pCtrl->GetSafeHwnd() == NULL);

  m_pCtrl->SubclassWindow((HWND)Handle.ToPointer());

  Control::OnHandleCreated(e);
}

private:
System::ComponentModel::Container ^components;
C3DMeterCtrl* m_pCtrl;
};
In order to expose the properties of the MFC control in .NET, you need to implement them yourself. Add following code in the public section of the ThreeDMeter wrapper class.
event EventHandler^ OnValueChanged;

[property: System::ComponentModel::CategoryAttribute("Meter")]
property Color NeedleColor
{
  Color get()
  {
    if( m_pCtrl == NULL )
      throw gcnew ObjectDisposedException(ThreeDMeter::GetType()->ToString());
  
    return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
  }
  
  void set(Color clr)
  {
    if (!m_pCtrl)
      throw gcnew ObjectDisposedException(ThreeDMeter::GetType()->ToString());
  
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
  
    m_pCtrl->SetNeedleColor(ColorTranslator::ToWin32(clr));
  }
}

[property: System::ComponentModel::CategoryAttribute("Meter")]
property String^ Units
{
  void set(String^ units)
  {
    if (!m_pCtrl)
      throw gcnew ObjectDisposedException(ThreeDMeter::GetType()->ToString());
  
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
  
    CString strUnits(units);
  
    m_pCtrl->SetUnits(strUnits);
  }
  
  String^ get()
  {
    if (!m_pCtrl)
      throw gcnew ObjectDisposedException(ThreeDMeter::GetType()->ToString());
  
    LPCTSTR szUnits = (m_pCtrl->m_strUnits);
  
    return gcnew String(szUnits);
  }
}

[property: System::ComponentModel::CategoryAttribute("Meter")]
property double Value
{
  double get()
  {
    if (!m_pCtrl)
      throw gcnew ObjectDisposedException(ThreeDMeter::GetType()->ToString());
  
    return m_pCtrl->m_dCurrentValue;
  }
  
  void set(double d)
  {
    if (!m_pCtrl)
      throw gcnew ObjectDisposedException(ThreeDMeter::GetType()->ToString());
  
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
  
    m_pCtrl->UpdateNeedle(d);
  
    OnValueChanged(this, EventArgs::Empty);
  }
}

.NET Test application

In order to test the managed ThreeDMeter control, add a .NET "Windows application" project to the solution in your favorite language. Put the Three3Meter control on the form and add a timer control
Public Class Form1
  Private Sub Timer1_Tick(ByVal sender As System.Object, 
                          ByVal e As system.EventArgs) Handles Timer1.Tick

    If Me.ThreeDMeter1.Value <= 4 Then
      Me.ThreeDMeter1.Value += (Me.Timer1.Interval / 1000)
    Else
      Me.ThreeDMeter1.Value = -5
    End If
  End Sub
End Class

This will make the meter move from left to right.

10 comments:

Sabeesh said...

Hello,
Thank you for your information. I try to create a control in MFC, just add a dialog box and add one text box to that dialog box and build it as a .dll file and create another file and do the steps like your blog. And I create another CLR project and give reference to the second program and can successfully build the application. But when I set the width and height of the control, the new control, in CLR project, then, the program crash at the running stage. How can i solve this problem, Please help me

Bart said...

Hi Shabeesh,
Can you please zip your code, put it somewhere on an internet sharing service (such as rapidshare or skydrive) and post the link here.
Regards
Bart

Sabeesh said...

Hello,

Thank you for your reply.

I upload the program into http://rapidshare.com/files/218819433/MainForm.zip.html Please unzip the file and open the program using MainForm file and set the project "LoadControl" as "Set As Startup Project".
Please help me to solve this problem.

Thank you for your valuable time.
Looking forward for your response.

Sincerely,
Sabeesh C.S.

Bart said...

Hi Sabeesh,
What immediately caught my eye is that your static MFC library "Controls" doesn't contain a working control. For example IDD_DUMMYVIEW doesn't have a corresponding resource (.rc file). Try making an MFC application first that can succesfully use your "Controls" libray and only if that works, try wrapping it in C++/CLI.

Success
Bart

Anonymous said...

Works like a charm!!! Thank you!!

developer906 said...

Hi Sabeesh

Can you please share source code of your sample. I want to export an MFC Dialog based application and facing an issue. When I load my Managed control (wrapping MFC Dialog) on C# winform based application the control shows nothing.

Thanks

Jason Zhang said...

It seems liklely that Sabeesh has ignored this thread. I have a similar need as developer906 did. Bart- do you happen to have anything new on wrapping a MFC dialog application with C++/CLI to share with us? Thanks.

Jason

Bart Jolling said...

Hi Jason, if you refer to the article on CodeProject that I took as a basis and then apply the modifications as described in my post above, you will have a working example. If that is what you are looking for?

Jason Zhang said...

Hi Bart- Thanks very much for your reply. I reproduced your 3DMeter control porting project in VS 2010 with some minor modifications on the code, and have made it work. However, that is not what I need. What I want to have is to make an existing MFC window, e.g. a dialog to be able to appear in a .NET form the same way it does in the native MFC environment. I want to use C++/CLI to bridge the cap between .NET and MFC. With your method, I haven't been able to make it work with the MFC dialog without adding any extra drawing functions such as OnPaint and OnSize to draw the window as in the 3DMeter control. If you could provide any suggestions on my requirement, it will be very helpful and appreciated. -Jason

MB said...

Hello
I'm trying to put on window form in C# object type CEdit (MFC)
,
once using a CEdit object instead C3DMeterCtrl,
once by creating additional CMyEdit class in which i use object
of type CEdit and then puts CMyEdit instead C3DMeterCtrl
.
The CMyEdit control is able to run with in MFC window,
I created it in a separate project
.

Generally C# project is able to compile it self,
but it did not displays in C#
form
any CEdit control.

In addition, the project starts until the moment of method CEdit -> Create( ...)
is used.



I'll be grateful for suggestions how to solve this problem.

ps I have used VS 2010.