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

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

$1.6k 字/约 11 min👁— views

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

在实际项目中,经常需要让 C# 程序与已有的 C++ 程序进行通信。基于 Windows 消息机制的进程间通信(IPC)是一种轻量、无需额外框架的方案。本文详细介绍如何用 C# 向 C++ 窗口发送消息和字符串数据。

跨语言 IPC 背景

Windows 进程间通信有多种方式:

方式 优点 缺点
Windows 消息(WM_COPYDATA) 简单,无额外依赖 仅限桌面应用,需要窗口
命名管道 双向,高吞吐 较复杂
共享内存 最快 需要同步机制
COM/DCOM 标准化,功能强 注册复杂,重量级
套接字(localhost) 跨语言通用 有网络开销
文件/注册表 简单 慢,需要轮询

对于简单的"C# 控制 C++ 程序"场景,FindWindow + WM_COPYDATA(或自定义消息)是最快的实现路径。

FindWindow/FindWindowEx 定位目标窗口

在发送消息前,需要获取目标窗口句柄:

using System;
using System.Runtime.InteropServices;

public class Win32Helper
{
    // P/Invoke 声明
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern IntPtr FindWindowEx(
        IntPtr hwndParent, 
        IntPtr hwndChildAfter, 
        string lpszClass, 
        string lpszWindow);
    
    [DllImport("user32.dll")]
    public static extern bool IsWindow(IntPtr hWnd);
}

// 使用
public class MessageSender
{
    public IntPtr FindTargetWindow()
    {
        // 按窗口标题查找
        IntPtr hwnd = Win32Helper.FindWindow(null, "My C++ App");
        
        if (hwnd == IntPtr.Zero)
        {
            int error = Marshal.GetLastWin32Error();
            Console.WriteLine($"FindWindow failed, error: {error}");
            return IntPtr.Zero;
        }
        
        // 验证窗口有效
        if (!Win32Helper.IsWindow(hwnd))
        {
            Console.WriteLine("Handle is no longer valid");
            return IntPtr.Zero;
        }
        
        return hwnd;
    }
    
    public IntPtr FindChildWindow(IntPtr hwndParent, string childClass)
    {
        // 查找子窗口(如特定控件)
        return Win32Helper.FindWindowEx(hwndParent, IntPtr.Zero, childClass, null);
    }
}

WM_COPYDATA 传递字符串

WM_COPYDATA 是 Windows 专门为跨进程传递数据设计的消息,系统会自动处理内存映射。

C# 发送端

using System;
using System.Runtime.InteropServices;
using System.Text;

[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
    public IntPtr dwData;    // 用户自定义的数据标识
    public int cbData;       // lpData 指向的数据大小(字节)
    public IntPtr lpData;    // 指向数据的指针
}

public class CopyDataSender
{
    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr SendMessage(
        IntPtr hWnd, 
        uint Msg, 
        IntPtr wParam, 
        ref COPYDATASTRUCT lParam);
    
    private const uint WM_COPYDATA = 0x004A;
    private const int DATA_TYPE_STRING = 1;  // 自定义的数据类型标识
    
    public bool SendString(IntPtr hwndTarget, IntPtr hwndSelf, string message)
    {
        if (hwndTarget == IntPtr.Zero || string.IsNullOrEmpty(message))
            return false;
        
        // 将 C# string 转为 UTF-16 字节(与 C++ wchar_t* 兼容)
        byte[] bytes = Encoding.Unicode.GetBytes(message + "\0");  // 包含 null 终止符
        
        // 在非托管内存中分配,避免 GC 移动
        IntPtr pData = Marshal.AllocHGlobal(bytes.Length);
        try
        {
            Marshal.Copy(bytes, 0, pData, bytes.Length);
            
            COPYDATASTRUCT cds = new COPYDATASTRUCT
            {
                dwData = new IntPtr(DATA_TYPE_STRING),
                cbData = bytes.Length,
                lpData = pData
            };
            
            IntPtr result = SendMessage(hwndTarget, WM_COPYDATA, hwndSelf, ref cds);
            return result != IntPtr.Zero;
        }
        finally
        {
            Marshal.FreeHGlobal(pData);  // 必须释放
        }
    }
}

C++ 接收端

#define WM_COPYDATA 0x004A

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_COPYDATA:
    {
        COPYDATASTRUCT* pCds = reinterpret_cast<COPYDATASTRUCT*>(lParam);
        
        if (pCds->dwData == 1)  // DATA_TYPE_STRING
        {
            // lpData 是 UTF-16 字符串(C# 的 Unicode 编码)
            const wchar_t* wstr = reinterpret_cast<const wchar_t*>(pCds->lpData);
            int charCount = pCds->cbData / sizeof(wchar_t) - 1;  // 减去 null 终止符
            
            std::wstring received(wstr, charCount);
            
            // 使用接收到的字符串
            MessageBoxW(hwnd, received.c_str(), L"Received", MB_OK);
        }
        
        return TRUE;  // 告诉发送方消息已处理
    }
    // ...
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

自定义 WM_APP 消息传递简单数值

对于简单的控制命令(不需要传字符串),使用 WM_APP 范围的自定义消息更简单:

// C# 发送端
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll", SetLastError = true)]
private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

// 自定义消息定义(需与 C++ 端一致)
private const uint WM_APP_CMD_PLAY  = 0x8000 + 1;  // WM_APP = 0x8000
private const uint WM_APP_CMD_PAUSE = 0x8000 + 2;
private const uint WM_APP_CMD_SEEK  = 0x8000 + 3;

public void SendCommand(IntPtr hwndTarget, int command, int param = 0)
{
    PostMessage(hwndTarget, WM_APP_CMD_PLAY + (uint)command, 
                new IntPtr(param), IntPtr.Zero);
}

// 求返回值时用 SendMessage
public int QueryStatus(IntPtr hwndTarget)
{
    IntPtr result = SendMessage(hwndTarget, WM_APP_CMD_SEEK, IntPtr.Zero, IntPtr.Zero);
    return result.ToInt32();
}
// C++ 接收端
#define WM_APP_CMD_PLAY  (WM_APP + 1)
#define WM_APP_CMD_PAUSE (WM_APP + 2)
#define WM_APP_CMD_SEEK  (WM_APP + 3)

case WM_APP_CMD_PLAY:
    StartPlayback();
    return 1;

case WM_APP_CMD_PAUSE:
    PausePlayback();
    return 1;

case WM_APP_CMD_SEEK:
    return GetCurrentPosition();  // 返回给 SendMessage 调用方

P/Invoke 声明完整参考

using System;
using System.Runtime.InteropServices;
using System.Text;

public static class User32
{
    // 基本消息发送
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, 
                                             IntPtr wParam, IntPtr lParam);
    
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool PostMessage(IntPtr hWnd, uint Msg, 
                                          IntPtr wParam, IntPtr lParam);
    
    // WM_COPYDATA 专用重载
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, 
                                             IntPtr wParam, ref COPYDATASTRUCT lParam);
    
    // 字符串版本(用于 WM_GETTEXT 等)
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int SendMessage(IntPtr hWnd, uint Msg, 
                                          int wParam, StringBuilder lParam);
    
    // 窗口查找
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter,
                                              string lpszClass, string lpszWindow);
    
    // 消息常量
    public const uint WM_COPYDATA  = 0x004A;
    public const uint WM_SETTEXT   = 0x000C;
    public const uint WM_GETTEXT   = 0x000D;
    public const uint WM_APP       = 0x8000;
}

字符编码对齐(UTF-16)

C# 的 string 和 C++ 的 std::wstring/wchar_t* 都使用 UTF-16LE,天然兼容。

// C# 端:string → UTF-16 字节
string msg = "Hello, 世界";
byte[] utf16Bytes = Encoding.Unicode.GetBytes(msg);  // Encoding.Unicode = UTF-16LE

// 验证:
// 'H' = 0x48, 0x00
// 'e' = 0x65, 0x00
// '世' = 0x16, 0x4E
// C++ 端:wchar_t* 就是 UTF-16LE
const wchar_t* wstr = reinterpret_cast<const wchar_t*>(pCds->lpData);
// 直接可用,无需转换

如果 C++ 端需要 std::string(UTF-8 或 ANSI),使用 WideCharToMultiByte 转换:

std::string WideToUtf8(const wchar_t* wstr) {
    int size = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
    std::string result(size - 1, 0);
    WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &result[0], size, nullptr, nullptr);
    return result;
}

错误处理

public bool SendStringSafe(IntPtr hwndTarget, IntPtr hwndSelf, string message)
{
    // 1. 验证目标窗口
    if (!Win32Helper.IsWindow(hwndTarget))
    {
        Console.Error.WriteLine("Target window is invalid");
        return false;
    }
    
    // 2. 验证输入
    if (string.IsNullOrEmpty(message))
        return false;
    
    try
    {
        byte[] bytes = Encoding.Unicode.GetBytes(message + "\0");
        IntPtr pData = Marshal.AllocHGlobal(bytes.Length);
        
        try
        {
            Marshal.Copy(bytes, 0, pData, bytes.Length);
            
            COPYDATASTRUCT cds = new COPYDATASTRUCT
            {
                dwData = new IntPtr(1),
                cbData = bytes.Length,
                lpData = pData
            };
            
            // SendMessage 会等待 C++ 处理完
            IntPtr result = SendMessageCopyData(hwndTarget, WM_COPYDATA, hwndSelf, ref cds);
            
            if (result == IntPtr.Zero)
            {
                int error = Marshal.GetLastWin32Error();
                Console.Error.WriteLine($"SendMessage failed, Win32 error: {error}");
                return false;
            }
            
            return true;
        }
        finally
        {
            Marshal.FreeHGlobal(pData);
        }
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine($"Exception: {ex.Message}");
        return false;
    }
}

完整示例:C# 控制 C++ 播放器

// C# 控制端
public class PlayerController : IDisposable
{
    private IntPtr _hwndPlayer = IntPtr.Zero;
    private IntPtr _hwndSelf;
    
    public const uint WM_APP_OPEN_FILE = 0x8001;
    public const uint WM_APP_PLAY     = 0x8002;
    public const uint WM_APP_PAUSE    = 0x8003;
    public const uint WM_APP_STOP     = 0x8004;
    
    public bool Connect(string windowTitle)
    {
        _hwndPlayer = FindWindow(null, windowTitle);
        return _hwndPlayer != IntPtr.Zero;
    }
    
    public bool OpenFile(string filePath)
    {
        if (_hwndPlayer == IntPtr.Zero) return false;
        return SendFilePath(_hwndPlayer, _hwndSelf, filePath);
    }
    
    public void Play()  => PostMessage(_hwndPlayer, WM_APP_PLAY, IntPtr.Zero, IntPtr.Zero);
    public void Pause() => PostMessage(_hwndPlayer, WM_APP_PAUSE, IntPtr.Zero, IntPtr.Zero);
    public void Stop()  => PostMessage(_hwndPlayer, WM_APP_STOP, IntPtr.Zero, IntPtr.Zero);
    
    // ... SendFilePath 实现使用 WM_COPYDATA
    
    public void Dispose()
    {
        _hwndPlayer = IntPtr.Zero;
    }
}

通过以上方案,C# 程序可以安全、可靠地与 C++ 程序进行消息通信,无需复杂的中间件或协议。