因此,在我想要控制各种子控件移动并消除闪烁的对话框中,用户首先看到Windows操作系统认为窗口将会变成什么样子(因为在发送WM_SIZE之前,这个操作系统使用位块传输方法来移动窗口内部的东西)——只有然后我的对话框才能处理移动其子控件或调整它们的大小等操作,之后必须强制刷新,这会导致闪烁(至少)。
我的主要问题是:有没有一种方法可以强制Windows不执行这个愚蠢的位块传输操作? 它在具有随着窗口调整而移动的控件或随着其父窗口调整而调整大小的窗口中肯定是错误的。 无论哪种情况,让操作系统预先绘制只会搞乱一切。
我曾经认为它可能与CS_HREDRAW和CSVREDRAW类标志有关。 但实际情况是,我不希望操作系统要求我清除窗口 - 我只想自己重新绘制,而不是操作系统首先更改我的窗口内容(即,我希望显示在用户开始调整大小之前就是原样的——不需要任何来自操作系统的位块传输)。 除非确实有一个在调整大小时被遮挡或显示的控件(这种情况下我才希望操作系统告诉每个控件它需要被重绘)。
我真正想要的是:
- 在屏幕上更新任何内容之前移动和调整大小子控件。
- 完全绘制所有移动或调整大小的子控件,使它们以新的大小和位置呈现出来,没有任何伪影。
- 绘制子控件之间的空间而不影响它们本身。
注意:步骤2和3可以交换顺序。
当我使用DeferSetWindowPos()与标记为WS_CLIPCHILDREN的对话框资源结合使用时,上述三个步骤似乎都能够正确进行。
如果我可以将上述操作应用于内存DC,然后仅在WM_SIZE处理程序的最后执行单个位块传输,我将获得额外的小好处。
我已经尝试过这个问题一段时间了,但无法摆脱两件事:
我仍然无法阻止Windows进行“预测性位块传输”。答案:请参见下面的解决方案,覆盖WM_NCCALCSIZE以禁用此行为。
我不知道如何构建一个对话框,其中其子控件绘制到双缓冲区。答案:请参见以下John的答案(标记为答案),了解如何要求Windows OS将您的对话框双缓冲(注意:根据文档,这会禁止任何GetDC()在绘画操作之间)。
我的最终解决方案(感谢所有做出贡献的人,尤其是John K.):
经过长时间的努力,我发现以下技术可以完美地工作,在Aero和XP中或在禁用Aero时都可以。 闪烁不存在(1)。
- 挂钩对话框处理程序。
- 覆盖WM_NCCALCSIZE以强制Windows验证整个客户端区域,并且不进行任何位块传输。
- 覆盖WM_SIZE以使用BeginDeferWindowPos / DeferWindowPos / EndDeferWindowPos为所有可见窗口执行所有移动和调整大小。
- 确保对话框窗口具有WS_CLIPCHILDREN样式。
- 不要使用CS_HREDRAW | CS_VREDRAW(对话框不会,因此通常不是问题)。
布局代码由您决定-可以很容易地在CodeGuru或CodeProject上找到布局管理器的示例,或者自己编写。
以下是一些代码摘录,应该可以帮助您完成大部分工作:
LRESULT ResizeManager::WinProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch (msg)
{
case WM_ENTERSIZEMOVE:
m_bResizeOrMove = true;
break;
case WM_NCCALCSIZE:
// The WM_NCCALCSIZE idea was given to me by John Knoeller:
// see: https://dev59.com/8-o6XIcBkEYKwwoYTzAw
//
// The default implementation is to simply return zero (0).
//
// The MSDN docs indicate that this causes Windows to automatically move all of the child controls to follow the client's origin
// and experience shows that it bitblts the window's contents before we get a WM_SIZE.
// Hence, our child controls have been moved, everything has been painted at its new position, then we get a WM_SIZE.
//
// Instead, we calculate the correct client rect for our new size or position, and simply tell windows to preserve this (don't repaint it)
// and then we execute a new layout of our child controls during the WM_SIZE handler, using DeferWindowPos to ensure that everything
// is moved, sized, and drawn in one go, minimizing any potential flicker (it has to be drawn once, over the top at its new layout, at a minimum).
//
// It is important to note that we must move all controls. We short-circuit the normal Windows logic that moves our child controls for us.
//
// Other notes:
// Simply zeroing out the source and destination client rectangles (rgrc[1] and rgrc[2]) simply causes Windows
// to invalidate the entire client area, exacerbating the flicker problem.
//
// If we return anything but zero (0), we absolutely must have set up rgrc[0] to be the correct client rect for the new size / location
// otherwise Windows sees our client rect as being equal to our proposed window rect, and from that point forward we're missing our non-client frame
// only override this if we're handling a resize or move (I am currently unaware of how to distinguish between them)
// though it may be adequate to test for wparam != 0, as we are
if (bool bCalcValidRects = wparam && m_bResizeOrMove)
{
NCCALCSIZE_PARAMS * nccs_params = (NCCALCSIZE_PARAMS *)lparam;
// ask the base implementation to compute the client coordinates from the window coordinates (destination rect)
m_ResizeHook.BaseProc(hwnd, msg, FALSE, (LPARAM)&nccs_params->rgrc[0]);
// make the source & target the same (don't bitblt anything)
// NOTE: we need the target to be the entire new client rectangle, because we want windows to perceive it as being valid (not in need of painting)
nccs_params->rgrc[1] = nccs_params->rgrc[2];
// we need to ensure that we tell windows to preserve the client area we specified
// if I read the docs correctly, then no bitblt should occur (at the very least, its a benign bitblt since it is from/to the same place)
return WVR_ALIGNLEFT|WVR_ALIGNTOP;
}
break;
case WM_SIZE:
ASSERT(m_bResizeOrMove);
Resize(hwnd, LOWORD(lparam), HIWORD(lparam));
break;
case WM_EXITSIZEMOVE:
m_bResizeOrMove = false;
break;
}
return m_ResizeHook.BaseProc(hwnd, msg, wparam, lparam);
}
调整大小实际上是由Resize()成员完成的,如下所示:
// execute the resizing of all controls
void ResizeManager::Resize(HWND hwnd, long cx, long cy)
{
// defer the moves & resizes for all visible controls
HDWP hdwp = BeginDeferWindowPos(m_resizables.size());
ASSERT(hdwp);
// reposition everything without doing any drawing!
for (ResizeAgentVector::const_iterator it = m_resizables.begin(), end = m_resizables.end(); it != end; ++it)
VERIFY(hdwp == it->Reposition(hdwp, cx, cy));
// now, do all of the moves & resizes at once
VERIFY(EndDeferWindowPos(hdwp));
}
也许最后一个棘手的部分可以看作是ResizeAgent的Reposition()处理程序:
HDWP ResizeManager::ResizeAgent::Reposition(HDWP hdwp, long cx, long cy) const
{
// can't very well move things that no longer exist
if (!IsWindow(hwndControl))
return hdwp;
// calculate our new rect
const long left = IsFloatLeft() ? cx - offset.left : offset.left;
const long right = IsFloatRight() ? cx - offset.right : offset.right;
const long top = IsFloatTop() ? cy - offset.top : offset.top;
const long bottom = IsFloatBottom() ? cy - offset.bottom : offset.bottom;
// compute height & width
const long width = right - left;
const long height = bottom - top;
// we can defer it only if it is visible
if (IsWindowVisible(hwndControl))
return ::DeferWindowPos(hdwp, hwndControl, NULL, left, top, width, height, SWP_NOZORDER|SWP_NOACTIVATE);
// do it immediately for an invisible window
MoveWindow(hwndControl, left, top, width, height, FALSE);
// indicate that the defer operation should still be valid
return hdwp;
}
“棘手的”问题是我们避免尝试干扰已被销毁的任何窗口,并且我们不会对不可见的窗口延迟SetWindowPos(因为这被记录为“将失败”)。
我在一个隐藏了一些控件并使用相当复杂布局的真实项目中测试了上述内容,取得了非常成功的效果。即使没有Aero,甚至在使用对话框窗口的左上角进行调整大小时(大多数可调整大小的窗口在抓住该句柄时会显示最多的闪烁和问题-如IE,FireFox等),也没有任何闪烁。
如果有足够的兴趣,我可以被说服在CodeProject.com或类似网站上编辑我的发现,并提供真实的示例实现。请私信联系我。
请注意,无法避免覆盖以前的任何内容。对于对话框中未更改的每个部分,用户什么都看不到(没有任何闪烁)。但是,在事物发生变化的地方,用户会看到变化-这是不可避免的,并且是100%的解决方案。