[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完成了

 

完整项目

WTL_For_MFC_Programmer_CN

 

posted @ 2020-09-23 21:23  七叶草  阅读(392)  评论(0编辑  收藏  举报
Live2D