令人生疑的Windows窗口消息WM_PAINT详解

Posted IT老张

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了令人生疑的Windows窗口消息WM_PAINT详解相关的知识,希望对你有一定的参考价值。

目录

1、WM_APINT消息的产生

2、WM_ERASEBKGND消息说明

3、InvalidateRect和UpdateWindow

4、何时使用BeginPaint和EndPaint?


       在做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里有变化,“调整视觉效果”,将“启用桌面组合”去掉)

窗口绘制有关的消息整理 WM_PAINT, WM_NCPAINT, WM_ERASEBKGND

C语言消息处理的问题,WM_PAINT自动关闭窗口

[转]Windows的窗口刷新机制

windows api 文本输出

WM_PAINT 与 WM_ERASEBKGND(三种情况,比较清楚)