Windows 預設視窗樣式單一且難以美化。許多開發者嘗試透過 GetSystemMetrics 計算非客戶區尺寸、攔截 WM_NCPAINT 繪製,卻因座標換算複雜而望而卻步。本文提供一套「以客戶區座標為基準、直接在 WM_PAINT 中繪製整個視窗外觀」的簡易方案,無需處理非客戶區訊息,即可實現自定標題列、最小/最大/關閉按鈕、邊框陰影等效果。
WS_POPUP 樣式(無標題列、無邊框),客戶區即為整個視窗。OnPaint 中手動劃分區域:標題列、左/右邊框、底部邊框、三大功能按鈕。CBitmap) + StretchBlt 填充各區域,再繪製標題文字。WM_NCHITTEST 回傳 HTCAPTION/HTMINBUTTON/HTMAXBUTTON/HTCLOSE/HTLEFT... 讓系統正確處理拖移、縮放、按鈕點擊。// 假設視窗客戶區寬度 cx, 高度 cy
// 標題列高度 30px,按鈕寬 20px,間距 6px
CRect rcTitle(6, 0, cx - 6, 30); // 標題列區域(左右留 6px 給邊框)
// 三顆按鈕由右至左排列
m_rcBtnClose.SetRect(cx - 26, 0, cx - 6, 30);
m_rcBtnMax.SetRect(cx - 46, 0, cx - 26, 30);
m_rcBtnMin.SetRect(cx - 66, 0, cx - 46, 30);

void CMyDlg::OnPaint()
{
CPaintDC dc(this);
CRect rcClient; GetClientRect(&rcClient);
int cx = rcClient.Width(), cy = rcClient.Height();
CDC memDC; memDC.CreateCompatibleDC(&dc);
CBitmap bmp, *pOldBmp;
BITMAP bmInfo;
// 1. 繪製標題列背景(拉伸貼圖)
bmp.LoadBitmap(IDB_TITLE_TOP);
bmp.GetBitmap(&bmInfo);
pOldBmp = memDC.SelectObject(&bmp);
dc.StretchBlt(6, 0, cx - 12, 30, &memDC, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY);
memDC.SelectObject(pOldBmp); bmp.DeleteObject();
// 2. 繪製標題文字
WriteCaption(m_clrCaption, m_strCaption); // 自訂函數,TextOut 至 rcTitle 區域
// 3. 繪製左右兩側標題列邊框裝飾
bmp.LoadBitmap(IDB_DOWN);
bmp.GetBitmap(&bmInfo);
pOldBmp = memDC.SelectObject(&bmp);
dc.StretchBlt(0, 0, 6, 30, &memDC, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY); // 左
dc.StretchBlt(cx - 6, 0, 6, 30, &memDC, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY); // 右
memDC.SelectObject(pOldBmp); bmp.DeleteObject();
// 4. 繪製三大按鈕(以常態圖示為例)
bmp.LoadBitmap(IDB_NORMAL);
bmp.GetBitmap(&bmInfo);
pOldBmp = memDC.SelectObject(&bmp);
dc.StretchBlt(m_rcBtnMin.left, m_rcBtnMin.top + 4, 20, 21, &memDC, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY);
dc.StretchBlt(m_rcBtnMax.left, m_rcBtnMax.top + 4, 20, 21, &memDC, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY);
dc.StretchBlt(m_rcBtnClose.left, m_rcBtnClose.top + 4, 20, 21, &memDC, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY);
memDC.SelectObject(pOldBmp); bmp.DeleteObject();
// 5. 繪製左右、底部邊框(同理 StretchBlt 邊框位圖)
// ...
}
LRESULT CMyDlg::OnNcHitTest(CPoint point)
{
ScreenToClient(&point); // 轉為客戶區座標
if (m_rcBtnClose.PtInRect(point)) return HTCLOSE;
if (m_rcBtnMax.PtInRect(point)) return HTMAXBUTTON;
if (m_rcBtnMin.PtInRect(point)) return HTMINBUTTON;
if (rcTitle.PtInRect(point)) return HTCAPTION;
// 邊框縮放區域(寬 4px)
CRect rcClient; GetClientRect(&rcClient);
if (point.x < 4) return point.y < 4 ? HTTOPLEFT : (point.y > rcClient.bottom - 4 ? HTBOTTOMLEFT : HTLEFT);
if (point.x > rcClient.right - 4) return point.y < 4 ? HTTOPRIGHT : (point.y > rcClient.bottom - 4 ? HTBOTTOMRIGHT : HTRIGHT);
if (point.y < 4) return HTTOP;
if (point.y > rcClient.bottom - 4) return HTBOTTOM;
return HTCLIENT;
}
在 OnLButtonDown/OnLButtonUp/OnMouseMove 中追蹤滑鼠是否在按鈕矩形內,切換按鈕位圖(IDB_NORMAL, IDB_HOVER, IDB_PRESSED),並呼叫 InvalidateRect(&rcBtn) 重繪該區域。
| 優點 | 缺點 |
|---|---|
| 邏輯直觀,座標全在客戶區,無需處理 NC 訊息 | 失去系統原生視窗動畫(最小化/最大化動畫) |
| 完全掌控像素級外觀,支援不規則形狀、透明、漸層 | 需自行實現 Aero Snap、任務列縮圖預覽等現代特性 |
| 相容性極佳(Win2000+),無 DWM 依賴 | 高 DPI 下位圖拉伸可能模糊,建議準備多解析度資源 |
適用於:工具類軟體、播放器、即時通訊主面板、遊戲啟動器等追求強品牌視覺識別的應用。
「全客戶區繪製 + WM_NCHITTEST 擊中測試」是實現高度客製化視窗外觀的經典且高效方案。配合位圖資源與 StretchBlt,幾十行代碼即可構建專業級非標準視窗。源碼中關鍵位圖資源:IDB_TITLE_TOP(標題列背景)、IDB_DOWN(側邊裝飾)、IDB_NORMAL/HOVER/PRESSED(按鈕三態)。