一、提出問題
在VCKBASE上讀到《自繪菜單的實現》[作者:querw]。應用的我自己的正在進行的工程後發現效果不錯,可是有存在許多問題。整個類的設計方面存在很多缺陷(先天,後天的),存在的主要問題如下:
菜單編輯器中的模菜單樣
使用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
{
......
}
......
}
摘錄自原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執行的模樣
代碼不夠簡練,程序粒度劃分不好,可讀性差(不過比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的時候需要修改以下地方。
#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; // 菜單項被選中時的圖像列表
...
}
// 聲明,因為下面的結構要用到 CMenuEx*,又不支持向後引用,又什麼辦法啊!
當然,要通過資源編輯器的Import功能將他們導入到資源文件中,不過因為是真彩,所以不能用VC的圖片編輯器編輯了。 告訴大家個敲門,我是用windows自帶的畫筆畫的:)
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
*/
/////////////////////////////////////////////////////////////////////////////
// 在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);
}
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的作者,沒有他們的辛勤勞動,後人是沒辦法站在他們肩膀上的!由於程序寫的匆忙,難免有不盡人意和錯誤的地方,歡迎大家任意修改源程序:) 要說這個菜單做的完美,那是吹牛,世界上哪有完美的東西啊 :) 只要自己覺得完美,就夠了。 希望大家能從文章中學到點東西,就好。