[WTL]ComboBox 自绘
这里会详细介绍wtl中Drop List的自绘步骤,对于安装wtl、新建项目这些步骤这里不给予介绍,完整的项目代码会在文末给出。
自绘完成后的combobox
一些设置
我们可以用WTL AppWizard新建一个基于对话框的项目,在VS资源模板编辑器中添加ComboBox控件后,需要修改控件的一些属性
Owner Draw: Variable
创建一个自定义的combobox控件,控件中item的高度可以不相同
Has Strings: True
防止GetLBText()获取指定item文本时获取到乱码
设置好属性后,由于自绘控件中会用到一些常用类型(CImage、CString这些)和wtl增强消息映射宏。我们还需要在stdafx.h中包含下面几个头文件,和一些宏
#define _WTL_NO_CSTRING
#include <atlstr.h>
使用ATL::CString,防止和WTL::CString冲突
#define _WTL_NO_WTYPES
防止xxx类型重定义
#include <atlcrack.h>
wtl增强消息映射宏
#include <atltypes.h>
一些基本类型 CRect这些
#include <atlimage.h>
CImage
自绘
完成控件属性设置以及头文件添加后,我们需要新建一个ComboEx类,并继承CWindowImpl的派生类:CComboBox,并在MSG_MAP中添DEFAULT_REFLECTION_HANDLER(),让DefWindowProc()处理未处理的消息。主对话框MSG_MAP中添加REFLECT_NOTIFICATIONS(),以便可以在ComboEx中处理控件通知消息
class ComboEx : public CWindowImpl<ComboEx, CComboBox> { public: BEGIN_MSG_MAP(ComboEx) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() };
在主对话框 CMainDlg类中包含 ComboEx类所在头文件并添加一个全局变量m_combo,在OnInitDialog()使用SubclassWindow()子类化一个combobox控件
LRESULT CMainDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { m_combo.SubclassWindow(GetDlgItem(IDC_COMCOLOR)); m_combo.AddString(TEXT("RGB/红")); m_combo.AddString(TEXT("RGB/绿")); m_combo.AddString(TEXT("RGB/蓝")); m_combo.AddString(TEXT("HSL/色调")); m_combo.AddString(TEXT("HSV/饱和度")); m_combo.AddString(TEXT("HSV/明度")); m_combo.SetCurSel(3); CenterWindow(); CMessageLoop* pLoop = _Module.GetMessageLoop(); ATLASSERT(pLoop != NULL); pLoop->AddMessageFilter(this); pLoop->AddIdleHandler(this); return TRUE; }
在ComboEx类MSG_MAP中,我们需要添加一些消息宏来处理combobox的一些通知消息,如果需要查看增强消息宏对应的消息处理函数,可以选中对应的消息宏,按F12跳转到宏定义处,函数的定义在宏上方的注释中
BEGIN_MSG_MAP(ComboEx) MSG_WM_ERASEBKGND(OnEraseBkgnd) MSG_OCM_DRAWITEM(OnReflectedDrawItem) // 处理 WM_DRAWITEM MSG_OCM_MEASUREITEM(OnReflectedMeasureItem) // 处理 WM_MEASUREITEM MSG_WM_MOUSEMOVE(OnMouseMove) MSG_WM_MOUSEHOVER(OnMouseHover) MSG_WM_MOUSELEAVE(OnMouseLeave) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP()
按照前面创建ComboEx类的步骤,我们新建一个CListBoxEx类,留着后面使用。我们在ComboEx中添加一些常用的颜色宏以及变量
#define COLOR_COMBOBOX_TEXT RGB(174, 175, 178) #define COLOR_COMBOBOX_BK RGB(32, 34, 37) #define COLOR_COMBOBOX_NORMAL RGB(47, 49, 54) #define COLOR_COMBOBOX_HOVER RGB(83, 81, 107)
public: CListBoxEx m_list; COMBOBOXINFO m_cinfo; CRect m_rtCombo, m_rtBtn; // combobox以及combobox button控件的RECT CImage m_img; // button控件的图片 BOOL m_bHover = FALSE; // 鼠标是否在combobox上
颜色预览
添加完成后,在OnReflectedDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDIS)中,我们可以用lpDIS->hDC来初始化一个CDCHandle来绘制item,如果使用的是CDC,在不需要使用的时候用Detach()解除关联,CDC在析构的时候会调用::DeleteDC(),会出现异常。
item的RECT可以通过lpDIS->rcItem获取,lpDIS->itemState & ODS_SELECTED为当前item的状态,这里为hover或push。在前面我们设置了combobox Has Strings: True,通过GetLBText(lpDIS->itemID, str)我们可以获取到指定item的文本
void OnReflectedDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDIS) { CDCHandle dc(lpDIS->hDC); CRect rect(lpDIS->rcItem), rt = rect; COLORREF clList = COLOR_COMBOBOX_BK; if (lpDIS->itemState & ODS_SELECTED) clList = COLOR_COMBOBOX_HOVER; dc.FillSolidRect(rt, clList); dc.SetBkMode(TRANSPARENT); dc.SetTextColor(COLOR_COMBOBOX_TEXT); CString str; GetLBText(lpDIS->itemID, str); rt.left += 3; dc.DrawText(str, -1, rt, DT_VCENTER | DT_SINGLELINE); }
在OnReflectedMeasureItem()中我们可以修改combobox item的高度
void OnReflectedMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMIS) { lpMIS->itemHeight = 23; }
到了这里,我们的combobox的外观大概类似
移除边框
由于wtl中没有提供像mfc的PreSubclassWindow函数,我们可以重写SubclassWindow(),在子类化combobox的时候用GetComboBoxInfo()获取combobox信息到全局变量COMBOBOXINFO m_cinfo中,其中包含有listbox控件的句柄,这时候我们可以用之前创建好的CListBoxEx类关联combobox的listbox控件。
这里可以使用ModifyStyle(WS_BORDER, LBS_OWNERDRAWVARIABLE)移除listbox边框。
BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd == NULL); ATLASSERT(::IsWindow(hWnd)); BOOL bRet = CWindowImpl<ComboEx, CComboBox>::SubclassWindow(hWnd); m_cinfo.cbSize = sizeof(COMBOBOXINFO); GetComboBoxInfo(&m_cinfo); m_list.SubclassWindow(m_cinfo.hwndList); m_list.ModifyStyle(WS_BORDER, LBS_OWNERDRAWVARIABLE); LoadImageFromResource(&m_img, IDB_DROPLIST, TEXT("png")); m_rtCombo = m_cinfo.rcItem; m_rtBtn = m_cinfo.rcButton; return bRet; }
这时候,我们的combobox
我们会注意到listbox底部有几个像素的空白,在CListBoxEx类中处理WM_ERASEBKGND消息就不会出现空白了。
绘制组合框
前面我们已经在ComboEx类中添加了WM_MOUSEHOVER和WM_MOUSEMOVE消息处理函数,在OnMouseMove()中,我们可以用TrackMouseEvent(LPTRACKMOUSEEVENT lpEventTrack)来判断鼠标是进入还是离开了控件。
如果TRACKMOUSEEVENT结构中dwFlags成员设置了TME_HOVER | TME_LEAVE,函数则会发送WM_MOUSEHOVER、WM_MOUSELEAVE消息。
void OnMouseMove(UINT nFlags, CPoint point) { TRACKMOUSEEVENT tme; tme.cbSize = sizeof(TRACKMOUSEEVENT); tme.dwFlags = TME_HOVER | TME_LEAVE; tme.dwHoverTime = 1; tme.hwndTrack = m_hWnd; TrackMouseEvent(&tme); }
在WM_MOUSEHOVER、WM_MOUSELEAVE对应的消息处理函数中,我们可以用一个全局变量m_bHover保存当前鼠标是进入还是离开的状态,并使用Invaldate(TRUE)更新OnPaint()中combobox背景
void OnMouseHover(WPARAM wParam, CPoint ptPos) { if (!m_bHover) { m_bHover = TRUE; Invalidate(TRUE); } } void OnMouseLeave() { if (m_bHover) { m_bHover = FALSE; Invalidate(TRUE); } }
在ComboEx类中添加WM_PAINT增强消息处理宏以及对应的消息处理函数,通常情况下响应WM_PAINT消息后,combobox会被完全擦除,但点击combobox所在区域还是可以弹出下拉列表
在OnPaint()中,我们可以直接使用CPaintDC绘制一个矩形作为combobox的背景,只需要根据m_bHover的值来切换combobox背景色即可。
void OnPaint(CDCHandle handledc) { CPaintDC dc(m_hWnd); CRect rect; GetClientRect(rect);
dc.FillSolidRect(rect, m_bHover ? COLOR_COMBOBOX_HOVER : COLOR_COMBOBOX_NORMAL); CString str; GetWindowText(str); dc.SelectFont(GetFont()); dc.SetBkMode(TRANSPARENT); dc.SetTextColor(COLOR_COMBOBOX_TEXT); rect.left += 3; dc.DrawText(str, -1, rect, DT_SINGLELINE | DT_VCENTER); }
绘制下拉按钮
我们可以在重写的SubclassWindow()中使用LoadImageFromResource()加载项目资源中的png,在OnPaint()直接绘制,图片的位置可能需要手动调整
m_img.TransparentBlt(dc, m_rtBtn.left + 1, m_rtBtn.top + 6, m_img.GetWidth(), m_img.GetHeight(), RGB(255, 255, 255));
资源中图片加载函数
BOOL LoadImageFromResource(CImage* pImage, UINT nResID, LPCWSTR lpTyp) { if (pImage == NULL) return FALSE; pImage->Destroy(); // 查找资源 HRSRC hRsrc = ::FindResource(GetModuleHandle(NULL), MAKEINTRESOURCE(nResID), lpTyp); if (hRsrc == NULL) return FALSE; // 加载资源 HGLOBAL hImgData = ::LoadResource(GetModuleHandle(NULL), hRsrc); if (hImgData == NULL) { ::FreeResource(hImgData); return FALSE; } // 锁定内存中的指定资源 LPVOID lpVoid = ::LockResource(hImgData); LPSTREAM pStream = NULL; DWORD dwSize = ::SizeofResource(GetModuleHandle(NULL), hRsrc); HGLOBAL hNew = ::GlobalAlloc(GHND, dwSize); LPBYTE lpByte = (LPBYTE)::GlobalLock(hNew); ::memcpy(lpByte, lpVoid, dwSize); // 解除内存中的指定资源 ::GlobalUnlock(hNew); // 从指定内存创建流对象 HRESULT ht = ::CreateStreamOnHGlobal(hNew, TRUE, &pStream); if (ht != S_OK) { GlobalFree(hNew); } else { // 加载图片 pImage->Load(pStream); GlobalFree(hNew); } // 释放资源 ::FreeResource(hImgData); return TRUE; }
最后我们还需要响应WM_KILLFOCUS消息,消息处理函数中不作任何处理,防止鼠标第一次移动到combobox上的时候会闪一下
最后我们自绘的combobox完成了