在开发中,我们有时需要让不同的框架程序(如 C# 的 WPF 和 C++ 的 Qt)进行实时数据交互。由于两者底层都运行在 Windows 系统上,利用 Win32 API 中的 WM_COPYDATA 消息是一种简单、高效且低延迟的解决方案。
本文将详细介绍如何通过 WM_COPYDATA 实现 WPF 与 Qt 的双向消息互传。
1. 核心原理:WM_COPYDATA
WM_COPYDATA 是 Windows 提供的一个用于进程间传递只读数据的消息。其核心数据结构如下:
csharp
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT {
public IntPtr dwData; // 自定义标识符
public int cbData; // 数据大小(字节)
public IntPtr lpData; // 指向数据的指针
}
2. WPF 端实现:接收与发送
WPF 默认封装了底层的 Win32 消息循环,因此我们需要通过 HwndSource 来挂载钩子(Hook)监听原始消息。
2.1 接收消息:挂载 WndProc 钩子
在 WPF 中,我们需要在窗口初始化完成后(OnSourceInitialized)获取窗口句柄并添加监听。
csharp
protected override void OnSourceInitialized(EventArgs e) {
base.OnSourceInitialized(e);
// 获取窗口句柄源并挂载钩子
HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
if (source != null) {
source.AddHook(WndProc);
}
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
if (msg == 0x004A) { // WM_COPYDATA
COPYDATASTRUCT cds = (COPYDATASTRUCT)Marshal.PtrToStructure(lParam, typeof(COPYDATASTRUCT));
// 将指针内容解析为字符串
string message = Marshal.PtrToStringAnsi(cds.lpData);
UpdateUI(message); // 处理业务逻辑
handled = true;
}
return IntPtr.Zero;
}
2.2 发送消息:查找窗口并发送
WPF 需要利用 FindWindow 定位 Qt 窗口的句柄。
csharp
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref COPYDATASTRUCT lParam);
public void SendToQt(string message) {
// 1. 根据窗口标题寻找 Qt 进程句柄
IntPtr targetHwnd = FindWindow(null, "Qt子程序");
if (targetHwnd == IntPtr.Zero) return;
// 2. 准备数据并分配内存
byte[] sBuffer = System.Text.Encoding.UTF8.GetBytes(message);
COPYDATASTRUCT cds;
cds.dwData = (IntPtr)1024;
cds.cbData = sBuffer.Length;
cds.lpData = Marshal.AllocHGlobal(sBuffer.Length);
Marshal.Copy(sBuffer, 0, cds.lpData, sBuffer.Length);
// 3. 发送消息
SendMessage(targetHwnd, 0x004A, IntPtr.Zero, ref cds);
// 4. 释放内存
Marshal.FreeHGlobal(cds.lpData);
}
3. Qt 端实现:接收与发送
Qt 通过重写 nativeEvent 函数可以非常方便地截获 Windows 原生消息。
3.1 接收消息:重写 nativeEvent
在 QMainWindow 或 QWidget 子类中实现:
cpp
bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, long* result) {
MSG* msg = static_cast<MSG*>(message);
if (msg->message == WM_COPYDATA) {
COPYDATASTRUCT* cds = reinterpret_cast<COPYDATASTRUCT*>(msg->lParam);
// 解析 UTF-8 编码的字节流
QString receivedMsg = QString::fromUtf8(static_cast<const char*>(cds->lpData), cds->cbData);
m_receivedMsgEdit->append("[From WPF]: " + receivedMsg);
*result = 1; // 标记已处理
return true;
}
return QMainWindow::nativeEvent(eventType, message, result);
}
3.2 发送消息:模糊匹配窗口标题
Qt 示例中使用 EnumWindows 回调函数来搜索 WPF 窗口,这种方式比 FindWindow 更灵活,支持模糊匹配。
cpp
// 查找包含特定标题的窗口
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
SearchData* data = (SearchData*)lParam;
wchar_t buffer[256];
GetWindowTextW(hwnd, buffer, 256);
QString title = QString::fromWCharArray(buffer);
if (title.contains(data->partTitle)) {
data->resultHandle = hwnd;
return FALSE; // 找到即停止遍历
}
return TRUE;
}
void MainWindow::sendMessageToWPF(const QString& message) {
SearchData sd;
sd.partTitle = "Qt进程通信";
EnumWindows(EnumWindowsProc, (LPARAM)&sd);
if (sd.resultHandle) {
QByteArray data = message.toUtf8();
COPYDATASTRUCT cds;
cds.dwData = 100;
cds.cbData = data.size() + 1;
cds.lpData = data.data();
SendMessage(sd.resultHandle, WM_COPYDATA, (WPARAM)this->winId(), (LPARAM)&cds);
}
}
4. 关键点总结与注意事项
编码统一性:
WPF 发送时使用了 UTF8.GetBytes。
Qt 接收时使用了 QString::fromUtf8。
警告:WPF 接收端代码中使用了 Marshal.PtrToStringAnsi,如果 Qt 发送的是中文字符,建议将 WPF 接收端也改为 UTF8 转换,避免乱码。
窗口标题: WM_COPYDATA 依赖句柄。如果窗口标题在运行时会改变,建议使用更稳定的查找方式(如类名查找)。
内存安全:
WM_COPYDATA 在 SendMessage 返回前,发送端的数据内存必须保持有效。
在 WPF 端,使用 Marshal.AllocHGlobal 申请的内存必须在使用完后通过 Marshal.FreeHGlobal 手动释放,防止内存泄漏。
同步性: SendMessage 是阻塞的。如果接收端处理逻辑非常耗时,会导致发送端 UI 界面卡死。建议接收端收到消息后通过异步方式(如 WPF 的 Dispatcher.BeginInvoke 或 Qt 的信号槽)进行后续处理。