您的位置:首页 > 其它

关于窗口重绘的初级问题&由UpdateData(FALSE)想到的窗口刷新问题

2006-10-09 13:48 351 查看
关于窗口重绘的初级问题

作者:zuilang

关于窗口重画的初级问题
既然是初级问题,我尽量少说一点原理,并且使用通俗易懂的话。

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

这就是窗口重绘或者说重画的问题。当窗口改变后,会产生无效区域,这个无效的区域需要重画。什么是无效区域?自己到网上搜索或者看相关资料。我这里给出一个特殊的解释:以最小化后还原为例,假设只有一个程序在运行,窗口最小化时显示计算机桌面,并不妨假设桌面是蓝色的背景,当窗口还原时,窗口所占的这一块区域该显示些什么东西呢?操作系统并不知道,因此,就形成一块无效区域。要是我们告诉操作系统,显示一个红方块,于是它就显示一个红方块,我们的程序过一会想显示一个绿方块呢?同样也要告诉它。在哪里告诉它呢?在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初学者。
二,前提知识
1请看 关于窗口重绘的初级问题
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。

五,后记
其实通常的解决方法不会使用Sleep和UpdateWindow。使用定时器或者新开一个线程效果会更好。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: