傳統按鈕僅支援單一功能。帶彈出式選單的按鈕將按鈕區域分為「主體區」與「選擇區」:
此設計整合多個按鈕功能,節省介面空間,提升使用者體驗。
CButton 建立 CMenuButton 類別// MenuButton.h
#pragma once
class CMenuButton : public CButton
{
DECLARE_DYNAMIC(CMenuButton)
public:
CMenuButton();
virtual ~CMenuButton();
protected:
// 繪製相關
CRect m_ButRect; // 完整按鈕矩形
CRect m_LRect; // 主體區矩形
CRect m_RRect; // 選擇區矩形(右側箭頭區)
CMenu m_Menu; // 彈出式選單
BOOL m_bTracking; // 是否正在追蹤選單
// 重寫虛函式
virtual void PreSubclassWindow();
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
// 自訂繪製
void DrawButton(CDC* pDC);
void DrawArrow(CDC* pDC, CRect rect, BOOL bPressed);
// 選單處理
void CreateMenu();
void TrackMenu(CPoint point);
// 訊息處理
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnCancelMode();
DECLARE_MESSAGE_MAP()
};
// MenuButton.cpp
#include "stdafx.h"
#include "MenuButton.h"
IMPLEMENT_DYNAMIC(CMenuButton, CButton)
BEGIN_MESSAGE_MAP(CMenuButton, CButton)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
ON_WM_CANCELMODE()
END_MESSAGE_MAP()
CMenuButton::CMenuButton() : m_bTracking(FALSE) {}
CMenuButton::~CMenuButton() {}
void CMenuButton::PreSubclassWindow()
{
// 設定為 Owner-Draw 模式
ModifyStyle(0, BS_OWNERDRAW);
CButton::PreSubclassWindow();
// 建立選單資源
CreateMenu();
}
void CMenuButton::CreateMenu()
{
// 從資源載入選單,或動態建立
// 假設資源 ID 為 IDR_MENUBUTTON_MENU
if (!m_Menu.LoadMenu(IDR_MENUBUTTON_MENU)) {
// 動態建立預設選單
m_Menu.CreatePopupMenu();
m_Menu.AppendMenu(MF_STRING, ID_MENU_SAVEAS, _T("另存新檔(&A)"));
m_Menu.AppendMenu(MF_STRING, ID_MENU_TEMPLATE, _T("存為範本(&T)"));
m_Menu.AppendMenu(MF_SEPARATOR);
m_Menu.AppendMenu(MF_STRING, ID_MENU_AUTOSAVE, _T("自動儲存設定(&S)"));
}
}
void CMenuButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
m_ButRect = lpDrawItemStruct->rcItem;
int nSavedDC = pDC->SaveDC();
VERIFY(pDC);
DrawButton(pDC);
pDC->RestoreDC(nSavedDC);
}
void CMenuButton::DrawButton(CDC* pDC)
{
// 計算區域:左側 80% 為主體區,右側 20% 為選擇區(最小 20px)
int nWidth = m_ButRect.Width();
int nSplit = max(nWidth - 20, nWidth * 4 / 5);
m_LRect.SetRect(m_ButRect.left, m_ButRect.top,
m_ButRect.left + nSplit, m_ButRect.bottom);
m_RRect.SetRect(m_ButRect.left + nSplit, m_ButRect.top,
m_ButRect.right, m_ButRect.bottom);
// 取得狀態
UINT nState = GetState();
BOOL bPressed = (nState & BST_PUSHED) != 0;
BOOL bFocused = (nState & BST_FOCUS) != 0;
BOOL bDisabled = !IsWindowEnabled();
// 繪製背景
DrawButtonBackground(pDC, bPressed, bFocused, bDisabled);
// 繪製主體區文字
DrawButtonText(pDC, bDisabled);
// 繪製選擇區箭頭
DrawArrow(pDC, m_RRect, bPressed);
// 繪製分隔線
CPen penDark(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
CPen penLight(PS_SOLID, 1, ::GetSysColor(COLOR_3DHIGHLIGHT));
CPen* pOldPen = pDC->SelectObject(&penDark);
pDC->MoveTo(m_RRect.left, m_RRect.top + 2);
pDC->LineTo(m_RRect.left, m_RRect.bottom - 2);
pDC->SelectObject(&penLight);
pDC->MoveTo(m_RRect.left + 1, m_RRect.top + 2);
pDC->LineTo(m_RRect.left + 1, m_RRect.bottom - 2);
pDC->SelectObject(pOldPen);
}
void CMenuButton::DrawButtonBackground(CDC* pDC, BOOL bPressed, BOOL bFocused, BOOL bDisabled)
{
CRect rect = m_ButRect;
COLORREF clrBg, clrBorder;
if (bDisabled) {
clrBg = ::GetSysColor(COLOR_3DFACE);
clrBorder = ::GetSysColor(COLOR_3DSHADOW);
} else if (bPressed) {
clrBg = ::GetSysColor(COLOR_3DFACE);
clrBorder = ::GetSysColor(COLOR_3DSHADOW);
} else {
clrBg = ::GetSysColor(COLOR_3DFACE);
clrBorder = ::GetSysColor(COLOR_3DSHADOW);
}
// 填充背景
CBrush brush(clrBg);
pDC->FillRect(rect, &brush);
// 繪製 3D 邊框
if (bPressed) {
// 凹入效果
pDC->Draw3dRect(rect, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DHIGHLIGHT));
} else {
// 凸出效果
pDC->Draw3dRect(rect, ::GetSysColor(COLOR_3DHIGHLIGHT), ::GetSysColor(COLOR_3DSHADOW));
}
}
void CMenuButton::DrawButtonText(CDC* pDC, BOOL bDisabled)
{
CString strText;
GetWindowText(strText);
if (strText.IsEmpty()) return;
// 設定文字顏色
COLORREF clrText = bDisabled ? ::GetSysColor(COLOR_GRAYTEXT) : ::GetSysColor(COLOR_BTNTEXT);
pDC->SetTextColor(clrText);
pDC->SetBkMode(TRANSPARENT);
// 文字置中繪製
CRect textRect = m_LRect;
textRect.DeflateRect(4, 2);
pDC->DrawText(strText, textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
void CMenuButton::DrawArrow(CDC* pDC, CRect rect, BOOL bPressed)
{
// 計算箭頭中心點
int cx = rect.left + rect.Width() / 2;
int cy = rect.top + rect.Height() / 2;
int size = 4;
// 箭頭三個點
CPoint pts[3];
pts[0] = CPoint(cx - size, cy - size / 2);
pts[1] = CPoint(cx + size, cy - size / 2);
pts[2] = CPoint(cx, cy + size / 2);
// 按下時向下位移 1 像素
if (bPressed) {
pts[0].Offset(1, 1);
pts[1].Offset(1, 1);
pts[2].Offset(1, 1);
}
// 繪製實心三角形
CBrush brush(::GetSysColor(COLOR_BTNTEXT));
CBrush* pOldBrush = pDC->SelectObject(&brush);
CPen pen(PS_SOLID, 1, ::GetSysColor(COLOR_BTNTEXT));
CPen* pOldPen = pDC->SelectObject(&pen);
pDC->Polygon(pts, 3);
pDC->SelectObject(pOldBrush);
pDC->SelectObject(pOldPen);
}
// === 滑鼠訊息處理 ===
void CMenuButton::OnLButtonDown(UINT nFlags, CPoint point)
{
SetFocus();
// 判斷點擊區域
if (m_RRect.PtInRect(point)) {
// 點擊選擇區:彈出選單
TrackMenu(point);
} else {
// 點擊主體區:標準按鈕行為
SetState(TRUE); // 按下狀態
SetCapture();
m_bTracking = TRUE;
}
CButton::OnLButtonDown(nFlags, point);
}
void CMenuButton::OnLButtonUp(UINT nFlags, CPoint point)
{
if (m_bTracking && !m_RRect.PtInRect(point)) {
// 主體區釋放:觸發點擊事件
ReleaseCapture();
m_bTracking = FALSE;
SetState(FALSE);
if (m_ButRect.PtInRect(point)) {
// 發送 BN_CLICKED 通知給父視窗
GetParent()->SendMessage(WM_COMMAND,
MAKEWPARAM(GetDlgCtrlID(), BN_CLICKED), (LPARAM)m_hWnd);
}
}
CButton::OnLButtonUp(nFlags, point);
}
void CMenuButton::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_bTracking && !m_RRect.PtInRect(point)) {
// 滑鼠移出主體區時取消按下狀態
if (!m_ButRect.PtInRect(point)) {
ReleaseCapture();
m_bTracking = FALSE;
SetState(FALSE);
}
}
CButton::OnMouseMove(nFlags, point);
}
void CMenuButton::OnCancelMode()
{
CButton::OnCancelMode();
ReleaseCapture();
m_bTracking = FALSE;
SetState(FALSE);
}
void CMenuButton::TrackMenu(CPoint point)
{
// 將客戶區座標轉為螢幕座標
ClientToScreen(&point);
// 選單位於按鈕下方
point.y += m_ButRect.Height();
// 取得子選單(第一個彈出式選單)
CMenu* pPopup = m_Menu.GetSubMenu(0);
if (pPopup) {
// 追蹤選單
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
point.x, point.y, GetParent(), NULL);
}
}
// Resource.h
#define IDR_MENUBUTTON_MENU 101
#define ID_MENU_SAVEAS 40001
#define ID_MENU_TEMPLATE 40002
#define ID_MENU_AUTOSAVE 40003
// .rc 檔案片段
IDR_MENUBUTTON_MENU MENU
BEGIN
POPUP ""
BEGIN
MENUITEM "另存新檔(&A)", ID_MENU_SAVEAS
MENUITEM "存為範本(&T)", ID_MENU_TEMPLATE
MENUITEM SEPARATOR
MENUITEM "自動儲存設定(&S)", ID_MENU_AUTOSAVE
END
END
CMenuButton
CMenuButton* pBtn = new CMenuButton();
pBtn->Create(_T("儲存 ▼"), WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
CRect(10, 10, 120, 40), this, IDC_MYMENUBTN);
pBtn->SetMenu(IDR_MYCUSTOM_MENU); // 可擴展:設定自訂選單資源
// 在對話方塊或視窗類別中
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
ON_COMMAND(ID_MENU_SAVEAS, &CMyDialog::OnMenuSaveAs)
ON_COMMAND(ID_MENU_TEMPLATE, &CMyDialog::OnMenuTemplate)
ON_COMMAND(ID_MENU_AUTOSAVE, &CMyDialog::OnMenuAutoSave)
END_MESSAGE_MAP()
void CMyDialog::OnMenuSaveAs() { /* 另存新檔邏輯 */ }
void CMyDialog::OnMenuTemplate() { /* 存為範本邏輯 */ }
void CMyDialog::OnMenuAutoSave() { /* 自動儲存設定邏輯 */ }
void CMenuButton::SetIcon(UINT nIconID) {
m_hIcon = (HICON)LoadImage(AfxGetResourceHandle(),
MAKEINTRESOURCE(nIconID), IMAGE_ICON, 16, 16, 0);
Invalidate();
}
// DrawButton 中繪製圖示
if (m_hIcon) {
pDC->DrawIcon(m_LRect.left + 4, m_LRect.top + (m_LRect.Height()-16)/2, m_hIcon);
// 文字區域右移
textRect.left += 20;
}
Space/Enter:觸發主體區點擊Alt+Down:彈出選單Esc:取消選單
#if (_WIN32_WINNT >= 0x0501) // XP+
// 使用 Visual Styles API
HTHEME hTheme = OpenThemeData(m_hWnd, L"BUTTON");
if (hTheme) {
// 使用 DrawThemeBackground 繪製符合系統主題的按鈕
CloseThemeData(hTheme);
}
#endif
| 問題 | 原因 | 解決 |
|---|---|---|
| 選單不彈出 | TrackPopupMenu 父視窗錯誤、座標錯誤 | 確保使用 GetParent()、ClientToScreen 轉換座標 |
| 按鈕不重繪 | 未呼叫 Invalidate()、OwnerDraw 模式未設定 | 檢查 BS_OWNERDRAW 樣式、狀態改變時呼叫 Invalidate() |
| 主體區點擊無反應 | 未發送 WM_COMMAND/BN_CLICKED | OnLButtonUp 中正確發送通知訊息 |
| 選單項目無效果 | 命令 ID 未在父視窗處理 | 父視窗加入 ON_COMMAND 對應處理函式 |
透過繼承 CButton、實作 Owner-Draw、處理滑鼠訊息區分點擊區域、整合 CMenu 彈出選單,即可建立功能完整的分割按鈕控件。此控件可進一步封裝為 ActiveX 或 MFC 擴充庫,在專案中重複使用。