代码之家  ›  专栏  ›  技术社区  ›  Mordachai

当用户调整对话框大小时,如何强制Windows不重新绘制对话框中的任何内容?

  •  32
  • Mordachai  · 技术社区  · 14 年前

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

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

    我的主要问题是: 有没有办法强迫Windows不要做这种愚蠢的事情? 如果窗口中的控件在窗口调整大小时移动,或者在父窗口调整大小时调整自身大小,这肯定是错误的。不管怎样,让操作系统做一个预涂,只是螺丝钉的工作。

    我曾一度认为它可能与cs-hredraw和csvredraw类标志相关。然而,现实情况是,我不希望操作系统要求我删除窗口-我只想在操作系统不首先更改窗口内容的情况下重新绘制自己(即,我希望显示的是用户开始调整大小之前的状态-而不需要操作系统的任何位置)。我不想让操作系统告诉每个控件它也需要重新绘制(除非它碰巧是一个被大小调整所遮蔽或显示的控件)。

    我真正想要的是:

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

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

    当我结合使用defersetwindowpos()和标记为ws-clipChildren的对话框资源时,上述三件事似乎正确发生。

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

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

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

    2. 我看不到如何建立一个对话框,其中它的子控件将绘制到一个双缓冲区。 答:有关如何要求Windows操作系统对对话框进行双重缓冲的信息,请参阅下面的john答案(标记为answer)(注意:根据docs,这不允许在绘制操作之间使用任何getdc())。


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

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

    1. 挂接对话进程。
    2. 重写wm_nccalcSize以强制Windows验证整个客户机区域,而不是BitBlt任何内容。
    3. 覆盖wm_大小以执行所有移动,并使用所有可见窗口的beginderwindowpos/deferwindowpos/enddeferwindowpos调整大小。
    4. 确保对话框窗口具有WS-ClipChildren样式。
    5. 不要使用cs_hredraw_cs_vredraw(对话框不使用,因此通常不是问题)。

    布局代码由您决定——它很容易在布局管理器的代码专家或代码项目上找到示例,或者自己滚动。

    以下是一些代码摘录,应该可以让您获得更多的方法:

    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的重定位()处理程序中看到:

    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,也会有零闪烁(1),即使使用对话框窗口的左上角调整大小也是如此(当您抓取该句柄时,大多数可调整大小的窗口都会显示最闪烁和问题,例如,Firefox等)。

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

    (1)请注意,不可能避免一次从任何曾经在那里的物体的顶部划过。对于对话框中没有更改的每个部分,用户都看不到任何内容(没有任何闪烁)。但是当事情发生变化时,用户会看到一个变化——这是不可能避免的,而且是一个100%的解决方案。

    8 回复  |  直到 6 年前
        1
  •  14
  •   John Knoeller    14 年前

    你不能在调整大小时阻止绘画,但你可以(小心)阻止 重漆 这就是闪烁的来源。首先是比特比特比特。

    有两种方法可以阻止bitblt事件。

    如果您拥有顶级窗口的类,那么只需使用 CS_HREDRAW | CS_VREDRAW 风格。这将导致窗口的大小调整使整个客户机区域无效,而不是试图猜测哪些位不会更改和位变。

    如果您不拥有该类,但具有控制消息处理的能力(大多数对话框都是这样)。默认处理 WM_NCCALCSIZE 是班级风格的地方吗 CS_HREDRAW CS_VREDRAW 被处理,默认行为是返回 WVR_HREDRAW | WVR_VREDRAW 当类具有 cs_hredraw_cs_vredraw .

    因此,如果您可以截获wm_ccalcSize,则可以在调用defwindowproc后强制返回这些值以执行其他正常处理。

    你可以听 WM_ENTERSIZEMOVE WM_EXITSIZEMOVE 要知道何时开始和停止调整窗口大小,并使用它临时禁用或修改图形和/或布局代码的工作方式,以最小化闪烁。您到底想做什么来修改这个代码将取决于您的正常代码通常在什么地方做 WM_SIZE WM_PAINT WM_ERASEBKGND .

    当绘制对话框的背景时,需要 在任何一扇小窗户后面刷油漆。确保对话框中有WS-ClipChildren解决了这个问题,所以您已经处理了这个问题。

    移动子窗口时,请确保使用 BeginDeferWindowPos /enddefwindowpos,这样所有重新绘制都会同时进行。否则,当每个窗口在每次setwindowpos调用上重绘其非工作区时,您将得到一束闪烁。

        2
  •  4
  •   GSerg    14 年前

    如果我能正确理解这个问题,那就是问题所在。 Raymond addressed today .

        3
  •  1
  •   John Knoeller    14 年前

    对于某些控件,可以使用wm_print message将控件绘制到DC中。但这并不能真正解决您的主要问题,即您希望窗口在调整大小时不绘制任何内容,而是允许您完成所有操作。

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

    我最终在自己的代码中解决这个问题的方法是切换到使用 Windowless Controls . 因为它们没有自己的窗口,所以它们总是与父窗口同时绘制(并绘制到同一个DC中)。这允许我使用简单的双缓冲来完全消除闪烁。当我需要的时候,我甚至可以把孩子们的画抑制得很小 在父级的draw例程中调用它们的draw例程。

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

        4
  •  1
  •   Louis Semprini    6 年前

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

    问题中的“最终解决方案”以及相关答案,其中提到了 WM_NCCALCSIZE CS_HREDRAW|CS_VREDRAW 对于阻止Windows XP/Vista/7执行 BitBlt 在调整大小的过程中会骚扰您的客户端区域。提到一个类似的技巧可能会很有用:你可以拦截 WM_WINDOWPOSCHANGING (首先传给 DefWindowProc 并设置 WINDOWPOS.flags |= SWP_NOCOPYBITS ,这将禁用 比特BLT 内部调用 SetWindowPos() 窗口在调整窗口大小时生成的。这与跳过 比特BLT .

    有人提到 wm_ccalcSize(wm_ccalcSize) 技巧在Windows10中不再有效。我想那可能是因为你写的代码返回 WVR_ALIGNLEFT|WVR_ALIGNTOP 什么时候该回来 WVR_VALIDRECTS 为了两个矩形( nccs_params->rgrc[1] nccs_params->rgrc[2] )供Windows使用,至少根据msdn页面中的skimpy dox WM_NCCALCSIZE NCCALCSIZE_PARAMS . 有可能Windows10对这个返回值更严格;我会试试看。

    但是,即使我们假设可以说服Windows10不要这样做 比特比特比特 里面 StWistWOWSPOSE() 结果发现有个新问题…

    Windows 10(也可能是Windows 8)添加了 另一个 在旧的XP/Vista/7旧版旧版molestation之上的客户端区域molestation层。

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

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

    所以为了避免客户区的骚扰,我们仍然需要 WMnCCALCHIZHEZSIZE 在控制之下,但我们还需要防止DWM干扰您的像素。

    我正与同样的问题作斗争,并创建了一个总结性的问题/答案,它汇集了10年来关于这个主题的文章,并提供了一些新的见解(太长了,无法将内容粘贴到这个问题中)。从WindowsVista开始,上面提到的bitblt不再是唯一的问题。享受:

    How to smooth ugly jitter/flicker/jumping when resizing windows, especially dragging left/top border (Win 7-10; bg, bitblt and DWM)?

        5
  •  0
  •   John Dibling    14 年前

    如果你能找到插电源的地方, CWnd::LockWindowUpdates() 在解锁更新之前,将阻止任何绘图的发生。

    但请记住,这是一个黑客,一个相当丑陋的黑客。调整大小时,您的窗口看起来很糟糕。如果您遇到的问题是在调整大小期间闪烁,那么最好的方法是诊断闪烁,而不是通过阻塞绘制来隐藏闪烁。

    要查找的一件事是重绘命令,它们在调整大小期间被频繁调用。如果您R窗口的控件正在调用 RedrawWindow() RDW_UPDATENOW 指定了标志,它将在那里重新绘制。但是你可以去掉这个标志并指定 RDW_INVALIDATE 相反,它告诉控件在不重新绘制的情况下使窗口无效。它将在空闲时重新上漆,保持显示器的新鲜而不产生水花。

        6
  •  0
  •   Community Mr_and_Mrs_D    7 年前

    有各种各样的方法,但我发现通常唯一可以使用的方法是双重缓冲:绘制到屏幕外的缓冲区,然后将整个缓冲区快速切换到屏幕上。

    在VistaAero和更高版本中,这是免费的,所以你的痛苦可能会很短暂。

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

    Keith Rule's CMemDC 对于双缓冲任何你用gdi绘制的东西
    WS_EX_COMPOSITED Window style (参见备注部分 here on stackoverflow )

        7
  •  0
  •   Chris Becke    14 年前

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

    获取第二台电脑。在上面安装msvsmon。添加将生成产品复制到远程PC的生成后步骤或实用程序项目。

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

    一些放置得很好的断点——在wm_paint、wm_eragebkgnd处理程序中,您应该很清楚为什么窗口在wm_大小循环的早期被同步重新绘制。

    系统中有许多窗口由带有分层子控件的父窗口组成-资源管理器窗口与列表视图、树视图预览面板等非常复杂。资源管理器在调整大小时没有闪烁问题,因此很可能不闪烁地调整父窗口的大小:-您需要做的是捕获重新喷漆,弄清楚是什么原因造成的,好吧,确保原因被排除。

        8
  •  0
  •   Mordachai    14 年前

    什么似乎有效:

    1. 使用父对话框上的ws_clippchildren(可以在wm_initdialog中设置)
    2. 在wm_size期间,使用defersetwindowpos()循环子控件移动和调整它们的大小。

    这是非常接近完美的,在我的测试Windows7与Aero。