C# 通过 SendMessage 向 C++ 窗口发送消息与字符串

使用 P/Invoke 调用 user32.dll 的 SendMessage,从 C# 发送自定义 WM_USER 消息及字符串指针给 C++ 原生窗口,并在 C++ 侧正确接收和转换。

背景

在 Windows 桌面开发中,C# 应用(WPF/WinForms)有时需要与同机运行的 C++ 原生程序通信。SendMessage 是最直接的进程内/跨进程 IPC 方式之一,尤其适合:

  • C# 插件/前端 + C++ 核心引擎的混合架构
  • 托管代码调用无法用 COM 或命名管道改造的老旧 C++ 模块
  • 简单的命令/通知信号(不需要传大量数据)

P/Invoke 声明

using System;
using System.Runtime.InteropServices;

public class NativeMessaging
{
    // 导入 user32.dll 中的 SendMessage
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
    private static extern IntPtr SendMessage(
        IntPtr hWnd,   // 目标窗口句柄
        uint   Msg,    // 消息 ID
        IntPtr wParam, // 附加参数 1
        IntPtr lParam  // 附加参数 2
    );

    // WM_USER = 0x0400,自定义消息从此偏移
    private const uint WM_USER = 0x0400;
    private const uint WM_MY_COMMAND    = WM_USER + 1;
    private const uint WM_MY_STRING_MSG = WM_USER + 2;
}

发送简单数值消息

// 发送一个枚举命令(整数值放在 wParam 或 lParam)
public static void SendCommand(IntPtr hWnd, int commandCode)
{
    // wParam 传命令码,lParam 不用
    IntPtr result = SendMessage(hWnd, WM_MY_COMMAND,
                                new IntPtr(commandCode),
                                IntPtr.Zero);

    Console.WriteLine($"SendMessage returned: {result}");
}

发送字符串

字符串需要先 Marshal 到非托管内存,再将指针传给 C++:

public static void SendStringToNative(IntPtr hWnd, string text)
{
    // 将托管字符串编码为 ANSI(char*)
    IntPtr pStr = Marshal.StringToHGlobalAnsi(text);

    try
    {
        // 将字符串指针作为 wParam 传递
        // lParam 可传附加标记或保留为 0
        SendMessage(hWnd, WM_MY_STRING_MSG, pStr, IntPtr.Zero);
    }
    finally
    {
        // 必须释放!Marshal.StringToHGlobalAnsi 在非托管堆分配内存
        // SendMessage 是同步的,C++ 侧在此行返回前已处理完毕
        Marshal.FreeHGlobal(pStr);
    }
}

重要SendMessage同步的——它会等 C++ 的 WndProc 处理完消息后才返回。因此在 finally 中释放内存是安全的,C++ 已经完成了对字符串内容的读取。

C++ 接收方

// 对应的消息 ID
constexpr UINT WM_MY_COMMAND    = WM_USER + 1;
constexpr UINT WM_MY_STRING_MSG = WM_USER + 2;

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_MY_COMMAND:
    {
        // wParam 就是命令码(整数)
        int code = static_cast<int>(wParam);

        switch (code)
        {
        case 1: /* 处理命令 1 */ break;
        case 2: /* 处理命令 2 */ break;
        }
        return 0;
    }

    case WM_MY_STRING_MSG:
    {
        // wParam 是指向 ANSI 字符串的指针
        // 注意:该指针由 C# 分配,SendMessage 返回后会被释放
        // 所以必须在此函数内完成读取,不能保存指针!
        LPCSTR pStr = reinterpret_cast<LPCSTR>(wParam);

        // 立即拷贝到本地变量
        CString cstr(pStr);          // MFC CString(自动转宽字节)
        std::string stdStr(pStr);    // 或 std::string

        // 使用字符串
        AfxMessageBox(cstr);         // 仅示例,生产代码勿用阻塞 UI
        return 0;
    }
    }

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

跨进程注意事项

上述方案适用于同进程(例如 C++ DLL 被 C# 加载)或同机跨进程

跨进程时,wParam/lParam 中的指针无法直接跨进程访问(虚拟地址空间隔离)。解决方案:

方案适用场景
WM_COPYDATA跨进程传递数据的官方消息
共享内存大量数据,高频次
命名管道 / Socket通用 IPC,推荐现代方案

WM_COPYDATA 示例(跨进程字符串)

// C# 发送方
[StructLayout(LayoutKind.Sequential)]
private struct COPYDATASTRUCT
{
    public IntPtr dwData;
    public int    cbData;
    public IntPtr lpData;
}

public static void SendStringCrossProcess(IntPtr hWnd, string text)
{
    byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text + "\0");
    IntPtr pData = Marshal.AllocHGlobal(bytes.Length);
    Marshal.Copy(bytes, 0, pData, bytes.Length);

    var cds = new COPYDATASTRUCT
    {
        dwData = new IntPtr(1),   // 自定义类型标记
        cbData = bytes.Length,
        lpData = pData
    };

    IntPtr pCds = Marshal.AllocHGlobal(Marshal.SizeOf(cds));
    Marshal.StructureToPtr(cds, pCds, false);

    SendMessage(hWnd, 0x004A /* WM_COPYDATA */, IntPtr.Zero, pCds);

    Marshal.FreeHGlobal(pCds);
    Marshal.FreeHGlobal(pData);
}
// C++ 接收方
case WM_COPYDATA:
{
    auto* cds = reinterpret_cast<COPYDATASTRUCT*>(lParam);
    if (cds->dwData == 1)
    {
        // lpData 是系统复制的副本,直接使用,无生命周期问题
        std::string text(reinterpret_cast<char*>(cds->lpData),
                         cds->cbData - 1); // 去掉 null terminator
    }
    return TRUE;
}

总结

  • 同进程:SendMessage + Marshal.StringToHGlobalAnsi → C++ 侧直接 reinterpret_cast<LPCSTR> 读取
  • 必须在 finally 释放 Marshal 分配的内存(SendMessage 同步返回后已安全)
  • C++ 侧接收后立即拷贝字符串,不要保存原始指针
  • 跨进程场景改用 WM_COPYDATA,系统会自动复制内存区域