当用户调整我的对话框大小时,如何强制窗口不在我的对话框中重绘任何内容?
当用户抓住一个可调整大小的窗口的一角,然后移动它时,窗口首先移动窗口的内容,然后向正在调整大小的窗口发出 WM_SIZE.
When the user grabs a corner of a resizable window, and then moves it, windows first moves the contents of the window around, then issues a WM_SIZE to the window being resized.
因此,在一个对话框中,我想控制各种子控件的移动,并且我想消除闪烁,用户首先会看到 windows 操作系统认为窗口会是什么样子(因为,AFAICT,操作系统使用 bitblt在发送 WM_SIZE 之前在窗口内移动东西的方法) - 只有 然后 我的对话框才能处理移动其子控件或调整它们的大小等,之后它必须强制执行重绘,现在会导致闪烁(至少).
Thus, in a dialog where I want to control the movement of various child controls, and I want to eliminate flickering, the user first sees what windows OS thinks the window will look like (because, AFAICT, the OS uses a bitblt approach to moving things around inside the window before sending the WM_SIZE) - and only then does my dialog get to handle moving its child controls around, or resize them, etc., after which it must force things to repaint, which now causes flicker (at the very least).
我的主要问题是:有没有办法强制 Windows 不做这种愚蠢的 bitblt 事情? 对于带有随窗口移动的控件的窗口来说,这肯定是错误的调整大小,或者在调整其父级大小时调整自己的大小.无论哪种方式,让操作系统进行预绘制只会让工作变得更糟.
My main question is: Is there a way to force windows NOT to do this stupid bitblt thing? Its definitely going to be wrong in the case of a window with controls that move as the window is resized, or that resize themselves as their parent is resized. Either way, having the OS do a pre-paint just screws the works.
我一度认为它可能与 CS_HREDRAW 和 CSVREDRAW 类标志有关.然而,现实情况是我不希望操作系统要求我擦除窗口 - 我只想在操作系统不先更改窗口内容的情况下自己重新绘制(即我希望显示是原来的样子在用户开始调整大小之前 - 没有来自操作系统的任何 bitblit'ing).而且我不希望操作系统告诉每个控件它也需要重新绘制(除非它恰好是一个实际上被调整大小隐藏或显示的控件.
I thought for a time that it might be related to CS_HREDRAW and CSVREDRAW class flags. However, the reality is that I don't want the OS to ask me to erase the window - I just want to do the repainting myself without the OS first changing the contents of my window (i.e. I want the display to be what it was before the user started resizing - without any bitblit'ing from the OS). And I don't want the OS to tell every control that it needs to be redrawn either (unless it happened to be one that was in fact obscured or revealed by the resize.
我真正想要的:
- 移动&调整子控件的大小在屏幕上的任何内容都得到更新.
- 完全绘制所有已移动或调整大小的子控件,以使它们以新的大小显示且没有伪影.位置.
- 在子控件之间绘制空格,而不影响子控件本身.
注意:步骤 2 和 3 可以颠倒.
NOTE: Steps 2 and 3 could be reversed.
当我将 DeferSetWindowPos() 与标记为 WS_CLIPCHILDREN 的对话框资源结合使用时,上述三件事似乎正确发生.
The above three things appear to happen correctly when I use DeferSetWindowPos() in combination with the dialog resource marked as WS_CLIPCHILDREN.
如果我可以对内存 DC 执行上述操作,然后只在 WM_SIZE 处理程序的末尾执行一次 bitblt,我将获得额外的小好处.
I'd get an additional small benefit if I could do the above to a memory DC, and then only do a single bitblt at the end of the WM_SIZE handler.
我已经玩了一段时间了,我无法逃脱两件事:
I have played with this for a while now, and I cannot escape two things:
我仍然无法阻止 Windows 执行预测 bitblt".回答:请参阅下文,了解覆盖 WM_NCCALCSIZE 以禁用此行为的解决方案.
我看不出如何构建一个对话框,其中它的子控件绘制到双缓冲区.答案:请参阅下面 John 的答案(标记为答案),了解如何要求 Windows 操作系统对您的对话框进行双重缓冲(注意:根据文档,这不允许任何 GetDC() 中间的绘制操作).
I cannot see how one can build a dialog where its child controls draw to a double buffer. Answer: See John's answer (marked as answer) below for how to ask Windows OS to double buffer your dialog (note: this disallows any GetDC() in-between paint operations, according to the docs).
<小时>
我的最终解决方案(感谢所有做出贡献的人,尤其是 John K.):
My Final Solution (Thank you everyone who contributed, esp. John K.):
经过大量汗水和泪水,我发现以下技术在 Aero 和 XP 中或在禁用 Aero 的情况下都能完美运行.不存在滑动(1).
After much sweat and tears, I have found that the following technique works flawlessly, both in Aero and in XP or with Aero disabled. Flicking is non-existent(1).
- 挂钩对话过程.
- 重写 WM_NCCALCSIZE 以强制 Windows 验证整个客户区,而不是 bitblt 任何内容.
- 覆盖 WM_SIZE 以执行所有动作和使用 BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos 调整所有可见窗口的大小.
- 确保对话窗口具有 WS_CLIPCHILDREN 样式.
- 请勿使用 CS_HREDRAW|CS_VREDRAW(对话框不使用,因此通常不会出现问题).
布局代码由您决定 - 它很容易在布局管理器的 CodeGuru 或 CodeProject 上找到示例,或者您自己滚动.
The layout code is up to you - its easy enough to find examples on CodeGuru or CodeProject of layout managers, or to roll your own.
这里有一些代码摘录应该可以帮助您了解大部分情况:
Here are some code excerpts that should get you most of the way:
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: http://stackoverflow.com/questions/2165759/how-do-i-force-windows-not-to-redraw-anything-in-my-dialog-when-the-user-is-resiz
//
// 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() 成员完成的,如下所示:
The resizing is really done by the Resize() member, like so:
// 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() 处理程序中看到:
And perhaps the final tricky bit can be seen in the ResizeAgent's Reposition() handler:
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 推迟到不可见的窗口(因为这被记录为将失败".
The 'tricky' being that we avoid trying to mess with any windows that have been destroyed, and we don't try to defer a SetWindowPos against a window that is not visible (as this is documented as "will fail".
我已经在一个隐藏了一些控件的真实项目中测试了上述内容,并使用了相当复杂的布局并取得了巨大的成功.即使没有 Aero,即使您使用对话框窗口的左上角调整大小,也有零闪烁 (1)(当您抓住该句柄时,大多数可调整大小的窗口会显示最多的闪烁和问题 - IE、FireFox 等).
I've tested the above in a real project that hides some controls, and makes use of fairly complex layouts with excellent success. There is zero flickering(1) even without Aero, even when you resize using the upper left corner of the dialog window (most resizable windows will show the most flickering and problems when you grab that handle - IE, FireFox, etc.).
如果有足够的兴趣,我可能会被说服使用 CodeProject.com 或类似地方的真实示例实现来编辑我的发现.给我发消息.
If there is interest enough, I could be persuaded to edit my findings with a real example implementation for CodeProject.com or somewhere similar. Message me.
(1) 请注意,不可能避免一次平局超过曾经存在的任何内容.对于对话框中没有改变的每一部分,用户什么都看不到(没有任何闪烁).但是当事情发生变化时,用户可以看到变化 - 这是无法避免的,并且是 100% 的解决方案.
(1) Please note that it is impossible to avoid one draw over the top of whatever used to be there. For every part of the dialog that has not changed, the user can see nothing (no flicker whatsoever). But where things have changed, there is a change visible to the user - this is impossible to avoid, and is a 100% solution.
推荐答案
您无法在调整大小的过程中阻止绘画,但您可以(小心地)阻止 重新绘画,这就是闪烁的来源.首先是bitblt.
You can't prevent painting during resizing, but you can (with care) prevent repainting which is where flicker comes from. first, the bitblt.
有两种方法可以停止 bitblt.
There a two ways to stop the bitblt thing.
如果您拥有顶级窗口的类,则只需将其注册到 CS_HREDRAW |CS_VREDRAW
样式.这将导致调整窗口大小以使整个客户区无效,而不是试图猜测哪些位不会改变和位比特化.
If you own the class of the top level window, then just register it with the CS_HREDRAW | CS_VREDRAW
styles. This will cause a resize of your window to invalidate the entire client area, rather than trying to guess which bits are not going to change and bitblting.
如果您不拥有该类,但有能力控制消息处理(大多数对话框都是如此).WM_NCCALCSIZE
的默认处理是处理类样式 CS_HREDRAW
和 CS_VREDRAW
的地方,默认行为是返回 WVR_HREDRAW |WVR_VREDRAW
来自处理 WM_NCCALCSIZE
当类有 CS_HREDRAW |CS_VREDRAW
.
If you don't own the class, but do have the ability to control message handling (true for most dialog boxes). The default processing of WM_NCCALCSIZE
is where the class styles CS_HREDRAW
and CS_VREDRAW
are handled, The default behavior is to return WVR_HREDRAW | WVR_VREDRAW
from processing WM_NCCALCSIZE
when the class has CS_HREDRAW | CS_VREDRAW
.
所以如果你能拦截到WM_NCCALCSIZE
,你可以在调用DefWindowProc
之后强制返回这些值来做其他正常的处理.
So if you can intercept WM_NCCALCSIZE
, you can force the return of these values after calling DefWindowProc
to do the other normal processing.
您可以收听 WM_ENTERSIZEMOVE
和 WM_EXITSIZEMOVE
以了解调整窗口大小的时间开始和停止,并使用它来临时禁用或修改绘图和/或布局代码可以最大限度地减少闪烁.您究竟想做什么来修改此代码将取决于您的正常代码通常在 WM_SIZE
WM_PAINT
和 WM_ERASEBKGND
中执行的操作.
You can listen to WM_ENTERSIZEMOVE
and WM_EXITSIZEMOVE
to know when resizing of your window starts and stops, and use that to temporarily disable or modify the way your drawing and/or layout code works to minimize the flashing. What exactly you want to do to modify this code will depend on what your normal code normally does in WM_SIZE
WM_PAINT
and WM_ERASEBKGND
.
当您绘制对话框的背景时,您需要不要在任何子窗口后面进行绘制.确保对话框有 WS_CLIPCHILDREN
解决了这个问题,所以你已经处理了这个问题.
When you paint the background of your dialog box, you need to not paint behind any of the child windows. making sure that the dialog has WS_CLIPCHILDREN
solves this, so you have this handled already.
当您移动子窗口时,请确保您使用 BeginDeferWindowPos
/EndDefwindowPos
以便所有重绘一次发生.否则,当每个窗口在每个 SetWindowPos
调用上重绘它们的非客户区时,你会得到一堆闪烁.
When you do move the child windows, Make sure that you use BeginDeferWindowPos
/ EndDefwindowPos
so that all of the repainting happens at once. Otherwise you will get a bunch of flashing as each window redraws their nonclient area on each SetWindowPos
call.
相关文章