首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 應用程式與 DLL 交互:自訂視窗訊息 (WM_COPYDATA) 完整實作

     編輯:關於C#

前言

在 C# 與非受控 DLL (C/C++) 交互時,透過 Windows 訊息機制 (SendMessage/PostMessage) 進行跨邊界通訊是經典且可靠的方式。本文示範自訂訊息定義、WM_COPYDATA 結構化資料傳遞、視窗程序重寫 (WndProc)、DLL 端發送訊息完整流程。

核心概念

  • 訊息識別碼:WM_USER (0x0400) 以上為自訂訊息範圍
  • SendMessage:同步阻塞,直到接收端處理完畢
  • PostMessage:非同步佇列,立即返回
  • WM_COPYDATA (0x004A):專門用於跨進程傳遞記憶體區塊,系統自動處理記憶體對映

C# 端:訊息定義與接收

2.1 P/Invoke 宣告


using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public static class Win32Api
{
    // 訊息常數
    public const int WM_USER = 0x0400;
    public const int WM_COPYDATA = 0x004A;
    public const int WM_APP = 0x8000;

    // 自訂訊息
    public const int WM_MY_CUSTOM_MSG = WM_USER + 100;
    public const int WM_DLL_NOTIFY = WM_USER + 101;

    // COPYDATASTRUCT 結構
    [StructLayout(LayoutKind.Sequential)]
    public struct COPYDATASTRUCT
    {
        public IntPtr dwData;   // 使用者定義資料
        public int cbData;      // 資料長度 (位元組)
        public IntPtr lpData;   // 資料指標
    }

    // SendMessage
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref COPYDATASTRUCT lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    // PostMessage
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    // 尋找視窗
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}

2.2 重寫 WndProc 接收訊息


public class MessageReceiverForm : Form
{
    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case Win32Api.WM_COPYDATA:
                HandleCopyData(ref m);
                break;

            case Win32Api.WM_MY_CUSTOM_MSG:
                HandleCustomMessage(m.WParam, m.LParam);
                break;

            case Win32Api.WM_DLL_NOTIFY:
                HandleDllNotification(m.WParam, m.LParam);
                break;

            default:
                base.WndProc(ref m); // 重要:未處理訊息傳給基底類別
                break;
        }
    }

    private void HandleCopyData(ref Message m)
    {
        var cds = Marshal.PtrToStructure(m.LParam);
        if (cds.cbData > 0 && cds.lpData != IntPtr.Zero)
        {
            // 依據 dwData 判斷資料類型
            string data = Marshal.PtrToStringAnsi(cds.lpData, cds.cbData);
            Console.WriteLine($"收到 WM_COPYDATA: Type={cds.dwData}, Data={data}");
            
            // 回應發送端 (透過 wParam 取得發送端視窗控制代碼)
            // m.Result = (IntPtr)1; // 表示處理成功
        }
    }

    private void HandleCustomMessage(IntPtr wParam, IntPtr lParam)
    {
        int param1 = wParam.ToInt32();
        int param2 = lParam.ToInt32();
        Console.WriteLine($"自訂訊息: Param1={param1}, Param2={param2}");
    }

    private void HandleDllNotification(IntPtr wParam, IntPtr lParam)
    {
        Console.WriteLine("收到 DLL 通知");
    }
}

2.3 發送訊息給 DLL (若 DLL 建立視窗)


public class DllCommunicator
{
    private IntPtr _dllWindowHandle;

    public void SetDllWindowHandle(IntPtr hwnd) => _dllWindowHandle = hwnd;

    public void SendStringToDll(string message)
    {
        if (_dllWindowHandle == IntPtr.Zero) return;

        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(message + "\0");
        GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
        try
        {
            var cds = new Win32Api.COPYDATASTRUCT
            {
                dwData = (IntPtr)1, // 自訂類型標識
                cbData = bytes.Length,
                lpData = handle.AddrOfPinnedObject()
            };
            Win32Api.SendMessage(_dllWindowHandle, Win32Api.WM_COPYDATA, this.Handle, ref cds);
        }
        finally
        {
            handle.Free();
        }
    }
}

C/C++ DLL 端:建立視窗與發送訊息

3.1 DLL 程式碼 (Visual C++)


// DllMain.cpp
#include 
#include 

HWND g_hTargetWnd = NULL; // C# 視窗控制代碼
HWND g_hMyWnd = NULL;     // DLL 自己的視窗

// 視窗程序
LRESULT CALLBACK DllWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_COPYDATA:
        {
            COPYDATASTRUCT* cds = (COPYDATASTRUCT*)lParam;
            char* data = (char*)cds->lpData;
            // 處理來自 C# 的資料
            OutputDebugStringA("DLL 收到資料: ");
            OutputDebugStringA(data);
            OutputDebugStringA("\n");
            return TRUE;
        }
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

// 建立隱藏視窗
extern "C" __declspec(dllexport) void InitializeDll(HWND targetWnd)
{
    g_hTargetWnd = targetWnd;
    
    WNDCLASS wc = {0};
    wc.lpfnWndProc = DllWndProc;
    wc.hInstance = GetModuleHandle(NULL);
    wc.lpszClassName = "MyDllWindowClass";
    RegisterClass(&wc);

    g_hMyWnd = CreateWindow("MyDllWindowClass", "DllHiddenWindow", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), NULL);
}

// 發送通知給 C#
extern "C" __declspec(dllexport) void NotifyHost(int eventId, const char* data)
{
    if (g_hTargetWnd == NULL) return;

    std::string str = data ? data : "";
    COPYDATASTRUCT cds = {0};
    cds.dwData = eventId;
    cds.cbData = (DWORD)str.length() + 1;
    cds.lpData = (PVOID)str.c_str();

    SendMessage(g_hTargetWnd, WM_COPYDATA, (WPARAM)g_hMyWnd, (LPARAM)&cds);
}

// 取得 DLL 視窗控制代碼供 C# 呼叫
extern "C" __declspec(dllexport) HWND GetDllWindowHandle()
{
    return g_hMyWnd;
}

3.2 C# 呼叫 DLL 匯出函數


public static class DllImports
{
    [DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void InitializeDll(IntPtr targetWnd);

    [DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void NotifyHost(int eventId, string data);

    [DllImport("MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr GetDllWindowHandle();
}

// 使用
public class DllHostForm : Form
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        DllImports.InitializeDll(this.Handle); // 傳遞 C# 視窗控制代碼給 DLL
        
        IntPtr dllWnd = DllImports.GetDllWindowHandle();
        _communicator.SetDllWindowHandle(dllWnd);
    }
}

WM_COPYDATA 完整資料傳遞範例 (結構體序列化)


// C# 定義結構 (需與 C++ 記憶體配置一致)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct SensorData
{
    public int SensorId;
    public float Temperature;
    public float Humidity;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string Location;
    public long Timestamp;
}

// 發送結構體
public void SendSensorData(SensorData data)
{
    int size = Marshal.SizeOf();
    IntPtr ptr = Marshal.AllocHGlobal(size);
    try
    {
        Marshal.StructureToPtr(data, ptr, false);
        var cds = new Win32Api.COPYDATASTRUCT
        {
            dwData = (IntPtr)100, // SensorData 類型
            cbData = size,
            lpData = ptr
        };
        Win32Api.SendMessage(_dllWindowHandle, Win32Api.WM_COPYDATA, this.Handle, ref cds);
    }
    finally
    {
        Marshal.FreeHGlobal(ptr);
    }
}

比較:訊息 vs 其他 IPC 方式

方式優點缺點適用場景
WM_COPYDATA簡單、系統管理記憶體、同步/非同步皆可需視窗控制代碼、資料大小限制 (~64KB)、單向C# 與 C++ DLL 簡單通訊、舊系統整合
Named Pipes雙向、流式、跨機器、大資料程式碼較複雜、需處理連線生命週期高效能、大量資料、服務間通訊
Shared Memory (MMF)極高效能、隨機存取需同步機制 (Mutex)、生命週期管理複雜即時資料共享、大型陣列/影像
COM Interop型別安全、標準化、支援事件註冊需求、版本控制、效能較低商業元件、Office 自動化
gRPC / HTTP跨平台、跨語言、契約優先需網路堆疊、序列化開銷微服務、現代架構

總結

Windows 訊息機制 (WM_COPYDATA) 是 C# 與非受控 DLL 交互的經典橋樑。關鍵實作要點:

  1. C# 端重寫 WndProc,處理 WM_COPYDATA 並正確解碼 COPYDATASTRUCT
  2. DLL 端建立 HWND_MESSAGE 視窗接收訊息,匯出函數供 C# 取得視窗控制代碼
  3. 資料封送:Marshal.StructureToPtr/PtrToStructure 確保記憶體配置一致 (Pack=1, CharSet.Ansi)
  4. 記憶體管理:發送端 AllocHGlobal/FreeHGlobal,接收端由系統自動管理
  5. 現代專案優先考慮 Named Pipes、gRPC 或共享記憶體,訊息機制作為相容舊系統的備選
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved