Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9728568
  • 博文数量: 1227
  • 博客积分: 10026
  • 博客等级: 上将
  • 技术积分: 20273
  • 用 户 组: 普通用户
  • 注册时间: 2008-01-16 12:40
文章分类

全部博文(1227)

文章存档

2010年(1)

2008年(1226)

我的朋友

分类: C/C++

2008-04-23 21:54:53

完美实现真彩自绘菜单

作者:



一、提出问题

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

阅读(370) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~