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

Mor*_*hai 32 c++ winapi mfc resize flicker

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

因此,在我想要控制各种子控件移动的对话框中,我想消除闪烁,用户首先看到Windows操作系统认为窗口看起来像什么(因为,AFAICT,操作系统使用bitblt方法移动在发送WM_SIZE之前窗口内部的东西) - 然后我的对话框才能处理移动其子控件或调整它们等等,之后它必须强制重新绘制内容,这会导致闪烁(在此处最小).

我的主要问题是:有没有办法强迫Windows不要做这个愚蠢的bitblt事情? 在窗口调整大小时移动控件的窗口或者调整其父级调整大小时,它肯定会出错.无论哪种方式,让操作系统进行预涂,只需拧紧工件即可.

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

我真正想要的是:

  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. 挂钩对话框proc.
  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);
}
Run Code Online (Sandbox Code Playgroud)

调整大小确实是由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));
}
Run Code Online (Sandbox Code Playgroud)

也许最后棘手的一点可以在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;
}
Run Code Online (Sandbox Code Playgroud)

"棘手"是我们避免试图弄乱任何已经被破坏的窗口,并且我们不会尝试将SetWindowPos推迟到不可见的窗口(因为这被记录为"将失败").

我在一个隐藏一些控件的真实项目中测试了上述内容,并使用相当复杂的布局并取得了很好的成功.即使没有Aero也没有闪烁(1),即使你使用对话框窗口的左上角调整大小(大多数可调整大小的窗口会显示最闪烁的问题,当你抓住那个句柄时 - IE,FireFox等).

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

(1)请注意,不可能避免在那里曾经有过一次平局.对于未更改的对话框的每个部分,用户都看不到任何内容(没有任何闪烁).但是在事情发生变化的情况下,用户可以看到变化 - 这是不可避免的,并且是100%的解决方案.

Joh*_*ler 14

在调整大小期间你无法阻止绘画,但你可以(小心)防止重新绘制闪烁来自哪里.首先,bitblt.

有两种方法可以阻止bitblt的事情.

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

如果您不拥有该类,但确实能够控制消息处理(对于大多数对话框都是如此).默认处理WM_NCCALCSIZE是类样式CS_HREDRAWCS_VREDRAW处理的位置,默认行为是WVR_HREDRAW | WVR_VREDRAW在类具有时从处理WM_NCCALCSIZE 返回CS_HREDRAW | CS_VREDRAW.

因此,如果您可以拦截WM_NCCALCSIZE,则可以在调用DefWindowProc执行其他正常处理后强制返回这些值.

您可以收听WM_ENTERSIZEMOVEWM_EXITSIZEMOVE了解调整窗口大小的开始和停止时间,并使用它来暂时禁用或修改绘图和/或布局代码的工作方式,以最大限度地减少闪烁.您想要修改此代码的具体操作取决于您的正常代码通常在WM_SIZE WM_PAINT其中执行的操作WM_ERASEBKGND.

当你画你的对话框的背景下,你需要落后任何的子窗口的油漆.确保对话框有WS_CLIPCHILDREN解决了这个问题,所以你已经处理过了.

当您移动子窗口时,请确保使用BeginDeferWindowPos/EndDefwindowPos,以便立即进行所有重新绘制.否则,当每个窗口在每个SetWindowPos调用上重绘其非客户区域时,您将获得一堆闪烁.

  • 好吧,我不知道该怎么做,但我似乎有一个解决我最初的动机:闪烁自由调整大小.如果我在WM_SIZE处理程序中移动子控件时使用DeferSetWindowPos,那么窗口几乎完美地调整大小(我还强制对话框本身具有WS_CLIPCHILDREN).我仍然发现所有这些都是一种黑色艺术.为什么管理子窗口重绘的顺序如此困难?为什么DeferSetWindowPos循环在手动移动控件的情况下成功,然后使它们失效失败?叹... (2认同)