系列第四篇:在移除視窗預設邊框 (FormBorderStyle=None) 與設定 Region 後,本文詳細示範如何使用 GDI+ 完整重繪非客戶區,包含標題列背景、視窗圖示、標題文字、控制盒按鈕 (最小化/最大化/關閉) 以及視窗邊框。
WinForms 視窗可視區域分為:
BackColor 即可。WM_NCPAINT、WM_NCCALCSIZE、WM_NCHITTEST 等訊息)。private bool NcPaint(SkinningForm form, SkinEngine engine)
{
// 1. 取得螢幕座標視窗矩形
RECT rectScreen = new RECT();
NativeMethod.GetWindowRect(_parentForm.Handle, ref rectScreen);
Rectangle rectBounds = rectScreen.ToRectangle();
rectBounds.Offset(-rectBounds.X, -rectBounds.Y); // 轉為客戶區原點座標
// 2. 建立裁剪區域:排除客戶區,僅保留非客戶區
Region region = new Region(rectBounds);
Rectangle rectClip = rectBounds;
rectClip.Inflate(-engine.SkinAppearance.BorderWidth, -engine.SkinAppearance.BorderWidth);
rectClip.Y += engine.SkinAppearance.CaptionHeight;
rectClip.Height -= engine.SkinAppearance.CaptionHeight;
region.Exclude(rectClip); // 關鍵:只繪製邊框與標題列
// 3. 取得裝置內容 (DC) 並套用裁剪區域
IntPtr hdc = NativeMethod.GetDCEx(_parentForm.Handle, IntPtr.Zero,
DCXFlags.DCX_CACHE | DCXFlags.DCX_CLIPSIBLINGS | DCXFlags.DCX_WINDOW);
Graphics g = Graphics.FromHdc(hdc);
IntPtr hrgn = region.GetHrgn(g);
NativeMethod.SelectClipRgn(hdc, hrgn);
// 4. 離螢幕緩衝 (BufferedGraphics) 避免閃爍
if (_bufferGraphics == null || _currentCacheSize != rectBounds.Size) { ... }
g = _bufferGraphics.Graphics; // 切換至緩衝 Graphics
// 5. 依序繪製非客戶區元素
Rectangle captionBounds = rectBounds;
captionBounds.Height = engine.SkinAppearance.BorderWidth + engine.SkinAppearance.CaptionHeight;
engine.SkinAppearance.DrawCaptionBackground(g, captionBounds, _formIsActive, engine); // 標題列背景
if (_parentForm.ShowIcon && _parentForm.Icon != null) DrawIcon(g); // 視窗圖示
DrawCaptionText(g, _parentForm.Text, _parentForm.Font); // 標題文字
DrawCaptionControlBox(g); // 控制盒按鈕 (核心難點)
engine.SkinAppearance.DrawBorder(g, rectBounds, engine); // 視窗邊框
// 6. 緩衝內容輸出至螢幕 + 資源清理
_bufferGraphics.Render();
// ... Dispose Region, ReleaseDC, SelectClipRgn(null)
}
DCX_CACHE:使用快取 DC,效能較佳。DCX_CLIPSIBLINGS:裁剪同層視窗避免重疊繪製。DCX_WINDOW:取得整個視窗 DC (含非客戶區),而非僅客戶區。先建立完整視窗矩形 Region,再 Exclude 客戶區矩形。客戶區矩形由邊框寬度 (BorderWidth) 與標題列高度 (CaptionHeight) 推算。此技巧確保 Graphics 僅在非客戶區繪製,不汙染客戶區內容。
視窗大小變更時重新配置緩衝區 (_bufferContext.Allocate)。所有繪圖操作導向記憶體位圖,最後 Render() 一次性輸出,徹底消除逐步繪製造成的閃爍。
按鈕區域陣列依標題列高度與右邊距計算。繪製時依按鈕狀態 (Normal/Hover/Pressed/Disabled) 選取不同筆刷 (漸層背景、邊框、圖示路徑)。滑鼠非客戶區訊息 (WM_NCHITTEST, WM_NCMOUSEMOVE 等) 驅動狀態機更新並觸發 Invalidate() 重繪。
