標題列屬於**非客戶區 (Non-Client Area)**,無法直接放置子視窗控制項。所謂「標題列按鈕」實為在非客戶區繪製按鈕外觀,並透過滑鼠訊息判斷點擊區域模擬按鈕行為。
| 訊息 | 用途 |
| WM_NCPAINT | 繪製非客戶區 (標題列、邊框),此處繪製按鈕外觀 |
| WM_NCHITTEST | 告知系統滑鼠位於非客戶區何處,**關鍵:返回 HTBUTTON 自定義區域** |
| WM_NCLBUTTONDOWN | 非客戶區左鍵按下,模擬按鈕按下狀態 |
| WM_NCLBUTTONUP | 非客戶區左鍵釋放,觸發按鈕動作 |
| WM_NCLBUTTONDBLCLK | 非客戶區雙擊,防止標題列雙擊最大化/還原干擾按鈕區域 |
| WM_NCRBUTTONDOWN | 非客戶區右鍵,防止按鈕區域彈出系統選單 |
| WM_NCMOUSEMOVE | 非客戶區滑鼠移動,追蹤懸停/離開按鈕區域更新外觀 |
| WM_NCACTIVATE | 視窗啟用/停用切換,更新按鈕啟用/停用視覺狀態 |
MFC 實作架構 (以 CFrameWnd 衍生類為例)
1. 成員變數
class CMainFrame : public CFrameWnd {
CRect m_rectCaptionBtn; // 按鈕在非客戶區座標 (螢幕座標)
BOOL m_bBtnPressed; // 按下狀態
BOOL m_bBtnHot; // 懸停狀態
BOOL m_bBtnEnabled; // 啟用狀態
// ...
};
2. WM_NCCALCSIZE 保留按鈕空間 (可選)
若需在標題列右側系統按鈕 (最小/最大/關閉) 左側放置按鈕,需調整非客戶區客戶矩形,或直接在 WM_NCPAINT 計算位置 (較簡單)。
3. WM_NCHITTEST 定義按鈕熱區
LRESULT CMainFrame::OnNcHitTest(CPoint point) {
// point 為螢幕座標
if (m_bBtnEnabled && m_rectCaptionBtn.PtInRect(point)) {
return HTBUTTON; // 告知系統此區域為按鈕
}
return CFrameWnd::OnNcHitTest(point); // 其他區域預設處理
}
4. WM_NCPAINT 繪製按鈕
void CMainFrame::OnNcPaint() {
CWindowDC dc(this); // 非客戶區 DC
// 或使用 GetWindowDC(m_hWnd) + ReleaseDC
// 計算按鈕矩形 (右上角系統按鈕左側)
CRect rectWindow;
GetWindowRect(&rectWindow);
// 系統按鈕寬度 GetSystemMetrics(SM_CXSIZE) ~ 系統邊框寬度
int btnWidth = 20, btnHeight = 18;
m_rectCaptionBtn.SetRect(
rectWindow.right - GetSystemMetrics(SM_CXSIZE)*3 - btnWidth - 4,
rectWindow.top + GetSystemMetrics(SM_CYSIZEFRAME) + 2,
rectWindow.right - GetSystemMetrics(SM_CXSIZE)*3 - 4,
rectWindow.top + GetSystemMetrics(SM_CYSIZEFRAME) + 2 + btnHeight
);
// 繪製按鈕邊框
UINT uEdge = m_bBtnPressed ? EDGE_SUNKEN : (m_bBtnHot ? EDGE_RAISED : EDGE_RAISED);
if (!m_bBtnEnabled) uEdge = EDGE_RAISED; // 停用態同凸起但灰色文字
dc.DrawEdge(&m_rectCaptionBtn, uEdge, BF_RECT | BF_SOFT | BF_MIDDLE);
// 繪製按鈕文字/圖示 (如 "?" 說明圖示)
CFont* pOldFont = dc.SelectObject(&afxGlobalData.fontDefault); // 或自訂字型
dc.SetBkMode(TRANSPARENT);
dc.SetTextColor(m_bBtnEnabled ? GetSysColor(COLOR_BTNTEXT) : GetSysColor(COLOR_GRAYTEXT));
dc.DrawText(_T("?"), &m_rectCaptionBtn, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
dc.SelectObject(pOldFont);
}
5. 滑鼠互動狀態機
void CMainFrame::OnNcLButtonDown(UINT nHitTest, CPoint point) {
if (nHitTest == HTBUTTON && m_bBtnEnabled) {
m_bBtnPressed = TRUE;
SetCapture(); // 捕獲滑鼠,確保收到 Up 即使移出區域
RedrawWindow(NULL, NULL, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW); // 重繪非客戶區
} else {
CFrameWnd::OnNcLButtonDown(nHitTest, point);
}
}
void CMainFrame::OnNcLButtonUp(UINT nHitTest, CPoint point) {
if (m_bBtnPressed) {
m_bBtnPressed = FALSE;
ReleaseCapture();
if (m_rectCaptionBtn.PtInRect(point)) {
// 執行按鈕動作
OnCaptionButtonClick();
}
RedrawWindow(NULL, NULL, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW);
} else {
CFrameWnd::OnNcLButtonUp(nHitTest, point);
}
}
void CMainFrame::OnNcMouseMove(UINT nHitTest, CPoint point) {
BOOL bHot = m_bBtnEnabled && m_rectCaptionBtn.PtInRect(point);
if (bHot != m_bBtnHot) {
m_bBtnHot = bHot;
RedrawWindow(NULL, NULL, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW);
}
CFrameWnd::OnNcMouseMove(nHitTest, point);
}
void CMainFrame::OnNcLButtonDblClk(UINT nHitTest, CPoint point) {
if (nHitTest == HTBUTTON) return; // 忽略按鈕區域雙擊
CFrameWnd::OnNcLButtonDblClk(nHitTest, point);
}
void CMainFrame::OnNcRButtonDown(UINT nHitTest, CPoint point) {
if (nHitTest == HTBUTTON) return; // 阻擋系統選單
CFrameWnd::OnNcRButtonDown(nHitTest, point);
}
void CMainFrame::OnNcActivate(BOOL bActive) {
CFrameWnd::OnNcActivate(bActive);
// 可根據 bActive 更新按鈕啟用狀態或視覺
RedrawWindow(NULL, NULL, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW);
}
6. 按鈕動作處理
void CMainFrame::OnCaptionButtonClick() {
// 例如:顯示說明、切換工具列、關於對話框...
AfxGetApp()->WinHelp(HID_BASE_RESOURCE + IDR_MAINFRAME, HELP_CONTEXT);
}
訊息映射
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_NCHITTEST()
ON_WM_NCPAINT()
ON_WM_NCLBUTTONDOWN()
ON_WM_NCLBUTTONUP()
ON_WM_NCMOUSEMOVE()
ON_WM_NCLBUTTONDBLCLK()
ON_WM_NCRBUTTONDOWN()
ON_WM_NCACTIVATE()
END_MESSAGE_MAP()
高 DPI / 主題 考量
- 使用
GetSystemMetricsForDpi (Win10 1607+) 取得正確系統按鈕/邊框尺寸。
- 啟用視覺主題 (Manifest) 時,標題列繪製由主題引擎處理,
WM_NCPAINT 可能不被呼叫。需呼叫 SetWindowTheme(m_hWnd, L"", L"") 禁用標題列主題,或使用 DwmExtendFrameIntoClientArea 擴展客戶區至標題列 (現代做法)。
- 上一篇文章: MFC 選單按鈕實作:CMenuButton 結合 DrawItem 與 TrackPopupMenu 完整封裝
- 下一篇文章: 氣球式工具提示:CToolTipWnd 類別實現類似 Windows XP 風格提示視窗
Copyright ©
程式碼之家 All Rights Reserved