移动/调整窗口大小时闪烁

Posted

技术标签:

【中文标题】移动/调整窗口大小时闪烁【英文标题】:Flicker when moving/resizing window 【发布时间】:2014-12-29 06:46:51 【问题描述】:

我开发了一个显示 jpeg 图像的应用程序。它可以显示 4 个图像,在屏幕的每个象限中显示一个。它为此使用了 4 个窗口。窗口没有边框(框架)也没有标题栏。 加载新图像时,为新图像调整窗口大小,然后显示图像。

特别是当窗口变大时,经常会出现闪烁。在我看来,在显示新内容之前调整大小时似乎旧内容被移动了。

我查阅了很多资源并使用了所有技巧:

窗口只有CS_DBLCLKS 样式(没有CS_HREDRAWCS_VREDRAW);

背景画笔为NULL;

WM_ERASEBKGND 返回 1;

WM_NCPAINT 返回 0;

WM_NCCALCSIZE 告诉对齐到未移动的一侧(你能告诉它丢弃客户区吗?);

WM_WINDOWPOSCHANGING返回0;

SetWindowPos 有标志SWP_NOCOPYBITS | SWP_DEFERERASE | SWP_NOREDRAW | SWP_NOSENDCHANGING

不过,在调整窗口大小时会发生闪烁(或内容移动)。我想要的是:

将WindowPos 设置为新的大小和位置;

InvalidateRect (hWnd, NULL, FALSE);

UpdateWindow(hWnd);

WM_PAINT之前没有任何绘画、背景擦除或内容移动。

WM_PAINT 会:

hDC= BeginPaint (hWnd, &ps);
hMemDC= CreateCompatibleDC (hDC);
hOldBitmap = SelectObject (hMemDC, hNewBitmap);
BitBlt (hDC,...,hMemDC,0,0,SRCCOPY);
SelectObject (hMemDC, hOldBitmap);
DeleteDC (hMemDC);
EndPaint (hWnd, &ps);

谁能告诉我我是否/在哪里犯了导致窗口旧内容被移动的错误?

硬件等:HP Elitebook Core7 64 位 Windows 7 和 NVIDIA Quadro K1000m 驱动程序 9.18.13.3265(更新至 341.44)。


更新(2017 年 7 月)

我也在另一台 Windows 计算机 (Windows 8/10) 上看到了程序的行为。它似乎不是 NVIDIA 显示驱动程序。

将平铺到屏幕中心的窗口(右下角 = w/2, h/2)调整到左上角或左上角 (0, 0) 时,该行为最为明显。

我可能在计算 WM_NCCALCSIZE 消息时遇到问题,告诉 Windows 不要做任何事情。任何人都可以为我的目的提供一个示例计算吗?另见How do I force windows NOT to redraw anything in my dialog when the user is resizing my dialog?

【问题讨论】:

WS_EX_COMPOSITED 添加到主窗口。如果没有什么可以帮助您使用 glew - glsl @АлексейНеудачин, WS_EX_COMPOSITED 并没有什么不同。请注意,所有窗口都是具有样式WS_OVERLAPPEDWINDOW 和类样式CS_DBLCLKS 的***窗口,因此我假设每个窗口都是您提到的“主窗口”。 【参考方案1】:

你有一个令人印象深刻的防闪烁技巧列表:-)

我不知道这是否重要(因为这取决于您的工具窗口是如何创建的,尤其是它们是否是共同父级的子窗口): 尝试在工具窗口的父窗口中设置窗口样式WS_CLIPCHILDREN(如果有的话)。

如果未设置,父窗口将擦除其(整个)背景,然后将绘制消息转发到子窗口,这将导致闪烁。如果设置了WS_CLIPCHILDREN,则父窗口对子窗口占用的客户区不做任何事情。结果子窗口的区域没有被绘制两次,也没有闪烁的机会。

【讨论】:

嗨 Lukas,所有窗口都是独立的,在桌面上不重叠(我将其设置为黑色 .bmp 文件,桌面图标隐藏)并且桌面上没有其他窗口。当观众忙碌时,父窗口将移出屏幕。因此我认为没有必要剪辑,因为没有什么可以剪辑的。使用 WS_OVERLAPPEDWINDOW 创建窗口,然后设置为: ww &= ~(WS_OVERLAPPED|WS_CAPTION|WS_MAXIMIZEBOX|WS_MINIMIZEBOX|WS_SIZEBOX|WS_SYSMENU) ww |= (WS_POPUP); @PaulOgilvie 嗨,保罗,我已经认为你的窗户是独立式的……但值得一试。有趣的是它即使在黑色背景下也会闪烁。我从来没有见过这样的行为。 我开始怀疑这更多的是 NVIDIA 及其驱动程序的问题,而不是我的编程问题。特别是当我透过我的眼睛看到窗口的内容在 WM_PAINT 发生之前移动时。该行为也是零星的(并非总是如此),这可能表明对刷新率的干扰。我几乎没有参考其他当前机器,但从未在我的任何旧机器上看到过这种行为。【参考方案2】:

这是一个理论而不是一个答案:

默认情况下,在现代 Windows 中,您的窗口只是显卡上的纹理,桌面窗口管理器会将其映射到屏幕上的矩形。您似乎已经做了所有必要的事情来确保一举更新纹理。

但是当您调整窗口大小时,桌面合成器可能会立即更新其几何图形,从而导致(仍然未更改的)纹理出现在屏幕上的新位置。只有稍后,当您进行绘制时,纹理才会更新。

您可以通过暂时关闭桌面合成来测试这个理论。在 Windows 7 上,导航到“系统属性”,选择“高级”选项卡,在“性能”下选择“设置...”,在“视觉效果”选项卡上,取消选择“启用桌面合成”设置。然后尝试重现问题。如果它消失了,那么这支持(但不能绝对证明)我的理论。

(请记住重新启用合成,因为大多数用户大部分时间都是这样运行的。)

如果这个理论是正确的,那么目标似乎是在调整窗口大小后尽快上漆。如果时间窗口足够小,那么两者都可能在一个显示器刷新周期内发生,并且不会出现闪烁。

具有讽刺意味的是,您消除闪烁的努力在这里可能对您不利,因为您故意抑制了通常由 SetWindowPos 导致的无效和重绘,直到您在以后的步骤中手动执行。

调试提示:尝试在流程的关键点引入延迟(例如,Sleep(1000);),这样您就可以看到调整大小和重绘是否实际上作为两个不同的步骤在屏幕上呈现。

【讨论】:

阿德里安,我要开始测试了。感谢您的建议;我非常渴望消除闪烁。【参考方案3】:

除了您的技巧列表之外。在使用左/上边缘调整窗口大小时,我的戴尔 XPS 笔记本电脑出现闪烁问题。你提到的所有技巧我都试过了。据我了解,窗口边框是在 GPU 中绘制的,窗口内容是在 GDI 子系统中准备的,然后传输到视频内存中进行窗口合成(Windows 8.1 中引入了 DWM)。我尝试完全删除 GDI 渲染(设置 WS_EX_NOREDIRECTIONBITMAP 样式),这会使窗口没有任何内容,然后直接使用 DXGI 子系统创建内容表面(使用 CreateSwapChainForComposition,关于如何执行此操作的示例很少)。但问题依然存在。在渲染窗口框架和调整/显示内容表面之间仍然存在延迟。

但是它可能会解决您的问题,因为您没有边框并且您不会注意到这种滞后。但是您将完全控制窗口重绘,并且它将在 GPU 端进行。

【讨论】:

【参考方案4】:

你确实有一套不错的技巧。

首先,我可以建议一些现有技巧的变体,尤其是在 XP/Vista/7 上可能会有所帮助,其次,我想提一下您在 Win8/10 上看到的持续闪烁可能来自哪里,以及一些可以减少的技巧那个。

首先,尽管在其他 OP 帖子中提出了相反的建议,但您可能会发现,如果您设置之前避免的 CS_HREDRAW|CS_VREDRAW 窗口样式,它实际上消除了在内部 SetWindowPos 窗口中完成的 BitBlt在调整窗口大小时会发生(但是——这是令人困惑的部分——在 Windows 8/10 上,您仍然会看到来自其他来源的闪烁...更多内容见下文)。

如果你不想包含CS_HREDRAW|CS_VREDRAW,你也可以拦截WM_WINDOWPOSCHANGING(首先将它传递给DefWindowProc)并设置WINDOWPOS.flags |= SWP_NOCOPYBITS,这会禁用@987654331内部调用中的BitBlt @Windows 在调整窗口大小时所做的。

或者,您可以添加到您的 WM_NCCALCSIZE 技巧,方法是让它返回一组值,这些值将告诉 Windows 仅在其自身顶部增加 1 个像素,这样即使 BitBlt 确实发生了,它也不会'不要做任何明显的事情:

case WM_NCCALCSIZE:

    RECT ocr; // old client rect
    if (wParam)
    
        NCCALCSIZE_PARAMS *np = (NCCALCSIZE_PARAMS *)lParam;
        // np->rgrc[0] is new window rect
        // np->rgrc[1] is old window rect
        // np->rgrc[2] is old client rect
        ocr = np->rgrc[2];
    
    else
    
        RECT *r = (RECT *)lParam;
        // *r is window rect
    

    // first give Windoze a crack at it
    lRet = DefWindowProc(hWnd, uMsg, wParam, lParam);

    if (wParam)
    
        NCCALCSIZE_PARAMS *np = (NCCALCSIZE_PARAMS *)lParam;

        // np->rgrc[0] is new client rect computed
        // np->rgrc[1] is going to be dst blit rect
        // np->rgrc[2] is going to be src blit rect
        // 
        ncr = np->rgrc[0];
        RECT &dst = np->rgrc[1];
        RECT &src = np->rgrc[2];

        // FYI DefWindowProc gives us new client rect that
        // - in y
        //   - shares bottom edge if user dragging top border
        //   - shares top edge if user dragging bottom border
        // - in x
        //   - shares left edge if user dragging right border
        //   - shares right edge if user dragging left border
        //
        src = ocr;
        dst = ncr; 

        // - so src and dst may have different size
        // - ms dox are totally unclear about what this means
        // https://docs.microsoft.com/en-us/windows/desktop/
        //         winmsg/wm-nccalcsize
        // https://docs.microsoft.com/en-us/windows/desktop/
        //         api/winuser/ns-winuser-tagnccalcsize_params
        // - they just say "src is clipped by dst"
        // - they don't say how src and dst align for blt
        // - resolve ambiguity

        // essentially disable BitBlt by making src/dst same
        // make it 1 px to avoid waste in case Windoze still does it

        dst.right  = dst.left + 1;
        dst.bottom = dst.top  + 1;

        src.right  = dst.left + 1;
        src.bottom = dst.top  + 1;

        lRet = WVR_VALIDRECTS;
    
    else // wParam == 0: Windoze wants us to map a single rect w->c
    
        RECT *r = (RECT *)lParam;
        // *r is client rect
    

    return lRet;

所以这一切都很好,但为什么 Windows 8/10 看起来如此糟糕?

Windows 8/10 Aero 下的应用程序不会直接绘制到屏幕上,而是绘制到屏幕外缓冲区,然后由邪恶的 DWM.exe 窗口管理器合成。 DWM 实际上在现有的旧版 XP/Vista/7 BitBlt 行为之上添加了另一层 BitBlt 类型的行为。

而且 DWM blit 行为更加疯狂,因为它们不仅复制客户区,而且实际上复制旧客户区边缘的像素以创建新客户区。不幸的是,让 DWM 不做它的 blit 比仅仅传递一些额外的标志要困难得多。

我没有 100% 的解决方案,但我希望以上信息会有所帮助,请参阅此问答,了解可用于减少应用程序发生 DWM 层 blit 的机会的计时技巧:

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

【讨论】:

感谢您的建议,路易斯。我目前正在测试@АлексейНеудачин 在他的评论中建议的 WS_EX_COMPOSITED 标志。这似乎有很大帮助。我已经做了一个 WM_NCCALCSIZE 但还不是 1 位的,这似乎是一个愚弄 Windows 的好主意。那将是下一个。 WS_EX_COMPOSITED 听起来很有趣:如果它正在工作,则可能意味着 BitBlt 仍在发生,但是对于屏幕外缓冲区,然后在缓冲区显示在屏幕上之前,您的绘制例程会更新其内容。这表明 WS_EX_COMPOSITED 不知何故在 DWM 上不存在:***.com/a/4188486/1046167 路易斯,您的链接表明该标志不会做任何事情。我将很快使用可以打开和关闭标志以研究结果的版本进行测试。我必须说,由于一段时间以来它似乎闪烁较少,这表明 MS 可能为我的 Windows 7 修复了 [未记录] 错误。在测试标志之后,我将测试 1 像素的 calcsize。 路易斯,我尝试了你所有的建议,但没有奏效。我包括CS_HREDRAW|CS_VREDRAW 有和没有拦截WM_NCCALCSIZE 一个像素的矩形。 WS_EX_COMPOSITED 也没有任何改进。在我的设置中,我看不到 WM_WINDOWPOSCHANGING 消息。 我的命令是:我调用SetWindowPos,然后是WM_NCCALCSIZEWM_WINDOWPOSCHANGED,然后我调用InvalidateRectUpdateWindow,然后是WM_PAINT

以上是关于移动/调整窗口大小时闪烁的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL 闪烁/损坏,窗口调整大小和 DWM 处于活动状态

如何在不闪烁的情况下调整 Swing JWindow 的大小?

我们如何防止当我调整窗口大小时,我的图像使用 HTML-CSS 移动?

Jquery 移动弹出窗口在页面调整大小或滚动时的错误位置重新打开

在窗口调整大小时禁用 jquery 函数

仅在窗口调整大小时防止 div 重叠(带样式组件)