MFC 控件重绘

1. MFC 控件的重绘原理

MFC 的基础控件有很多,常见的就是按钮、列表、标签、输入框等,通常一个应用程序的界面都是由这些小小的控件组合在一起形成了最终的用户界面。系统为每一个基础控件都绘制了一个默认的外观,让我们看个例子:

MFC 控件重绘_第1张图片

大家可以看到,这是一个基础的 MFC 的对话框,上面有 2 个按钮,2 个标签,2 个输入框,也就组合成了大家熟悉的登录界面。这个界面确实非常"朴素",可能不太符合当下的审美需求,那如果要美化这个界面,要怎么做呢?通常有两种做法:

第一种,找美工设计界面,然后给你切图,直接贴图到程序界面上,就是做网站一样,这种做法的好处是程序实现上相对容易,但是美工的工作量大,比如一个按钮就可能有 4 种状态,如果是 10 个不同的按钮,那就是 40 张图,如果按钮大小还不一样,那又会针对各个尺寸出图,灵活性不够,而且最后的程序资源会非常大。

第二种,也就是我们今天讲的重绘控件,相当于你不用系统默认的控件外观了,而是自己实现控件的外观绘制,定制出符合自己需求的外观,这种方法的优点是灵活,资源占用小,能自适应控件尺寸,方便进行换肤。缺点是程序员需要深入理解控件重绘的过程,以及掌握 GDI,GDI+的绘制函数,才能实现相关效果。

OK,说了这么多,让我们先来了解下 MFC 都是在什么地方绘制控件的外观的。

首先我们需要知道几个重要的跟绘制相关的 WINDOWS 消息和对应的消息处理函数 WMPAINT ==> OnPaint WMNCPAINT ==> OnNcPaint WMDRAWITEM ==> OnDrawItem WMERASEBKGND ==> OnEraseBackground WM_CTRCOLOR ==> OnCtrlColor

MFC 大部分控件都继承自 CWnd 类,当一个 Windows 窗口需要重新绘制的时候,系统就会向该窗口投递 WMPAINT 消息,窗口通过 WMPAINT 的消息响应函数 OnPaint 实现客户区域绘制过程。

对于窗口的非客户区域,比如窗口的标题栏的绘制,就不是通过 WMPAINT 消息,而是响应 WMNCPAINT 消息,在 OnNcPaint 里面进行绘制的。

那么 WMDRAWITEM 又是什么消息呢?当一个控件具有 OWNER DRAW 的属性并且需要重绘的时候,那么其父窗口就会响应 WMDRAWITEM 消息,调用 OnDrawItem 函数,在这个函数里面调用子控件的 DrawItem 函数,完成子控件的重绘。

接下来看:WMERASEBKGND,这个消息是用来擦除背景的,就好比你在一个画板上画画的时候,你先得把已经画好得内容擦掉,换一个干净的背景。在响应 OnPaint 的时候,都会先触发 WMERASEBKGND,我们可以在其响应函数 OnEraseBackground 里面准备好我们需要的背景。

WM_CTRCOLOR 是子控件将要绘制的时候,向父窗口发送的消息,父窗口可以在这个消息里面设置子控件 DC,前景色、背景色、画刷等等,也可以把这个消息反射给子控件自己处理。

让我们用一张图总结一下:MFC 控件重绘_第2张图片理解了上面的窗口和界面的绘制过程和消息以后,我们就找到了重绘控件的方法。 对于支持 OWNERDRAW 属性的控件,我们一般会通过重载控件的 DrawItem 函数来进行控件外观的重新绘制。 为什么不在 OnPaint 里面重绘控件呢?其实也可以,但是如果通过 OnPaint 来重绘的话,你就得从 0 开始,绘制要复杂很多。

接下来我们着重讲的就是支持 OWNERDRAW 属性的控件重绘。

2. 常见控件的重绘制示例

  • CButton 按钮控件应该是应用程序界面中必备的控件之一,也是最简单的控件,它支持 OWNERDRAW 属性,那接来下我们就来重绘一个按钮,看代码:
class CCustomDrawBtn : public CButton
{
    DECLARE_DYNAMIC(CCustomDrawBtn)

public:
    CCustomDrawBtn();
    virtual ~CCustomDrawBtn();
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
    virtual void PreSubclassWindow();
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
    afx_msg void OnTimer(UINT nIDEvent);
    ...
    省略非关键代码
};

第一步我们自绘的按钮类继承 CButton,因为我们只是重绘外观,对按钮控件 行为还是使用默认行为,重点是重载函数是:

virtual void PreSubclassWindow();
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);

PreSubclassWindow 是窗口的子类化,可以在这里修改窗口的属性,我们看下具体实现。

void CCustomDrawBtn::PreSubclassWindow()
{

    ModifyStyle(0, BS_OWNERDRAW); // 修改按钮风格,支持 OWNERDRAW
    CButton::PreSubclassWindow();
}

目的就是修改按钮的默认风格,支持 OWNERDRAW,因为系统默认按钮控件是没有这个属性的。 DrawItem 这个是核心的重绘制函数,我们前面讲了子控件就是通过这个函数来绘制自身的外观,因此我们就在这个函数里面,把按钮绘制成我们想要的样子。

void CCustomDrawBtn::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
        CRect rect,rectFocus;
        GetClientRect(&rect);
        //焦点框
        rectFocus.CopyRect(&rect);
        rectFocus.DeflateRect(4,4,4,4);

        COLORREF textColor = m_textColor;
        COLORREF borderColor = m_borderColor;
        //禁用状态绘制
        if(lpDrSt->itemState & ODS_DISABLED)
        {
            bkColor = SKinColors::lightgray;
            textColor = SKinColors::dark_gray;
            borderColor = SKinColors::dark_gray;
        }
        CPen pen(PS_SOLID,1,borderColor);//边框颜色
        CPen* pOldPen = pDC->SelectObject(&pen);
        CFont font;
        font.CreateFont(m_fontSize,0,0,0,700,FALSE,FALSE,FALSE,DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH,"宋体");
        CFont* pOldFont = pDC->SelectObject(&font);
        // 填充颜色
        CBrush brush;

        brush.CreateSolidBrush(bkColor);
        pDC->SelectObject(&brush);
        pDC->RoundRect(0,0,rect.right,rect.bottom,roundRadis,roundRadis);
        pDC->SelectObject(pOldPen);
        //绘制按钮文本
        //定义一个 CRect 用于绘制文本 
        CRect textRect;
        //拷贝矩形区域 
        textRect.CopyRect(&m_rcDrawClient);
        //获得字符串尺寸
        CString btnCaption = m_btnText;
        CSize sz = pDC->GetTextExtent(btnCaption);
        //调整文本位置 居中 
        textRect.top += (textRect.Height()- sz.cy)/2;
        //设置文本背景透明 
        pDC->SetBkMode(TRANSPARENT);
        //设置文本颜色
        pDC->SetTextColor(textColor); 
        //绘制文本内容
        pDC->DrawText(btnCaption,&textRect,DT_RIGHT|DT_CENTER|DT_BOTTOM);

        if (lpDrSt->itemState & ODS_FOCUS)
        {
            ...焦点状态下的外观绘制
        }
        ...省略其他代码
}

效果图:

按钮效果

上面就是绘制的核心代码了,涉及了很多 GDI 函数和 Windows 系统函数,希望不熟悉的自己查一下,文章的最后我会给出 GitHub 地址,大家可以去看下完整的 demo 代码。

  • CEdit 编辑控件也是非常常用的界面控件,用于接收用户的输入,通常我们可以通过重绘改变其背景色,字体大小,边框等,难点是它不支持 OWNER DRAW 属性,所以这里介绍一种方式是通过 CStatic 控件和 CEdit 控件的组合来实现效果,比如圆角边框,看下例子:
class CCEditEx : public CStatic
{
    DECLARE_DYNAMIC(CCEditEx)

public:
    CCEditEx();
    virtual ~CCEditEx();
    ...

protected:
    DECLARE_MESSAGE_MAP()
    virtual void PreSubclassWindow();
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
    afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
    void DrawBorder(CDC* dc, CRect &rc);
private:
    CEdit m_edit;
};

可以看到我们的重绘类 CEditEx 是继承 CStatic,然后其包含了一个成员:m_edit 绘制代码

void CCEditEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{

    CRect rc;
    rc = lpDrawItemStruct->rcItem;

    CDC dc;
    dc.Attach(lpDrawItemStruct->hDC);
    dc.SelectObject(&m_bkBrush);
    //绘制边框
    DrawBorder(&dc,rc);

    rc.DeflateRect(m_padding, m_padding);

    if (m_edit.GetSafeHwnd() && !m_bDisable) 
    {
        m_edit.ShowWindow(SW_SHOW);
        m_edit.SetBkColor(m_bkColor);
        m_edit.SetWindowText(m_strText);
    }
    else if(m_edit.GetSafeHwnd() && m_bDisable)
    {
        m_edit.EnableWindow(FALSE);
        m_edit.SetBkColor(m_bkColor);
        m_edit.SetWindowText(m_strText);
    }
    else 
    {
        //首次创建子控件
        GetClientRect(&rc);
        rc.DeflateRect(10,10);
        m_edit.Create(WS_CHILD | WS_VISIBLE | ES_LEFT | ES_AUTOHSCROLL, rc, this, 1);
        m_edit.SetFont(GetFont());
        if(m_bPwdInput){
            m_edit.SetPasswordChar('*');
        }
        m_edit.SetWindowText(m_strText);
        m_edit.SetBkColor(m_bkColor);
        m_edit.ShowWindow(SW_SHOW);
        if(m_bDisable)
        {
            m_edit.EnableWindow(FALSE);
        }

    }
    ReleaseDC(&dc);
}

效果图:

输入框效果

这里其实可以看到 medit.Create(WSCHILD | WSVISIBLE | ESLEFT | ES_AUTOHSCROLL, rc, this, 1); edit 作为其子控件创建出来,CStatic 主要绘制边框,完整代码请看最后的 GitHub 地址。

  • CComboBox 组合框控件,这个控件的绘制要复杂一些,分为两个部分,一是文字输入下拉框部分,一个外面的边框和三角箭头按钮。
class CCustomDrawCombox : public CComboBox
{
    DECLARE_DYNAMIC(CCustomDrawCombox)
    afx_msg void OnPaint();

public:
    CCustomDrawCombox();
    virtual ~CCustomDrawCombox();
private:
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
    virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
    virtual int CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);
private:
    virtual void PreSubclassWindow();
    ...
};

核心有两个一个是 DrawItem 里面绘制下拉框样式,一个是在 OnPaint 里面绘制 外层部分。

void CCustomDrawCombox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    CString strDrawText;
    if(!lpDrawItemStruct->itemID!=-1)
    {
        GetLBText(lpDrawItemStruct->itemID,strDrawText);
    }
    else
    {
        strDrawText = "请选择";
    }
    TEXTMETRIC TextMetr;
    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    if(pDC)
    {
        pDC->GetTextMetrics(&TextMetr);
        CRect rcClient;
        GetClientRect(&rcClient);

        if((lpDrawItemStruct->itemState & ODS_SELECTED)&&
            (lpDrawItemStruct->itemAction&(ODA_DRAWENTIRE|ODA_SELECT)))
        {
            pDC->FillSolidRect(&lpDrawItemStruct->rcItem,RGB(101,160,251));
            pDC->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
            pDC->SetTextColor(RGB(255,255,255));
            pDC->TextOut(lpDrawItemStruct->rcItem.left,lpDrawItemStruct->rcItem.top,strDrawText);
        }
        else if (lpDrawItemStruct->itemAction&(ODA_SELECT|ODA_DRAWENTIRE))
        {
            pDC->FillSolidRect(&lpDrawItemStruct->rcItem,RGB(255,255,255));
            pDC->SetBkColor(::GetSysColor(COLOR_WINDOW));
            pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
            pDC->TextOut(lpDrawItemStruct->rcItem.left,lpDrawItemStruct->rcItem.top,strDrawText);
        }
        ReleaseDC(pDC);
    }

}

下面是画外层的效果

void CCustomDrawCombox::OnPaint()
{
    CPaintDC dc(this); // device context for painting
    //绘制客户区
    CDC dMemDC;
    dMemDC.CreateCompatibleDC(pDC);
    dMemDC.SetMapMode(pDC->GetMapMode());

    //画动作
    CBitmap mNewBmp;
    CRect rc;
    GetClientRect(&rc);

    mNewBmp.CreateCompatibleBitmap(pDC, rc.right - rc.left, rc.bottom - rc.top);
    CBitmap* pOldBmp = dMemDC.SelectObject(&mNewBmp);
    CPen pen(PS_SOLID,1,RGB(200,200,200));
    CPen *pOldPen = dMemDC.SelectObject(&pen);
    CBrush bkBrush;
    bkBrush.CreateSolidBrush(RGB(255,255,255));
    dMemDC.SelectObject(&bkBrush);
    CPoint pt(10,10);
    dMemDC.Rectangle(rc);//画整个客户区域

    CRect rcEnd(rc);//按钮区域
    rcEnd.left = rc.right - 20;
    //画右边的三角形按钮
    CBrush bkBrushRect;
    bkBrushRect.CreateSolidBrush(RGB(21,123,237));
    dMemDC.SelectObject(&bkBrushRect);
    dMemDC.Rectangle(rcEnd);
    //画三角形
    CRgn rgn;
    CPoint ptAngle[3];
    int angleSideWidth = 8;//三角形边长
    //第一个点的坐标
    ptAngle[0].x = rcEnd.left+rcEnd.Width()/2-angleSideWidth/2;
    ptAngle[0].y = rcEnd.top+rcEnd.Height()/2-2;
    //第二个点的坐标
    ptAngle[1].x = ptAngle[0].x + angleSideWidth;
    ptAngle[1].y = ptAngle[0].y;
    //第三个点的坐标
    ptAngle[2].x = rcEnd.left+rcEnd.Width()/2;
    ptAngle[2].y = ptAngle[0].y + 5;
    CBrush brushAngle;
    rgn.CreatePolygonRgn(ptAngle, 3, ALTERNATE); //创建区域  
    brushAngle.CreateSolidBrush( RGB(255,255,255) ); //创建画刷
    dMemDC.FillRgn( &rgn, &brushAngle ); //填充区域
    //dMemDC.DrawFrameControl( &rcEnd,DFC_SCROLL,DFCS_SCROLLDOWN|DFCS_FLAT|DFCS_MONO );
    pDC->BitBlt(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, &dMemDC,
        rc.left ,rc.top, SRCCOPY);
    //恢复
    dMemDC.SelectObject(pOldBmp);
    dMemDC.SelectObject(pOldPen);
    pOldPen->DeleteObject();
    pOldBmp->DeleteObject();
    dMemDC.DeleteDC();
    bkBrush.DeleteObject();
    bkBrushRect.DeleteObject();
    brushAngle.DeleteObject();
}

效果图:自绘 combbox

  • CListCtrl 最后说一下列表控件,这个控件的绘制应该说是几个里面最复杂的一个,包含两个部分,一个是 header 的绘制,一个是每一个列表项的绘制,当然原理都是一样,我的 demo 里面有完整例子,大家可以自己去下载来看一下,这里就不再贴代码了。

3. MFC 通用皮肤库的实现思路

经过上面的讲解,相信大家对简单的 MFC 控件的自绘制有了一个初步的认识,通过上面的方法我们确实可以自绘控件,但是大家可能都接触过有一类皮肤库,它们可以在修改少量代码的情况下,就可以把你应用程序里面所有的控件都换成自定义的皮肤,这是怎么做的呢?

这类皮肤库通常是通过 WIN32 的消息函数勾子,底层截获了应用程序的相关 WIN32 绘制消息,并进行过滤和处理,需要对整个 WIN32 程序的运行过程,WINDOWNS 消息机制有深入理解后,方能实现,感兴趣的朋友可以自行查资料做深入研究。

最后本 Chat 相关的 GitHubi 地址: https://gitee.com/xmsharp/MFCSkin.git 由于本人水平有限,只能抛砖引玉,希望大家有所收获。

MFC 控件重绘_第3张图片

 

你可能感兴趣的:(MFC,mfc,重绘)