最近要写一个MFC的对话框程序,发现要把MFC的对话框写的有色彩点并不容易,不像在C#里设置属性指就好,而是要自己去写一些代码完成对话框的绘画操作。比如一个简单的鼠标移入、移出操作,都要自己去写代码。由于我只用到了Button和Static两种控件,一切看上去还是比较顺利,所以谈谈自己的经验。
    1、对话框的背景
    MFC中没有属性能够设定对话框的背景颜色或是图片,需要我们在程序中进行操作。首先,需要实现WM_CTLCOLOR的消息操作,通过这个消息我们能够控制对话框
以及Static控件(包括Group)
的背景色、前景色。该消息的处理函数原型如下:
    HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
    其中,通过pDC能够文件的颜色、背景模式,通过pWnd可以获取正在绘制的控件ID,通过nCtlColor可以判断当前正在绘制的控件类型。这里我需要控制对话框的背景,所以要进行如下操作:
    HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);
    if (nCtlColor == CTLCOLOR_DLG)
    {
        return m_backgroundBrush;    //返回对话框背景的画刷
    }
    else if (
nCtlColor == CTLCOLOR_STATIC
)
    {
        
pDC->SetBkMode(TRANSPARENT);    //所有Static控件的背景色为透明
        if (pWnd->GetDlgCtrlID() == IDC_NOTE
)
        {
    pDC->SetTextColor(RGB(255, 255, 255));    //针对特殊的static控件,设置单独的文字颜色
        }
    }
    return hbr;
//不是要自绘的控件,返回默认值
    2、按钮(Button)控件
    一开始很奇怪,在
WM_CTLCOLOR的消息处理函数进行如下操作竟然没有用:
            
if (
pWnd->GetDlgCtrlID() == IDB_TEST
)  //按钮文字颜色
            
{
                pDC->SetTextColor(RGB(0, 0, 255));
            
}
    原来按钮控件的颜色、背景等属性无法通过
WM_CTLCOLOR消息实现,要改变这些属性,就必须要自己从CButton类继承一个类,然后改写其绘画函数——DrawItem。通过DrawItem函数的参数可以获取控件的大小、状态、类型、绘画DC等信息,有了它们,重绘Button就简单了。下面给出简单实例:
    void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
    {
if (lpDrawItemStruct->CtlType != ODT_BUTTON)  //由于继承自CButton,这一句肯定成立
{
return;
}
CRect rect =  lpDrawItemStruct->rcItem;
CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
UINT state = lpDrawItemStruct->itemState;
TCHAR strText[MAX_PATH] = {0};
::GetWindowText(m_hWnd, strText, MAX_PATH);
//获取按钮的状态
if (state & ODS_FOCUS)
{
m_bSelected = TRUE;
}
else
{
m_bSelected = FALSE;
}
//根据按钮的状态填充按钮的底色
if (m_bOver)
{
pDC->FillRect(&rect, &m_overBrush); 
}
else if (m_bSelected)
{
pDC->FillRect(&rect, &m_selectedBrush); 
}
else
{
pDC->FillRect(&rect, &m_normalBrush); 
}
// 画按钮标题,水平和垂直都居中。把DC当作一个画布
// 如果要使DT_VCENTER(垂直居中)有效,必须同时设置DT_SINGLELINE(单行)风格
if (_tcslen(strText) > 0)
{
pDC->SelectObject(m_font);
pDC->SetBkMode(TRANSPARENT);
pDC->SetTextColor(RGB(255, 255, 255));
pDC->DrawText(strText, &lpDrawItemStruct->rcItem, DT_SINGLELINE | DT_CENTER | DT_VCENTER); 
}
    }
    但是写了以上代码,发现还是不管用,原来,还需要设置按钮的属性为自绘类型,即需要设置BS_OWNERDRAW属性。这个可以在对话框的OnInitDialog函数或Button类的PreSubClassWindow函数中,调用Button的SetButtonStyle方法设置,如下:
    UINT uStyle = GetButtonStyle();       // 得到按钮的愿风格
    SetButtonStyle(uStyle | BS_OWNERDRAW);     // 加入自画风格
    为了判断按钮当前的状态,如是否有鼠标停留、焦点等,就需要跟踪鼠标的移动,这个可以通过WM_MOUSEMOVE消息实现。但是鼠标移入、移出的操作无法通过Vc的Wizard实现,在Vs2010里可以通过Wizard添加,但是函数不会被调用。为了能够知道鼠标移入、移出操作,需要在WM_MOUSEMOVE消息中增加如下代码,已跟踪鼠标事件:
        
if (!m_bTrack)    //记录是否已经设置跟踪事件
{
TRACKMOUSEEVENT ent = {0};
ent.dwFlags = TME_HOVER | TME_LEAVE;
ent.cbSize = sizeof(TRACKMOUSEEVENT);
ent.dwHoverTime = 10;
ent.hwndTrack = this->m_hWnd;
m_bTrack = TrackMouseEvent(&ent);
}
    通过以上代码,我们就可以收到WM_MOUSEHOVER、WM_MOUSELEAVE事件,通过将鼠标停留时间
ent.dwHoverTime(触发
WM_MOUSEHOVER的时间
)设置为足够短,我们就可以模拟出鼠标移入事件。为了能够处理这两个消息,需要通过MFCWizard增加
WM_MOUSEHOVER、WM_MOUSELEAVE的事件处理函数。
    
void CMyButton::OnMouseHover(UINT nFlags, CPoint point)
    {
m_bOver = TRUE;
InvalidateRect(NULL, TRUE);
CButton::OnMouseHover(nFlags, point);
    }
    void CMyButton::OnMouseLeave()
    {
m_bTrack = FALSE;
m_bOver = FALSE;
InvalidateRect(NULL, FALSE);
CButton::OnMouseLeave();
    }
    3、Static控件
    虽然可以通过对话框的
WM_CTLCOLOR消息设置Static控件的字体颜色和背景模式,但是如果要更好地控制其字体、背景等属性,以及添加鼠标事件,就要自己写代码了。方法很简单,和Button类似,需要自己写一个继承自CStatic的类,然后改写DrawItem方法,实现自己的绘画方法。
    只是有一点需要注意,默认情况下Static是不发送消息的,即鼠标点击、移动等事件是没有效果的,需要设置SS_NOTIFY属性,这可以在继承Static类的PreSubClassWindow中进行入操作设置:
    ModifyStyle(SS_TYPEMASK, SS_OWNERDRAW|
SS_NOTIFY
);
    以上代码同时设置自绘和消息通知属性。
    4、无标题对话框的移动
    在Windows的应用程序中,都可以通过点击标题栏拖拽窗口进行移动,但是如果窗口没有标题栏怎么办呢,即将对话框的Border属性设置为None,如何实现窗口移动呢?在我的对话框应用中,在窗口顶部放了一个Static控件,想把它作为窗口的标题栏,通过拖拽Static空间拖动窗口。有两种方法可以实习这个目的。
    一个很容易想到的方法是写一个继承自CStatic类的自绘控件,然后关联该类的对象到充当标题栏的Static控件。通过处理Static控件的WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTONUP消息,就可以得知鼠标是否按下、移动、松开,然后在
WM_MOUSEMOVE的处理函数计算鼠标移动的距离,然后获得父窗口的指针,通过MoveWindow函数移动父窗口。显然,这种方法比较复杂,需要进行的操作也比较繁琐。
    既然点击标题栏能够移动窗口,而点击其他地方却效果不同,那么windows肯定通过什么标志来判断要采取什么操作,我们应该能够不需要自己去实现移动窗口,而是告诉Windows,点击Static这个“标题”时就是点击真正的标题栏。控件在响应点击操作之前,都会发送WM_NCHITTEST消息,用于判断鼠标点击的位置,而消息处理函数的返回值就指出了位置。
WM_NCHITTEST的处理函数如下:
    LRESULT CWinAppDlg::OnNcHitTest(CPoint point)
    {
CRect rect;
GetDlgItem(IDC_TITLE)->GetClientRect(rect);
ClientToScreen(rect);
return rect.PtInRect(point) ? HTCAPTION : CDialogEx::OnNcHitTest(point);
    }
    在上面的代码中,首先获得Static控件IDC_TITLE的位置,然后判断鼠标点击位置是否位于控件内,如果是,则返回HTCAPTION,告诉Windows点击的是标题栏,否则返回默认值。显然这种移动窗口的做法要简单的多,而且它利用MFC的原理进行操作,更稳定也更具兼容性。这几句代码也有一个限制,即标题栏只能是Static控件,因为Static默认是不接收事件、消息,所以
WM_NCHITTEST由对话框进行处理,如果是Button等其他控件,
WM_NCHITTEST就会发送到控件而不是对话框上。