65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (6投票s)

2007年12月24日

CPOL

4分钟阅读

viewsIcon

68765

downloadIcon

2550

一种在 .NET Framework 和 MFC 之间实现进程间通信的简单方法。

Screenshot - DownloadSource

引言

我最近接到一个任务,我希望使用 Microsoft .NET Framework 中内置的一些功能,同时为一个现有的 MFC 文件查看器应用程序。更具体地说,我想利用 FileSystemWatcher 类在创建新文件时通知 MFC 查看器应用程序,以便它可以自动显示它。由于 MFC 无法直接访问此服务,我只有两个选择:

  1. 在非托管的 Visual C++ 中实现我自己的 FileSystemWatcher 类。
  2. 提供某种托管机制或 .NET 和 MFC 之间的链接。

第一个选项本可以成为一项有趣的功能,但它会花费我没有的时间。托管 MFC 查看器的想法有一个优点,即我可以在 .NET 中实现文件检测,并在托管机制内部调用 MFC 应用程序。Alexey Shalnov 撰写了一篇关于 MFC 托管的优秀文章,请参阅:从 WinForms 中托管 MFC MDI 应用程序。然而,此解决方案需要对 MFC 应用程序进行大量重写。

我的托管解决方案只需要 .NET 应用程序在新文件出现时将新程序的文件路径发送给 MFC 程序。因此,我需要在运行的应用程序之间建立一个简单的进程间通信机制。因此,我使用了 WM_COPYDATA 将文件名发送到 MFC 应用程序。当 MFC 应用程序收到 COPYDATA 消息时,它会获取文件路径并调用自己的显示函数。

.NET 应用程序

获取目标进程的句柄

Init 函数将 MFC 应用程序作为子进程启动,并保存传输数据所需的主窗口句柄。对我而言,这种方法比使用 Win32 函数 FindWindowEnumWindows 来“查找进程”的传统方法要简单得多。此外,它还可以保证目标应用程序正确实例的句柄。

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 函数之一来分配非托管内存块:AllocCoTaskMemAllocHGlobal。然后使用 System.Runtime.InteropServices 向 MFC 应用程序执行 SendMessage

SendMessageWithData 函数分配包含要发送到 MFC 程序的内存。要发送的字符串必须转换为 ASCII 格式。

内存分配和字符串转换可以通过以下两种方式之一完成:

  • 第一种方法使用 Marshal 函数 AllocCoTaskMemStringToCoTaskMemAnsi 来执行内存分配和字符串转换。
  • 第二种方法使用函数 AllocHGlobalStringToHGlobalAnsi 来完成相同的任务。

这两组函数之间的区别在于,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。
© . All rights reserved.