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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (22投票s)

2009年10月10日

CPOL

5分钟阅读

viewsIcon

97616

downloadIcon

2571

本文介绍了如何在 C++ 项目中集成 C# 窗口窗体

Sample Image - maximum width is 600 pixels

引言

本文介绍如何在 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 解决方案所做的更改列表:

  1. ClassInterface 设置为 ClassInterfaceType.None(这样,我们只向 COM 公开特定方法)。更多信息将在以下部分介绍。
  2. 项目应设置为**对 COM 可见**。可以通过以下方式完成:**项目属性 --> 应用程序选项卡 --> 程序集信息 --> 使程序集 COM 可见**

  3. 你还应该注册项目以进行**COM 互操作**(请注意,在 VS8 的发布版本中,生成属性窗口的设计与 Beta 版本不同)。启用此功能后,Visual Studio 将在成功编译后自动注册 .NET ActiveX 控件。

  4. 在 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 一起安装。

  1. 通过**编辑**菜单 -->**插入新控件**插入控件。

  2. 现在选择**控件**菜单 -->**调用方法**。
  3. 在“方法名称”组合框中选择 setButtonCaption
  4. 在“参数值”文本框中键入“Hello”,然后按“调用”按钮。

  5. 这是我们测试的结果:

将 C# ActiveX 控件添加到 C++ 窗口

使用 ATL 控件容器

任何窗口都可以使用 Active Template Library (ATL) 来承载 ActiveX 控件。
在本指南的这一部分,我们将:

  1. 创建 C++ Win32 应用程序项目
  2. 将我们的 ActiveX 控件插入 C++ 窗口
  3. 向 ActiveX 发送命令
  4. 接收来自我们 ActiveX 的消息

创建 C++ Win32 应用程序项目

  1. 创建新的 Win32 项目并命名为 CPP_Container

  2. 使用默认设置,然后按“确定”。

将 C# ActiveX 控件插入 C++ 窗口

  1. CPP_Container.cpp 的开头添加以下代码:
    #define DOT_NET_BUTTON_PRESSED  0x0400
    
    HWND                                            _hAtl;
    HWND                                            _hSelf;
    IUnknown*                                       _pUnk;
    DotNetActiveX::ICSSExplorerInterfacePtr         _pDotNetCOMPtr;
    
    HINSTANCE _hWebLib = ::LoadLibrary(TEXT("ATL.DLL")); 
  2. 当 Visual Studio 编译我们的 C# 项目时,它会创建 DotNetActiveX.tlb 文件。此文件包含项目中的所有 public 方法和结构。我们将通过以下命令导入这些数据:
    // import C# control function and structures
    #import "DotNetActiveX.tlb" named_guids raw_interfaces_only 
  3. 将以下函数添加到 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);
        }
    }  
  4. 为了精确编程,请在 WndProc 函数的 WM_DESTORY case 中添加以下代码:
    _pDotNetCOMPtr->Release();
    ::DestroyWindow(_hAtl);
    _pUnk->Release(); 
    ::FreeLibrary(_hWebLib); 
  5. 最后,在 _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 插件。本文中提到的所有想法都在我们的插件中得到了实现。

参考文献

  1. C# 编程指南 - COM 类示例(C# 编程指南)
  2. 如何在 Visual C++ 中为任何窗口添加 ATL 控件容器支持
  3. 将 Windows Forms 控件公开为 ActiveX 控件

历史

  • 2009 年 10 月 10 日:初始发布
© . All rights reserved.