首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC 教學

VC++ 製作帶彈出式選單按鈕完整教程

     編輯:VC 教學

需求分析

傳統按鈕僅支援單一功能。帶彈出式選單的按鈕將按鈕區域分為「主體區」與「選擇區」:

  • 主體區: 點擊執行主要功能(如「儲存」)
  • 選擇區: 點擊彈出下拉選單,提供延伸功能(如「另存新檔」、「存為範本」、「自動儲存設定」)

此設計整合多個按鈕功能,節省介面空間,提升使用者體驗。

實作架構

  1. 繼承 CButton 建立 CMenuButton 類別
  2. 使用 Owner-Draw(自繪)模式自訂外觀
  3. 處理滑鼠訊息判斷點擊區域
  4. 右鍵或選擇區點擊時彈出選單

完整程式碼實作

1. 類別宣告

// 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()
};

2. 核心實作

// 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);
    }
}

3. 資源定義

// 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

使用方式

1. 對話方塊編輯器中加入

  1. 在對話方塊放入標準按鈕
  2. 屬性設定:Owner Draw = True
  3. 類別向導將按鈕關聯變數類型改為 CMenuButton

2. 動態建立


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);  // 可擴展:設定自訂選單資源

3. 父視窗處理選單命令


// 在對話方塊或視窗類別中
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() { /* 自動儲存設定邏輯 */ }

進階擴展

1. 支援圖示


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;
}

2. 支援鍵盤操作

  • Space/Enter:觸發主體區點擊
  • Alt+Down:彈出選單
  • Esc:取消選單

3. 主題感知繪製


#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_CLICKEDOnLButtonUp 中正確發送通知訊息
選單項目無效果命令 ID 未在父視窗處理父視窗加入 ON_COMMAND 對應處理函式

結語

透過繼承 CButton、實作 Owner-Draw、處理滑鼠訊息區分點擊區域、整合 CMenu 彈出選單,即可建立功能完整的分割按鈕控件。此控件可進一步封裝為 ActiveX 或 MFC 擴充庫,在專案中重複使用。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved