Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2096346
  • 博文数量: 909
  • 博客积分: 4000
  • 博客等级: 上校
  • 技术积分: 12260
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-06 20:50
文章分类

全部博文(909)

文章存档

2008年(909)

我的朋友

分类:

2008-05-06 22:20:47

一起学习
完美实现真彩自绘菜单

作者:阿福(geforce_zf)

下载源代码

一、提出问题

  在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 0xX.\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的作者,没有他们的辛勤劳动,后人是没办法站在他们肩膀上的!由于程序写的匆忙,难免有不尽人意和错误的地方,欢迎大家任意修改源程序:) 要说这个菜单做的完美,那是吹牛,世界上哪有完美的东西啊 :) 只要自己觉得完美,就够了。 希望大家能从文章中学到点东西,就好。
 

下载本文示例代码


完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单完美实现真彩自绘菜单
阅读(399) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~