而我的审美观觉得我做的东西都不会很colorful,因为我是个素色主义者,平日里穿的衣服裙子都是黑白灰红的深女风。(咳跑题了)
于是我做的东西都是十分简约的扁平化设计。但是问了身边的朋友,貌似女生更能接受这种风格,男生貌似认为xp那种左边有条蓝边的更好看,我表示十分不能理解。
废话一大堆,入正题吧。
从网上的大多数例程来看,做菜单自绘主要分为两类,一类是继承CMenu,另一类是继承CObject完全自己画。对于新手来说,后者看起来就完全无力,而且前者已经基本满足了功能,所以本文主要讲继承CMenu的。
继承CMenu也有两种做法,一是在资源里画一个Menu,然后在程序中通过ID获取这个Menu,再进行重绘。另一种是完全靠AppendMenu来生成。看起来是第一种比较方便,要怎么排列都在资源里直观的表现出来,像我这种懒人一般能拖控件的我都不会自己手写代码。但事实上,要靠代码去改变一个已有控件的属性,其实比代码里设置好了再生成更麻烦。当然,本文两种方法都会分析,希望以后不要为了偷一点懒,而费更多的时间,走更多的弯路。
作为自绘菜单,首先当然是需要允许菜单自绘了,如果是资源里生成的,就需要遍历整个菜单,让每一项的属性都改为允许自绘,如果是代码生成,在AppendMenu的nFlags参数加上允许自绘就可以了。哦对了,允许自绘的宏定义是MF_OWNERDRAW。其实这么一来资源生成的菜单劣势就开始显示出来了,不过更坑的还在后面。先分析一下资源生成菜单更改属性的主要函数。(想直接看代码生成可以跳过这一part往下拉,从红色标题开始看起)
BOOL ModifyMenu(UINT nPosition,UINT nFlags,UINT_PTR nIDNewItem = 0,LPCTSTR lpszNewItem = NULL );nPosition填写需要修改项的索引可以填写项id(可以在资源那里查看)或者项pos(第几项)。如果选择id,nFlags要|MF_BYCOMMAND;如果选择pos,nFlags要|MF_BYPOSITION,表明用什么方式去找你的项。
nIDNewItem 目前的了解是,MeasureItem函数和DrawItem函数的参数里的itemID成员拿到的是什么。如果填id,后面在那两个自绘函数里面只能拿到id,pos同理。这个参数和前面nPosition不一定需要相同,前面用id后面用pos也是可以的,反之亦然。但是有很重要的一点,一旦确定了传输类型,在,MeasureItem函数和DrawItem函数里连CMenu的转换函数UINT GetMenuItemID(int pos)都不凑效了,传进去pos,出来还是pos,原因不明。这样后面做起来会相当麻烦,因为用id的话,分隔符id是0,子菜单id是-1,至于pos我忘记为什么不好用,反正做到最后无论id还是pos都有些功能实现不了,不太如意,最后一咬牙还是不得不用上最后一个参数。
lpszNewItem 允许放置任意的类(目前理解是这样)。这个方法大部分参考例程都这样做,开始就是不死心所以,最后还是得屈服。先在.h文件定义一个item的结构体,里面的东西看你需要,例程大多定义了三个 id,string,imageIndex,分别保存项id,项文字,项图标索引。然后通过强制转换成LPCTSTR 指针,传到函数中,那么在MeasureItem函数和DrawItem函数的参数的itemData成员再强转回来就ok了,这种方法的好处是结构体想定义什么就定义什么,nIDNewItem参数可以直接无视,完全私人订制无压力,哈哈哈哈哈。
贴上当时写的一段代码,这个也不舍得删,教训太惨痛了。
void NMenu::MenuToOwerDraw (CMenu* pMenu) { for(int i=0;i< pMenu- >GetMenuItemCount();i++) { UINT id = pMenu- >GetMenuItemID(i); CString str; pMenu->GetMenuString (i, str, MF_BYPOSITION ) ; ItemInfo *info = new ItemInfo; info->m_id = id; info->m_strText = str; info->m_icon = NULL; m_InfoList.AddTail (info); BOOL bModi = pMenu- >ModifyMenu( id, MF_BYCOMMAND |MF_OWNERDRAW, id,(LPCTSTR) info); if (!bModi) { ; printf("Modify Menu fail!"); } if(id == -1)//子菜单 { MenuT oOwerDraw(pMenu->GetSubMenu (i)) ;//递归调用 } } }
补上资源菜单获取代码
BOOL CMenuDlg::OnInitDialog () { ... menu.LoadMenu (IDR_MENU1);//menu为NMenu类型的数据成员 pMenu = (NMenu*)menu.GetSubMenu (0);//menu为NMenu*类型的数据成员 } void CMenuDlg::OnContextMenu (CWnd* pWnd, CPoint point) { pMenu->TrackPopupMenu(TPM_LEFTALIGN| TPM_LEFTBUTTON| TPM_RIGHTBUTTON,point.x,point.y,this); }
OnContextMenu函数是鼠标右击消息的处理,跟OnRButtonUp的区别是, OnContextMenu响应界面和控件上的消息,而OnRButtonUp仅在没有控件的地方响应。
还需要注意的是,menu不能是局部变量,不过后来有看到说用Detach()就可以了,有兴趣可以试试。
OnContextMenu是消息函数,这个无论哪一种方法都是需要这个函数的,但是资源画菜单还有一个比较坑的地方,就是尽管我的菜单获取用的都是继承类NMenu,而且也选择了自绘模式,但是菜单生成并不能自己跳转到自绘函数MeasureItem和DrawItem函数中,必须通过窗口指定,如果不指定,生成的菜单就是小小的黑黑的一小块。也就是说,其实用不着继承CMenu,你不嫌难看的话,可以直接在对话框窗口写代码的。这样对于以后封装使用有大大的麻烦,也就是最最不能接受的地方了。
void CMenuDlg::OnDrawItem (int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: 在此添加消息处理程序代码和/或调用默认值 //如果为菜 单发出的DrawItem消息 if (nIDCtl == 0) { m_menu->DrawItem (lpDrawItemStruct); } CDialogEx::OnDrawItem (nIDCtl, lpDrawItemStruct); } void CMenuDlg::OnMeasureItem (int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // TODO: 在此添加消息处理程序代码和/或调用默认值 //如果为菜 单发出的DrawItem消息 if (nIDCtl == 0) { m_menu->MeasureItem (lpMeasureItemStruct); } CDialogEx::OnMeasureItem (nIDCtl, lpMeasureItemStruct); }
好了,如果觉得上面净讲废话,不好意思,浪费大家时间,只是希望如果自己走过这些弯路别人就不要再走了。
下面就说说真真正正继承CMenu的自绘菜单~
提醒一点,在添加类的类向导里面是找不到选择CMenu类的,据说是因为CMenu已经被淘汰,现在用的是CMFCMenuBar,但是这个类的例程比较少,所以还是乖乖用CMenu好了。在添加类的时候,随便找一个基础类CObject或者别的什么的来继承,然后再手动改为CMenu,并且删掉消息传递的代码(CMenu不支持消息),就可以了。
1、首先根据自己的需求定义一个结构体:
struct ItemInfo { int m_id; int m_itemState; HICON m_icon; CString m_strText; CString m_strShortcut; int m_nFlags; };
前面说过,由于系统默的项id,分隔符是0,子菜单是-1,这样比较麻烦,每次只能通过pos找到,所以我定义的时候,将分隔符、子菜单的标识分开来,每项菜单都保存自己的id,分隔符和子菜单靠项状态来识别,比较方便索引。
项图标大部分例程都会用CImageList保存,然后在结构体里放一个索引,表示CImageList里第几个是该项图标,但是使用下来发现,用CImageList的draw画出来的图标会有黑边(我用的是资源里的图标,例程都是用系统里获取的图标貌似就不会),据说原因是CImageList是相当于把Icon转为图片显示,所以会有失真。找了好久发现用系统函数DrawIconEx可以解决这个问题,而这个函数用的参数是HICON,所以项图标保存的是HICON。
项文字和快捷键文字分开只是为了方便管理和排位罢了。
新建项参数,是最后加上去的,为了实现项移动的功能。项移动的时候,其实是删了重新添加,然后需要再写入nFlags,保存了就可以直接放进去了,就没有其他用处了,用不上可以不加的。ps CMenu貌似没有自带的移动函数吧,反正我找不到。。。
2、然后每次创建菜单项前,先创建一个对象,然后存在CList中,再作为参数添加到列表中。如果什么时候需要知道菜单的pos,看看在CList的第几个就知道了。
void NMenu::AppendItem (UINT id,CString strText,CString strShortcut,UINT iconID,UINT nFlags) { ItemInfo *info = new ItemInfo; info->m_id = id; if(iconID == 0) { info->m_icon = NULL; } else { info->m_icon = (HICON)::LoadImage (AfxGetInstanceHandle(),MAKEINTRESOURCE (iconID),IMAGE_ICON, 16,16,0); //info->m_icon = AfxGetApp ()- >LoadIconA(iconID) ; } info->m_strText = strText; info->m_strShortcut = strShortcut; info->m_itemState = 1; nFlags |= MF_OWNERDRAW; info->m_nFlags = nFlags; m_InfoList.AddTail (info); CMenu::AppendMenuA (nFlags, info->m_id, (LPCTSTR)info); }
从资源ID获得HICON,要用LoadImage而不是LoadIcon,用LoadIcon会导致一直的DrawIconEx绘图有点失真。网上说因为LoadIcon会有点缩放还是什么的,而LoadImage则不会。
3、传递子菜单和传递分隔符。
//子菜单 void NMenu::AppendSubMenu (UINT id,NMenu* subMenu,CString strText,UINT iconID,UINT nFlags) { ItemInfo *info = new ItemInfo; info->m_id = id; if(iconID == 0) { info->m_icon = NULL; } else { info->m_icon = (HICON)::LoadImage (AfxGetInstanceHandle(),MAKEINTRESOURCE (iconID),IMAGE_ICON, 16,16,0); } info->m_strText = strText; info->m_strShortcut = ""; info->m_itemState = -1; nFlags |= MF_POPUP|MF_OWNERDRAW; info->m_nFlags = nFlags; m_InfoList.AddTail (info); CMenu::AppendMenu (nFlags, (UINT) subMenu->GetSafeHmenu (), (LPCTSTR)info); } //分隔符 void NMenu::AppendSeparator (UINT nID,UINT nFlags) { ItemInfo *info = new ItemInfo; info->m_id = nID; info->m_icon = NULL; info->m_strText = ""; info->m_strShortcut = ""; info->m_itemState = 0; nFlags |= MF_SEPARATOR|MF_OWNERDRAW; info->m_nFlags = nFlags; m_InfoList.AddTail (info); CMenu::AppendMenu (nFlags, 0, (LPCTSTR)info); }
4、好了,接下来就是重中之重的两个自绘函数了。
CMenu的虚函数并不多,其中MeasureItem和DrawItem就是用来自绘的虚函数。在类向导里添加虚函数编写代码就可以了。
void NMenu::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // TODO: 添加您的代码以确定指定项的大小 lpMeasureItemStruct->itemWidth = NUM_ITEM_WIDTH; //ItemInfo *info; //info = (ItemInfo*)lpMeasureItemStruct->itemData; //if(info->m_itemState == 0) if(lpMeasureItemStruct->itemID == 0) { lpMeasureItemStruct->itemHeight = NUM_SEPARATOR_SPACE; } else { lpMeasureItemStruct->itemHeight = NUM_ITEM_HEIGHT; } }
每当菜单需要进行自绘时,都会先进来这个函数,拿到菜单尺寸。在前面添加分隔符的函数里,AppendMenu的第二个参数为0,同理于ModifyMenu里的nIDNewItem参数,把数据传递到itemID成员。如果不理解,也可以用我们自己的结构体里成员区别分隔符。当然更简单的可以忽略让分隔符和普通菜单尺寸相同。代码里的宏定义是我自己为了方便管理而定义的尺寸,随便填自己喜欢的尺寸即可。
void NMenu::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: 添加您的代码以绘制指定项 CString strText; CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC); //获取菜单项的设备句柄 ItemInfo *info = (ItemInfo*)lpDrawItemStruct->itemData; CRect rect(lpDrawItemStruct->rcItem); if(info->m_itemState==0)//分隔条 { pDC->FillSolidRect(rect,COLOR_BK); CRect r = rect; r.top =r.Height()/2+r.top ; r.bottom =r.top +NUM_SEPARATOR_HEIGHT; r.left += 5; r.right -= 5; pDC->Draw3dRect(r,COLOR_SEPARAROR,COLOR_SEPARAROR);//RGB(64,0,128)); return; } if (lpDrawItemStruct->itemState & ODS_GRAYED) { pDC->FillSolidRect(rect,COLOR_BK); pDC->SetTextColor(COLOR_DISABLE); } else if (lpDrawItemStruct->itemState & ODS_SELECTED ) { //在菜单项上自绘矩形框的背景颜色 pDC->FillSolidRect(rect,COLOR_SEL); //设置菜单文字颜色 pDC->SetTextColor(COLOR_TEXT); } else { pDC->FillSolidRect(rect,COLOR_BK); pDC->SetTextColor(COLOR_TEXT); } pDC->SetBkMode(TRANSPARENT); if(info->m_icon != NULL) { DrawIconEx(pDC->m_hDC,rect.left+7,rect.top+16,info->m_icon,16,16,0,NULL,DI_NORMAL); } //文字字体和字号设置 LOGFONT fontInfo; pDC->GetCurrentFont()->GetLogFont(&fontInfo); fontInfo.lfHeight = 18; lstrcpy(fontInfo.lfFaceName, _T("华文细黑")); CFont fontCh; fontCh.CreateFontIndirectA(&fontInfo); pDC->SelectObject(&fontCh); if(info->m_itemState == -1)//子菜单 { pDC->TextOutA(rect.left + 36, rect.top + 13, info->m_strText, info->m_strText.GetLength()); //::ExcludeClipRect(pDC->m_hDC,rect.right-15,rect.top,rect.right,rect.bottom); //DrawIconEx(pDC->m_hDC,rect.right-40,rect.top+7,AfxGetApp()->LoadIconA(IDI_ICON1),32,32,1,NULL,DI_NORMAL); } else { pDC->TextOutA(rect.left + 36, rect.top + 13, info->m_strText, info->m_strText.GetLength()); fontInfo.lfHeight = 16; CFont fontEn; lstrcpy(fontInfo.lfFaceName, _T("Arial")); fontEn.CreateFontIndirectA(&fontInfo); pDC->SelectObject(&fontEn); pDC->TextOutA(rect.left + 86, rect.top + 16, info->m_strShortcut, info->m_strShortcut.GetLength()); } }
菜单只用了背景色、选择色、文字色,如果喜欢更缤纷一点,也可以定义边框颜色,文字背景色等等。
代码里首先判断是否分隔符,因为不希望分隔符有其他状态,所以画了分割线后就马上返回。
然后对菜单当前状态进行判断,根据状态选择颜色。菜单识别禁用状态,选择状态和一般状态(else),禁用态用ODS_GRAYED和ODS_DISABLED并没什么区别。利用CMenu的EnableMenuItem设置菜单项禁用,菜单就能检测到这个状态了。
- m_pMenu->EnableMenuItem(ID_1_15,MF_BYCOMMAND|MF_GRAYED);
最后设置完字体和字号,菜单基本上就出来了,我用VS2012,菜单默认微软雅黑,可好看了,但是更担心如果客户系统里没有微软雅黑会不会变成宋体,所以设置了比较常见的华文细黑。而快捷键文字是英文的,用Arial会更好看一些。
菜单出来后,几乎都是自己定义的,唯独子菜单那个小箭头,好不好看就见仁见智了。59和60行两行代码就是把那个小箭头也弄掉,换上自己的。ExcludeClipRect网上大神介绍说可以阻止某个区域拦截不进行绘图,可以把小箭头区域拦截掉,在别的地方放上自己的箭头就可以了。是的每次,拦截掉系统的,我们自己的也画不上。只能挪到相对靠近的地方画,所以知道为啥我把这两句注释了吧,因为小黑箭头还算勉强在我的审美观范围内的,挪了位貌似更别扭。
5、去外边框,下钩子。
兴高采烈以为做完了,脑补出来美美的画面,去有一个煞风景的边框,是的,这个没办法去掉,是系统自带的。想没有边框你就自己画去吧,要是这样说估计你的屏幕上将是满满的口水了。网上介绍的两种去边框的方式,自己画菜单,或者下钩子。
至于什么是钩子,抱歉了,我这个新手是没办法解释了,还不如你自己问度娘来得准确。我的理解钩子就相当于预处理,比如你点击一下鼠标,系统接收到这个命令,准备实现点击之前,先问问钩子,要不要先做点什么,钩子说不许点,这个点击就没了,相当于return掉了,钩子说左移一下再点,钩子里处理左移,然后把点击传递下去。有点类似消息传递中的PreTranslateMessage吧。以上是我的理解,也是从度娘那拼拼凑凑来的,错了欢迎指正。
//下钩子需要调用SetWindowsHookEx,参数包括主窗口的实例句柄theApp.m_hInstance,可以在调用时作为参数传入, //同时也设一个参数作为是否安装钩子的标识,以便在退出时判断是否需要卸载钩子(UnhookWindowsHookEx(m_hook))。 //示例如下: void NMenu::InstallHook(HINSTANCE hInst) { // 需要移除边框时,要安装钩子 if (m_hook == NULL) { DWORD id = ::GetCurrentThreadId(); // 获取当前线程的ID m_hook = SetWindowsHookEx(WH_CALLWNDPROC,CallWndProc,hInst,id);//负责将回调函数放置于钩子链表的开始位置。 //HHOOK SetWindowsHookEx(int idHook;HOOKPROC lpfn;HINSTANCE hMod;DWORD dwThreadId); //idHook 指定了钩子的类型,WH_CALLWNDPROC 系统将消息发送到指定窗口之前的“钩子” //参数lpfn为指向钩子函数的指针,也即回调函数的首地址; //参数hMod标识了钩子处理函数所处模块的句柄; //参数dwThreadId 指定被监视的线程,如果明确指定了某个线程的ID就只监视该线程,此时的钩子即为线程钩子; //如果该参数被设置为0,则表示此钩子为监视系统所有线程的全局钩子。 //此函数在执行完后将返回一个钩子句柄。 } } void NMenu::UnInstallHook() { if (m_hook != NULL) { UnhookWindowsHookEx(m_hook); m_hook = NULL; } }
这两个函数是用于创建钩子和销毁钩子的,所以写的是成员函数,m_hook是HHOOK类型的数据成员。别人说要用全局钩子,如果没把握的还是全局,定义为数据成员只是觉得因为它只在成员函数里面调用。
下面的函数,经过一番努力,我能够知道基本流程,然后做了下注释,但是原理什么的,是一窍不通,我也是直接搬别人的来用。嗯效果不错。
static LRESULT WINAPI CallWndProc(int, WPARAM, LPARAM); // 安装的钩子的窗口过程 static LRESULT WINAPI MenuWndProc(HWND, UINT, WPARAM, LPARAM); // 用来处理菜单的窗口过程 WNDPROC oldWndProc = NULL; // 用来保存被替换的窗口过程 // 如果需要去除菜单的外部边框,需要通过安装钩子,设置外框属性并改变菜单大小 LRESULT WINAPI CallWndProc(int code, WPARAM wParam, LPARAM lParam) { CWPSTRUCT* pStruct = (CWPSTRUCT*)lParam; while (code == HC_ACTION)//HC_ACTION 为必须处理 { HWND hWnd = pStruct->hwnd; // 捕捉创建消息WM_CREATE,后面筛选为是否是菜单的创建 if ( pStruct->message != WM_CREATE) break; TCHAR sClassName[10]; int Count = ::GetClassName(hWnd, sClassName, sizeof(sClassName)/sizeof(sClassName[0])); // 检查是否菜单窗口,#32768为菜单类名 if ( Count != 6 || _tcscmp(sClassName, _T("#32768")) != 0 ) break; WNDPROC lastWndProc = (WNDPROC)GetWindowLong(hWnd, GWL_WNDPROC); //获得指定窗口的信息 GWL_WNDPROC得到窗口回调函数的地址或句柄 //获取传入进程中窗口的窗口过程,这个窗口过程用于接收和处理系统向窗口发送的消息 if (lastWndProc != MenuWndProc) { // 替换菜单窗口过程 SetWindowLong(hWnd, GWL_WNDPROC, (long)MenuWndProc); // 保留原有的窗口过程 oldWndProc = lastWndProc; } break; } //每一个钩子函数在进行处理时都要考虑是否需要把事件传递给下一个钩子处理函数。如果需要传递,就要调用函数CallNestHookEx()。 //在实际使用时还是强烈建议无论是否需要进行事件传递都要在过程的最后调用一次CallNextHookEx( ),否则将会引起一些无法预知的系统行为或是系统锁定。 return CallNextHookEx((HHOOK)WH_CALLWNDPROC, code, wParam, lParam); } // 处理菜单的窗口过程 LRESULT WINAPI MenuWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT lResult; switch (message) { case WM_CREATE://是否是菜单的创建 { // 首先要去掉菜单窗口的一些扩展风格 // 包括:WS_BORDER、WS_EX_DLGMODALFRAME、WS_EX_WINDOWEDGE lResult = CallWindowProc(oldWndProc, hWnd, message, wParam, lParam); DWORD dwStyle = ::GetWindowLong(hWnd, GWL_STYLE); DWORD dwNewStyle = (dwStyle & ~WS_BORDER); ::SetWindowLong(hWnd, GWL_STYLE, dwNewStyle); DWORD dwExStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE); DWORD dwNewExStyle = (dwExStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE)); ::SetWindowLong(hWnd, GWL_EXSTYLE, dwNewExStyle); return lResult; } case WM_PRINT: // 此处阻止非客户区地绘制 return CallWindowProc( oldWndProc, hWnd, WM_PRINTCLIENT, wParam, lParam); case WM_WINDOWPOSCHANGING: { // 最后,由于我们在MeasureItem里指定了菜单大小,而系统会自动替菜单加边框, // 因此必须去掉此部分额外地尺寸,将菜单大小改小 LPWINDOWPOS lpPos = (LPWINDOWPOS)lParam; lpPos->cx -= 2*GetSystemMetrics(SM_CXBORDER)+4; lpPos->cy -= 2*GetSystemMetrics(SM_CYBORDER)+4; lResult = CallWindowProc(oldWndProc, hWnd, message, wParam, lParam); return 0; } case WM_GETICON: return 0; default: return CallWindowProc( oldWndProc, hWnd, message, wParam, lParam); } }
END