自绘RadioButton

前言:没想到这么久不来这里写东西了。其实真的是前段时间没什么东西好写的,毕竟肚子里面墨水不多。还有就是没有什么有价值的东西,只是觉得最近自己进步很慢,不过倒是不想以前那么散漫了,看到喜欢什么就学什么。看来我还是专心研究C++吧,呵呵。真是门很好很强大的语言。这里自己在做任务的时候接到了一个自绘RadioButton的控件,开始以为很简单,但是由于自己知识点的缺乏,以及对WINDOWS编程的生疏,导致一个很简单的问题拖了几乎一个礼拜才弄好,真的是非常郁闷的一件事情。不过正是印证了一句话,难者不会,会者不难啊。 

  自绘按钮控件其实是司空见惯的事情了,不过由于不知道其中的细节问题,并且说实话我在网上也没有搜到相关的解释资料(实在是我搜索能力的问题,这个问题的答案应该是很简单的),着实让我郁闷了很久。后来拿了别人的源码一看才恍然大悟,真是惭愧啊。惭愧的不是那技术我不懂,而是自己的懒惰,碰到拦路虎就绕开走了。

  RadioButton的自绘只牵涉到两个步骤,一个绘制前面单选的位图,剩下的就是绘制后面的文字了。绘制其实不麻烦,只要设置了BS_OWNERDRAW样式,并且在DrawItem内部得到CDC后按位置贴图和输出文字就可以了。关键的就是要去响应设置和按下的消息,并且要让Windows通过API去得到对应的标记位,做到跟系统提供的按钮一样。所以只是在鼠标按下和释放的时候绘制不同的位图上去是不够的,还需要设置其标记位(我没有自己去求证过,但是通过程序的编写和运行,可以得知,当设置了BS_OWNERDRAW样式后,CButton::SetCheck(int)和CButton::GetCheck()的调用是不能起到作用的,必须自己设置,但是内部的结构是如何,我无从知晓,如果能知道内部保存Check标记的变量的话,那就又好做了,只要直接设置其值就可以了,不过CButton::SetCheck(int)和CButton::GetCheck()的源码是可以看的,大家看一下就明白它是怎么做的了)。

  流程和关键点已经说明了,现在就来看下接下来会用到的一些知识点:

  1.自绘按钮

  要自绘按钮,首先要设置当前控件为BS_OWNERDRAW风格,这个可以在控件的PreSubclassWindow函数里面完成。比如RadioButton在属性里面是没有BS_OWNERDRAW样式可以选择的,那么就只能通过手动添加代码来达到这一目的:CWnd::ModifyStyle(0, BS_OWNERDRAW。然后重写DrawItem函数,从函数的LPDRAWITEMSTRUCT lpDrawItemStruct参数上得到CDC的句柄,并且通过CDC::FromHandle来转化为一个CDC指针。)

2.自绘一个位图到按钮上
这个其实是最简单的知识点了,就是在DrawItem里面得到一个CDC,然后确定位置后绘制,但是这里也有个绘制的方法,就是先要创建一个和目标CDC相同的设备环境,然后把这个位图选进去,最后再通过CDC::BitBlt函数从这个内存设备环境里把位图缩放显示到当前的CDC之上。这样就完成了绘制,这样做的好处就是能自由缩放一个位图大小,看起来失真小。
          3.响应鼠标消息
XP风格上的Button会响应鼠标的滑动消息,就是说,当鼠标经过按钮上方的时候,会有一个类似焦点的标记在按钮上出现,这就需要相应鼠标的 WM_MOUSEHOVERWM_MOUSELEAVE两个消息了。这来两个消息是要自己手动添加的,是_TrackMouseEvent( LPTRACKMOUSEEVENT)函数发送的,而TRACKMOUSEEVENT类型的参数是一个结构体,然后通过设置内部的dwFlags为 TME_HOVER 和 TME_LEAVE,它表明了当前鼠标需要发送的HOVER和LEAVE消息,以及判断并发送该消息的频率。然后,通过ON_MESSAGE( WM_MOUSEHOVER宏映射到处理函数中去就可以处理HOVER消息了,而LEAVE消息也是同样类似的。一般来说,这个消息是在WM_MOUSEMOVE中处理的。, func)
4.判断是否是ReleaseOutside
有很多种方法。首先设置一个成员变量,m_bPressed,然后再 WM_LBUTTONDOWN消息处理函数中设置m_bPressed为 true,然后在 WM_LBUTTONUP消息处理函数中用判断当前鼠标下方的窗口句柄和当前响应 WM_LBUTTONUP消息的窗口句柄是否相同。具体为设置两个变量:

POINT pt = point;
::ClientToScreen(m_hWnd, &pt);
HWND hWndMouseOver = ::WindowFromPoint(pt);
//  当鼠标在当前控件上,并且按键按下标记为 True
if(hWndMouseOver == m_hWnd && m_bPressed)
{
                   …
}
m_bPressed =  FALSE;

WindowFromPoint函数输入的点参数一定是要在屏幕坐标下,所以先要把point用ClientToScreen函数转化一下。
这样就能判断并处理ReleaseOutside状况了。不过我以前有另一个做法,但是由于不知道哪个更好,所以还是先用这个放到程序中,毕竟这个方法以前就是在实际程序中用着的。我自己的方法就是先设置一个 CRect,然后得到控件的区域,用 CRect的PtInRect(point)方法去判断当前鼠标是否在控件区域。
5RadioButton不能简单使用的状态标识
这里用了红色表示特别要注意的,因为当时就在这里被迷惑住了。
DrawItem( LPDRAWITEMSTRUCT)内部的这个 DRAWITEMSTRUCT结构如下:

typedefstructtagDRAWITEMSTRUCT{
         UINT CtlType; // 控件类型
         UINT CtlID;// 控件的ID
         UNIT itemID;//菜单项的索引
         UINT itemAction;// 绘图操作
         UINT itemState; // 状态
         HWND hwndItem; // 控件的窗口句柄
         HDC hDC; // 相关的设备环境
         RECT rcItem;//控件的范围
         DWORD itemData;// 指定与菜单项相联系的应用程序定义的32位值
DRAWITEMSTRUCT;

这里的具体属性可以去查询MSDN得到,而这个结构体内的itemState就是指明了当前空间的状态,比如是否 被选中、是否 获得焦点、是否 Check标志等等,而这里再其他地方不做具体的设置(指SetCheck和GetCheck),是不能得到正确的标记的。
6.HWND GetNextDlgGroupItem(HWND Hdlg, HWND hCtl, BOOL bPrevious)
这个API函数是用来返回同组控件的窗口句柄的。它会从给出的第一个Control(控件)开始,返回同组内下一个控件的窗口句柄。如果遭遇到下一个控件有Group标记的(应该是按照连续的资源ID顺序得到),那么就返回去取得本组第一个拥有Group标记的控件,并返回其窗口句柄。这里牵涉到一个正向和反向的取得GroupItem,通过第三个参数bPrevious指明: true就代表往前得到, false表示向后next得到。这个在下面的OnSetCheck函数中有用到。
7.处理Check标记
由于自绘的控件不能通过DRAWITEMSTRUCT中的itemState得到正确的状态,即使用SetCheck(1)后,用GetCheck()仍然是0。所以,要单独设置一个Check的标记。在成员变量中设置一个m_bChecked变量。这个就是自绘按钮控件的Check标记了。CButton::SetCheck和CButton::GetCheck的源码是这样写的:

_AFXWIN_INLINE  void CButton::SetCheck( int nCheck)
{  ASSERT(::IsWindow(m_hWnd)); ::SendMessage(m_hWnd, BM_SETCHECK, nCheck, 0); }
_AFXWIN_INLINE  int CButton::GetCheck()  const
ASSERT(::IsWindow(m_hWnd)); return (int)::SendMessage(m_hWnd, BM_GETCHECK, 0, 0); }

         先说下简单的GetCheck(),这里可以看到,返回的是处理 BM_GETCHECK消息的函数返回值,那么只需要把消息用ON_MESSAGE映射到一个处理函数里面,然后直接返回m_bChecked的值就是当前RadioButton的选中状态了。
再来看SetCheck,它内部发送了 BM_SETCHECK消息。我们在对一个RadioButton去SetCheck( BST_CHECKED的时候,应该对同组内的其他RadioButton做一个SetCheck(BST_UNCHECKED,说得更直接点儿,就是要把当前控件的m_bChecked设置成false。现在的关键就是如何去设置组内其它控件的m_bChecked了。6号知识点就提供了得到组内所有控件的方法,只需要简单循环一下就可以得到所有控件,然后响应设置其m_bChecked为false即可。需要注意的是,不要重复发送WM_UNCHECK(自定义的)消息。程序里面是采用设定一个链表,然后开始就把被按下的空间句柄放入,然后下面就开始6号知识点的循环,循环中,每得到一个同组的控件的句柄就和链表内数据比对下,如果句柄相同,那么就直接退出循环。因为一般只有一种情况,就是所有同组控件遍历完毕,并且回到开始点击的那个RadioButton,这个RadioButton也是不需要发送WM_UNCHECK消息的。))
WM_UNCHECK的处理函数内,只需要简单设置m_bChecked = false就OK了。
8.CWnd::GetCheckedRadioButton(int nIDFirstButton, int nIDLastButton)
这个CWnd提供的函数,会返回资源ID号在nIDFirstButton到nIDLastButton之间被Check的控件的资源ID。如果没有被Check的控件,那么就返回0。
它内部其实很简单的用一个循环,判断CWnd::IsDlgButtonChecked(int uId)来得到传入的uId对应的控件是否是Check的。而这个函数内部又调用了API的IsDlgButtonChecked函数,源码是这样的:

COleControlContainer* m_pCtrlCont;  // for containing OLE controls
UINT  CWnd::IsDlgButtonChecked(int nIDButton) const
{
ASSERT(::IsWindow(m_hWnd));
 
if (m_pCtrlCont ==  NULL)
return ::IsDlgButtonChecked(m_hWnd, nIDButton);
else
          return m_pCtrlCont->IsDlgButtonChecked(nIDButton);
}

MSDN上面在Remarks上说明,这个函数会给对应的控件发送 BM_GETCHECK消息。这就是回到了上面的 BM_GETCHECK处理函数里面了。
好了,具体要用到的知识点大体都在上面了。接下来讲一下关于位图的存放问题。我这里做的位图一共是6个状态: 选中未选中选中高亮未选中高亮按下以及 不可用等。次序就是如上,我做了一个枚举如下:
typedef  enum  _RadioState_
{
    RADIOSTATE_SELECT,                
//  选中
    RADIOSTATE_UNSELECT,             //  未选中
    RADIOSTATE_SELMOUSEHOVER,         //  选中状态鼠标滑过
    RADIOSTATE_UNSELMOUSEHOVER,         //  未选中状态鼠标滑过
    RADIOSTATE_PRESS,                 //  选中状态按下
    RADIOSTATE_DISABLE                 //  不可用(灰色)
} RadioState;

  关于高亮的解释就是当鼠标滑过按钮时,有一个表示可以捕获焦点的提示性显示。具体在位图上表现为一圈阴影。当前位图的存储格式为单行存储,一行6个位图为一套。绘制的时候,用这个枚举变量的值作为位图状态的偏移量来裁剪需要的状态位图到设备环境中。

  大体的说明介绍到这里,下面提供具体的源码,压缩包内提供响应的使用说明。觉得好用的话大家可以随意使用,但是请不要删除最上方的说明信息,谢谢。如果能提出您宝贵的意见就更好了,欢迎交流。

呃,好老的帖子了。好久没回来了,当时源码没写到帖子里(写到评论里面了),抱歉。现在在帖子里面补上。

http://download.csdn.net/detail/Sozell/350429

你可能感兴趣的:(自绘RadioButton)