使用 WM_COPYDATA 在 .NET 和 MFC 之间进行进程间通信






4.58/5 (6投票s)
一种在 .NET Framework 和 MFC 之间实现进程间通信的简单方法。
引言
我最近接到一个任务,我希望使用 Microsoft .NET Framework 中内置的一些功能,同时为一个现有的 MFC 文件查看器应用程序。更具体地说,我想利用 FileSystemWatcher
类在创建新文件时通知 MFC 查看器应用程序,以便它可以自动显示它。由于 MFC 无法直接访问此服务,我只有两个选择:
- 在非托管的 Visual C++ 中实现我自己的
FileSystemWatcher
类。 - 提供某种托管机制或 .NET 和 MFC 之间的链接。
第一个选项本可以成为一项有趣的功能,但它会花费我没有的时间。托管 MFC 查看器的想法有一个优点,即我可以在 .NET 中实现文件检测,并在托管机制内部调用 MFC 应用程序。Alexey Shalnov 撰写了一篇关于 MFC 托管的优秀文章,请参阅:从 WinForms 中托管 MFC MDI 应用程序。然而,此解决方案需要对 MFC 应用程序进行大量重写。
我的托管解决方案只需要 .NET 应用程序在新文件出现时将新程序的文件路径发送给 MFC 程序。因此,我需要在运行的应用程序之间建立一个简单的进程间通信机制。因此,我使用了 WM_COPYDATA
将文件名发送到 MFC 应用程序。当 MFC 应用程序收到 COPYDATA
消息时,它会获取文件路径并调用自己的显示函数。
.NET 应用程序
获取目标进程的句柄
Init
函数将 MFC 应用程序作为子进程启动,并保存传输数据所需的主窗口句柄。对我而言,这种方法比使用 Win32 函数 FindWindow
和 EnumWindows
来“查找进程”的传统方法要简单得多。此外,它还可以保证目标应用程序正确实例的句柄。
private void Init(string processName, string cmdParams)
{
// start the MFC application as a process in the .NET app
Process mfcApp = new Process();
mfcApp.StartInfo.FileName = processName;
mfcApp.StartInfo.Arguments = cmdParams;
// run the MFC app
mfcApp.Start();
// the line below is very important to ensure getting
// the main window handle of the MFC application
if (mfcApp.WaitForInputIdle(5000))
{
destMainHandle = mfcApp.MainWindowHandle;
}
}
为了利用 COPY_DATA
消息机制向另一个进程发送数据,必须使用以下 Win32 结构和函数:
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
[System.Runtime.InteropServices.DllImport("user32.dll",
CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hwnd, int msg,
IntPtr wparam, IntPtr lparam);
const int WM_COPYDATA = 0x4A;
将数据发送到目标进程
下面的函数将数据从 .NET 应用程序发送到 MFC 应用程序。由于 MFC 程序不是为 Unicode 兼容而构建的,因此在发送字符串之前必须将其转换为 ASCII 格式。AStringToCoTaskMemAnsi
将 .NET 程序中的 Unicode 字符串转换为 MFC 所需的 ASCII 格式。
接下来要做的就是为 COPYDATASTRUCT
结构分配内存。由于正在将数据发送到非托管世界,可以使用以下两种 Marshal 函数之一来分配非托管内存块:AllocCoTaskMem
或 AllocHGlobal
。然后使用 System.Runtime.InteropServices
向 MFC 应用程序执行 SendMessage
。
SendMessageWithData
函数分配包含要发送到 MFC 程序的内存。要发送的字符串必须转换为 ASCII 格式。
内存分配和字符串转换可以通过以下两种方式之一完成:
- 第一种方法使用 Marshal 函数
AllocCoTaskMem
和StringToCoTaskMemAnsi
来执行内存分配和字符串转换。 - 第二种方法使用函数
AllocHGlobal
和StringToHGlobalAnsi
来完成相同的任务。
这两组函数之间的区别在于,AllocCoTaskMem
本质上是 COM 函数 CoTaskMem
的包装器,并使用 COM Task 内存分配器分配内存。CoTaskMemAlloc
调用 COM API IMalloc::Alloc
。根据 Microsoft 的说法,此函数分配内存的方式与 C 函数 Alloc
基本相同,而 AllocHGlobal
则暴露了 Win32 LocalAlloc
(与 Win32 中的 GlobalAlloc
相同)API,该 API 位于 kernel32.dll 中,用于内存分配。
public static void SendMessageWithData(IntPtr destHandle,
string str, IntPtr srcHandle)
{
COPYDATASTRUCT cds;
cds.dwData = srcHandle;
str = str + '\0';
cds.cbData = str.Length + 1;
cds.lpData = Marshal.AllocCoTaskMem(str.Length);
cds.lpData = Marshal.StringToCoTaskMemAnsi(str);
IntPtr iPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf( cds));
Marshal.StructureToPtr(cds,iPtr, true);
// send to the MFC app
SendMessage(destHandle, WM_COPYDATA, IntPtr.Zero, iPtr);
// Don't forget to free the allocated memory
Marshal.FreeCoTaskMem(cds.lpData);
Marshal.FreeCoTaskMem(iPtr);
}
public static void SendMessageWithDataUsingHGlobal(IntPtr destHandle,
string str, IntPtr srcHandle)
{
COPYDATASTRUCT cds;
cds.dwData = srcHandle;
str = str + '\0';
cds.cbData = str.Length + 1;
cds.lpData = Marshal.AllocHGlobal(str.Length);
cds.lpData = Marshal.StringToHGlobalAnsi(str);
IntPtr iPtr = Marshal.AllocHGlobal(Marshal.SizeOf(cds));
Marshal.StructureToPtr(cds, iPtr, true);
// send to the MFC app
SendMessage(destHandle, WM_COPYDATA, IntPtr.Zero, iPtr);
// Don't forget to free the allocated memory
Marshal.FreeHGlobal(cds.lpData);
Marshal.FreeHGlobal(iPtr);
}
MFC 应用程序
在 MFC 方面,必须在 MainFrame
类中创建一个 OnCopyData
消息处理程序。(请注意,C# 程序中 SendMessage
调用中使用的目标句柄是 MFC 程序主框架窗口的 MainWindow
句柄。)
然后可以轻松地从 COPYDATASRUCT
块中转换从托管 .NET 世界发送过来的文件路径字符串,并供 MFC 应用程序使用。MFC 程序在收到 C# 应用程序的消息后,会向调用者发送一个确认。
见下文
// CMainFrame message handlers
BOOL CMainFrame::OnCopyData(CWnd* pWnd, COPYDATASTRUCT *pCopyDataStruct)
{
char szbuf[256];
::ZeroMemory(szbuf,256);
// get the file path from the CopyData structure
strncpy(szbuf,(char *)pCopyDataStruct->lpData,pCopyDataStruct->cbData);
CString filePath = CString(szbuf);
// import the file into the MFC application
CMFCTestProgramDoc* pDocument =
(CMFCTestProgramDoc *) GetActiveView()->GetDocument();
pDocument->FileImportAutomatic(filePath);
// send ack message back to the caller
::SendMessage((HWND)pCopyDataStruct->dwData,WM_COPYDATA,0,0);
return TRUE;
}
接收确认
为了接收 MFC 应用程序的确认消息,必须重写 C# 应用程序的 WndProc
处理程序。下面的代码显示了如何从消息变量中解码 COPYDATASTRUCT
结构,以及如何将 ASCII 字符串转换为 UNICODE 格式。
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_COPYDATA:
if (m.Msg == WM_COPYDATA)
{
COPYDATASTRUCT cds = new COPYDATASTRUCT();
cds = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam,
typeof(COPYDATASTRUCT));
if (cds.cbData > 0)
{
byte[] data = new byte[cds.cbData];
Marshal.Copy(cds.lpData, data, 0, cds.cbData);
Encoding unicodeStr = Encoding.ASCII;
char[] myString = unicodeStr.GetChars(data);
string returnText = new string(myString);
MessageBox.Show("ACK Received: " + returnText);
m.Result = (IntPtr)1;
}
}
break;
}
base.WndProc(ref m);
}
关注点
有关使用 WM_COPY
进行进程间通信主题的进一步研究,请参阅以下链接:
历史
- 12 月 24 日,完成版本 1.0.0.0。