C# 雖以事件驅動為主,但透過 P/Invoke 呼叫 SendMessage/PostMessage、覆寫 DefWndProc,仍能完整使用 Windows 訊息機制。本文系統講解自訂訊息定義、跨 DLL 訊息傳遞、系統訊息處理 (WM_PAINT)、訊息迴圈攔截,並提供完整 WinForms 範例代碼。
HWND (視窗句柄) + UINT Msg (訊息代碼) + WPARAM + LPARAM + LRESULTWM_PAINT (0x000F)、WM_CREATE、WM_DESTROY 等WM_USER (0x0400) 以上,如 WM_USER + 101RegisterWindowMessage 保證全域唯一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 的訊息
}
[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, ...)
}
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/Resultbase.DefWndProc 處理非自訂訊息,否則視窗基本功能 (移動、繪製、關閉) 會失效WParam/LParam 為 IntPtr,需視語義轉換為 int/uint/long/IntPtr覆寫 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;
// 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 無法直接被 SendMessage 呼叫,需:
UnmanagedCallersOnly (C# 9+) 導出非託管入口點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 架構優先用事件/通道/管道,但訊息機制永遠是打通非託管世界的鑰匙。