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

C# 視窗訊息機制實戰:自訂訊息、DLL 互通、WM_PAINT 繪圖與 DefWndProc 覆寫

     編輯:關於C語言

C# 雖以事件驅動為主,但透過 P/Invoke 呼叫 SendMessage/PostMessage、覆寫 DefWndProc,仍能完整使用 Windows 訊息機制。本文系統講解自訂訊息定義、跨 DLL 訊息傳遞、系統訊息處理 (WM_PAINT)、訊息迴圈攔截,並提供完整 WinForms 範例代碼。

核心概念:Windows 訊息基礎

  • 訊息結構:HWND (視窗句柄) + UINT Msg (訊息代碼) + WPARAM + LPARAM + LRESULT
  • 訊息分類:
    • 系統訊息:WM_PAINT (0x000F)、WM_CREATE、WM_DESTROY 等
    • 自訂訊息:WM_USER (0x0400) 以上,如 WM_USER + 101
    • 已註冊訊息:RegisterWindowMessage 保證全域唯一
  • 發送 vs 張貼:SendMessage 同步阻塞直到處理完畢;PostMessage 非同步投遞至佇列立即返回

自訂訊息定義與發送

訊息代碼定義


public static class WinMsg
{
    public const int WM_USER = 0x0400;
    public const int WM_TEST = WM_USER + 101; // 應用程式內部訊息
    public const int WM_DLL_NOTIFY = WM_USER + 102; // 來自 DLL 的訊息
}

P/Invoke 宣告:SendMessage


[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SendMessage(
    IntPtr hWnd,      // 目標視窗句柄
    uint Msg,         // 訊息代碼
    UIntPtr wParam,   // 參數 1
    IntPtr lParam);   // 參數 2

發送自訂訊息


// 按鈕點擊:發送 WM_TEST 給自己
private void btnSendTest_Click(object sender, EventArgs e)
{
    SendMessage(this.Handle, WinMsg.WM_TEST, new UIntPtr(100), new IntPtr(200));
}

// 按鈕點擊:觸發 DLL 發送訊息
[DllImport("MessageDLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void StartSendMessage(IntPtr hWnd);

private void btnTriggerDll_Click(object sender, EventArgs e)
{
    StartSendMessage(this.Handle); // DLL 內部會呼叫 SendMessage(hWnd, WM_DLL_NOTIFY, ...)
}

訊息接收:覆寫 DefWndProc


protected override void DefWndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WinMsg.WM_TEST:
            string msg1 = $"收到應用程式訊息:wParam={m.WParam}, lParam={m.LParam}";
            MessageBox.Show(msg1, "WM_TEST");
            break;

        case WinMsg.WM_DLL_NOTIFY:
            string msg2 = $"收到 DLL 訊息:wParam={m.WParam}, lParam={m.LParam}";
            MessageBox.Show(msg2, "WM_DLL_NOTIFY");
            break;

        default:
            base.DefWndProc(ref m); // 其餘訊息交給基類處理
            break;
    }
}

關鍵點:

  • Message 結構已封裝 HWnd/Msg/WParam/LParam/Result
  • 必須呼叫 base.DefWndProc 處理非自訂訊息,否則視窗基本功能 (移動、繪製、關閉) 會失效
  • WParam/LParam 為 IntPtr,需視語義轉換為 int/uint/long/IntPtr

系統訊息處理:WM_PAINT 自繪

覆寫 OnPaint 是 .NET 封裝後的推薦方式,等價於攔截 WM_PAINT:


protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e); // 重要:讓基類處理背景擦除等

    using (Font font = new Font("微軟正黑體", 28, FontStyle.Bold))
    using (SolidBrush shadowBrush = new SolidBrush(Color.FromArgb(160, 160, 176)))
    using (SolidBrush foreBrush = new SolidBrush(Color.Blue))
    {
        // 立體文字效果:陰影偏移 4 像素
        e.Graphics.DrawString("VC知識庫", font, shadowBrush, 65, 25);
        e.Graphics.DrawString("VC知識庫", font, foreBrush, 61, 21);
    }
}

等價的 DefWndProc 寫法 (不推薦,繁瑣):


case 0x000F: // WM_PAINT
    // 需手動 BeginPaint/EndPaint、取得 HDC、繪圖...
    break;

DLL 端實作要點 (C++/C# DLL)

C++ DLL (MessageDLL.cpp)


// MessageDLL.h
#ifdef MESSAGEDLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif

extern "C" DLL_API void StartSendMessage(HWND hWnd);

// MessageDLL.cpp
#include "MessageDLL.h"
#define WM_DLL_NOTIFY (WM_USER + 102)

void StartSendMessage(HWND hWnd)
{
    // 模擬工作...
    Sleep(1000);
    // 發送訊息回主程式
    SendMessage(hWnd, WM_DLL_NOTIFY, (WPARAM)42, (LPARAM)0x12345678);
}

C# DLL (純託管,需 UnmanagedCallersOnly 或 COM 互操作)

純 C# DLL 無法直接被 SendMessage 呼叫,需:

  • 方案 A:C++/CLI 混合模式 DLL 包裝
  • 方案 B:UnmanagedCallersOnly (C# 9+) 導出非託管入口點
  • 方案 C:改用 WM_COPYDATA 跨進程傳遞結構化資料

WM_COPYDATA:跨進程傳遞複雜資料


// 發送端
[StructLayout(LayoutKind.Sequential)]
struct COPYDATASTRUCT
{
    public IntPtr dwData;
    public int cbData;
    public IntPtr lpData;
}

[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref COPYDATASTRUCT lParam);

public void SendStruct(IntPtr targetHwnd, MyData data)
{
    byte[] bytes = Serialize(data); // JSON/Protobuf/BinaryFormatter
    GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
    try
    {
        var cds = new COPYDATASTRUCT
        {
            dwData = IntPtr.Zero,
            cbData = bytes.Length,
            lpData = handle.AddrOfPinnedObject()
        };
        SendMessage(targetHwnd, 0x004A, this.Handle, ref cds); // WM_COPYDATA = 0x004A
    }
    finally { handle.Free(); }
}

// 接收端 (DefWndProc)
case 0x004A: // WM_COPYDATA
    var cds = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam, typeof(COPYDATASTRUCT));
    byte[] bytes = new byte[cds.cbData];
    Marshal.Copy(cds.lpData, bytes, 0, cds.cbData);
    var data = Deserialize<MyData>(bytes);
    // 處理 data...
    break;

常見問題與排查

問題原因解決
自訂訊息不觸發訊息代碼衝突 / 未正確覆寫 DefWndProc使用 WM_USER+ 偏移,確保 base.DefWndProc 被呼叫
DLL 發送訊息主程式收不到DLL 載入上下文不同 / HWND 無效確保傳遞正確 HWND,DLL 同進程載入;跨進程用 WM_COPYDATA
WM_PAINT 重繪閃爍未雙緩衝 / OnPaint 邏輯過重SetStyle(OptimizedDoubleBuffer, true)、簡化繪圖
SendMessage 死結UI 執行緒互相 SendMessage 等待改用 PostMessage、或非同步模式
64 位元參數截斷wParam/lParam 定義為 int 而非 IntPtr全部使用 IntPtr/UIntPtr,轉換時注意平台位數

現代替代方案對比

方案適用場景優點缺點
Windows 訊息 (本文)與舊 C++ DLL 互操作、系統級攔截、全域熱鍵無需額外依賴、效能高、系統原生僅限 Windows、易出錯、需 P/Invoke
.NET 事件/委派純託管元件間通訊型別安全、語法簡潔、GC 友好無法跨進程/非託管邊界
Named Pipe / TCP / gRPC跨進程、跨機器、跨語言標準化、可序列化、支援雙向流延遲較高、需序列化協議
WM_COPYDATA同機器進程間小資料傳遞簡單、無需伺服器、系統保證同步資料量限制 (~64KB)、僅限 Windows

結語:C# 操作 Windows 訊息的核心在於正確定義訊息代碼、P/Invoke 簽名精確、DefWndProc 覆寫並呼叫基類、參數用 IntPtr 避免 64 位元截斷。掌握這套機制,能解決與舊系統 (C++/VB6/Delphi DLL) 整合、全域熱鍵註冊、視窗行為深度定製等棘手問題。現代純 .NET 架構優先用事件/通道/管道,但訊息機制永遠是打通非託管世界的鑰匙。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved