如何在 C++ 项目中集成 C# 窗口






4.83/5 (22投票s)
本文介绍了如何在 C++ 项目中集成 C# 窗口窗体

引言
本文介绍如何在 C++ 窗口中集成 C# 窗体(也称为 Windows 窗体)。我们的 C++ 窗口是使用纯 Win32 API 构建的,不需要 MFC。
背景
总的来说,C# 中的 Windows 编程比 C++ 容易得多,尤其是在我们不能在 C++ 中使用 MFC 库的情况下。因此,许多程序员更喜欢在 C# 环境中构建他们的项目。但有时有必要在 C++ 环境中进行编程。例如:当你想要为 C++ 应用程序开发一个插件时。我们在开发一个众所周知的 Notepad++ 应用程序的插件时遇到了这个问题。Notepad++ 是用 C++ 编写的,并使用纯 Win32 API 和 STL,这确保了更高的执行速度和更小的程序大小。但是,这也使得 UI 设计和开发变得困难。因此,我们接受挑战,决定在 C# 中构建我们的插件。我们是如何成功地将我们的 .NET 插件集成到用纯 Win32 API 构建的 Windows 中的?我希望本文能帮助你理解。
我们为本文构建了一个小型示例。如果你想查看我们的 Notepad++ 插件的完整源代码,请访问我们的 插件主页。
使用 C# 创建 ActiveX 控件
基础
该解决方案的这部分基于 Morgan Skinner 发布的文章(将 Windows 窗体控件公开为 ActiveX 控件)。虽然 Skinner 为 Visual Studio 8 的 Beta 版本提供了他的解决方案,但他的示例在 VS8 的发布版本中(稍作修改后)也能正常工作。以下是我们对 Skinner 解决方案所做的更改列表:
- 将
ClassInterface
设置为ClassInterfaceType.None
(这样,我们只向 COM 公开特定方法)。更多信息将在以下部分介绍。 - 项目应设置为**对 COM 可见**。可以通过以下方式完成:**项目属性 --> 应用程序选项卡 --> 程序集信息 --> 使程序集 COM 可见**
- 你还应该注册项目以进行**COM 互操作**(请注意,在 VS8 的发布版本中,生成属性窗口的设计与 Beta 版本不同)。启用此功能后,Visual Studio 将在成功编译后自动注册 .NET ActiveX 控件。
- 在 Skinner 的文章中,
ComUnregisterFunction()
函数有一个小错误。这是正确的函数:/// <summary> /// Unregister ActiveX DLL function /// </summary> /// <param name="i_Key"></param> [ComUnregisterFunction()] public static void UnregisterClass(string i_Key) { // strip off HKEY_CLASSES_ROOT\ from the passed key as I don't need it StringBuilder sb = new StringBuilder(i_Key); sb.Replace(@"HKEY_CLASSES_ROOT\", ""); // open HKCR\CLSID\{guid} for write access RegistryKey registerKey = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true); // delete the 'Control' key, // but don't throw an exception if it does not exist registerKey.DeleteSubKey("Control", false); // next open up InprocServer32 RegistryKey inprocServer32 = registerKey.OpenSubKey("InprocServer32", true); // and delete the CodeBase key, again not throwing if missing inprocServer32.DeleteSubKey("CodeBase", false); // finally close the main key registerKey.Close(); }
将特定方法公开给 COM
为了更精确的编程,我们只向 COM 公开特定方法。任何使用我们控件的外部应用程序将只获得对必要方法的访问权限。
公开特定方法的最佳方法是创建一个包含所有相关方法的接口。然后,应将特殊属性添加到此接口。窗体类应实现此接口。
/// <summary>
/// COM Interface - enables to run c# code from c++
/// </summary>
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICSSExplorerInterface
{
void setButtonCaption(String strNewCaption);
void setAdapterDllPtr(IntPtr i_AdapterDllPtr);
}
使用 Microsoft Message System
我们使用 Microsoft Message System 来与我们的容器窗口以及 VC++ 项目中的其他窗口进行通信。我们不处理事件,因为这更复杂,而且对于我们的解决方案来说不是必需的。
我们将此代码添加到我们的 MyDotNetActiveX
类中,以允许消息传输。
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
在初始化时,我们假设我们的容器窗口将向我们发送其句柄 (hwnd
) 以进行通信。
编译项目
现在我们可以编译并测试控件了。Visual Studio 会在成功编译后自动注册我们的 ActiveX 控件。你可以通过免费的 RegDllView 应用程序查看已注册的数据。
在 ActiveX 控件测试容器中测试控件
在进入文章的下一步之前,现在是时候在第三方应用程序中测试我们的控件了。测试时,我们使用 ActiveX 控件测试容器 (tstcon32.exe)。此应用程序随 Visual Studio 一起安装。
- 通过**编辑**菜单 -->**插入新控件**插入控件。
- 现在选择**控件**菜单 -->**调用方法**。
- 在“方法名称”组合框中选择
setButtonCaption
。 - 在“参数值”文本框中键入“Hello”,然后按“调用”按钮。
- 这是我们测试的结果:
将 C# ActiveX 控件添加到 C++ 窗口
使用 ATL 控件容器
任何窗口都可以使用 Active Template Library (ATL) 来承载 ActiveX 控件。
在本指南的这一部分,我们将:
- 创建 C++ Win32 应用程序项目
- 将我们的 ActiveX 控件插入 C++ 窗口
- 向 ActiveX 发送命令
- 接收来自我们 ActiveX 的消息
创建 C++ Win32 应用程序项目
- 创建新的 Win32 项目并命名为
CPP_Container
。 - 使用默认设置,然后按“确定”。
将 C# ActiveX 控件插入 C++ 窗口
- 在 CPP_Container.cpp 的开头添加以下代码:
#define DOT_NET_BUTTON_PRESSED 0x0400 HWND _hAtl; HWND _hSelf; IUnknown* _pUnk; DotNetActiveX::ICSSExplorerInterfacePtr _pDotNetCOMPtr; HINSTANCE _hWebLib = ::LoadLibrary(TEXT("ATL.DLL"));
- 当 Visual Studio 编译我们的 C# 项目时,它会创建 DotNetActiveX.tlb 文件。此文件包含项目中的所有
public
方法和结构。我们将通过以下命令导入这些数据:// import C# control function and structures #import "DotNetActiveX.tlb" named_guids raw_interfaces_only
- 将以下函数添加到 CPP_Container.cpp 文件。此函数将 ATL 容器插入窗口并加载我们的 C# ActiveX:
void loadActiveX(LPCTSTR strActiveXName) { //Initialize ATL control containment code. BOOL (WINAPI *m_AtlAxWinInit)(); m_AtlAxWinInit = (BOOL (WINAPI *)(void))::GetProcAddress (_hWebLib, "AtlAxWinInit"); m_AtlAxWinInit(); // Get the dimensions of the main window's client // area, and enumerate the child windows. Pass the // dimensions to the child windows during enumeration. RECT rcClient; GetClientRect(_hSelf, &rcClient); _hAtl = ::CreateWindowEx( WS_EX_CLIENTEDGE,\ TEXT("AtlAxWin"),\ strActiveXName,\ WS_CHILD | WS_VISIBLE | /*WS_CLIPCHILDREN | */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 { // Get the dimensions of the main window's client // area, and enumerate the child windows. Pass the // dimensions to the child windows during enumeration. 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); } }
- 为了精确编程,请在
WndProc
函数的WM_DESTORY
case 中添加以下代码:_pDotNetCOMPtr->Release(); ::DestroyWindow(_hAtl); _pUnk->Release(); ::FreeLibrary(_hWebLib);
- 最后,在
_tWinMain
函数中添加一个对loadActiveX
函数的调用。loadActiveX(TEXT("DotNetActiveX.MyDotNetActiveX"));
向 C# ActiveX 控件发送命令
插入 TLB 文件后,C++ 文件将了解我们在 C# 项目中公开的所有方法。现在,我们可以简单地调用相关方法:
char *strHelloWorld = "Hello World!";
_bstr_t bstrHelloWorld(strHelloWorld);
_pDotNetCOMPtr->setButtonCaption(bstrHelloWorld);
这将把按钮标题更改为“Hello World!”。
从 C# ActiveX 控件接收消息
来自 C# 控件的消息通过 Microsoft Message System 到达。我们已经将我们的窗口句柄发送给了 C# 控件(在 loadActiveX
函数中)。所以现在,我们只需要在 **WndProc** 函数中添加一些代码。WndProc
是处理到达窗口的每条消息的函数。因此,我们将简单地向该函数添加一个额外的 case
语句:
case DOT_NET_BUTTON_PRESSED:
MessageBox(NULL, TEXT("Message from C# arrived: Button Pressed!!"),
szTitle, MB_OK | MB_ICONINFORMATION);
break;
现在,你可以按 C# ActiveX 控件中的按钮并查看结果。
结论
我们希望本文能帮助其他人开发 C# 和 C++ 的组合项目。你可以查看我们的 Notepad++ 的 CSSExplorer 插件。本文中提到的所有想法都在我们的插件中得到了实现。
参考文献
历史
- 2009 年 10 月 10 日:初始发布