33

当用户抓住一个可调整大小的窗口的一角,然后移动它时,窗口首先移动窗口的内容,然后向正在调整大小的窗口发出 WM_SIZE。

因此,在一个对话框中,我想控制各种子控件的移动,并且我想消除闪烁,用户首先会看到 windows 操作系统认为窗口会是什么样子(因为,AFAICT,操作系统使用 bitblt 方法来移动在发送 WM_SIZE 之前窗口内部的东西)——只有这样我的对话框才能处理移动它的子控件,或者调整它们的大小等,之后它必须强制重新绘制东西,这现在会导致闪烁(在非常至少)。

我的主要问题是:有没有办法强制 Windows 不做这个愚蠢的 bitblt 事情? 如果窗口的控件在调整窗口大小时移动,或者在调整其父级大小时调整自身大小,那么它肯定是错误的。无论哪种方式,让操作系统进行预涂漆只会让作品变得更糟。

我一度认为它可能与 CS_HREDRAW 和 CSVREDRAW 类标志有关。然而,现实情况是我不希望操作系统要求我擦除窗口 - 我只想在操作系统不先更改窗口内容的情况下自己重新绘制(即我希望显示是原来的样子在用户开始调整大小之前 - 没有来自操作系统的任何 bitblit'ing)。而且我不希望操作系统告诉每个控件它也需要重新绘制(除非它恰好是一个实际上被调整大小遮蔽或显示的控件。

我真正想要的:

  1. 在屏幕上更新任何内容之前移动和调整子控件的大小。
  2. 完全绘制所有移动或调整大小的子控件,以使它们在新的大小和位置上没有伪影。
  3. 在子控件之间绘制空格,而不影响子控件本身。

注意:步骤 2 和 3 可以颠倒。

当我将 DeferSetWindowPos() 与标记为 WS_CLIPCHILDREN 的对话框资源结合使用时,上述三件事似乎正确发生。

如果我可以对内存 DC 执行上述操作,然后只在 WM_SIZE 处理程序的末尾执行一个 bitblt,我将获得额外的小好处。

我已经玩了一段时间了,我无法逃避两件事:

  1. 我仍然无法阻止 Windows 执行“预测 bitblt”。 答:请参阅下面的解决方案,该解决方案覆盖 WM_NCCALCSIZE 以禁用此行为。

  2. 我看不出如何构建一个对话框,其中它的子控件绘制到双缓冲区。答案:请参阅下面约翰的答案(标记为答案),了解如何要求 Windows 操作系统双缓冲您的对话框(注意:根据文档,这不允许任何 GetDC() 中间的绘制操作)。


我的最终解决方案(感谢所有做出贡献的人,尤其是 John K.):

经过大量的汗水和泪水,我发现以下技术在 Aero 和 XP 中或在禁用 Aero 的情况下都能完美运行。不存在轻弹(1)。

  1. 挂钩对话过程。
  2. 覆盖 WM_NCCALCSIZE 以强制 Windows 验证整个客户区,而不是 bitblt 任何东西。
  3. 覆盖 WM_SIZE 以使用 BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos 对所有可见窗口执行所有移动和调整大小。
  4. 确保对话窗口具有 WS_CLIPCHILDREN 样式。
  5. 不要使用 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: 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() 成员完成的,如下所示:

// 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 等时会显示最多的闪烁和问题),也有零闪烁 (1)。

如果有足够的兴趣,我可能会被说服使用 CodeProject.com 或类似地方的真实示例实现来编辑我的发现。给我发短信。

(1) 请注意,不可能避免一次平局超过曾经存在的任何内容。对于对话框中没有改变的每一部分,用户什么都看不到(没有任何闪烁)。但是在事情发生变化的地方,用户可以看到变化——这是无法避免的,并且是 100% 的解决方案。

4

8 回答 8

16

您无法在调整大小期间阻止绘画,但您可以(小心地)防止重新绘画,这是闪烁的来源。首先,bitblt。

有两种方法可以停止 bitblt 的事情。

如果您拥有顶级窗口的类,则只需将其注册到CS_HREDRAW | CS_VREDRAW样式中。这将导致调整窗口大小以使整个客户区无效,而不是试图猜测哪些位不会改变和位比特化。

如果您不拥有该类,但具有控制消息处理的能力(对于大多数对话框都是如此)。的默认处理WM_NCCALCSIZE是类样式CS_HREDRAW和被处理的地方,默认行为是当类具有 时从处理CS_VREDRAW中返回。 WVR_HREDRAW | WVR_VREDRAWWM_NCCALCSIZECS_HREDRAW | CS_VREDRAW

所以如果可以拦截WM_NCCALCSIZE,就可以在调用后强制返回这些值进行DefWindowProc其他正常处理。

您可以聆听WM_ENTERSIZEMOVEWM_EXITSIZEMOVE了解何时开始和停止调整窗口大小,并使用它来临时禁用或修改绘图和/或布局代码的工作方式以最小化闪烁。您究竟想做什么来修改此代码将取决于您的正常代码通常在WM_SIZE WM_PAINT和中执行的操作WM_ERASEBKGND

当您绘制对话框的背景时,您不需要任何子窗口后面进行绘制。确保对话框已经WS_CLIPCHILDREN解决了这个问题,所以你已经处理了这个问题。

当您移动子窗口时,请确保您使用BeginDeferWindowPos/EndDefwindowPos以便所有重新绘制一次发生。SetWindowPos否则,当每个窗口在每次调用时重绘它们的非客户区时,你会得到一堆闪烁。

于 2010-01-29T23:27:16.123 回答
4

如果我正确理解了这个问题,这正是雷蒙德今天提出的问题。

于 2010-01-30T00:58:49.760 回答
2

这是 2018 年的更新,因为我刚刚经历了和你一样的挑战。

您问题中的“最终解决方案”以及相关答案,其中提到了技巧,WM_NCCALCSIZE并且CS_HREDRAW|CS_VREDRAW有助于防止 Windows XP/Vista/7BitBlt在调整大小期间干扰您的客户区域。提到一个类似的技巧甚至可能很有用:您可以拦截WM_WINDOWPOSCHANGING(首先将其传递到DefWindowProc)和 set WINDOWPOS.flags |= SWP_NOCOPYBITS,这会禁用在窗口调整大小期间BitBlt对 Windows 进行的内部调用。SetWindowPos()这与跳过BitBlt.

有些人提到您的WM_NCCALCSIZE技巧在 Windows 10 中不再适用。我认为这可能是因为您编写的代码WVR_ALIGNLEFT|WVR_ALIGNTOP在应该返回时返回WVR_VALIDRECTS,以便您构建的两个矩形 (nccs_params->rgrc[1]nccs_params->rgrc[2]) 至少根据 Windows 使用到 MSDN 页面中非常轻薄的 doxWM_NCCALCSIZENCCALCSIZE_PARAMS. Windows 10 可能对该返回值更加严格;我会试试看。

然而,即使我们假设我们可以说服 Windows 10 不要在BitBlt内部做SetWindowPos(),但事实证明这是一个新问题......

Windows 10(也可能还有 Windows 8)在 XP/Vista/7 的旧式干扰之上添加了另一层客户区域干扰

在 Windows 10 下,应用程序不会直接绘制到帧缓冲区,而是绘制到 Aero 窗口管理器 (DWM.exe) 合成的屏幕外缓冲区。

事实证明,DWM 有时会决定通过在您的客户区域上绘制自己的内容来“帮助”您(有点像BitBlt但更不正常,甚至更不受您的控制)。

因此,为了避免客户区骚扰,我们仍然需要WM_NCCALCSIZE控制,但我们还需要防止 DWM 弄乱您的像素。

我正在与完全相同的问题作斗争,并创建了一个综合问题/答案,汇集了 10 年来关于该主题的帖子并提供了一些新见解(太长,无法在此问题中粘贴内容)。与 Windows Vista 一样,上面提到的 BitBlt 不再是唯一的问题。享受:

如何在调整窗口大小时平滑难看的抖动/闪烁/跳跃,尤其是拖动左/上边框(Win 7-10;bg、bitblt 和 DWM)?

于 2018-10-26T08:12:58.887 回答
1

对于某些控件,您可以使用 WM_PRINT 消息使控件绘制成 DC。但这并不能真正解决您的主要问题,即您希望 Windows 在调整大小期间不绘制任何内容,而是让您完成所有操作。

答案是只要你有子窗口,你就不能做你想做的事。

我最终在自己的代码中解决这个问题的方法是切换到使用Windowless Controls。由于它们没有自己的窗口,因此它们总是与它们的父窗口同时(并进入同一个 DC)绘制。这使我可以使用简单的双缓冲来完全消除闪烁。当我需要时,我什至可以通过不在父母的绘图例程中调用他们的绘图例程来简单地抑制孩子的绘画。

这是我所知道的在调整大小操作期间完全消除闪烁和撕裂的唯一方法。

于 2010-02-02T22:21:21.907 回答
0

只有一种方法可以有效地诊断重绘问题——远程调试。

获得第二台电脑。在其上安装 MSVSMON。添加将构建产品复制到远程 PC 的构建后步骤或实用程序项目。

现在您应该能够在 WM_PAINT 处理程序、WM_SIZE 处理程序等中放置断点,并在执行大小和重绘时实际跟踪您的对话框代码。如果您从 MS 符号服务器下载符号,您将能够看到完整的调用堆栈。

一些放置良好的断点 - 在您的 WM_PAINT、WM_ERAGEBKGND 处理程序中,您应该很好地了解为什么您的窗口在 WM_SIZE 周期的早期同步重绘。

系统中有很多窗口由带有分层子控件的父窗口组成 - 资源管理器窗口与列表视图、树视图预览面板等非常复杂。资源管理器在调整大小时没有闪烁问题,因此很可能得到父窗口的无闪烁调整大小:- 您需要做的是捕捉重绘,找出导致它们的原因,并确保消除原因。

于 2010-01-30T11:34:05.113 回答
0

有多种方法,但我发现唯一可以使用的通常是双缓冲:绘制到屏幕外缓冲区,然后将整个缓冲区 blit 到屏幕。

这在 Vista Aero 及更高版本中是免费的,因此您的痛苦可能是短暂的。

我不知道 XP 下 Windows 和系统控件的一般双缓冲实现,但是,这里有一些需要探索的东西:

Keith Rule 的 CMemDC用于对您使用 GDI
WS_EX_COMPOSITED 窗口样式绘制的任何内容进行双缓冲(请参阅备注部分,以及stackoverflow 上的内容

于 2010-01-29T23:17:30.407 回答
0

什么似乎有效:

  1. 在父对话框上使用 WS_CLIPCHILDREN(可以在 WM_INITDIALOG 中设置)
  2. 在 WM_SIZE 期间,循环通过子控件移动并使用 DeferSetWindowPos() 调整它们的大小。

在我使用 Aero 的 Windows 7 下进行的测试中,这非常接近完美。

于 2010-02-01T20:07:31.177 回答
0

如果您能找到插入它的地方,CWnd::LockWindowUpdates()将阻止任何绘图发生,直到您解锁更新。

但请记住,这是一种 hack,而且相当丑陋。在调整大小期间,您的窗口看起来会很糟糕。如果您遇到的问题是在调整大小期间闪烁,那么最好的办法是诊断闪烁,而不是通过阻挡颜料来隐藏闪烁。

要寻找的一件事是在调整大小期间经常调用的重绘命令。如果您的 r 窗口控件使用指定RedrawWindow()RDW_UPDATENOW标志调用,那么它将在那里重新绘制。但是您可以去掉该标志并指定RDW_INVALIDATE,它告诉控件使窗口无效而不重新绘制。它将在空闲时间重新绘制,使显示保持新鲜而不会溢出。

于 2010-01-29T23:01:21.387 回答