WinForms 本質是對 Win32 API 的封裝。視窗透過訊息迴圈接收系統/使用者事件 (WM_PAINT, WM_MOUSEMOVE, WM_KEYDOWN, WM_TIMER, 自訂訊息等)。C# 提供兩種處理層級:
Paint, Click, KeyDown 等事件 (推薦、類型安全)WndProc / DefWndProc / OnXxx 虛擬方法 (攔截非事件化訊息、效能敏感、互操作)
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):啟用雙緩衝防閃爍
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
}
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);
}
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;
}
}
}
| 方法 | 層級 | 用途 | 呼叫時機 | 是否需呼叫 base |
|---|---|---|---|---|
WndProc | 最低 | 攔截所有訊息,完全控制 | 訊息迴圈分派時 | 是 (除非完全攔截) |
DefWndProc | 中 | 處理未被 OnXxx 處理的訊息 | WndProc 內部預設分派 | 是 (預設行為) |
OnPaint 等 | 高 | 特定訊息的標準化封裝 | DefWndProc 針對 WM_PAINT 呼叫 | 建議 (繼承鏈) |
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); // 其他訊息正常處理
}
}
// 背景執行緒通知 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 未呼叫 base | default case 必須 base.DefWndProc(ref m) |
| WM_PAINT 連續觸發 CPU 100% | OnPaint 內呼叫 Invalidate() 造成無限迴圈 | 移除 Invalidate、檢查邏輯、使用 Update() 而非 Invalidate() |
| 參數傳遞截斷 (64-bit) | 用 int 強制轉換 IntPtr | 用 m.WParam.ToInt64() 或 (long)m.WParam |
掌握 OnPaint 繪圖管線、DefWndProc 訊息分派、SendMessage/PostMessage 跨邊界通訊,是 WinForms 進階開發、互操作舊系統、實作全域熱鍵/系統托盤/自訂非客戶區繪製的核心技能。雖然現代 UI 框架已淡化訊息迴圈,但在維護既有 WinForms 專案、開發 Windows 殼層擴充、或需要極致效能/底層控制時,這些知識依然不可或缺。