分类: C/C++
2008-04-23 21:54:53
完美实现真彩自绘菜单
作者:
一、提出问题
在VCKBASE上读到《自绘菜单的实现》[作者:querw]。应用的我自己的正在进行的工程后发现效果不错,可是有存在许多问题。整个类的设计方面存在很多缺陷(先天,后天的),存在的主要问题如下:
使用BCMENU并且映射了这两个消息后的执行情况
使用BCMENU没有映射两个消息的执行情况
原作者分析的自绘的是因为把主菜单(top-level menu)的子菜单都加载成弹出菜单(popupmenu),是不正确的。真正的原因是因为MFC框架会自动调用CMenu的两个虚拟函数MeasureItem()和OnDrawItem()。
因此,当CMenuEx派生于CMenu,并且重写这两个虚拟函数以后。
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 0xX.\n", lpMeasureItemStruct->itemID); } else { ...... } ...... }
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); }菜单编辑器中没有分割条菜单的菜单
代码不够简练,程序粒度划分不好,可读性差(不过比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)三、实现方法
#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*,又不支持向后引用,又什么办法啊! 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); }
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的作者,没有他们的辛勤劳动,后人是没办法站在他们肩膀上的!由于程序写的匆忙,难免有不尽人意和错误的地方,欢迎大家任意修改源程序:)
要说这个菜单做的完美,那是吹牛,世界上哪有完美的东西啊 :) 只要自己觉得完美,就够了。 希望大家能从文章中学到点东西,就好。