令人生疑的Windows窗口消息WM_PAINT详解
Posted IT老张
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了令人生疑的Windows窗口消息WM_PAINT详解相关的知识,希望对你有一定的参考价值。
目录
在做Windows应用程序开发时,我们要处理多个Windows窗口消息,其中WM_PAINT窗口绘制消息在我们自绘窗口时会频繁地用到。有很多新人遇到与WM_PAINT消息相关的InvalidateRect、UpdateWindow、BeginPaint、EndPaint等函数时,会有很多疑惑,搞不清楚它们之间的关系,今天就来详细地给大家讲述一下这方面的内容。
1、WM_APINT消息的产生
WM_APINT消息是系统产生的,应用程序是不能给窗口发送该消息的。系统会去定期检测窗口中是否有无效区域,如果有则系统会产生一个WM_PAINT消息,并将消息放置到窗口所属线程的消息队列中。然后消息循环从消息队列中取出WM_PAINT消息,派发给对应的窗口:
这样消息就进入了窗口的窗口处理过程函数,进而执行到处理WM_PAINT消息的代码。
那什么时候窗口才会有无效区域呢?有很多场景会使窗口产生无效区域。比如窗口初次创建时整个窗口区域都是无效的,需要去绘制;当目标窗口被其他窗口覆盖,其他窗口移走时,目标窗口就会产生需要刷新绘制的无效区域;当窗口大小发生变化时,也需要重新绘制,会让整个窗口区域无效。
除了自动产生无效区域,还可以通过调用API函数InvalidateRect来使窗口产生无效区域。比如当我们修改内存中的数据之后,我们想将内存中最新的数据绘制到窗口上,就可以调用InvalidateRect使窗口区域无效,从而让系统产生WM_PAINT消息,这样在处理WM_PAINT消息代码中去重新绘制窗口了。
2、WM_ERASEBKGND消息说明
WM_ERASEBKGND擦除背景消息,是在当窗口背景必须被擦除时(比如窗口移动时,窗口大小改变时) 产生的。对于系统默认的窗口或者MFC窗口,系统会在处理该消息时,给窗口绘制一个默认的背景。
如果我们要拦截WM_PAINT消息去自己绘制窗口,不再需要系统的默认绘制,则需要同时拦截WM_ERASEBKGND消息,直接在处理该消息的分支直接return:
case WM_ERASEBKGND:
{
// An application should return nonzero in response to WM_ERASEBKGND if it processes the
// message and erases the background; this indicates that no further erasing is required.
return TRUE;
}
即不需要系统去自动绘制背景了。
如果不将WM_ERASEBKGND过滤掉,那么系统会绘制默认的背景,我们又去绘制窗口内容,会导致窗口的闪烁。
3、InvalidateRect和UpdateWindow
调用InvalidateRect使窗口区域无效,但InvalidateRect本身不会产生WM_PAINT消息,只是让窗口区域无效,系统检测到窗口有无效区域时才会产生WM_PAINT消息。所以InvalidateRect会触发WM_PAINT消息的产生。系统检测到窗口有无效区域后产生WM_PAINT消息,将消息放置到窗口所属线程的消息队列中等待处理,所以InvalidateRect不会立即让窗口重新绘制。
那如何让窗口立即刷新呢?比如我修改了一些数据,我想让窗口立即刷新绘制新的内容。那就需要再调用UpdateWindow函数了!下面是微软MSDN中对该函数的说明:
The UpdateWindow function updates the client area of the specified window by sending a WM_PAINT message to the window if the window's update region is not empty. The function sends a WM_PAINT message directly to the window procedure of the specified window, bypassing the application queue. If the update region is empty, no message is sent.
即当调用UpdateWindow时,UpdateWindow会查看窗口是否有无效区域,如果有无效区域,会立即产生了WM_PAINT消息,并且直接将WM_PAINT消息投递到目标窗口的窗口处理过程中,这样WM_PAINT消息就会得到立即处理(系统自动产生的WM_PAINT消息只是被放到消息队列中等待处理,并不会立即被处理),这样窗口就立即绘制了。
所以要强制窗口立即刷新,先调用InvalidateRect,然后再调用UpdateWindow就可以了,即:
// ....
InvalidateRect( hTargetWnd, &rect ); // 让窗口区域无效
UpdateWindow( hTargetWnd ); // 立即产生WM_PAINT消息,并直接投递到窗口过程中立即得到处理
4、何时使用BeginPaint和EndPaint?
在我们拦截WM_PAINT消息自己去绘制窗口中的内容时,我们需要调用BeginPaint和EndPaint。因为窗口有无效区域,会产生WM_PAINT消息,调用BeginPaint和EndPaint是为了将窗口的无效区域清除掉的,因为无效区域已经重新绘制了。
如果不调用BeginPaint和EndPaint清除无效区域,窗口将始终有无效区域,系统会频繁不断地产生WM_PAINT消息,停不下来了,这样会导致窗口的闪烁,因为代码在不停的绘制窗口。同时也会让低优先级的WM_TIMER等消息得不到处理。以前我们就遇到这样的问题,在处理WM_PAINT消息的分支中忘记调用这两个函数,导致窗口有严重的闪烁:
case WM_PAINT:
{
// 注意此处必须调用BeginPaint和EndPaint,因为BeginPaint会在刷新之后将无效区域清空,如果不调用
// BeginPaint,则无效区域一直有效,系统检测到无效区域会多次发送WM_PAINT消息,该消息过多会导致
// 如下的问题:
// 1、WM_PAINT消息过多,导致低优先级的WM_TIMER消息收不到的问题
// 2、WM_PAINT消息过多,导致矩形、椭圆等图元实时绘制刷新的问题
// 因为绘制需要已经将BeginPaint和EndPaint的调用放到OnPaint函数内部了
lRes = OnPaint( wParam, lParam, bHandled );
// 不用再走dui的窗口绘制,直接将bHandled设置为TRUE
bHandled = TRUE;
break;
}
所以一般在处理WM_PAINT消息的代码中都会调用BeginPaint和EndPaint函数,如下:
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint( m_hWnd, &ps );
// 处理窗口绘制的代码省略
// ...
::EndPaint( m_hWnd, &ps );
有些可能会有疑问,我在处理MFC窗口自绘时,在处理WM_PAINT消息的代码中并没有调用BeginPaint和EndPaint函数,为啥没有你说的这些问题呢?其实代码中是由调用这两个函数的,因为你代码使用了MFC库中的CPaintDC类,比如如下的代码:
void CTestDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // 用于绘制的设备上下文
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// 使图标在工作区矩形中居中
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// 绘制图标
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
而CPaintDC类将这两个函数调用的代码封装到类中了,在构造函数和析构函数中调用了这两个函数,代码如下:
CPaintDC::CPaintDC(CWnd* pWnd)
{
ASSERT_VALID(pWnd);
ASSERT(::IsWindow(pWnd->m_hWnd));
if (!Attach(::BeginPaint(m_hWnd = pWnd->m_hWnd, &m_ps))) // 调用BeginPaint
AfxThrowResourceException();
}
CPaintDC::~CPaintDC()
{
ASSERT(m_hDC != NULL);
ASSERT(::IsWindow(m_hWnd));
::EndPaint(m_hWnd, &m_ps); // 调用EndPaint
Detach();
}
以上是关于令人生疑的Windows窗口消息WM_PAINT详解的主要内容,如果未能解决你的问题,请参考以下文章
WM_PAINT消息详解,使用InvalidateRect或InvalidateRgn函数刻意产生WM_PAINT消息(WIN7里有变化,“调整视觉效果”,将“启用桌面组合”去掉)