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++ 程序进行消息通信,无需复杂的中间件或协议。