Introduction
This article describes how to integrate C# Forms (also called Windows Forms) in C++ Window. Our C++ Window is built with pure Win32 API and doesn't require MFC.
Background
In general, Windows programming in C# is much easier than in C++, especially when we can't use MFC library in C++. Therefore many programmers prefer to build their projects in C# environment. But sometimes it is necessary to program in C++ environment. For example: when you want to develop an add-on to C++ application. We contend with this problem while developing a plug-in for, well known, Notepad++ application. Notepad++ is written in C++ and uses pure Win32 API and STL which ensures a higher execution speed and smaller program size. BUT, it also makes it difficult to design and develop UI. So, we took a challenge and decided to build our plug-in in C#. How we succeeded to integrate our .NET plug-in in Windows that builds with pure Win32 API? I hope that this article will help you understand.
We have built a small example for this article. If you prefer to view the whole source code of our plug-in for Notepad++, visit our plug-in homepage.
Creating ActiveX Control with C#
Basics
This part of the solution is based on the article that was posted by Morgan Skinner (Exposing Windows Forms Controls as ActiveX controls). Although Skinner provides his solution for Beta version of Visual Studio 8, his example works fine in the release version of VS8 (with small changes). Here is a list of changes we have made to Skinner's solution:
- Set
ClassInterface
to ClassInterfaceType.None
(In this way, we expose only specific methods to COM). More information in the following section.
- The project should be set as visible to COM. This can be done via: Project Properties --> Application Tab --> Assembly Information --> Make assembly COM visible
- You should also register the project for COM Interop (Pay attention that in release version of VS8, Build Properties Window has a different design that in the beta version). When this feature enabled, Visual Studio will automatically register .NET ActiveX control after successful compilation.
- In Skinner's article, there is a little mistake in the
ComUnregisterFunction()
function. This is the correct function:
[ComUnregisterFunction()]
public static void UnregisterClass(string i_Key)
{
StringBuilder sb = new StringBuilder(i_Key);
sb.Replace(@"HKEY_CLASSES_ROOT\", "");
RegistryKey registerKey =
Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);
registerKey.DeleteSubKey("Control", false);
RegistryKey inprocServer32 =
registerKey.OpenSubKey("InprocServer32", true);
inprocServer32.DeleteSubKey("CodeBase", false);
registerKey.Close();
}
Exposing Specific Methods to COM
For more accurate programming, we expose only specific methods to COM. Every external application that will use our control will receive access to necessary methods only.
The best way to expose specific methods is by creating an interface that contains all the relevant methods. Then, special attribute should be added to this interface. Form Class should implement this interface:
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICSSExplorerInterface
{
void setButtonCaption(String strNewCaption);
void setAdapterDllPtr(IntPtr i_AdapterDllPtr);
}
Using Microsoft Message System
We use Microsoft Message System to communicate with our container window, and with other windows in VC++ project. We don't deal with events, because it's more complicated and it isn't necessary for our solution.
We added this code to our MyDotNetActiveX
class to allow message transmit:
private static uint DOT_NET_BUTTON_PRESSED = 0x0400;
private void btnOK_Click(object sender, EventArgs e)
{
SendMessage(m_AdapterDllPtr.ToInt32(),
DOT_NET_BUTTON_PRESSED, IntPtr.Zero, IntPtr.Zero);
}
#region MAPPING_OF_USER32_DLL_SECTION
[DllImport("user32.dll", EntryPoint = "SendMessage")]
public static extern IntPtr SendMessage(
int hwnd, uint wMsg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", EntryPoint = "SendMessage")]
public static extern int SendMessage(
int hwnd, uint wMsg, int wParam, string lParam);
[DllImport("user32.dll", EntryPoint = "SendMessage")]
public static extern int SendMessage(
int hwnd, uint wMsg, int wParam, out int lParam);
[DllImport("user32.dll", EntryPoint = "SendMessage")]
public static extern int GetNbFiles(
int hwnd, uint wMsg, int wParam, int lParam);
[DllImport("user32.dll", EntryPoint = "SendMessage")]
public static extern int GetFileNames(
int hwnd, uint wMsg,
[MarshalAs(UnmanagedType.LPArray)]IntPtr[] wParam,
int lParam);
[DllImport("user32.dll", EntryPoint = "SendMessage")]
public static extern int SendMessage(
int hwnd, uint wMsg, int wParam, StringBuilder lParam);
#endregion
On initialization, we suppose that our container window will send us its handle (hwnd
) for communication.
Compiling the Project
Now we are ready to compile and test the control. Visual Studio automatically registers our ActiveX control after successful compilation. You can see registered data via freeware RegDllView application.
Testing the Control on ActiveX Control Test Container
Before we jump to the next step in the article, this is a good time to test our control in third-party applications. For the test, we use ActiveX Control Test Container (tstcon32.exe). This application is available with the installation of Visual Studio.
- Insert the Control via Edit menu -->Insert New Control
- Now choose Control menu -->Invoke Methods
- Choose setButtonCaption from Method Name combo-box
- Type "Hello" in Parameter Value text-box & press Invoke button
- Here is the result of our test:
Adding C# ActiveX Control to C++ Window
Using ATL Control Containment
Any window can host ActiveX control by using Active Template Library (ATL).
In this part of the guide, we will:
- Create C++ Win32 Application Project
- Insert our ActiveX control into C++ window
- Send commands to the ActiveX
- Receive message from our ActiveX
Creating C++ Win32 Application Project
- Create new Win32 Project and name it
CPP_Container
:
- Use default settings and press OK:
Inserting C# ActiveX Control into C++ Window
- Add the following code at the beginning of CPP_Container.cpp:
#define DOT_NET_BUTTON_PRESSED 0x0400
HWND _hAtl;
HWND _hSelf;
IUnknown* _pUnk;
DotNetActiveX::ICSSExplorerInterfacePtr _pDotNetCOMPtr;
HINSTANCE _hWebLib = ::LoadLibrary(TEXT("ATL.DLL"));
- When Visual Studio compiled our C# project, it created DotNetActiveX.tlb file. This file contains all
public
methods and structures in the project. We will import this data via the following command:
#import "DotNetActiveX.tlb" named_guids raw_interfaces_only
- Add the following function to CPP_Container.cpp file. This function inserts ATL container to the Window and loads our C# ActiveX:
void loadActiveX(LPCTSTR strActiveXName)
{
BOOL (WINAPI *m_AtlAxWinInit)();
m_AtlAxWinInit = (BOOL (WINAPI *)(void))::GetProcAddress
(_hWebLib, "AtlAxWinInit");
m_AtlAxWinInit();
RECT rcClient;
GetClientRect(_hSelf, &rcClient);
_hAtl = ::CreateWindowEx(
WS_EX_CLIENTEDGE,\
TEXT("AtlAxWin"),\
strActiveXName,\
WS_CHILD | WS_VISIBLE | WS_EX_RTLREADING,\
0, 0, rcClient.right, rcClient.bottom,\
_hSelf,\
NULL,\
NULL,\
NULL);
if (!_hAtl)
{
MessageBox( NULL, TEXT("Can not load AtlAxWin!"),
szTitle, MB_OK | MB_ICONSTOP);
throw int(106901);
}
HRESULT (WINAPI *m_AtlAxGetControl) (HWND h, IUnknown** pp);
m_AtlAxGetControl = (HRESULT (WINAPI *)
(HWND, IUnknown**))::GetProcAddress(_hWebLib, "AtlAxGetControl");
m_AtlAxGetControl(_hAtl, &_pUnk);
_pUnk->QueryInterface(__uuidof(DotNetActiveX::ICSSExplorerInterface),
(LPVOID *) &_pDotNetCOMPtr);
if (_pDotNetCOMPtr != NULL)
{
_pDotNetCOMPtr->setAdapterDllPtr((long) _hSelf);
}
else
{
RECT rcClient;
GetClientRect(_hSelf, &rcClient);
::DestroyWindow(_hAtl);
_hAtl = ::CreateWindowEx(
WS_EX_CLIENTEDGE,\
TEXT("AtlAxWin"),\
TEXT("MSHTML:""Please register ActiveX
control before using this plugin."""),\
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |
WS_EX_RTLREADING,\
0, 0, rcClient.right, rcClient.bottom,\
_hSelf,\
NULL,\
NULL,\
NULL);
}
}
- For accurate programming, add the following code in
WM_DESTORY
case in WndProc
function:
_pDotNetCOMPtr->Release();
::DestroyWindow(_hAtl);
_pUnk->Release();
::FreeLibrary(_hWebLib);
- Finally add a call to
loadActiveX
function in _tWinMain
function:
loadActiveX(TEXT("DotNetActiveX.MyDotNetActiveX"));
Sending Commands to C# ActiveX Control
After TLB file insertion, C++ file is familiar with all the methods that we exposed in C# project. Now, we can simply call the relevant method:
char *strHelloWorld = "Hello World!";
_bstr_t bstrHelloWorld(strHelloWorld);
_pDotNetCOMPtr->setButtonCaption(bstrHelloWorld);
This will change button caption to "Hello World!"
Receiving Messages from C# ActiveX Control
Messages from C# control arrive via Microsoft Message System. We already sent the handle of our Window to the C# control (in loadActiveX
function). So now, we only need to add some code in WndProc function. WndProc
is the function that takes care of every message that arrives to the Window. So we will simply add additional case
switch to this function:
case DOT_NET_BUTTON_PRESSED:
MessageBox(NULL, TEXT("Message from C# arrived: Button Pressed!!"),
szTitle, MB_OK | MB_ICONINFORMATION);
break;
Now, you can press the button in the C# ActiveX control and look at the results:
Conclusion
We hope that this article will help others to develop combined C# & C++ projects. You can check our CSSExplorer plug-in for Notepad++. All the ideas that are mentioned in this article are implemented in our plug-in.
References
- C# Programming Guide - Example COM Class (C# Programming Guide)
- How to add ATL control containment support to any window in Visual C++
- Exposing Windows Forms Controls as ActiveX controls
History
- 10th October, 2009: Initial post