WPF通過(guò)?WM_COPYDATA?實(shí)現(xiàn)與Qt的進(jìn)程間通信
在開發(fā)中,我們有時(shí)需要讓不同的框架程序(如 C# 的 WPF 和 C++ 的 Qt)進(jìn)行實(shí)時(shí)數(shù)據(jù)交互。由于兩者底層都運(yùn)行在 Windows 系統(tǒng)上,利用 Win32 API 中的 WM_COPYDATA 消息是一種簡(jiǎn)單、高效且低延遲的解決方案。
本文將詳細(xì)介紹如何通過(guò) WM_COPYDATA 實(shí)現(xiàn) WPF 與 Qt 的雙向消息互傳。
1. 核心原理:WM_COPYDATA
WM_COPYDATA 是 Windows 提供的一個(gè)用于進(jìn)程間傳遞只讀數(shù)據(jù)的消息。其核心數(shù)據(jù)結(jié)構(gòu)如下:
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT {
public IntPtr dwData; // 自定義標(biāo)識(shí)符
public int cbData; // 數(shù)據(jù)大?。ㄗ止?jié))
public IntPtr lpData; // 指向數(shù)據(jù)的指針
}
2. WPF 端實(shí)現(xiàn):接收與發(fā)送
WPF 默認(rèn)封裝了底層的 Win32 消息循環(huán),因此我們需要通過(guò) HwndSource 來(lái)掛載鉤子(Hook)監(jiān)聽原始消息。
2.1 接收消息:掛載 WndProc 鉤子
在 WPF 中,我們需要在窗口初始化完成后(OnSourceInitialized)獲取窗口句柄并添加監(jiān)聽。
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));
// 將指針內(nèi)容解析為字符串
string message = Marshal.PtrToStringAnsi(cds.lpData);
UpdateUI(message); // 處理業(yè)務(wù)邏輯
handled = true;
}
return IntPtr.Zero;
}
2.2 發(fā)送消息:查找窗口并發(fā)送
WPF 需要利用 FindWindow 定位 Qt 窗口的句柄。
[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. 根據(jù)窗口標(biāo)題尋找 Qt 進(jìn)程句柄
IntPtr targetHwnd = FindWindow(null, "Qt子程序");
if (targetHwnd == IntPtr.Zero) return;
// 2. 準(zhǔn)備數(shù)據(jù)并分配內(nèi)存
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. 發(fā)送消息
SendMessage(targetHwnd, 0x004A, IntPtr.Zero, ref cds);
// 4. 釋放內(nèi)存
Marshal.FreeHGlobal(cds.lpData);
}
3. Qt 端實(shí)現(xiàn):接收與發(fā)送
Qt 通過(guò)重寫 nativeEvent 函數(shù)可以非常方便地截獲 Windows 原生消息。
3.1 接收消息:重寫 nativeEvent
在 QMainWindow 或 QWidget 子類中實(shí)現(xiàn):
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 編碼的字節(jié)流
QString receivedMsg = QString::fromUtf8(static_cast<const char*>(cds->lpData), cds->cbData);
m_receivedMsgEdit->append("[From WPF]: " + receivedMsg);
*result = 1; // 標(biāo)記已處理
return true;
}
return QMainWindow::nativeEvent(eventType, message, result);
}3.2 發(fā)送消息:模糊匹配窗口標(biāo)題
Qt 示例中使用 EnumWindows 回調(diào)函數(shù)來(lái)搜索 WPF 窗口,這種方式比 FindWindow 更靈活,支持模糊匹配。
// 查找包含特定標(biāo)題的窗口
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進(jìn)程通信";
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. 關(guān)鍵點(diǎn)總結(jié)與注意事項(xiàng)
編碼統(tǒng)一性:
- WPF 發(fā)送時(shí)使用了 UTF8.GetBytes。
- Qt 接收時(shí)使用了 QString::fromUtf8。
警告:WPF 接收端代碼中使用了 Marshal.PtrToStringAnsi,如果 Qt 發(fā)送的是中文字符,建議將 WPF 接收端也改為 UTF8 轉(zhuǎn)換,避免亂碼。
窗口標(biāo)題: WM_COPYDATA 依賴句柄。如果窗口標(biāo)題在運(yùn)行時(shí)會(huì)改變,建議使用更穩(wěn)定的查找方式(如類名查找)。
內(nèi)存安全:
WM_COPYDATA 在 SendMessage 返回前,發(fā)送端的數(shù)據(jù)內(nèi)存必須保持有效。
在 WPF 端,使用 Marshal.AllocHGlobal 申請(qǐng)的內(nèi)存必須在使用完后通過(guò) Marshal.FreeHGlobal 手動(dòng)釋放,防止內(nèi)存泄漏。
同步性: SendMessage 是阻塞的。如果接收端處理邏輯非常耗時(shí),會(huì)導(dǎo)致發(fā)送端 UI 界面卡死。建議接收端收到消息后通過(guò)異步方式(如 WPF 的 Dispatcher.BeginInvoke 或 Qt 的信號(hào)槽)進(jìn)行后續(xù)處理。
5.知識(shí)拓展
在跨技術(shù)棧的桌面應(yīng)用開發(fā)中,常常需要讓 WPF(.NET)與 Qt(C++)兩個(gè)獨(dú)立進(jìn)程協(xié)同工作,比如 WPF 負(fù)責(zé)主界面和業(yè)務(wù)邏輯,Qt 負(fù)責(zé)高性能圖形渲染或視頻處理。此時(shí)就需要一種高效、穩(wěn)定的進(jìn)程間通信(IPC)機(jī)制。
下面我會(huì)分析幾種常見(jiàn)的 IPC 方式,對(duì)比它們的優(yōu)缺點(diǎn),并給出 WPF 和 Qt 兩端的具體實(shí)現(xiàn)示例。
主流 IPC 方式對(duì)比
| IPC 方式 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| 命名管道 (Named Pipe) | Windows 原生,高性能,支持雙向通信,流式傳輸 | 僅限 Windows(Qt 的 QLocalSocket 在 Windows 上底層就是命名管道),跨平臺(tái)需額外適配 | Windows 平臺(tái)首選,WPF 和 Qt 支持均好 |
| TCP Socket (loopback) | 完全跨平臺(tái),代碼通用,支持網(wǎng)絡(luò)遠(yuǎn)程 | 需處理粘包、連接狀態(tài)等,性能略低于命名管道 | 需要跨平臺(tái)(如 Qt 運(yùn)行在 Linux/macOS)或已有網(wǎng)絡(luò)編程經(jīng)驗(yàn) |
| 共享內(nèi)存 (Shared Memory) | 吞吐量最大,延遲最低,適合傳輸大量數(shù)據(jù)(如圖像幀) | 需要手動(dòng)同步(互斥鎖、信號(hào)量),數(shù)據(jù)格式復(fù)雜 | 實(shí)時(shí)視頻流、大數(shù)據(jù)塊傳輸 |
| 消息隊(duì)列 (MSMQ / RabbitMQ) | 解耦、持久化、支持廣播 | 重量級(jí),引入中間件,延遲較高 | 異步任務(wù)、高可靠場(chǎng)景 |
| COM/DCOM | Windows 深度集成,可遠(yuǎn)程調(diào)用對(duì)象 | 學(xué)習(xí)曲線陡峭,配置復(fù)雜,易出問(wèn)題 | 老舊系統(tǒng)集成,不推薦新項(xiàng)目 |
推薦組合:
- 一般控制指令、小數(shù)據(jù)量 → 命名管道(簡(jiǎn)單可靠)
- 大量數(shù)據(jù)(如實(shí)時(shí)視頻) → 共享內(nèi)存 + 命名管道(同步信號(hào))
- 需要跨平臺(tái) → TCP Socket
1.命名管道實(shí)現(xiàn)(Windows 推薦)
WPF 端(C#, .NET 8)
使用 System.IO.Pipes.NamedPipeServerStream。
using System.IO.Pipes;
using System.Text;
using System.Threading.Tasks;
public class PipeServer
{
private NamedPipeServerStream _server;
public async Task StartAsync()
{
_server = new NamedPipeServerStream("MyPipe", PipeDirection.InOut, 1, PipeTransmissionMode.Message);
Console.WriteLine("等待 Qt 客戶端連接...");
await _server.WaitForConnectionAsync();
// 接收消息(異步)
byte[] buffer = new byte[4096];
int bytesRead = await _server.ReadAsync(buffer, 0, buffer.Length);
string msg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到: {msg}");
// 回復(fù)
string reply = "Hello from WPF";
byte[] replyData = Encoding.UTF8.GetBytes(reply);
await _server.WriteAsync(replyData, 0, replyData.Length);
await _server.FlushAsync();
_server.Disconnect();
}
}Qt 端(C++,使用 QLocalSocket)
#include <QLocalSocket>
#include <QCoreApplication>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QLocalSocket socket;
socket.connectToServer("MyPipe");
if (!socket.waitForConnected(3000)) {
qDebug() << "連接失敗:" << socket.errorString();
return -1;
}
// 發(fā)送數(shù)據(jù)
QByteArray sendData = "Hello from Qt";
socket.write(sendData);
socket.waitForBytesWritten();
// 接收回復(fù)
socket.waitForReadyRead();
QByteArray recvData = socket.readAll();
qDebug() << "收到:" << recvData;
socket.disconnectFromServer();
return 0;
}注意:QLocalSocket 在 Windows 上底層使用的就是命名管道,因此管道名不需要前綴(不要用 \\.\pipe\,直接寫 MyPipe 即可)。
2.TCP Socket 實(shí)現(xiàn)(跨平臺(tái))
WPF 端(C#)
using System.Net;
using System.Net.Sockets;
using System.Text;
public class TcpServer
{
private TcpListener _listener;
public async Task StartAsync()
{
_listener = new TcpListener(IPAddress.Loopback, 12345);
_listener.Start();
using var client = await _listener.AcceptTcpClientAsync();
using var stream = client.GetStream();
// 接收
byte[] buffer = new byte[4096];
int len = await stream.ReadAsync(buffer, 0, buffer.Length);
string msg = Encoding.UTF8.GetString(buffer, 0, len);
Console.WriteLine($"收到: {msg}");
// 發(fā)送
string reply = "Hello from WPF";
byte[] replyData = Encoding.UTF8.GetBytes(reply);
await stream.WriteAsync(replyData, 0, replyData.Length);
}
}Qt 端(C++,使用 QTcpSocket)
#include <QTcpSocket>
#include <QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QTcpSocket socket;
socket.connectToHost(QHostAddress::LocalHost, 12345);
if (!socket.waitForConnected(3000)) {
qDebug() << "連接失敗";
return -1;
}
// 發(fā)送
socket.write("Hello from Qt");
socket.waitForBytesWritten();
// 接收
socket.waitForReadyRead();
QByteArray data = socket.readAll();
qDebug() << "收到:" << data;
socket.disconnectFromHost();
return 0;
}到此這篇關(guān)于WPF通過(guò) WM_COPYDATA 實(shí)現(xiàn)與Qt的進(jìn)程間通信的文章就介紹到這了,更多相關(guān)WPF與Qt進(jìn)程間通信內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#的靜態(tài)工廠方法與構(gòu)造函數(shù)相比有哪些優(yōu)缺點(diǎn)
這篇文章主要介紹了C#的靜態(tài)工廠方法與構(gòu)造函數(shù)對(duì)比的優(yōu)缺點(diǎn),文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07
C#中無(wú)邊框窗體移動(dòng)的簡(jiǎn)單實(shí)例
拖動(dòng)無(wú)邊框窗體Form至桌面任何位置,有需要的朋友可以參考一下2013-08-08
WindowsForm移動(dòng)一個(gè)沒(méi)有標(biāo)題欄的窗口的方法
這篇文章主要介紹了WindowsForm移動(dòng)一個(gè)沒(méi)有標(biāo)題欄的窗口的方法,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07
C#中加鹽(salting)的實(shí)現(xiàn)示例
在密碼存儲(chǔ)和驗(yàn)證中,加鹽是一種增加密碼安全性的技術(shù),本文主要介紹了C#中加鹽(salting)的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
詳解C#中TcpClient對(duì)象的方法實(shí)現(xiàn)
TcpClient 類是用于基于 TCP 協(xié)議進(jìn)行客戶端通信的重要類,它封裝了連接、數(shù)據(jù)傳輸、異常處理等常見(jiàn)操作,提供了易用的 API,下面就來(lái)詳細(xì)的介紹一下如何使用2026-05-05
C#調(diào)用百度地圖API根據(jù)地名獲取經(jīng)緯度geocoding
本文主要介紹了C#調(diào)用百度地圖API根據(jù)地名獲取經(jīng)緯度geocoding,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04

