• 关于窗口重画的初级问题


    关于窗口重画的初级问题

    初初级问题:

    我在视图画的图象或者文字,当窗口改变后(包括最小化后还原,被别的窗口挡住后重新显示等)为什么不见了?

    这 就是窗口重绘或者说重画的问题。当窗口改变后,会产生无效区域,这个无效的区域需要重画。什么是无效区域?自己到网上搜索或者看相关资料。我这里给出一个 特殊的解释:以最小化后还原为例,假设只有一个程序在运行,窗口最小化时显示计算机桌面,并不妨假设桌面是蓝色的背景,当窗口还原时,窗口所占的这一块区 域该显示些什么东西呢?操作系统并不知道,因此,就形成一块无效区域。要是我们告诉操作系统,显示一个红方块,于是它就显示一个红方块,我们的程序过一会 想显示一个绿方块呢?同样也要告诉它。在哪里告诉它呢?在OnDraw或OnPaint函数这里(这里不准备讨论他们的区别,并且只关注OnDraw)。OnDraw函数在什么时候执行呢?在存在无效区域窗口需要重绘时执行。

    下面开始做例子(为简化程序,不显示方块显示线条),建立一个SDI工程,并添加一个菜单项DrawLine,添加OnDrawline函数:

    显示线条A(20,20,50,50)

    void CDrawView::OnDrawline()

    {

           // TODO: Add your command handler code here

           CClientDC dc(this);

           dc.MoveTo(20,20);

           dc.LineTo(50,50);

    }

    编译运行,出现一个线条,最小化还原,没有了。更改OnDraw函数:

    显示线条B(100,100,70,70)

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           pDC->MoveTo(100,100);

           pDC->LineTo(70,70);

    }

    运行,最小化还原,你会发现线条B一直存在,线条A没有了。

    到这里,你应该有一个疑问:我们新建一个SDI程序,什么都没做啊,为什么这个时候还是能显示菜单,框架这些东西?我们并没有告诉操作系统要显示什么啊?这是因为一般Windows会发送两个消息WM_PAINT(通知客户区有变化)和WM_NCPAINT(通知非客户区有变化)。非客户区的重画系统自己搞定了,而客户区的重画需要我们自己来完成。

    初级问题:

    我要在菜单函数里面画图怎么办?或者说,我希望点击菜单就画图,不想把代码放在OnDraw里面。

    这个问题主要是思路没有转过弯来。其实很简单,在菜单函数里面设置一个开关,然后在OnDraw里面根据这个开关来决定是否显示就可以了。例如:

    BOOL bShowLineA=FALSE;

    void CDrawView::OnDrawline()

    {

           // TODO: Add your command handler code here

           bShowLineA=TRUE;

            Invalidate();//这个函数暂时只需要知道是用来“调用”OnDraw函数的

    }

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

            if(bShowLineA)

           {

               pDC->MoveTo(20,20);

              pDC->LineTo(50,50);

           }    

    }

    单击菜单项DrawLine就会画线条A,当然你还可以再搞一个函数令bShowLineA=FALSE达到删除线条的目的。

    上述做法存在很大局限:必须事先知道要画什么图。考虑这样一种情况:我们用鼠标画图,鼠标按下时决定开始线条开始点,鼠标弹起时决定结束点并画图。你也许会说,还是可以用上面的做法啊,用变量来代替数字就可以了,例如:

    void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)

    {

           // TODO: Add your message handler code here and/or call default

           start=point;

           CView::OnLButtonDown(nFlags, point);

    }

    void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)

    {

           // TODO: Add your message handler code here and/or call default

           end=point;

           Invalidate();

           CView::OnLButtonUp(nFlags, point);

    }

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           pDC->MoveTo(start.x,start.y);

           pDC->LineTo(end.x,end.y);

    }

    这样的确是可以解决一部分问题,考虑更复杂的情况:画若干条线条,还要画一些圆、正方形,甚至还要显示一行文字。这个时候上面的方法就不太适合了。但原理是一样的:在菜单函数里面计算,保存数据结果,通知系统更新;在OnDraw函数里面根据新的结果数据进行重画。在菜单函数里面如何保存数据结果呢?可以使用一个结构(或者类)保存在内存里(例如可以使用数组,链表等等方式),也可以把数据保存在一个文件里,然后,在OnDraw里面读取这个结构(前面说的数组、链表、文件等)的数据进行重画。总之,具体问题具体分析。

    记住一个大原则:画图代码(确切的说是用来显示的代码)放在OnDraw里面。

    那么,是不是任何时候画图代码都必须要放在OnDraw里面?也不全是,例如操作的一些中间过程就要放在其他函数。一个经典的例子是用鼠标画直线,并且动态显示中间画图过程,鼠标弹起才最终决定最后的直线。那么,在鼠标移动事件中就要动态的画线了,然后保存最终结果,在OnDraw里面只需要画这个最终结果就可以了。当然,这个中间过程其实还是可以放到OnDraw里面来的。特别是一些复杂的图形处理,例如画不规则图形,就需要开一个线程专门用来计算显示画线的过程,这个不在我们讨论范围之内。

    我还发现一个初学者有趣的心理活动,包括我自己也是一样的,就是舍不得在OnDraw里面放较多的代码。如下:

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           pDC->MoveTo(start.x,start.y);

           pDC->LineTo(end.x,end.y);

            ....

            下面还有很多的画图的代码。

    }

    上面这种情况他们总是担心会不会效率太低了?因为窗口动不动就要刷新,这么多代码会不会太慢了?但是,要是下面这样的代码他们就放心多了:

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           fun();

    }

    fun

    {

    很多很多很多的画图的代码

    }

    呵呵,有时候我跟别人说,把画图代码放在OnDraw里面,他们就会认为,每一行代码都放到OnDraw里面。

    还 有一些“顽固分子”说,我把画图的代码放在菜单函数里面,然后禁止窗口重画就行了,然后到处去问怎么禁止窗口自动重画。他们都忽略了一个事实,显示器只有 一个!他们都把窗口当作一个个的实体了。他们的想法是:把这个窗口画好了,就再也不动了,最小化吗?好办,请工人把这个窗口拿走,就可以看到后面的窗口 了,还原,就让工人再搬回来就是了。可事实是:窗口(显示器)永远都只有一个。我们只看显示器里面中间的一个象素点:这个时候,这个点在我们的程序里面是 红色的,然后把我们的窗口最小化,这个点并没有随着我们的窗口跑掉啊,它还是在那里,可是下一个窗口是绿色的,你说,这个时候是不是要重新把这个点涂成绿 色的?这就是重画。还原时,又要重新把它涂成红色,如此反复。

    觉得太罗嗦,好像还没说清楚,累啊^_^

    ------------------------------------------------------------------------------
    由UpdateData(FALSE)想到的窗口刷新问题
    作者:zuilang
    一,前言
        有网友提醒我:“在MSDN裡面能找到的東西,再寫BLOG是要被罵的。”确实,全抄MSDN没有一点意思,但加一点自己的理解,或许对初学者有一点用。因此,首先声明,本文适合MFC初学者。
    二,前提知识
    2关于Invalidate、InvalidateRect和UpdateWindow
        以下资料来源不祥,似乎是vckbase讨论的(不保证每一句都正确,如有错误,请指出)。
    (1)Invalidate
           Invalidate标记一个需要重绘的无效 区域,并不意味着调用该函数后就立刻进行重绘。类似于PostMessage(WM_PAINT),需要处理到WM_PAINT消息时才真正重绘。以为您 Invalidate之后还有其他的语句正在执行,程序没有机会去处理WM_PAINT消息,但当函数执行完毕后,消息处理才得以进行。
           Invalidate只是放一个WM_PAINT消息在队列里,不做别的,所以只有当当前函数返回后,进入消息循环,取出WM_PAINT,才执行PAINT,所以不管Invalidate放函数哪个地方,(作用相当于)都是(放在)最后的(但并不是推荐你一律放在函数最后一行)。
           Invalidate()之后:...OnPaint()->OnPrepareDC()->OnDraw(),所以只是刷新在OnPaint()和OnDraw()函数中的绘图语句。其它地方没有影响。
     
    (2)InvalidateRect
           InvalidateRect只是增加重绘区域,在下次WM_PAINT的时候才生效,InvalidateRect函数中的参数TRUE表示系统会在你画之前用背景色将所选区域覆盖一次,默认背景色为白色,可以通过设置BRUSH来改变背景色。
           InvalidateRect(hWnd,&rect,TRUE);向 hWnd窗体发出WM_PAINT的消息,强制客户区域重绘制,rect是你指定要刷新的区域,此区域外的客户区域不被重绘,这样防止客户区域的一个局部 的改动,而导致整个客户区域重绘而导致闪烁,如果最后的参数为TRUE,则还向窗体发送WM_ERASEBKGND消息,使背景重绘,当然在客户区域重绘 之前。
     
    (3)UpdateWindow
           UpdateWindow只向窗体发送 WM_PAINT消息,在发送之前判断GetUpdateRect(hWnd,NULL,TRUE)看有无可绘制的客户区域,如果没有,则不发送 WM_PAINT。如果希望立即刷新无效区域,可以在调用InvalidateRect之后调用UpdateWindow,如果客户区的任一部分无效,则 UpdateWindow将导致Windows用WM_PAINT消息调用窗口过程(如果整个客户区有效,则不调用窗口过程)。这一WM_PAINT消息 不进入消息队列,直接由WINDOWS调用窗口过程。窗口过程完成刷新以后立刻退出,WINDOWS将控制返回给程序中UpdateWindow调用之后 的语句。(windows程序设计第5版 P98)
    三,问题
        初学者很容易碰到下面这个问题:(其中m_nEdit是一个编辑框的int型成员变量)
    void CTestDlg::OnButton1()
    {
           // TODO: Add your control notification handler code here
           for(int i=0;i<10;i++)
           {
                  m_nEdit=i;
                  UpdateData(FALSE);
           }
    }
        程序运行的结果是,编辑框里面直接就显示了9,是程序运行太快了看不清楚吗?改:
    void CTestDlg::OnButton1()
    {
           // TODO: Add your control notification handler code here
           for(int i=0;i<10;i++)
           {
                  m_nEdit=i;
                  Sleep(1000);
                  UpdateData(FALSE);
           }
    }关于窗口重画的初级问题

    初初级问题:

    我在视图画的图象或者文字,当窗口改变后(包括最小化后还原,被别的窗口挡住后重新显示等)为什么不见了?

    这 就是窗口重绘或者说重画的问题。当窗口改变后,会产生无效区域,这个无效的区域需要重画。什么是无效区域?自己到网上搜索或者看相关资料。我这里给出一个 特殊的解释:以最小化后还原为例,假设只有一个程序在运行,窗口最小化时显示计算机桌面,并不妨假设桌面是蓝色的背景,当窗口还原时,窗口所占的这一块区 域该显示些什么东西呢?操作系统并不知道,因此,就形成一块无效区域。要是我们告诉操作系统,显示一个红方块,于是它就显示一个红方块,我们的程序过一会 想显示一个绿方块呢?同样也要告诉它。在哪里告诉它呢?在OnDraw或OnPaint函数这里(这里不准备讨论他们的区别,并且只关注OnDraw)。OnDraw函数在什么时候执行呢?在存在无效区域窗口需要重绘时执行。

    下面开始做例子(为简化程序,不显示方块显示线条),建立一个SDI工程,并添加一个菜单项DrawLine,添加OnDrawline函数:

    显示线条A(20,20,50,50)

    void CDrawView::OnDrawline()

    {

           // TODO: Add your command handler code here

           CClientDC dc(this);

           dc.MoveTo(20,20);

           dc.LineTo(50,50);

    }

    编译运行,出现一个线条,最小化还原,没有了。更改OnDraw函数:

    显示线条B(100,100,70,70)

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           pDC->MoveTo(100,100);

           pDC->LineTo(70,70);

    }

    运行,最小化还原,你会发现线条B一直存在,线条A没有了。

    到这里,你应该有一个疑问:我们新建一个SDI程序,什么都没做啊,为什么这个时候还是能显示菜单,框架这些东西?我们并没有告诉操作系统要显示什么啊?这是因为一般Windows会发送两个消息WM_PAINT(通知客户区有变化)和WM_NCPAINT(通知非客户区有变化)。非客户区的重画系统自己搞定了,而客户区的重画需要我们自己来完成。

    初级问题:

    我要在菜单函数里面画图怎么办?或者说,我希望点击菜单就画图,不想把代码放在OnDraw里面。

    这个问题主要是思路没有转过弯来。其实很简单,在菜单函数里面设置一个开关,然后在OnDraw里面根据这个开关来决定是否显示就可以了。例如:

    BOOL bShowLineA=FALSE;

    void CDrawView::OnDrawline()

    {

           // TODO: Add your command handler code here

           bShowLineA=TRUE;

            Invalidate();//这个函数暂时只需要知道是用来“调用”OnDraw函数的

    }

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

            if(bShowLineA)

           {

               pDC->MoveTo(20,20);

              pDC->LineTo(50,50);

           }    

    }

    单击菜单项DrawLine就会画线条A,当然你还可以再搞一个函数令bShowLineA=FALSE达到删除线条的目的。

    上述做法存在很大局限:必须事先知道要画什么图。考虑这样一种情况:我们用鼠标画图,鼠标按下时决定开始线条开始点,鼠标弹起时决定结束点并画图。你也许会说,还是可以用上面的做法啊,用变量来代替数字就可以了,例如:

    void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)

    {

           // TODO: Add your message handler code here and/or call default

           start=point;

           CView::OnLButtonDown(nFlags, point);

    }

    void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)

    {

           // TODO: Add your message handler code here and/or call default

           end=point;

           Invalidate();

           CView::OnLButtonUp(nFlags, point);

    }

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           pDC->MoveTo(start.x,start.y);

           pDC->LineTo(end.x,end.y);

    }

    这样的确是可以解决一部分问题,考虑更复杂的情况:画若干条线条,还要画一些圆、正方形,甚至还要显示一行文字。这个时候上面的方法就不太适合了。但原理是一样的:在菜单函数里面计算,保存数据结果,通知系统更新;在OnDraw函数里面根据新的结果数据进行重画。在菜单函数里面如何保存数据结果呢?可以使用一个结构(或者类)保存在内存里(例如可以使用数组,链表等等方式),也可以把数据保存在一个文件里,然后,在OnDraw里面读取这个结构(前面说的数组、链表、文件等)的数据进行重画。总之,具体问题具体分析。

    记住一个大原则:画图代码(确切的说是用来显示的代码)放在OnDraw里面。

    那么,是不是任何时候画图代码都必须要放在OnDraw里面?也不全是,例如操作的一些中间过程就要放在其他函数。一个经典的例子是用鼠标画直线,并且动态显示中间画图过程,鼠标弹起才最终决定最后的直线。那么,在鼠标移动事件中就要动态的画线了,然后保存最终结果,在OnDraw里面只需要画这个最终结果就可以了。当然,这个中间过程其实还是可以放到OnDraw里面来的。特别是一些复杂的图形处理,例如画不规则图形,就需要开一个线程专门用来计算显示画线的过程,这个不在我们讨论范围之内。

    我还发现一个初学者有趣的心理活动,包括我自己也是一样的,就是舍不得在OnDraw里面放较多的代码。如下:

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           pDC->MoveTo(start.x,start.y);

           pDC->LineTo(end.x,end.y);

            ....

            下面还有很多的画图的代码。

    }

    上面这种情况他们总是担心会不会效率太低了?因为窗口动不动就要刷新,这么多代码会不会太慢了?但是,要是下面这样的代码他们就放心多了:

    void CDrawView::OnDraw(CDC* pDC)

    {

           CDrawDoc* pDoc = GetDocument();

           ASSERT_VALID(pDoc);

           // TODO: add draw code for native data here

           fun();

    }

    fun

    {

    很多很多很多的画图的代码

    }

    呵呵,有时候我跟别人说,把画图代码放在OnDraw里面,他们就会认为,每一行代码都放到OnDraw里面。

    还 有一些“顽固分子”说,我把画图的代码放在菜单函数里面,然后禁止窗口重画就行了,然后到处去问怎么禁止窗口自动重画。他们都忽略了一个事实,显示器只有 一个!他们都把窗口当作一个个的实体了。他们的想法是:把这个窗口画好了,就再也不动了,最小化吗?好办,请工人把这个窗口拿走,就可以看到后面的窗口 了,还原,就让工人再搬回来就是了。可事实是:窗口(显示器)永远都只有一个。我们只看显示器里面中间的一个象素点:这个时候,这个点在我们的程序里面是 红色的,然后把我们的窗口最小化,这个点并没有随着我们的窗口跑掉啊,它还是在那里,可是下一个窗口是绿色的,你说,这个时候是不是要重新把这个点涂成绿 色的?这就是重画。还原时,又要重新把它涂成红色,如此反复。

    觉得太罗嗦,好像还没说清楚,累啊^_^

    ------------------------------------------------------------------------------
    由UpdateData(FALSE)想到的窗口刷新问题
    作者:zuilang
    一,前言
        有网友提醒我:“在MSDN裡面能找到的東西,再寫BLOG是要被罵的。”确实,全抄MSDN没有一点意思,但加一点自己的理解,或许对初学者有一点用。因此,首先声明,本文适合MFC初学者。
    二,前提知识
    2关于Invalidate、InvalidateRect和UpdateWindow
        以下资料来源不祥,似乎是vckbase讨论的(不保证每一句都正确,如有错误,请指出)。
    (1)Invalidate
           Invalidate标记一个需要重绘的无效 区域,并不意味着调用该函数后就立刻进行重绘。类似于PostMessage(WM_PAINT),需要处理到WM_PAINT消息时才真正重绘。以为您 Invalidate之后还有其他的语句正在执行,程序没有机会去处理WM_PAINT消息,但当函数执行完毕后,消息处理才得以进行。
           Invalidate只是放一个WM_PAINT消息在队列里,不做别的,所以只有当当前函数返回后,进入消息循环,取出WM_PAINT,才执行PAINT,所以不管Invalidate放函数哪个地方,(作用相当于)都是(放在)最后的(但并不是推荐你一律放在函数最后一行)。
           Invalidate()之后:...OnPaint()->OnPrepareDC()->OnDraw(),所以只是刷新在OnPaint()和OnDraw()函数中的绘图语句。其它地方没有影响。
     
    (2)InvalidateRect
           InvalidateRect只是增加重绘区域,在下次WM_PAINT的时候才生效,InvalidateRect函数中的参数TRUE表示系统会在你画之前用背景色将所选区域覆盖一次,默认背景色为白色,可以通过设置BRUSH来改变背景色。
           InvalidateRect(hWnd,&rect,TRUE);向 hWnd窗体发出WM_PAINT的消息,强制客户区域重绘制,rect是你指定要刷新的区域,此区域外的客户区域不被重绘,这样防止客户区域的一个局部 的改动,而导致整个客户区域重绘而导致闪烁,如果最后的参数为TRUE,则还向窗体发送WM_ERASEBKGND消息,使背景重绘,当然在客户区域重绘 之前。
     
    (3)UpdateWindow
           UpdateWindow只向窗体发送 WM_PAINT消息,在发送之前判断GetUpdateRect(hWnd,NULL,TRUE)看有无可绘制的客户区域,如果没有,则不发送 WM_PAINT。如果希望立即刷新无效区域,可以在调用InvalidateRect之后调用UpdateWindow,如果客户区的任一部分无效,则 UpdateWindow将导致Windows用WM_PAINT消息调用窗口过程(如果整个客户区有效,则不调用窗口过程)。这一WM_PAINT消息 不进入消息队列,直接由WINDOWS调用窗口过程。窗口过程完成刷新以后立刻退出,WINDOWS将控制返回给程序中UpdateWindow调用之后 的语句。(windows程序设计第5版 P98)
    三,问题
        初学者很容易碰到下面这个问题:(其中m_nEdit是一个编辑框的int型成员变量)
    void CTestDlg::OnButton1()
    {
           // TODO: Add your control notification handler code here
           for(int i=0;i<10;i++)
           {
                  m_nEdit=i;
                  UpdateData(FALSE);
           }
    }
        程序运行的结果是,编辑框里面直接就显示了9,是程序运行太快了看不清楚吗?改:
    void CTestDlg::OnButton1()
    {
           // TODO: Add your control notification handler code here
           for(int i=0;i<10;i++)
           {
                  m_nEdit=i;
                  Sleep(1000);
                  UpdateData(FALSE);
           }
    }
        程序开始没有变化,静静运行了一会,直接显示9!看来不是显示太快的原因。
    四,思考
        因为UpdateData(FALSE)是更新窗口(编辑框也是窗口)的内容,当然也会更新窗口的“画面”,那么,是不是也是跟Invalidate、InvalidateRect一样的问题呢?尝试一下:
    void CTestDlg::OnButton1()
    {
           // TODO: Add your control notification handler code here
           for(int i=0;i<10;i++)
           {
                  m_nEdit=i;
                  Sleep(100);//去掉这一句在这里确实因为显示太快而看不清。
                  UpdateData(FALSE);
                  UpdateWindow();
           }
    }
        终于得到我们要的结果了^_^。那么UpdateData(FALSE)到底做了什么?想了解,就去看深入浅出MFC。
        程序开始没有变化,静静运行了一会,直接显示9!看来不是显示太快的原因。
    四,思考
        因为UpdateData(FALSE)是更新窗口(编辑框也是窗口)的内容,当然也会更新窗口的“画面”,那么,是不是也是跟Invalidate、InvalidateRect一样的问题呢?尝试一下:
    void CTestDlg::OnButton1()
    {
           // TODO: Add your control notification handler code here
           for(int i=0;i<10;i++)
           {
                  m_nEdit=i;
                  Sleep(100);//去掉这一句在这里确实因为显示太快而看不清。
                  UpdateData(FALSE);
                  UpdateWindow();
           }
    }
        终于得到我们要的结果了^_^。那么UpdateData(FALSE)到底做了什么?想了解,就去看深入浅出MFC。
  • 相关阅读:
    Mac使用Homebrew进行软件包管理
    RNN模拟二进制加法
    虚拟机安装ubuntu18.04
    github合并分支到master
    Python配置虚拟环境
    Python的进程、线程、协程
    原码,反码,补码
    MySQL中的截位函数:RIGHT与LEFT
    MySQL查询和删除重复记录
    Mysql中的数据类型
  • 原文地址:https://www.cnblogs.com/sideny/p/3278779.html
Copyright © 2020-2023  润新知