Windows开发进阶之VC++中如何实现对话框的界面重绘

发布时间:2018-12-21
技术:Windows 系统+Visual studio 2008

概述

应用程序界面是用户与应用程序之间的交互的桥梁和媒介,用户界面是应用程序中最重要的组成部分,也是最为直观的视觉体现。对用户而言,界面就是应用程序,界面设计的好坏,会直接影响应用程序的可用性,从而影响用户的体验。 在软件开发过程中,对界面的设计椅子都是一项很重要的技术,如今的应用软件界面可谓是“丰富多彩、美丽绝伦”,如大家熟悉的360安全卫士、腾讯QQ聊天软件、Visual C++ 编程词典软件等,都是非常不同于普通的对话框应用程序,因为他们的界面都是重新绘制过的,从而实现了漂亮、易用的用户体验。鉴于VC++编程技术,本文将通过对对话框的重新绘制来达到自定义的界面效果。

详细


一、概述

1 引言:

应用程序界面是用户与应用程序之间的交互的桥梁和媒介,用户界面是应用程序中最重要的组成部分,也是最为直观的视觉体现。对用户而言,界面就是应用程序,界面设计的好坏,会直接影响应用程序的可用性,从而影响用户的体验。

在软件开发过程中,对界面的设计椅子都是一项很重要的技术,如今的应用软件界面可谓是“丰富多彩、美丽绝伦”,如大家熟悉的360安全卫士、腾讯QQ聊天软件、Visual C++ 编程词典软件等,都是非常不同于普通的对话框应用程序,因为他们的界面都是重新绘制过的,从而实现了漂亮、易用的用户体验。鉴于VC++编程技术,本文将通过对对话框的重新绘制来达到自定义的界面效果。 

 

2 方案概述:

本文的主要是实现对话框界面的重新自定义绘制,主要包括标题栏的重绘、对话框边框的重绘、对话框背景重绘、以及最小化按钮、最小化按钮和关闭按钮等的重绘实现。


二、编程平台技术实现原理方案设计及过程

编程平台与技术:

   本文实现的编程平台是基于Microsoft Visual Studio 2008 集成开发环境,编程技术采用Visual C++ 编程技术,以及相关的开发软件如Photoshop CS5等。

方案分析:

       在对话框重绘中,使用的主要技术有两个,一个是绘制对话框的背景位图,在对话框大学改变时能够输出位图,使位图能够适应对话框的大小。另一个是在对话框的指定区域输出位图。

2.1绘制对话框的背景位图

绘制对话框背景位图本文采用的是处理对话框的WM_PAINT消息,该消息初始化时候对对话框进行绘制,从而绘制背景位图。绘制背景位图的主要代码如下:

CRect   rect;
    CPaintDC   dc(this);
    GetClientRect(&rect); //获取客户区 
//设置对话框背景颜色       
dc.FillSolidRect(rect,RGB(14,94,157));   //设置为窗口背景

2.2在指定的区域中输出位图

为了能够在指定的区域中输出位图,需要使用设备上下文CDC类的StretchBlt方法。由于我们需要在窗口的非客户区域绘制位图,因此需要使用CWindowDC类的StretchBlt方法, CWindowDC类派生与CDC类,它提供了在窗口非客户区域绘制位图的功能。该方法数从源矩形中复制一个位图到目标矩形,必要时按目前目标设备设置的模式进行图像的拉伸或压缩。输出位图的主要实现代码如下:

CRect winRC;
    CDC* pDC=GetWindowDC();//获取窗口设备上下文
    CDC memDC;
    memDC.CreateCompatibleDC(pDC);//创建兼容内存位图
    BITMAPINFO bmpInfo;
    CBitmap bmp;    //定义位图对象
    GetWindowRect(&winRC);
    bmp.LoadBitmap(nID);//加载位图
    bmp.GetObject(sizeof(BITMAPINFO),&bmpInfo);//获取位图信息
    int nBmpCX = bmpInfo.bmiHeader.biWidth;//获取位图宽度
    int nBmpCY = bmpInfo.bmiHeader.biHeight;//获取位图高度
    memDC.SelectObject(bmp);//选中位图对象
    pDC->StretchBlt(x,y,w,h,
        &memDC,0,0,nBmpCX,nBmpCY,SRCCOPY);//在窗口中绘制位图
    bmp.DeleteObject();//释放位图对象
ReleaseDC(pDC);//释放DC


 

方案设计与实现: 

3.1方案的整体设计

    对界面的整体重绘包括两部分,一部分是对话框自身的重绘,二是对话框控件的重绘,本文主要介绍按钮控件的重绘。 

3.2对话框绘制的实现

    在对话框重绘设计与实现过程中,一般需要绘制的对话框区域主要有标题部分、边框部分和客户区部分。具体的区域划分如图1所示。

00.jpg

图1 对话框绘制区域图

既然要对多个区域进行位图显示输出,所以我们先封装一个bmp位图显示输出函数如下:

void CCTestDlg::ShowBmp(int x,int y,int w,int h,int nID)
{//nID 表示位图资源的ID
    CRect winRC;
    CDC* pDC=GetWindowDC();
    CDC memDC;
    memDC.CreateCompatibleDC(pDC);
    BITMAPINFO bmpInfo;
    CBitmap bmp; 
    GetWindowRect(&winRC);
    bmp.LoadBitmap(nID);
    bmp.GetObject(sizeof(BITMAPINFO),&bmpInfo);
    int nBmpCX = bmpInfo.bmiHeader.biWidth;
    int nBmpCY = bmpInfo.bmiHeader.biHeight;
    memDC.SelectObject(bmp);
    pDC->StretchBlt(x,y,w,h,
       &memDC,0,0,nBmpCX,nBmpCY,SRCCOPY);//在窗口中绘制位图
    bmp.DeleteObject();
    ReleaseDC(pDC);
}

(1)对各个区域进行位图输出重绘。由于标题栏以及边框主要都是非客户区域绘制,因此应该在WM_NCPAINT 消息中绘制。当然得先通过添加资源的方式将所用到的bmp位图资源导入到项目中。

首先定义一些常量值,表示对话框各个组成区域部分。代码如下:

 

#define LEFTTITLE 1//左标题

#define MIDTITLE 1//中间标题

#define RIGHTTITLE 1//右标题

#define MINBUTTON 1//右标题

#define MAXBUTTON 1//右标题

#define CLOSEBUTTON 1//右标题

#define APPICON 1 //程序icon图标

#define LEFTBAR 1 //左边框

#define RIGHTBAR 1 //右边边框

#define BOTTOMBAR 1 //底边框

在 WM_NCPAINT消息对于的 方法OnNcPaint()中调用对话框绘制方法SetFace()。该方法的功能就是绘制对话框各个区域的位图。主要代码如下:

void CCTestDlg::SetFact()
{
    // TODO: 在此添加控件通知处理程序代码
    int nFrameCY = GetSystemMetrics(SM_CYFIXEDFRAME);//获取对话框边框的高度
    int nFrameCX = GetSystemMetrics(SM_CXDLGFRAME);//获取对话边框的宽度
    int m_nBorderCY;
    int m_nBorderCX;
    int m_nTitleBarCY ;
    int m_nTitleBarCX;
    if(GetStyle()&WS_BORDER)//获取对话框是否有边框
    {
        m_nBorderCY = GetSystemMetrics(SM_CYBORDER) + nFrameCY;
        m_nBorderCX = GetSystemMetrics(SM_CXBORDER) +nFrameCX;
    }
    else
    {
        m_nBorderCX = nFrameCX;
        m_nBorderCY = nFrameCY;
    }
    m_nTitleBarCY = GetSystemMetrics(SM_CYCAPTION) + m_nBorderCY;//计算标题栏高度
    m_nTitleBarCX =m_nBorderCX;
    CRect winRect,factRect;
    GetWindowRect(&winRect); //获取窗口区域
    factRect.CopyRect(CRect(0,0,winRect.Width(),winRect.Height()));
    CWindowDC windowsDC(this);//获取窗口设备上下文
    //获取整个MFC窗口的高度和宽度
    int winCX = winRect.Width();
    int winCY = winRect.Height();          
    if(LEFTTITLE)
    {//绘制对话框左标题栏位图
        ShowBmp(0,0,100,m_nTitleBarCY,IDB_RIGHTTITLE);
    }      
    if(RIGHTTITLE)
    {//绘制对话框右标题栏位图
        ShowBmp(winCX-100,0,100,m_nTitleBarCY,IDB_RIGHTTITLE);
    }      
    if(MIDTITLE)
    {//绘制对话框中标题栏位图
       
        ShowBmp(100,0,winCX-200,m_nTitleBarCY,IDB_MIDTITLE);
    }  
    if(LEFTBAR)
{//绘制对话框左边框位图                   ShowBmp(0,m_nTitleBarCY,m_nBorderCX,factRect.Height()-m_nBorderCY,IDB_LEFTBAR);
    }
    if(BOTTOMBAR)
    {//绘制对话框底边框位图     ShowBmp(m_nBorderCX,winCY-m_nBorderCX,winCX-2*m_nBorderCX,m_nBorderCX,IDB_BOTTOMBAR);
    }  
    if(RIGHTBAR)
    {//绘制对话框左边框位图 ShowBmp(winCX-m_nBorderCX,m_nTitleBarCY,m_nBorderCX,factRect.Height()-m_nBorderCY,IDB_RIGHTBAR);
    }  
    if(MINBUTTON)
    {//给对话框绘制最小化按钮
        ShowBmp(winCX-3-24-3-24-3-24,1,24,24,IDB_MINBUTTON);
    }  
    if(MAXBUTTON)
    {//给对话框绘制最大化按钮
        ShowBmp(winCX-3-24-3-24,1,24,24,IDB_MAXBUTTON);
    }  
    if(CLOSEBUTTON)
    {//给对话框绘制关闭按钮
        ShowBmp(winCX-3-24,1,24,24,IDB_CLOSEBUTTON);
    }
    ReleaseDC(&windowsDC);
    DrawTitleBarText();//输出标题栏文本
}

上面代码中最后的绘制对话框标题文本的方法DrawTitleBarText()的主要代码如下:

CString strTitle ="自绘窗口标题栏和边框";
    CDC* pDC= GetWindowDC();//获取窗口设备上下文
    pDC->SetBkMode(TRANSPARENT);//设置透明的背景模式
    pDC->SetTextColor(RGB(255,255,255));//设置文本颜色
    pDC->SetTextAlign(TA_CENTER);//设置文本对齐方式
    CRect rect;
    GetClientRect(&rect);//获取窗口客户区域
    CSize szText = pDC->GetTextExtent(strTitle);//获取文本高度
    pDC->TextOut(rect.Width()/2,3,strTitle,20);//在窗口中输出文本
    ReleaseDC(pDC);//释放窗口设备上下文

绘制后的效果图如图2所示。

02.jpg

图2 对话框绘制效果图

在完成对话框相应区域的位图后,并没有完成任务,还需要处理标题栏按钮的热点效果,以及按钮的单击事件。首先得处理鼠标在非客户区域移动时的事件,即WM_NCMOUSEMOVE消息,在其消息处理函数中判断当前的鼠标点是否位于标题栏的按钮区域,如果是则设置按钮的热点效果,并且记录当前的按钮状态,及鼠标点在哪个按钮上。同样的,处理对话框非客户区域的单击事件,即WM_NCLBUTTONDOWN消息,在其消息处理函数中完成单击事件操作。主要代码如下:

void CCTestDlg::OnNcMouseMove(UINT nHitTest, CPoint point)
{
    // TODO: 在此添加消息处理程序代码和/或调用默认值
    CRect minRC,maxRC,closeRC,winRC;
   
    GetWindowRect(&winRC);
   
    closeRC.CopyRect(CRect(winRC.Width()-27,1,winRC.Width()-27+24,1+24));  
    maxRC.CopyRect(CRect(winRC.Width()-27*2,1,winRC.Width()-27*2+24,1+24));
    minRC.CopyRect(CRect(winRC.Width()-27*3,1,winRC.Width()-27*3+24,1+24));
    point.Offset(-winRC.left,-winRC.top);//由于point为屏幕坐标,这里将其转换为窗口坐标
    if(closeRC.PtInRect(point)) //判断鼠标是否在关闭按钮区域上
    {      
        ShowBmp(winRC.Width()-3-24,1,24,24,IDB_CLOSEBUTTON2);
    }
    else if(maxRC.PtInRect(point)) //判断鼠标是否在最大化按钮区域上
    {
        ShowBmp(winRC.Width()-27*2,1,24,24,IDB_MAXBUTTON2);
    }
    else if(minRC.PtInRect(point)) //判断鼠标是否在最小化按钮区域上
    {
        ShowBmp(winRC.Width()-27*3,1,24,24,IDB_MINBUTTON2);
    }
    else//鼠标没有在标题栏的按钮区域上
    {      
        ShowBmp(winRC.Width()-3-24-54,1,24,24,IDB_MINBUTTON);
        ShowBmp(winRC.Width()-3-24-27,1,24,24,IDB_MAXBUTTON);
        ShowBmp(winRC.Width()-3-24,1,24,24,IDB_CLOSEBUTTON);
    }
}

添加热点效果后的效果如图3所示。

03.jpg

图3 绘制对话框之热点效果图

3.3按钮控件重绘的实现

   MFC下编程,很多时候对于标准的按钮控件不是很满意,想要弄的美观些。这就需要按钮重绘。重绘按钮一般的实现方法就是重写CButton类。

       首先给工程添加一个自绘按钮类MyDrawButton,基类为CButton。要想让按钮具备自绘功能,就要为按钮添加BS_OWNERDRAW属性。为类CButton重载PreSubclassWindow虚函数。在该函数中添加如下一行代码:

      SetButtonStyle(GetButtonStyle() | BS_OWNERDRAW);

       当按钮控件具有了自绘功能之后,每次控件状态改变都会触发DrawItem函数,在该函数中来绘制按钮的形态外观,所以第二步就要重载DrawItem虚函数。在这个函数中就可以自由发挥了,比如绘制背景,底色,按钮标题,绘制文本字体样式等等。

       一般都会为按钮定义几种不同状态时的外观,比如光标滑过时的状态,按钮按下时的状态,按钮禁用时的状态,以及按钮的正常状态等等。这就要为新的按钮添加几种重要的消息响应。比如WM_MOUSELEAVE息,WM_MOUSEHOVER消息和WM_MOUSEMOVE消息等等,值得一提的是前两个消息的响应函数需要自己手动添加,微软提供了一个TrackMouseEvent函数在光标离开一个窗口时投递WM_MOUSELEAVE消息,光标滑过窗口时投递WM_MOUSEHOVER消息。一般来说可以在WM_MOUSEMOVE消息响应函数中调用TrackMouseEvent函数来投递WM_MOUSELEAVE消息WM_MOUSEHOVER消息。然后在WM_MOUSELEAVE消息的响应函数中标记光标已经离开按钮,然后调用InvalidateRect函数让按钮重绘。在WM_MOUSEHOVER消息的响应函数中标记光标正在按钮上方,并调用InvalidateRect函数让按钮重绘。

       在本文中,重绘按钮分为3个部分。

1)绘制按钮背景样式,即绘制背景bmp位图,使得按钮具有自定义的样式,同时在绘制按钮背景的输出位图时采用TransparentBlt()函数,该函数的作用是使窗体上显示位图的背景与窗体背景色融为一体,不仅可以显示按钮bmp位图样式,而且还可以使背景透明。

2)就是绘制按钮上的文本。主要绘制按钮上文本的样式,包括字体大小,字体样式,字体颜色等属性。

3)实现不同状态下的按钮的外观样式,主要包括WM_MOUSEMOVE和WM_MOUSELEAVE两个消息的消息处理函数。分别实现鼠标在按钮区域上和不在按钮区域上的状态。为了标记鼠标移动到按钮区域内停留,需要用到一个定时器来标记鼠标是否还在按钮区域内停留。在WM_MOUSEMOVE内启动定时器,触发WM_MOUSELEAVE消息时结束定时器即销毁定时器。定时器的主要代码如下:

void MyDrawButton::OnTimer(UINT_PTR nIDEvent)

{

    // TODO: 在此添加消息处理程序代码和/或调用默认值

    if(nIDEvent != 24)

        return;

    CPoint  point;

    CRect   rect;

    GetWindowRect(&rect);  

    GetCursorPos(&point);

    // 如果鼠标离开按钮区域,重绘按钮

    if (!rect.PtInRect(point) && m_bMove)

    {

        KillTimer (24);

        m_DrawState=ST_MOVEOUT;

        m_bMove=FALSE;

        Draw();

    }

    CButton::OnTimer(nIDEvent);

}

重绘按钮类MyDrawButton的主要实现代码如下:


   

消息处理函数和定义的函数:

void MyDrawButton::PreSubclassWindow()
{
    SetButtonStyle(GetButtonStyle() | BS_OWNERDRAW);
}
void MyDrawButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    Draw();//绘制按钮
}
void MyDrawButton::Draw()//绘制按钮
{
    DrawBackground();//绘制按钮bmp位图,并使背景透明化
    DrawText();//绘制按钮上的文本
}
void MyDrawButton::DrawText()
{//绘制按钮上的文本的字体大小、样式等
    CString     itemString;
    CRect  clientRect;
    CClientDC  dc(this);
    GetClientRect(&clientRect);
    GetWindowText(itemString);
    if(itemString)
    {
        CSize size=dc.GetTextExtent (itemString);//获得所选字体中指定字符串的高度和宽度
        int rectwidth=clientRect.Width();
        int rectheight=clientRect.Height();
        int textwidth=size.cx ;
        int textheight=size.cy ;       
        int x,y; // 文本的位置
        // 计算文本的输出位置
        x=(rectwidth-textwidth)/2;//水平居中
        y=(rectheight-textheight)/2;//垂直居中         
        switch(m_DrawState)
        {
        case ST_MOVEIN://鼠标进入按钮区域
            m_clText=m_clActiveText;
            break;
        case ST_MOVEOUT://鼠标离开按钮区域
            m_clText=m_clNormalText;
            break;
        default:
            m_clText=m_clNormalText;
            break;
        }
        dc.SetTextColor(m_clText);
        dc.SetBkMode(TRANSPARENT);
        CFont *font ;
        font =new CFont();
        int fontSize = 14;   font->CreateFont(fontSize,0,0,0,FW_BOLD,FALSE,FALSE,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,FF_SWISS,_T("宋体"));
        dc.SelectObject(font);
        dc.TextOut (x,y,itemString);
    }
}
void MyDrawButton::SetBkBmp(int nBmpID)
{//设置按钮bmp位图样式
    m_nBmpID = nBmpID;
}
void MyDrawButton::DrawBackground()
{//绘制按钮bmp位图,并使背景透明化
    CRect winRC;
    CDC* pDC=GetWindowDC();
    CDC memDC;
    memDC.CreateCompatibleDC(pDC);
    BITMAPINFO bmpInfo;
    CBitmap bmp;   
    GetWindowRect(&winRC);
   
    bmp.LoadBitmap(m_nBmpID);
    bmp.GetObject(sizeof(BITMAPINFO),&bmpInfo);
    int nBmpCX = bmpInfo.bmiHeader.biWidth;
    int nBmpCY = bmpInfo.bmiHeader.biHeight;
    memDC.SelectObject(bmp);
    pDC->TransparentBlt(0,0,nBmpCX,nBmpCY,&memDC,0,0,
        nBmpCX,nBmpCY,RGB(14,94,157));//在窗口中绘制位图,RGB(14,94,157)是透明色
    bmp.DeleteObject();
    ReleaseDC(pDC);
}

到此,按钮的自定义重绘完成了,接下来就可以使用自己重绘的按钮类MyDrawButton了。首先往对话框中添加一个按钮控件,假设它的ID值为IDC_TEST。进入类向导(Class Wizard)的成员变量属性页,为IDC_ TEST添加一个变量m_ testButton。如下:

MyDrawButton m_testButton;

然后就可以调用MyDrawButton的方法来设置按钮的样式了。如下:

m_testButton.SetBkBmp(IDB_TEST);//IDB_TEST为所设置的bmp位图资源ID。

到现在为止,按钮类的重绘完成了,可以随意定义自己喜欢的样式的按钮了。带有自定义按钮的对话框重绘效果图如下:

04.jpg

三、总结


通过本文涉及到知识和技术的分析、设计与实现,首先我们了解了VC++应用程序尤其是MFC应用系统设计与开发的流程和解决方案。

其次,我们掌握了VC++编程技术和面向对象技术以及bug的调试技术和解决bug的能力,重点掌握了对话框应用程序界面重绘和控件重绘的知识和技术,尤其是重中之重的位图显示技术,同时也学会了如何设计并实现VC++应用程序主界面的设计与美化。在设计界面过程中,渐渐的学会了如何设计漂亮、美观、友好的用户界面。

最后,最为重要的是通过这次设计与开发,使自己懂得如何在困难重重中一步一步细心的发现问题,解决问题。另外,提高了对编程认知与总结,不断加强编程基本功,不断总结经验,学习他人的优秀成果,并提高了独立思考和解决问题的能力。知道了在软件设计开发中对用户界面的方向的把握和用户心理的把握,从而开发出满足用户最为满意的软件程序。


四、项目代码目录结构图


05.jpg

 


本实例支付的费用只是购买源码的费用,如有疑问欢迎在文末留言交流,如需作者在线代码指导、定制等,在作者开启付费服务后,可以点击“购买服务”进行实时联系,请知悉,谢谢
手机上随时阅读、收藏该文章 ?请扫下方二维码