程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 完美實現真彩自繪菜單

完美實現真彩自繪菜單

編輯:關於VC++

一、提出問題

在VCKBASE上讀到《自繪菜單的實現》[作者:querw]。應用的我自己的正在進行的工程後發現效果不錯,可是有存在許多問題。整個類的設計方面存在很多缺陷(先天,後天的),存在的主要問題如下:

  1. 當應用在多文檔界面(MDI)中的時候,無法對系統自動添加菜單和文檔模板菜單進行自繪(比如無法對文件->最近文件(MRU)菜單項中的文件列表就是系統自動添加)。原因是類內部沒有對CMainFrame::OnInitPopupMenu()消息進行處理的函數, 因此不具備修改系統自動添加菜單項的功能。(BCMENU有這功能,而且工作的不錯)
  2. 作者提到的 BCMENU 不用映射 WM_DRAWITEM 和 WM_MEASUREITEM 兩個消息就能實現自畫功能,實際上是錯誤的。不映射這兩個重要的消息,即使能自繪,也是有問題的,不信看圖。

    菜單編輯器中的模菜單樣

    使用BCMENU並且映射了這兩個消息後的執行情況

    使用BCMENU沒有映射兩個消息的執行情況

    原作者分析的自繪的是因為把主菜單(top-level menu)的子菜單都加載成彈出菜單(popupmenu),是不正確的。真正的原因是因為MFC框架會自動調用CMenu的兩個虛擬函數MeasureItem()和OnDrawItem()。 因此,當CMenuEx派生於CMenu,並且重寫這兩個虛擬函數以後。

    1、MFC框架調用的GetMenu()->MeasureItem()就相當於調用了CMenuEx::MeasureItem(),從而實現自繪菜單控件尺寸的測量。

    2、MFC框架調用GetMenu()->DrawItem()就相當於調用了CMenuEx::DrawItem()來實現自繪菜單控件的自繪操作(不懂??,這正是C++的虛擬的妙用,指向派生類對象的基類指針可以調用派生類的虛擬函數,多麼偉大的發明,誰想出來的???)。與子菜單是否為彈出菜單(popupmenu)沒有什麼關系。以下是摘自WINCORE.CPP的一段程序,也就是WM_MEASUREITEM消息的默認流向的地方,相信大家會從中看出一些端倪。 void CWnd::OnMeasureItem(int /*nIDCtl*/, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
    {
      if (lpMeasureItemStruct->CtlType == ODT_MENU)
      {
        ......
        // 如果沒有主菜單
        if (pThreadState->m_hTrackingWindow == m_hWnd)
        {
          ......
        }
        else
        {
          // 如果有主菜單
          pMenu = GetMenu(); // 找到窗體的主菜單,注意,pMenu的是CMenu* 類型
        }
        
        // 在當前菜單中尋找ID匹配的菜單項
        pMenu = _AfxFindPopupMenuFromID(pMenu, lpMeasureItemStruct->itemID);
        if (pMenu != NULL)      
          // 如果找到,就調用MeasureItem()
          // 這就是所謂的基類指針指向派生類對象,可以調用派生類虛擬函數的情況了
          pMenu->MeasureItem(lpMeasureItemStruct); 
        else
          TRACE1("Warning: unknown WM_MEASUREITEM for menu item 0x%04X.\n",
            lpMeasureItemStruct->itemID);
      }
      else
      {
        ......
      }
      ......
    }    

  3. 當菜單項中含有子菜單(submenu),而不含有分割條的時候,子菜單項的高度不可調。原因為原CMenuEx程序中將分割條的原COMMAND ID(0)改為菜單項的COMMADN ID(-1), 以欺騙MFC框架調用CMenuEx::MeasureItem()來計算子菜單項(submenu)的高度。(很令我失望,這也是促使我自己動手重寫該類的原因之一。不信看程序,看圖)

    摘錄自原CMenuEx.cpp第546-560行 if(uID == 0) //分隔符
    {
      ::AppendMenu(hNewMenu,MF_SEPARATOR,0,NULL);
      ......
      // 注意,就是下面那個-1,把分割條的ID從0改到-1,
         // 從而是MFC框架誤以為找到了ID為-1的菜單項,並且測量了它的尺寸
      // 而實際上ID為-1的菜單項是不可能被void CWnd::OnMeasureItem()找到的
      ::ModifyMenu(hNewMenu,i,MF_BYPOSITION | MF_OWNERDRAW,-1,(LPCTSTR)pMenuItem);
    }    
    菜單編輯器中沒有分割條菜單的菜單

    原CMenuEx執行的模樣

    菜單編輯器中有分割條菜單的菜單

    原CMenuEx執行的模樣

  4. 代碼不夠簡練,程序粒度劃分不好,可讀性差(不過比BCMENU的代碼可讀性強多了:))。

二、解決問題

針對以上遇到的問題,我參考BCMENU和原作者的CMenuEx,對CMenuEx類重新進行了組織,類定義如下:

// 聲明,因為下面的結構要用到 CMenuEx*,又不支持向後引用,又什麼辦法啊!
class CMenuEx;
//自繪菜單數據項結構,就是要傳給系統的那個牛X的LPCTSTR指針所指向的東東
class CMenuEx : public CMenu
{
  DECLARE_DYNAMIC( CMenuEx )
    
// Constructor
public:  
  CMenuEx();  
  virtual ~CMenuEx();
  virtual BOOL DestroyMenu();
  
// Operation
public:
  // 加載菜單操作
  BOOL LoadMenu(UINT nIDResource);
  BOOL LoadMenu(LPCTSTR lpszResourceName);
  BOOL LoadMenu(HMENU hMenu);
  BOOL LoadMenu(CMenu & Menu);
  
  // 菜單項操作,如果當前菜單為主菜單(top-level)就調用相應的CMenu的操作。如果是彈出菜單,
     // 就將新加入的菜單項定義為自繪菜單
  BOOL AppendMenu(UINT nFlags, UINT nIDNewItem = 0,LPCTSTR lpszNewItem = NULL);
  BOOL InsertMenu(UINT nPosition,UINT nFlags,UINT nIDNewItem=0,LPCTSTR lpszNewItem=NULL );
  BOOL ModifyMenu(UINT nPosition,UINT nFlags,UINT nIDNewItem=0,LPCTSTR lpszNewItem=NULL );  
  BOOL RemoveMenu(UINT nPosition, UINT nFlags);  
  
  // 加載菜單圖像操作
  //通過菜單索引表加載圖像索引,此操作必須在設置過菜單圖像後調用
  void SetImageIndex(const UINT* nIDResource,UINT nIDCount);
  void LoadToolBar(const CToolBar* pToolBar);// 通過工具欄加載圖像,和圖像索引
  
  // 取自繪菜單項的數據項
  UINT GetMenuItemSize() const;
  LPMENUITEM GetMenuItem(UINT nPosition);  
  
  // 取子菜單操作,如果位置nPosition存在子菜單,返回該子菜單指針
  // 如果不存在子菜單,返回NULL
  CMenuEx* GetSubMenu(int nPosition);
  // 在當前菜單和所以子菜單中中尋找相應ID
  // 如果找到,返回ID所在菜單的指針,沒找到返回NULL
  CMenuEx* FindPopupMenuFromID(UINT nID);
  
// Attributes
protected:
  // 指示為主菜單(top-level menu or menubar)還是彈出菜單(popupmenu)
  BOOL m_bPopupMenu;
  
  // 分割條的默認高度
  int m_nSeparator;
  
  // 繪制菜單需要的顏色
  COLORREF m_crBackground;    // 菜單背景色  
  COLORREF m_crTextSelected;    // 菜單項被選中時的文字顏色
  COLORREF m_crText;      // 菜單項文字顏色
  COLORREF m_crLeft;      // 菜單左側的背景顏色
  COLORREF m_crSelectedBroder;    // 菜單選中框的線條顏色
  COLORREF m_crSelectedFill;    // 菜單選中框的填充顏色
  
  // 菜單項圖像的尺寸  
  CSize m_szImage;
  
  CImageList* m_pImageList;    // 菜單項正常的圖像列表
  CImageList* m_pDisabledImageList;  // 菜單項禁用時的圖像列表
  CImageList* m_pHotImageList;    // 菜單項被選中時的圖像列表
  
protected:
  // 包含所有菜單項的數組
  CArray m_MenuItemArr;
  
public:
  // 設置顏色操作
  void SetTextSelectedColor(COLORREF color);
  void SetBackgroundColor(COLORREF color);
  void SetTextColor(COLORREF color);
  void SetLeftColor(COLORREF color);
  void SetSelectedBroderColor(COLORREF color);
  void SetSelectedFillColor(COLORREF color);
  
  // 設置圖像列表操作
  void SetImageList(CImageList* pImageList);
  void SetDisabledImageList(CImageList* pImageList);
  void SetHotImageList(CImageList* pImageList);
  
  // 設置當前菜單為主菜單還是彈出菜單
  void SetPopupMenu(BOOL bPopupMenu);
  
  // Implementation
public:
  // 繪制菜單項的虛擬函數,由MFC框架自動調用
  virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
  
  // 更新彈出菜單菜單項操作
  // 因為有時候系統會通過菜單句柄插入一些非自繪菜單
  // 該函數就是更新這些非自繪菜單為自繪菜單
  void UpdatePopupMenu();
  
protected:
  // 繪制菜單項的輔助函數,想自己的菜單看上去更COOL,就拿他們開刀
  void DrawBackground(CDC* pDC,CRect rect);
  void DrawMenuImage(CDC* pDC,CRect rect,LPDRAWITEMSTRUCT lpDIS);
  void DrawMenuText(CDC* pDC,CRect rect,LPDRAWITEMSTRUCT lpDIS);
  void DrawSelected(CDC* pDC,CRect rect,LPDRAWITEMSTRUCT lpDIS);
  
  // Static Member
public:
  // 在CMainFrame的OnMeasureItem()消息映射函數中調用它,用來測量所有菜單項尺寸
  static void MeasureItem(LPMEASUREITEMSTRUCT lpMIS);
  
  // 在CMainFrame的OnInitPopupMenu()消息映射函數中調用它,
  // 用來更新系統自動添加的菜單項為自繪菜單
  static void InitPopupMenu(CMenu* pPopupMenu,UINT nIndex,BOOL bSystem);
  
};
#endif // !defined(MENUEX_H)   
三、實現方法

有了以上的強有力的武器,就可以對我們的程序下手了:)在MDI或SDI中使用CMenuEx的時候需要修改以下地方。

  1. 先將MenuEx.h和MenuEx.cpp添加到工程中,在CMainFrame中添加頭文件,CMenuEx對象,用於存儲菜單圖像的CImageList對象和初始化菜單程序。

    #include "MenuEx.h" // 添加頭文件
    class CMainFrame : public CMDIFrameWnd
    {
      ...
    public:
      HMENU InitMainFrameMenu();    // 初始化主菜單
      HMENU InitImageTypeMenu();    // 初始化文檔模板菜單
      
    protected: // CMenuEx members
      CMenuEx m_menuMainFrame;    // 主窗體沒有打開任何文檔時菜單
      CMenuEx m_menuImageType;    // 主窗體打開文檔時菜單(文檔模板菜單)
      
    protected: // CMenuEx''s image list members  
      CImageList  m_imageMenu;    // 菜單項正常的圖像列表
      CImageList  m_imageMenuDisable;  // 菜單項禁用時的圖像列表
      CImageList  m_imageMenuHot;    // 菜單項被選中時的圖像列表
      ...
    }    

  2. 撰寫菜單圖像索引表,初始化菜單程序,初始化菜單圖像列表程序, 和兩個重要的消息映射函數CMainFrame::OnMeasureItem()和CMainFrame::OnInitPopupMenu()。 (什麼?不會添加!,找ClassWizard幫忙或許有點幫助了:))

    // 聲明,因為下面的結構要用到 CMenuEx*,又不支持向後引用,又什麼辦法啊!
    class CMenuEx;
    //自繪菜單數據項結構,就是要傳給系統的那個牛X的LPCTSTR指針所指向的東東
    typedef struct tagMENUITEM
    {
      CString    strText;    // 菜單名稱
      UINT    nID;    // 菜單ID號
      // 分割條的ID是 0
      // 子菜單的ID是 -1
            
      CSize    itemSize;    // 菜單項的尺寸,不包括菜單圖像的尺寸
      
      CImageList*   pImageList;    // 菜單項的正常圖像列表
      CImageList*   pDisabledImageList;  // 菜單項的禁用圖像列表
      CImageList*   pHotImageList;  // 菜單項的選中圖像列表
      UINT    nImageIndex;  // 菜單項的圖像列表索引,-1表示沒有圖像
      
      BOOL    bIsSubMenu;    // 表示當前菜單項是否為子菜單項
      
      CMenuEx*  pSubMenu;    // 如果是一般菜單,該值為NULL
      // 如果bIsSubMenu為TRUE,該值為指向子菜單項的CMenuEx*指針
      
    } MENUITEM,*LPMENUITEM;
    ///////////////////////////////////////////
    // 在ManiFram.cpp 中添加菜單圖像索引表
    static UINT nMenuImageIndex[] =
    {
      ID_FILE_OPEN,
        ID_FILE_SAVE,
        ID_FILE_PRINT,    
        ID_EDIT_COPY,
        ID_EDIT_PASTE,  
        ID_EDIT_UNDO,
        ID_EDIT_REDO,    
        ID_APP_ABOUT,
        
        ID_IMAGE_LEVEL,
        ID_IMAGE_EQUALIZE,    
        ID_IMAGE_SMOOTH,
        ID_IMAGE_SHARP,    
        ID_IMAGE_SIZE,
        ID_IMAGE_RA,    
        ID_IMAGE_HISTOGRAM,    
        ID_ZOOMOUT,
        ID_ZOOMIN,
    };
    /////////////////////////////////////////////////////////////////////////////
    // 在ManiFram.cpp 中添加初始化菜單程序
    void CMainFrame::InitMenuImage()
    {
      // 初始化菜單圖像列表
      CBitmap bm;  
      
      m_imageMenu.Create(20, 20, TRUE | ILC_COLOR24, 9, 0);
      // 要問我IDB_SMALLMENUCOLOR是什麼,當然是是真彩位圖了,看圖說話了
      bm.LoadBitmap(IDB_SMALLMENUCOLOR);  
      m_imageMenu.Add(&bm,(CBitmap*)NULL);
      bm.Detach();
      // 還有IDB_SMALLMENUDISABLE
      m_imageMenuDisable.Create(20, 20, TRUE | ILC_COLOR24, 9, 0);
      bm.LoadBitmap(IDB_SMALLMENUDISABLE);  
      m_imageMenuDisable.Add(&bm,(CBitmap*)NULL);
      bm.Detach();
      // 還有IDB_SMALLMENUHOT
      m_imageMenuHot.Create(20, 20, TRUE | ILC_COLOR24, 9, 0);
      bm.LoadBitmap(IDB_SMALLMENUHOT);  
      m_imageMenuHot.Add(&bm,(CBitmap*)NULL);
      bm.Detach();  
      
    }
    /*
    IDB_SMALLMENUCOLOR
       
       
    IDB_SMALLMENUHOT
        
               
           
    IDB_SMALLMENUDISABLE
            
                  
    當然,要通過資源編輯器的Import功能將他們導入到資源文件中,不過因為是真彩,所以不能用VC的圖片編輯器編輯了。 告訴大家個敲門,我是用windows自帶的畫筆畫的:) */
    /////////////////////////////////////////////////////////////////////////////
    // 在ManiFram.cpp 中添加初始化菜單圖像列表程序
    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
      // 在CMainFrame::OnCreate中調用菜單圖標初始化程序
      。。。。。。
        
        InitMenuImage();
      
      。。。。。。
    }
    /////////////////////////////////////////////////////////////////////////////
    HMENU CMainFrame::InitMainFrameMenu()
    {  
      //初始化主菜單  
      m_menuMainFrame.LoadMenu(IDR_MAINFRAME);
      
      {
        // 這只加載圖像的一種方法,是一種兩步方法,先加載圖像列表
        m_menuMainFrame.SetImageList(&m_imageMenu);
        m_menuMainFrame.SetDisabledImageList(&m_imageMenuDisable);
        m_menuMainFrame.SetHotImageList(&m_imageMenuHot);
        
        // 再通過菜單圖像索引表為菜單加載圖像索引,
        m_menuMainFrame.SetImageIndex(nMenuImageIndex,
                      sizeof(nMenuImageIndex)/sizeof(UINT));  
      }
      
      // 也可以使用另外一種一步方法加載圖像
      /*
      // 假設MAINFRAM具有m_wndToolBar成員,並且已經設置了真彩位圖
      // 關於設置工具欄的真彩位圖,請參考 http://www.vckbase.com/document/viewdoc/?id=576
      // 或者看我的另外一篇文章 《完美實現真彩工具欄》(還沒寫出來那:))
         // 不過源程序裡面已經有實現方法了
      // 自己看也可以明白的
      m_menuMainFrame.LoadToolBar(&m_wndToolBar);  
      */
      
      return m_menuMainFrame.Detach();
    }
    /////////////////////////////////////////////////////////////////////////////
    HMENU CMainFrame::InitImageTypeMenu()
    {    
      // 初始化文檔模板菜單
      
      m_menuImageType.LoadMenu(IDR_IMAGETYPE);
      
      m_menuImageType.SetImageList(&m_imageMenu);
      m_menuImageType.SetDisabledImageList(&m_imageMenuDisable);
      m_menuImageType.SetHotImageList(&m_imageMenuHot);
      //通過菜單圖像索引表為菜單加載圖像索引
      m_menuImageType.SetImageIndex(nMenuImageIndex,sizeof(nMenuImageIndex)/sizeof(UINT));
      
      return m_menuImageType.Detach();
    }
    /////////////////////////////////////////////////////////////////////////////
    void CMainFrame::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
    {  
      // 記住,順序一定不能反,因為有些MFC自動添加的菜單是在CMDIFrameWnd::OnInitMenuPopup()
         // 中添加的.
      // 如果反了,當然就找不到新加入的菜單了
      CMDIFrameWnd::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
      // 靜態函數,看好了,別忘了寫CMenuEx啊  
      CMenuEx::InitPopupMenu(pPopupMenu, nIndex, bSysMenu);  
    }
    /////////////////////////////////////////////////////////////////////////////
    void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
    {
      // 都是她惹的禍"CMDIFrameWnd::OnMeasureItem()",不對子菜單項的尺寸進行測量
      // 害的我們只好映射這個函數了    
      CMDIFrameWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
      // 靜態函數,看好了,別忘了寫CMenuEx啊
      CMenuEx::MeasureItem(lpMeasureItemStruct);
    }   

  3. 在CXXXApp::InitInstance()中添加代碼,XXX代表你自己的程序了 BOOL CXXXApp::InitInstance()
    {
      ......
      CMultiDocTemplate* pDocTemplate;
      pDocTemplate = new CMultiDocTemplate(
        IDR_IMAGETYPE,
        RUNTIME_CLASS(CImageDoc),
        RUNTIME_CLASS(CChildFrame), // custom MDI child frame
        RUNTIME_CLASS(CImageView));
      AddDocTemplate(pDocTemplate);
      
      // create main MDI Frame window
      CMainFrame* pMainFrame = new CMainFrame;
      if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
        return FALSE;
      m_pMainWnd = pMainFrame;
      
      // 這些才是要添加的代碼,別弄錯了
      // 初始化文檔模板菜單
      pDocTemplate->m_hMenuShared=pMainFrame->InitImageTypeMenu();  
      // 初始化主窗體菜單
      pMainFrame->m_hMenuDefault=pMainFrame->InitMainFrameMenu();  
      
      // 更新,具體干什麼沒研究,反正不調用就出錯了:)
      pMainFrame->OnUpdateFrameMenu(pMainFrame->m_hMenuDefault);  
      // 要添加的代碼到這結束  
      ......
    }    

三、總結

說了這麼多,也不知道大家看明白沒有,沒關系,先貼個圖,大家看看效果再說了。

效果圖一,使用圖像索引表加載的小圖標菜單

效果圖一,工具條加載的大圖標菜單

四、結束語

感謝querw和BCMenu的作者,沒有他們的辛勤勞動,後人是沒辦法站在他們肩膀上的!由於程序寫的匆忙,難免有不盡人意和錯誤的地方,歡迎大家任意修改源程序:) 要說這個菜單做的完美,那是吹牛,世界上哪有完美的東西啊 :) 只要自己覺得完美,就夠了。 希望大家能從文章中學到點東西,就好。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved