循序渐进实现仿QQ界面(四):圆形按钮与工具栏自绘(转载)

这一篇本来应该演示如何实现仿QQ界面的中间客户区与底部工具栏,不过在实现底部工具栏的时候发现圆形按钮与工具栏自绘有不少取巧的方法,因此加插这么一篇,讲解一下如何实现圆形按钮和工具栏自绘。
前面几篇都是在讲解如何实现QQ顶部的标题栏,是用窗口贴图实现,也讲到底部区域会用不同的方法实现,因此这里底部的QQ按钮和工具栏不是在主窗口上画图了,而是用控件实现。并且这里讲解的方法不局限于使用RingSDK界面库及实现这个仿QQ界面程序,类似的效果用MFC或API都可以轻易实现。
讲到圆形按钮,大家一定会想到要实现按钮的自绘,然而有取巧的方法,就是用静态文本控件进行模拟,完全不需要自绘。先截个QQ的图作为资源,如下图:

这张图包含了两个按钮,一个收起/展开侧边栏的按钮和弹出主菜单的QQ按钮,两个都是圆形按钮。我们先建一个跟这张图一样大小的无边框的子窗口,用这张图作为窗口背景,想来这个大家都会,用RingSDK界面库只要调用一下SetBkgBitmap就一切搞定。然后建两个静态文本控件,一个13*13大小,一个36*36大小,位置正好覆盖那两个按钮。静态文本控件本来就是透明的,只要不设置文字,两个控件就跟不存在一样,不影响背景,但是占据了位置却正好可以模拟出按钮的动作。当然,这两个控件还要先CreateEllipticRgn,再SetWindowRgn一下,使其成为圆形,这样鼠标就必须进入到这个圆形按钮区域才会有响应动作,这下不用费劲在主窗口判断鼠标位置了:)
首先是模拟鼠标移上去的高亮状态,鼠标移到按钮上,父窗口是没有WM_MOUSEMOVE消息的,因此这里必须要由控件处理这个消息,需要子类化这两个静态文本控件,MFC有个第3方的静态文本控件,类名不记得了,是实现超文本链接的,如果有这个类的话,可以不用自己进行子类化,用这个类就可以了,RingSDK界面库则已经封装了这个功能,只要调用RingStatic::SetHyperlink就可以实现。实现这个超文本链接功能的原理是调用TrackMouseEvent,然后处理WM_MOUSEHOVER和WM_MOUSELEAVE消息,这两个消息会在鼠标移入控件和离开控件各发送一次,因此不需要象前面的贴图按钮一样用个标志记录是否绘制过按钮的各种状态,只需要在WM_MOUSEHOVER消息里绘制按钮的高亮状态,在WM_MOUSELEAVE消息里恢复按钮的原始状态就行了。界面库的RingStatic类因为在这两个消息里有自己的处理,没有把这两个消息转发给父窗口,而是发送了WM_LINKHOVER和WM_LINKLEAVE两个自定义消息,因此演示程序的绘制是在这两个消息里处理,实际是跟WM_MOUSEHOVER和WM_MOUSELEAVE消息一样的。绘制代码很简单,把两张用到的资源图片贴出来,大家一看就知道了,就是把图片的不同区域绘制到窗口。第2张图片采用连续绘制实现了动画显示,因为需要调色的关系,这张图片是实时生成的。

C/C++ code

RINGMSG(WndQQButton,WM_LINKHOVER) { if((HWND)param.wParam == m_btn->Handle()) { int sx = BTN_PICWIDTH; if(m_bIsSideToolHide) sx += m_dibArrBtn.Width()/2; m_dibArrBtn.Draw((BTN_WIDTH - BTN_PICWIDTH)/2,0,sx,0,BTN_PICWIDTH,BTN_PICHEIGHT,BTN_PICWIDTH,BTN_PICHEIGHT); } else if((HWND)param.wParam == m_btnQQ->Handle()) { m_dibQQBtn.Draw(0,0,0,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibQQBtn.Draw(0,0,QQBTN_WIDTH,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibQQBtn.Draw(0,0,QQBTN_WIDTH*2,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); } return TRUE; } RINGMSG(WndQQButton,WM_LINKLEAVE) { if((HWND)param.wParam == m_btn->Handle()) { int sx = 0; if(m_bIsSideToolHide) sx = m_dibArrBtn.Width()/2; m_dibArrBtn.Draw((BTN_WIDTH - BTN_PICWIDTH)/2,0,sx,0,BTN_PICWIDTH,BTN_PICHEIGHT,BTN_PICWIDTH,BTN_PICHEIGHT); } else if((HWND)param.wParam == m_btnQQ->Handle()) { m_dibQQBtn.Draw(0,0,QQBTN_WIDTH,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibQQBtn.Draw(0,0,0,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibBkg.Draw(0,0,QQBTN_X,QQBTN_Y,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); } return TRUE; }


代码里面的m_dibXXX几个对象在初始化的时候设定了绘制目标窗口是两个静态文本控件,因此这里看不到GetDC之类的代码,封装起来了,相应的因为目标DC是两个静态文本控件,坐标也比较好计算。这些代码改成GDI操作也比较容易。静态文本控件已经设置成圆形,DC以外的图象会绘制到父窗口,这样正好满足我们的要求,因为那个小按钮鼠标移上去会有一圈光晕(不是很明显),而这个光晕是在按钮范围以外。
除去初始化创建静态文本控件,图片调色和子类化静态文本控件代码以外,只要响应上面两个消息就实现了QQ2009左下角两个按钮的模拟,是不是很简单?子类化静态文本控件的代码可以去看RingSDK界面库的ringstatic.cpp或者是扩展的MFC超文本链接控件类的代码。QQ2009的这两个按钮没有按下的状态,但是很容易添加实现,演示程序里的相关代码也很容易就可以改成MFC或API的,可以实现自己的比较酷的按钮效果。顺便说一下,创建的静态文本控件必须带上SS_NOTIFY窗口类型,否则点击后父窗口是收不到消息的。父窗口的窗口类型不需要带上WS_CLIPCHILDREN和WS_CLIPSIBLINGS类型,否则不会绘制静态文本控件占据的区域背景。
接下来说一下工具栏的自绘。
实现自绘工具栏一种方法是子类化,自己处理其WM_PAINT消息。但是工具栏按钮的状态实在太多,按钮形态,扁平形态,高亮,按下,不可用等等状态,是否显示文字,是否显示下拉箭头等等,要做出一个通用的自绘工具栏实在是一件很麻烦的事。这里教你一个取巧的办法,以QQ2009的工具栏为例,子类化工具栏是逃不掉的,但是只要处理WM_ERASEBKGND消息,刷上你要的背景就可以了,不需要处理WM_PAINT消息。先创建工具栏,带上TBSTYLE_FLAT类型,扁平按钮,然后准备好如下两张图片:


接下来,下面所列出的代码是调用的API:
C/C++ code

HBITMAP = LoadBitmap(...); HIMAGELIST himg = ImageList_Create(20,20,ILC_COLOR32|ILC_MASK,9,4); //这里的参数请根据图片自行调整 ImageList_AddMasked(himg,hbm,0x00FF00FF); //指定紫红为透明色 SendMessage(hWndToolbar,TB_SETIMAGELIST,0,(LPARAM)himg);


另一张图片如法炮制,SendMessage(m_hWndToolbar,TB_SETHOTIMAGELIST,0,(LPARAM)himg);设置为高亮图案。然后发送TB_INSERTBUTTON消息给工具栏添加按钮,TBBUTTON结构的iBitmap设置为图片中相应按钮图案的序号(从0开始),这样生成的工具栏按钮图案是不局限于256色的。鼠标移到按钮上,就显示为第2张图相应的高亮图案,但是按钮周围还是有一个边框,这是系统帮你画的,高亮图案上已经画了个漂亮的边框,我们不需要这个边框,怎么去掉呢,父窗口响应NM_CUSTOMDRAW的WM_NOTIFY消息:
C/C++ code

case WM_NOTIFY: { LPNMHDR lpnm = (LPNMHDR)lParam; if(lpnm->code == NM_CUSTOMDRAW) { LPNMCUSTOMDRAW lpnmdraw = (LPNMCUSTOMDRAW)lParam; if(lpnmdraw->dwDrawStage == CDDS_PREPAINT) return CDRF_NOTIFYITEMDRAW; //指定通知按钮自绘 else if(lpnmdraw->dwDrawStage == CDDS_ITEMPREPAINT) return TBCDRF_NOEDGES; //其余交给系统绘制,指定不需要绘制按钮外边框 } break; }


要求自绘按钮时返回TBCDRF_NOEDGES,系统就不绘制按钮边框了,这样这个工具栏乍看就跟QQ2009的工具栏一样了。但是按钮按下去的状态就露馅了,因为高亮图案上画的边框也跟着图标一起按下去了,这个边框毕竟不是真的边框,只是高亮图案罢了。当然如果这个效果你能接受,那也可以了,至此打住,可以省不少事。
现在要使按钮按下去仅按下图标,边框位置不动。可是工具栏只能设置两个IMAGELIST,没有按下状态的IMAGELIST可以设置,因此只能自绘,就需要判断lpnmdraw->uItemState标志,如果是CDIS_SELECTED,就自绘按钮的按下状态,可是要自绘按钮,又很麻烦了,画边框,画图标,画文字...,嘿嘿,这里又有取巧的方法,我们只要画个按下状态的边框就可以了,还是返回TBCDRF_NOEDGES,其他的系统会帮我们画的。不过系统帮我们画上去的是高亮图案,又有一个按下去的边框,所以我们要取消高亮图案,创建工具栏的时候只需要发送TB_SETIMAGELIST消息设置按钮图标就可以了,高亮和按下的边框我们自己画,准备好下面一张图:

左边是高亮边框,右边是按下的边框。自绘时根据lpnmdraw->uItemState标志把这张图的相应边框画上去:
C/C++ code

case WM_NOTIFY: { LPNMHDR lpnm = (LPNMHDR)lParam; if(lpnm->code == NM_CUSTOMDRAW) { LPNMCUSTOMDRAW lpnmdraw = (LPNMCUSTOMDRAW)lParam; if(lpnmdraw->dwDrawStage == CDDS_PREPAINT) return CDRF_NOTIFYITEMDRAW; //指定通知按钮自绘 else if(lpnmdraw->dwDrawStage == CDDS_ITEMPREPAINT) { int sx; int off = (lpnmdraw->rc.bottom - lpnmdraw->rc.top - 20)/2; if((lpnmdraw->uItemState & CDIS_SELECTED)) sx = 20; else if((lpnmdraw->uItemState,CDIS_HOT)) sx = 0; else return TBCDRF_NOEDGES; HDC hMemDC = CreateCompatibleDC(lpnmdraw->hdc); SelectObject(hMemDC,hbm); //hbm为事先加载的边框图案的HBITMAP BitBlt(lpnmdraw->hdc,lpnmdraw->rc.left + off,lpnmdraw->rc.top + off,20,20,hMemDC,sx,0,SRCCOPY); DeleteDC(hMemDC); return TBCDRF_NOEDGES; //其余交给系统绘制,指定不需要绘制按钮外边框 } } break; }


OK,就这么简单,现在这个工具栏就跟QQ2009的工具栏的效果一样了。
现在说说演示程序里的工具栏实现,代码差不多,但是实际复杂得多,因为界面库对此进行了封装。界面库实现了任意子窗口和控件都可以停靠,因此QQ按钮窗口和工具栏是作为停靠窗口停靠在主界面的下方,只是设置了不能拖动。因为是停靠窗口,所以你在主窗口的WM_SIZE消息里看不到移动QQ按钮窗口和工具栏的代码,封装起来了。这个不是本篇重点,有兴趣的可以去看界面库里ringdocksite.cpp的代码。
现在看看程序的截图:

变化不是很大,本篇旨在教大家一个自绘按钮和自绘工具栏的比较简单的方法,不知道说清楚没有,有问题或是不同意见欢迎留言讨论。下一篇会实现QQ2009的客户区域和左下角按钮弹出的异型菜单。
演示程序下载地址: http://d.download.csdn.net/down/2025754/ringphone
本文来自CSDN博客,转载请标明出处: http://blog.csdn.net/ringphone/archive/2010/01/26/5259819.aspx
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值