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

C# WinForms 訊息處理機制深度解析:WM_PAINT、自訂訊息、DefWndProc

     編輯:關於C語言

Windows 訊息機制基礎

WinForms 本質是對 Win32 API 的封裝。視窗透過訊息迴圈接收系統/使用者事件 (WM_PAINT, WM_MOUSEMOVE, WM_KEYDOWN, WM_TIMER, 自訂訊息等)。C# 提供兩種處理層級:

  1. 高層事件模型:Paint, Click, KeyDown 等事件 (推薦、類型安全)
  2. 低層訊息覆寫:覆寫 WndProc / DefWndProc / OnXxx 虛擬方法 (攔截非事件化訊息、效能敏感、互操作)

覆寫 OnPaint (WM_PAINT) 繪製自訂內容


public class CustomPanel : Panel
{
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e); // 重要:呼叫基類繪製背景/邊框/子控制項
        
        Graphics g = e.Graphics;
        g.SmoothingMode = SmoothingMode.AntiAlias;
        
        // 1. 繪製漸層背景
        using (var brush = new LinearGradientBrush(
            ClientRectangle, Color.Blue, Color.LightBlue, LinearGradientMode.Vertical))
        {
            g.FillRectangle(brush, ClientRectangle);
        }
        
        // 2. 繪製立體文字
        using (var font = new Font("微軟正黑體", 24, FontStyle.Bold))
        using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0)))
        using (var foreBrush = new SolidBrush(Color.White))
        {
            string text = "Hello WinForms";
            SizeF sz = g.MeasureString(text, font);
            float x = (Width - sz.Width) / 2;
            float y = (Height - sz.Height) / 2;
            
            g.DrawString(text, font, shadowBrush, x + 2, y + 2); // 陰影
            g.DrawString(text, font, foreBrush, x, y);           // 主文字
        }
        
        // 3. 繪製幾何圖形
        using (var pen = new Pen(Color.Yellow, 3))
        {
            g.DrawEllipse(pen, 10, 10, Width - 20, Height - 20);
        }
    }
    
    // 強制重繪 (觸發 WM_PAINT)
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        Invalidate(); // 標記需重繪
    }
}

關鍵點

  • PaintEventArgs.Graphics:雙緩衝繪圖表面,無需手動建立/釋放
  • base.OnPaint(e):確保控制項預設繪製邏輯執行 (背景、邊框、子控制項)
  • Invalidate():非同步請求重繪,Update():同步強制立即重繪
  • SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true):啟用雙緩衝防閃爍

自訂訊息定義與發送

1. 定義訊息常數 (WM_USER = 0x0400 起)


public static class CustomMessages
{
    public const int WM_USER = 0x0400;
    public const int WM_MY_CUSTOM = WM_USER + 1;   // 0x0401
    public const int WM_DATA_READY = WM_USER + 2;  // 0x0402
    public const int WM_START_TASK = WM_USER + 3;  // 0x0403
}

2. 發送訊息 (P/Invoke SendMessage/PostMessage)


using System.Runtime.InteropServices;

public static class NativeMethods
{
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
}

// 使用範例:按鈕點擊發送自訂訊息給主視窗
private void btnSend_Click(object sender, EventArgs e)
{
    // SendMessage:同步,等待處理完成返回
    // wParam, lParam 可傳遞 32/64 位元整數 (指標、ID、狀態碼)
    IntPtr result = NativeMethods.SendMessage(
        this.Handle, 
        CustomMessages.WM_MY_CUSTOM, 
        new IntPtr(100),   // wParam
        new IntPtr(200)    // lParam
    );
    
    // PostMessage:非同步,放入訊息佇列立即返回 (跨執行緒安全)
    // NativeMethods.PostMessage(this.Handle, CustomMessages.WM_DATA_READY, IntPtr.Zero, IntPtr.Zero);
}

3. 覆寄 DefWndProc 處理自訂訊息


public class MainForm : Form
{
    protected override void DefWndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case CustomMessages.WM_MY_CUSTOM:
                // m.WParam, m.LParam 對應 SendMessage 的 wParam, lParam
                int param1 = m.WParam.ToInt32();
                int param2 = m.LParam.ToInt32();
                
                string msg = $"收到自訂訊息 WM_MY_CUSTOM\n參數: wParam={param1}, lParam={param2}";
                MessageBox.Show(msg, "訊息通知", MessageBoxButtons.OK, MessageBoxIcon.Information);
                break;
                
            case CustomMessages.WM_DATA_READY:
                // 處理資料就緒通知
                RefreshDataGrid();
                break;
                
            default:
                // 關鍵:未處理的訊息必須傳給基類,否則標準行為失效 (如關閉視窗、鍵盤導航)
                base.DefWndProc(ref m);
                break;
        }
    }
}

WndProc vs DefWndProc vs OnXxx 比較

方法層級用途呼叫時機是否需呼叫 base
WndProc最低攔截所有訊息,完全控制訊息迴圈分派時是 (除非完全攔截)
DefWndProc中處理未被 OnXxx 處理的訊息WndProc 內部預設分派是 (預設行為)
OnPaint 等高特定訊息的標準化封裝DefWndProc 針對 WM_PAINT 呼叫建議 (繼承鏈)

完整 WndProc 覆寫範例 (熱鍵攔截)


public class HotkeyForm : Form
{
    private const int WM_HOTKEY = 0x0312;
    private const int HOTKEY_ID = 1;

    [DllImport("user32.dll")]
    private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
    [DllImport("user32.dll")]
    private static extern bool UnregisterHotKey(IntPtr hWnd, int id);

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        // 註冊 Ctrl+Shift+F12 為全域熱鍵
        RegisterHotKey(Handle, HOTKEY_ID, 0x0003 | 0x0004, (uint)Keys.F12); // MOD_CONTROL | MOD_SHIFT
    }

    protected override void OnHandleDestroyed(EventArgs e)
    {
        UnregisterHotKey(Handle, HOTKEY_ID);
        base.OnHandleDestroyed(e);
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_HOTKEY && m.WParam.ToInt32() == HOTKEY_ID)
        {
            // 熱鍵觸發
            this.Visible = !this.Visible; // 切換視窗顯示/隱藏
            if (this.Visible) this.Activate();
            return; // 已處理,不傳給基類
        }
        base.WndProc(ref m); // 其他訊息正常處理
    }
}

跨執行緒訊息傳遞 (Invoke/Post)


// 背景執行緒通知 UI 執行緒
Task.Run(() => 
{
    // ... 耗時運算 ...
    int result = 42;
    
    // 方式 1:BeginInvoke (非同步,排入 UI 訊息佇列)
    this.BeginInvoke((Action)(() => 
    {
        lblResult.Text = $"計算完成: {result}";
    }));
    
    // 方式 2:PostMessage 自訂訊息 (更底層)
    // NativeMethods.PostMessage(this.Handle, WM_DATA_READY, new IntPtr(result), IntPtr.Zero);
});

常見問題與除錯技巧

現象可能原因解決
自訂繪製不顯示/閃爍未啟用雙緩衝、未呼叫 base.OnPaint、Graphics 物件誤用SetStyle DoubleBuffer、正確覆寫 OnPaint、使用 e.Graphics
自訂訊息不觸發訊息 ID 衝突、Handle 尚未建立時發送、跨執行緒未用 PostMessage用 WM_USER+ 範圍、OnHandleCreated 後發送、跨執行緒用 PostMessage
標準行為失效 (無法關閉、鍵盤不工作)DefWndProc/WndProc 未呼叫 basedefault case 必須 base.DefWndProc(ref m)
WM_PAINT 連續觸發 CPU 100%OnPaint 內呼叫 Invalidate() 造成無限迴圈移除 Invalidate、檢查邏輯、使用 Update() 而非 Invalidate()
參數傳遞截斷 (64-bit)用 int 強制轉換 IntPtr用 m.WParam.ToInt64() 或 (long)m.WParam

現代替代方案

  • WPF / UWP / WinUI 3:保留模式圖形、宣告式 UI、無訊息迴圈概念
  • Blazor Hybrid (WebView2):Web 技術建構桌面應用
  • .NET 7+ WinForms 改進:高 DPI 支援、新控制項、效能優化

結語

掌握 OnPaint 繪圖管線、DefWndProc 訊息分派、SendMessage/PostMessage 跨邊界通訊,是 WinForms 進階開發、互操作舊系統、實作全域熱鍵/系統托盤/自訂非客戶區繪製的核心技能。雖然現代 UI 框架已淡化訊息迴圈,但在維護既有 WinForms 專案、開發 Windows 殼層擴充、或需要極致效能/底層控制時,這些知識依然不可或缺。

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