调整窗口大小时,如何平滑丑陋的抖动/闪烁/跳跃(尤其是拖动左/上边框(Win 7-10; bg,bitblt和DWM))? [英] How to smooth ugly jitter/flicker/jumping when resizing windows, especially dragging left/top border (Win 7-10; bg, bitblt and DWM)?

查看:310
本文介绍了调整窗口大小时,如何平滑丑陋的抖动/闪烁/跳跃(尤其是拖动左/上边框(Win 7-10; bg,bitblt和DWM))?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问题::当我抓住Windows应用的调整大小边框(尤其是顶部或左侧边框)并调整窗口大小时,在拖动时,窗口内容的确会调整实时大小,但是它们以一种令人毛骨悚然的方式调整大小,即使对于大多数新手来说,也像是个过时的错误:窗口边缘相对于边缘的内容,我在疯狂地前后拖动抖动/闪烁/跳转。视情况而定,该现象可能看起来像:




  • 内容似乎从窗口边缘移开并在我们放慢速度时突然回弹向下或停止拖动

  • 似乎拉入窗户的内容被各种颜色的边界(通常是黑色或白色)间断地断断续续

  • a丑陋的双重图像,其内容的两个重叠副本之间的偏移距离与我们拖动的速度成多大/多快



我停止拖动后,丑陋的现象就停止了,但是在拖动过程中,它使该应用看起来像是业余的和不专业的。



说这个Windows问题已经令成千上万的应用程序开发人员疯狂



以下是该现象的两个示例图片,为:





使用任务管理器显示此现象的另一个示例视频是此处



此问题的目的:任何遇到此问题的开发人员都会很快发现至少有30个StackOverflow问题,其中一些是最近的,有些是2008年以来的,充满了听起来很有希望的答案,但很少起作用。现实情况是,这个问题有很多原因,并且现有的StackOverflow问题/答案从未使更广泛的上下文变得清晰。这个问题旨在回答:




  • 这种丑陋的抖动/闪烁/跳跃最可能的原因是什么?

  • 如何确定我看到的原因?

  • 原因是特定于特定的图形驱动程序还是Windows的常规原因?

  • 如何解决每个原因?应用程序可以修复它吗?



此问题的范围:对于此StackOverflow问题的范围,出现以下现象:




  • 都是本机Win32和托管的.NET / WPF / Windows Forms应用程序

  • 正常的Win32窗口和Win32对话框窗口

  • 包括XP,Vista,7、8和10的Windows版本(但请参见下面的多种原因的黑暗事实)



请勿以杂音或DUP标记:在标记为dup或标记为dup之前,请仔细阅读以下完整答案。我们已经经历了一个这样的循环,因此SO用户在理解以下内容之后重新打开了该问题:该问题作为第一个Q& A来说明窗口调整大小抖动的所有不同原因,从而使社区受益,以便用户可以确定哪个问题。原因是造成他们的问题并解决。长期以来,下面的答案之所以简单,是因为很难解释为什么通常会发生抖动,而不是因为问题涵盖了许多单独的错误/问题,而单独的问答可以更好地解决这些问题/问题。正如您将看到的,上面的所有排列(本机/托管,窗口/对话框,XP-10)都归结为两个根本原因,但要确定您拥有的是棘手的部分。这就是这个问题的意思。拆分问题将完全否定该好处。我们将在下一节中进一步限制问题的范围...



此问题的范围:




  • 如果您的应用程序有一个或多个子窗口(子HWND),则此问题中的信息对您很有用(因为导致 BitBlts (我们将描述将其与父窗口一起应用于子窗口),但是在调整窗口大小的过程中,您还有一个其他问题需要处理,超出了此问题的范围:您需要使您的所有子窗口原子移动并与父窗口同步。对于此任务,您可能需要 BeginDeferWindowPos / DeferWindowPos / EndDeferWindowPos ,然后可以找到有关它们的信息此处


  • 此该问题假定,如果您的应用使用GDI,DirectX或OpenGL绘制到窗口,则您已经在 wndproc 中实现了 WM_ERASEBKGND 处理程序。 code>仅返回1。 WM_ERASEBKGND 是Windows 3.1中神秘的Windows残余,出现在 WM_PAINT 之前。在绘制窗口之前,让您的应用有机会擦除窗口的背景...嗯。如果让 WM_ERASEBKGND 消息进入 DefWindowProc(),将导致整个窗口被涂成纯色,通常在每次重绘时为白色,包括在调整实时窗口大小期间发生的重绘。结果是一个丑陋的全窗口闪烁,但不是我们在这个问题中谈论的那种抖动/闪烁/跳跃。拦截 WM_ERASEBKGND 立即解决此问题。


  • 此问题主要是通过拖动窗口边框来实时调整大小用鼠标。但是,此处编写的大部分内容也适用于丑陋的工件,您可以在应用程序使用 SetWindowPos()手动调整一次性窗口大小时看到这些丑陋的工件。这些内容很少见,因为它们只在屏幕上滑动了一秒钟,而不是经过长时间的拖动。


  • 此问题不是关于如何即使在许多情况下,这样做也可以解决丑陋的尺寸调整问题,但也可以使应用程序特定的绘图代码运行得更快。如果您的应用确实确实需要大量时间来在实时窗口调整大小期间重新显示其内容,请考虑总体上优化您的绘图代码,或者至少在截取大小时切换到更快,质量较低的绘图模式,方法是拦截 WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE 消息以检测调整大小。


  • 如果在调整应用大小期间您的应用根本无法调整大小(例如,挂起在调整大小时,尤其是在使用GLFW或其他库的OpenGL中)时,请参见以下其他问题,这些问题解释了Microsoft在 WM_SYSCOMMAND 内的可怕的嵌套/模态事件循环,在拖动期间:这里,尤其是这个好答案此处此处此处此处




初始答案



为了开始工作,我们提供了两个初步答案,您可以应该按以下顺序阅读:





以及可以帮助其他人收集见解的源材料列表:





我们希望人们能够通过创造性的方式来避免2a尤其是2b中描述的问题,从而做出更多的答案。

解决方案

PART 2:确定并修复Windows调整大小问题



注意:您要阅读 PART 1 首先,此答案有意义。



此答案无法解决您所有的尺寸调整问题。



它整理了其他帖子中仍然有用的想法,并添加了一些新颖的想法。



Microsoft MSDN上完全没有记录这种行为,以及什么以下是我自己进行实验并查看其他StackOverflow帖子的结果。



2a。从 SetWindowPos() BitBlt 和背景填充调整大小



所有版本的Windows 都会发生以下问题。它们可以追溯到Windows平台(Windows XP)上实时滚动的第一天,并且仍然存在于Windows 10上。在最新的Windows版本中,其他调整大小问题可能会叠加在此问题之上,如下所述。



以下是与单击窗口边框并拖动该边框的典型会话相关的Windows事件。缩进表示嵌套的 wndproc (由于发送(未发布)消息或上述问题的不在此范围内中提到的可怕的Windows模态事件循环而嵌套) ):

  msg = 0xa1(WM_NCLBUTTONDOWN)[单击边框上的鼠标按钮] 
msg = 0x112( WM_SYSCOMMAND)[窗口大小调整命令:模式事件循环]
msg = 0x24(WM_GETMINMAXINFO)
msg = 0x24(WM_GETMINMAXINFO)完成
msg = 0x231(WM_ENTERSIZEMOVE)[开始至大小/移动窗口]
msg = 0x231(WM_ENTERSIZEMOVE)完成
msg = 0x2a2(WM_NCMOUSELEAVE)
msg = 0x2a2(WM_NCMOUSELEAVE)完成

循环:
msg = 0x214( WM_SIZING)[鼠标拖动]
msg = 0x214(WM_SIZING)完成
msg = 0x46(WM_WINDOWPOSCHANGING)
msg = 0x24(WM_GETMINMAXINFO)
msg = 0x24(WM_GETMINMAXINFO)完成
msg = 0x46(WM_WINDOWPOSCHANGING)已完成
msg = 0x83(WM_NCCALCSIZE)
msg = 0x83(WM_NCC ALCSIZE)完成
msg = 0x85(WM_NCPAINT)
msg = 0x85(WM_NCPAINT)完成
msg = 0x14(WM_ERASEBKGND)
msg = 0x14(WM_ERASEBKGND)完成
msg = 0x47(WM_WINDOWPOSCHANGED)
msg = 0x3(WM_MOVE)
msg = 0x3(WM_MOVE)完成
msg = 0x5(WM_SIZE)
msg = 0x5(WM_SIZE)完成
msg = 0x47(WM_WINDOWPOSCHANGED)已完成
msg = 0xf(WM_PAINT)[可能出现或未出现:见下文]
msg = 0xf(WM_PAINT)已完成
转到循环;

msg = 0x215(WM_CAPTURECHANGED)[已释放鼠标]
msg = 0x215(WM_CAPTURECHANGED)完成
msg = 0x46(WM_WINDOWPOSCHANGING)
msg = 0x24(WM_GETMINMAXINFO)
msg = 0x24(WM_GETMINMAXINFO)已完成
msg = 0x46(WM_WINDOWPOSCHANGING)已完成
msg = 0x232(WM_EXITSIZEMOVE)
msg = 0x232(WM_EXITSIZEMOVE)已完成[最终尺寸/移动窗口]
msg = 0x112(WM_SYSCOMMAND)完成
msg = 0xa1(WM_NCLBUTTONDOWN)完成

每次拖动鼠标时,Windows都会为您提供以上循环中显示的一系列消息。最有趣的是,您得到 WM_SIZING 然后得到 WM_NCCALCSIZE 然后得到 WM_MOVE / WM_SIZE ,那么您(可能会在下面获得更多信息)会收到 WM_PAINT



请记住,我们假设您提供了 WM_ERASEBKGND 处理程序返回1(请参见上面的问题此问题的范围),以便该消息不起作用,我们可以忽略它。



在处理这些消息期间(在 WM_WINDOWPOSCHANGING 返回之后不久),Windows对 SetWindowPos()<进行内部调用/ code>来实际调整窗口大小。 SetWindowPos()调用首先调整非客户区域的大小(例如标题栏和窗口边框),然后将注意力转向客户区域(该窗口的主要部分)



在一次拖拽的每个消息序列中,Microsoft会给您一定的时间来自行更新客户端区域。



WM_NCCALCSIZE 返回之后,此截止日期的时钟显然开始计时。在OpenGL窗口中,当您调用 SwapBuffers()来显示新缓冲区时,最后期限显然已得到满足(而不是当您的 WM_PAINT 输入或返回)。我不使用GDI或DirectX,所以我不知道 SwapBuffers()的等效调用是什么,但是您可能会做出一个很好的猜测,并且可以通过以下方式验证在代码的各个位置插入 Sleep(1000)来查看何时触发以下行为。



多少您必须按时完成任务吗?根据我的实验,这个数字似乎在40到60毫秒左右,但是考虑到Microsoft经常拉出的那种恶作剧,如果这个数字取决于您的硬件配置甚至是应用程序的先前行为,我也不会感到惊讶。



如果您在截止日期之前执行更新您的客户区域,那么Microsoft将使您的客户区域保持美丽。您的用户将看到您绘制的像素,并且您将获得尽可能平滑的调整大小。



如果您不选择在截止日期之前更新您的客户区域,然后Microsoft会基于填充某些背景颜色技术(< a href = https://stackoverflow.com/a/53032800/1046167> PART 1 )和剪切掉某些像素技术(部分1 )。微软显示给您的用户的确切像素是复杂的:




  • 如果您的窗口具有 WNDCLASS .style 包括 CS_HREDRAW | CS_VREDRAW 位(您传递将WNDCLASS 结构转换为 RegisterClassEx ):




    • 出人意料的合理的事情发生了。您获得了 PART 1 的图1c3-1、1c3-2、1c4-1和1c4-2中所示的逻辑行为。 >。当扩大工作区时,Windows将在您拖动的窗口的同一侧以背景色(见下文)填充新显示的像素。如果需要(左边界和上边界情况),Microsoft将执行 BitBlt 来完成此操作。缩小工作区时,Microsoft将在您拖动的窗口的同一侧切掉像素。这意味着您要避免真正令人发指的伪影,该伪影会使您客户区域中的对象看起来朝一个方向移动,然后又朝另一个方向移动。


    • 足以给您带来可调整的调整大小行为,除非您真的想推送它,看看有没有机会在绘图之前完全阻止Windows破坏客户区域(见下文)。


    • 在这种情况下,请不要实现您自己的 WM_NCCALCSIZE 处理程序,以避免出现如下所述的Windows错误行为。



  • 如果您的窗口具有 WNDCLASS.style 不包含 CS_HREDRAW | CS_VREDRAW 位(包括对话框,而Windows不允许您设置 WNDCLASS.style ):




    • Windows尝试通过执行 BitBlt 从您的旧客户区域复制一个特定像素矩形并将该矩形写入新客户区域中的某个位置。 BitBlt 是1:1(它不会缩放或缩放像素)。


    • 然后, Windows用背景色填充新客户区的其他部分(Windows在 BitBlt 操作期间未覆盖的部分)。


    • BitBlt 操作通常是调整大小看起来如此糟糕的关键原因。这是因为Windows对调整大小后您的应用程序将如何重绘工作区进行了错误的猜测。 Windows将您的内容放在错误的位置。最终结果是,当用户首先看到 BitBlt 像素,然后看到您的代码绘制的实际像素时,您的内容似乎首先朝一个方向移动,然后急速返回在另一个方向。正如我们在 PART 1 中所述,这会创建最丑陋的调整大小工件。


    • 因此,解决尺寸调整问题的大多数解决方案都涉及禁用 BitBlt


    • 如果您实现了 WM_NCCALCSIZE 处理程序,并且该处理程序在 wParam 时返回 WVR_VALIDRECTS 为1,您实际上可以控制Windows从旧客户区域复制哪些像素( BitBlts ),以及Windows将这些像素放置在新客户区域中的位置。 WM_NCCALCSIZE 几乎没有记录,但是看到有关 WVR_VALIDRECTS NCCALCSIZE_PARAMS.rgrc [1]的提示]和[2] WM_NCCALCSIZE NCCALCSIZE_PARAMS 。您甚至可以提供 NCCALCSIZE_PARAMS.rgrc [1]和[2] 返回值,这些值完全阻止Windows BitBlting 中的任何一个从旧客户区域到新客户区域的像素,或导致Windows从同一位置到同一位置 BitBlt 一个像素,这实际上是相同的,因为没有打开-屏幕像素将被修改。只需将 NCCALCSIZE_PARAMS.rgrc [1]和[2] 都设置为相同的1像素矩形。结合消除背景色(请参见下文),这为您提供了一种方法,可防止Windows在有时间绘制窗口像素之前扰乱窗口像素。


    • 如果实现 WM_NCCALCSIZE 处理程序,并且当 wParam时,它返回除 WVR_VALIDRECTS 以外的任何内容。 为1,则您得到的行为(至少在Windows 10上)与MSDN所说的完全不同。 Windows似乎会忽略您返回的任何左/右/上/下对齐标志。我建议您不要这样做。特别是流行的StackOverflow文章当用户调整对话框大小时,如何强制Windows不要在对话框中重绘任何内容?返回 WVR_ALIGNLEFT | WVR_ALIGNTOP 并至少在我的Windows 10测试系统上,这似乎已经完全被打破。如果将该文章中的代码更改为返回 WVR_VALIDRECTS ,则可能会起作用。


    • 没有自己的自定义 WM_NCCALCSIZE 处理程序,您会得到一个非常无用的行为,最好避免:




      • 如果缩小客户端区域,则什么也不会发生(您的应用程序根本没有获得 WM_PAINT )!如果您使用的是顶部或左侧边框,则客户区内容将与客户区的左上方一起移动。为了在缩小窗口时获得实时调整大小,您必须手动从 wndproc 消息中绘制,例如 WM_SIZE ,或调用 InvalidateWindow()触发稍后的 WM_PAINT


      • 如果扩大客户区域




        • 如果拖动底部或右侧窗口边框,则Microsoft将具有背景色的新像素(见下文)


        • 如果拖动窗口的顶部或左侧边框,Microsoft会将现有像素复制到左上角扩展窗口的大小,并在新打开的空间中留下旧的旧像素旧副本







因此,您可以从这个肮脏的故事中看到两个有用的组合: / p>


  • 2a1。 WNDCLASS.style CS_HREDRAW | CS_VREDRAW 为您提供了图1c3-1、1c3-2、1c4-1的行为,以及 PART 1 的1c4-2,虽然它并不完美,但至少您的客户区域内容不会向一个方向移动


  • 2a2移回另一个方向。 WNDCLASS.style ,没有 CS_HREDRAW | CS_VREDRAW 加上 WM_NCCALCSIZE 处理程序返回 WVR_VALIDRECTS (当 wParam 为1时)表示 BitBlts 没有内容,再加上禁用背景色(见下文),可能会完全禁用Windows对您客户区的骚扰。




显然存在另一种方式来实现组合2a2的效果。您可以拦截 WM_WINDOWPOSCHANGING (首先将其传递到 DefWindowProc <,而不是实现自己的 WM_NCCALCSIZE / code>)并设置 WINDOWPOS.flags | = SWP_NOCOPYBITS ,这会在内部调用中禁用 BitBlt 到Windows在调整窗口大小时创建的 SetWindowPos()。我自己没有尝试过这种技巧,但是许多SO用户报告说它起作用了。



在上面的几点,我们提到了背景色。此颜色由传递给 RegisterClassEx WNDCLASS.hbrBackground 字段确定。此字段包含 HBRUSH 对象。大多数人使用以下样板代码进行设置:

  wndclass.hbrBackground =(HBRUSH)(COLOR_WINDOW + 1); 

COLOR_WINDOW + 1 咒语会给你一个白色背景色。有关 WNDCLASS 的信息,请参见MSDN dox。 +1说明和注释在StackOverflow和MS论坛上有关+1的信息很多。



您可以选择自己的颜色,如下所示:

  wndclass.hbrBackground = CreateSolidBrush(RGB(255,200,122)); 

您还可以使用以下方式禁用后台填充:

  wndclass.hbrBackground = NULL; 

这是上面组合2a2的另一个关键要素。但是请注意,新发现的像素将呈现某种本质上随机的颜色或图案(无论图形帧缓冲区中是否有垃圾),直到您的应用程序赶上并绘制新的客户区域像素为止,因此使用2a1和选择适合您的应用的背景色。



2b。 DWM成分填充中的调整大小问题



在Aero开发过程中的某个时刻,Microsoft首先添加了另一个实时调整大小抖动问题上面描述的Windows版本问题。



阅读前面的StackOverflow帖子,实际上很难说出何时引入了此问题,但是我们可以这样说:




  • 此问题肯定在Windows 10中发生

  • 此问题几乎肯定在Windows 8中发生

  • 在启用了Aero的Windows Vista中也可能发生此问题(许多在Vista下具有调整大小问题的帖子并未说明是否启用了Aero)。

  • 此问题即使启用了Aero,Windows 7可能也不会发生。



问题与Microsoft在Windows中引入的体系结构的重大变化有关。称为 DWM桌面合成。应用程序不再直接绘制到图形帧缓冲区。相反,所有应用程序实际上都被绘制到屏幕外的帧缓冲区中,然后通过Windows的新的邪恶桌面窗口管理器(DWM)进程与其他应用程序的输出进行合成。



因此,因为显示像素涉及到另一个过程,所以还有另一种弄乱像素的机会。



Microsoft永远不会错过这样的机会。



DWM组合显然是这样:




  • 用户在窗口边框上单击鼠标并开始拖动鼠标


  • 每次用户拖动鼠标时,都会触发 wndproc 事件。


  • 但是,同时,DWM(记住是一个与您的应用异步运行的单独进程)启动其自己的截止时间计时器。


  • 类似于上面的第2a节,计时器显然是s在 WM_NCCALCSIZE 返回之后滴答作响的蛋art,当您的应用程序绘制并调用 SwapBuffers()时会感到满意。


  • 如果您在截止日期之前执行更新您的客户区,则DWM会为您的客户区锦上添花。仍然有一定的机会使您的客户区域仍然被第2a节中的问题困扰,因此请务必也阅读第2a节。


  • 如果您不要在截止日期之前更新您的客户区域,那么Microsoft将会做一些真正令人毛骨悚然的事情(微软不是从中学到教训吗?):




    • 假设这是调整大小之前的客户区域,其中A,B,C和D代表客户区域顶部,左侧,右侧和底部边缘中间的像素颜色:



     
    -------------- AAA ----- ------------
    | |
    B C
    B C
    B C
    | |
    -------------- DDD -----------------




    • 假设您正在使用鼠标在两个维度上扩大客户区域。 Genius Windows DWM(或Nvidia:稍后会再介绍)将始终将客户区的像素复制到新客户区的左上角(无论您拖动哪个窗口边框),然后做最荒唐的事情可以想象到其他客户区域。 Windows将采用客户区域底部边缘的所有像素值,将其拉伸到新的客户区域宽度(这是我们在 PART 1 ,然后复制这些像素以填充底部所有新打开的空间(请参阅D的操作),然后Windows将使用右侧的所有像素值客户区域的边缘,将其拉伸到新的客户区域高度,然后复制它们以填充右上角新打开的空间:



     
    -------------- AAA ----------------------- ------------------------
    | | |
    BC |
    BC |
    B CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    | | C CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    -------------- DDD ----------------- CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    | DDDDDDDDD |
    | DDDDDDDDD |
    | DDDDDDDDD |
    | DDDDDDDDD |
    | DDDDDDDDD |
    ------------------------------ DDDDDDDDD --------------- ----------




    • 我什至无法想象他们在吸烟。在许多情况下,此行为可能会导致最坏的结果。首先,几乎可以保证产生 PART 1的图1c3-3和图1c4-3中所示的可怕的来回运动在拖动窗口的顶部和左侧边框时,由于复制的矩形始终位于左上角,而不管您拖动的是哪个窗口边框。其次,如果您碰巧设置了除背景颜色以外的任何像素,则在复制边缘像素时发生的更为荒谬的事情将产生难看的条形。请注意,创建的C和D的条如何甚至不与复制的旧像素中的原始C和D对齐。我能理解为什么他们要复制边缘,希望在那里找到背景像素以自动进行背景颜色检测过程,但是看起来这种实际起作用的可能性在很大程度上受到黑客因素和失败机会的影响。如果DWM使用应用程序选择的背景色(在 WNDCLASS.hbrBackground 中)会更好,但是我怀疑DWM可能无法访问该信息,因为DWM位于不同的过程,因此被黑客入侵。




,但是我们还没有到最糟糕的部分:




  • 在DWM凭此笨拙的猜测破坏DWM破坏它之前,DWM给您绘制自己的客户区域的最后期限是多少? Apparently (from my experiments) the deadline is on the order of 10-15 milliseconds! Given that 15 milliseconds is close to 1/60, I would guess that the deadline is actually the end of the current frame. And the vast majority of apps are unable to meet this deadline most of the time.



That is why, if you launch Windows Explorer on Windows 10 and drag the left border, you will most likely see the scroll bar on the right jitter/flicker/jump around erratically as if Windows were written by a fourth grader.



I cannot believe that Microsoft has released code like this and considers it \"done.\" It is also possible that the responsible code is in the graphics driver (e.g. Nvidia, Intel, ...) but some StackOverflow posts led me to believe that this behavior is cross-device.



There is very little you can do to prevent this layer of incompetence from generating hideous jitter/flicker/jump when resizing using the left or top window border. That is because the rude, non-consentual modification of your client area is happening in another process.



I am really hoping that some StackOverflow user will come up with some magic DWM setting or flag in Windows 10 that we can make to either extend the deadline or disable the horrible behavior completely.



But in the meantime, I did come up with one hack that somewhat reduces the frequency of the hideous back-and-forth artifacts during window resize.



The hack, inspired by a comment in https://stackoverflow.com/a/25364123/1046167 , is to do a best-effort at synchronizing the app process with the vertical retrace that drives DWM’s activity. Actually making this work in Windows is not trivial. The code for this hack should be the very last thing in your WM_NCCALCSIZE handler:

LARGE_INTEGER freq, now0, now1, now2; 
QueryPerformanceFrequency(&freq); // hz

// this absurd code makes Sleep() more accurate
// - without it, Sleep() is not even +-10ms accurate
// - with it, Sleep is around +-1.5 ms accurate
TIMECAPS tc;
MMRESULT mmerr;
MMC(timeGetDevCaps(&tc, sizeof(tc)), {});
int ms_granularity = tc.wPeriodMin;
timeBeginPeriod(ms_granularity); // begin accurate Sleep() !

QueryPerformanceCounter(&now0);

// ask DWM where the vertical blank falls
DWM_TIMING_INFO dti;
memset(&dti, 0, sizeof(dti));
dti.cbSize = sizeof(dti);
HRESULT hrerr;
HRC(DwmGetCompositionTimingInfo(NULL, &dti), {});

QueryPerformanceCounter(&now1);

// - DWM told us about SOME vertical blank
// - past or future, possibly many frames away
// - convert that into the NEXT vertical blank

__int64 period = (__int64)dti.qpcRefreshPeriod;

__int64 dt = (__int64)dti.qpcVBlank - (__int64)now1.QuadPart;

__int64 w, m;

if (dt >= 0)
{
w = dt / period;
}
else // dt < 0
{
// reach back to previous period
// - so m represents consistent position within phase
w = -1 + dt / period;
}

// uncomment this to see worst-case behavior
// dt += (sint_64_t)(0.5 * period);

m = dt - (period * w);

assert(m >= 0);
assert(m < period);

double m_ms = 1000.0 * m / (double)freq.QuadPart;

Sleep((int)round(m_ms));

timeEndPeriod(ms_granularity);

You can convince yourself that this hack is working by uncommenting the line that shows \"worst-case\" behavior by trying to schedule the drawing right in the middle of a frame rather than at vertical sync, and noticing how many more artifacts you have. You can also try varying the offset in that line slowly and you will see that artifacts abruptly disappear (but not completely) at about 90% of the period and come back again at about 5-10% of the period.



Since Windows is not a real-time OS, it is possible for your app to be
preempted anywhere in this code, leading to inaccuracy in the pairing of now1 and dti.qpcVBlank. Preemption in this small code section is rare, but possible. If you want, you can compare now0 and now1 and loop around again if the bound is not tight enough. It is also possible for preemption to disrupt the timing of Sleep() or the code before or after Sleep(). There’s not much you can do about this, but it turns out timing errors in this part of the code are swamped by the uncertian behavior of DWM; you are still going to get some window resize artifacts even if your timing is perfect. It’s just a heuristic.



There is a second hack, and it is an incredibly creative one: as explained in the StackOverflow post Can't get rid of jitter while dragging the left border of a window, you can actually create two main windows in your app, and every time Windows would do SetWindowPos, you intecept that and instead hide one window and show the other! I haven’t tried this yet but the OP reports that it bypasses the insane pixel DWM pixel copy described above.



There is a third hack, which might work depending on your application (especially in combination with the timing hack above). During live resizing (which you can detect by intercepting WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE), you could modify your drawing code to initially draw something much simpler that is much more likely to complete within the deadline imposed by problem 2a and 2b, and call SwapBuffers() to claim your prize: that will be enough to prevent Windows from doing the bad blit/fill described in section 2a and 2b. Then, immediately after the partial draw, do another draw that fully updates the window contents and call SwapBuffers() again. That might still look somewhat odd, since the user will see your window update in two parts, but it’s likely to look much better than the hideous back-and-forth motion artifact from Windows.



One more tantalizing point: some apps in Windows 10, including the console (start cmd.exe), are rock-solid free of DWM Composition artifacts even when dragging the left border. So there is some way of bypassing the problem. Let’s find it!



2c. How to Diagnose Your Problem



As you try to solve your particular resize problem, you may wonder which of the overlapping effects from Section 2a and Section 2b you are seeing.



One way to separate them is to debug on Windows 7 (with Aero disabled, just to be safe) for a bit.



Another way to quickly identify if you are seeing the problem in Section 2b is to modify your app to display the test pattern described in Section 2b, like this example (note the 1-pixel-thin colored lines on each of the four edges):





Then grab any window border and start resizing that border rapidly. If you see intermittent giant colored bars (blue or green bars in the case of this test pattern, since there is blue on the bottom edge and green on the right edge) then you know you are seeing the problem in Section 2b.



You can test if you are seeing the problem in Section 2a by setting WNDCLASS.hbrBackground to a distinct background color, like red. As you resize the window, newly exposed parts will show up with that color. But read through Section 2a to make sure your message handlers are not causing Windows to BitBlt the entire client area, which would cause Windows not to draw any background color.



Remember that the problems in Section 2a and 2b only show up if your app fails to draw by a certain deadline, and each problem has a different deadline.



So, without modification, your app might show the Section 2b problem only, but if you modify your app to draw more slowly (insert Sleep() in WM_PAINT before SwapBuffers() for example), you may miss the deadline for both Section 2a and Section 2b and start to see both problems simultaneously.



This may also happen when you change your app between a slower DEBUG build and a RELEASE build, which can make chasing these resize problems very frustrating. Knowing what’s going on under the hood can help you deal with the confusing results.


THE PROBLEM: When I grab the resize border of my Windows app, especially the top or left borders, and resize the window, the contents of the window do resize "live" as I drag, but they resize in a hideous manner that looks like a blatant bug to even the most novice user: the contents at the opposite edge of the window from the edge I am dragging jitter/flicker/jump back and forth wildly. Depending on the situation, the phenomenon may look like:

  • contents that seem to walk off the edge of the window and snap back when we slow down or stop dragging
  • contents that seem to pull into the window, intermittently displaced by a border of varying colors, often black or white
  • a seriously ugly "double image" with two overlapping copies of the content displaced by a distance proportional to how much/how fast we are dragging

The ugly phenomenon stops as soon as I stop dragging, but during the dragging it makes the app look amateurish and unprofessional.

It is not an understatement to say this Windows problem has driven thousands of app developers crazy.

Here are two example pictures of the phenomenon, kindly prepared for a related question by Roman Starkov:

Another example showing the evil "double image" phenomenon (note the quick flash) from Kenny Liu:

Another example video of the phenomenon with Task Manager is here.

PURPOSE OF THIS QUESTION: Any developer who has experienced this problem quickly finds that there are at least 30 StackOverflow questions, some recent and some dating from 2008, full of promising-sounding answers that rarely work. The reality is that this one problem has many causes, and the existing StackOverflow questions/answers never make the wider context clear. This question seeks to answer:

  • what are the most likely causes of this kind of ugly jitter/flicker/jumping?
  • how do I tell which cause I am seeing?
  • is this cause specific to particular graphics drivers or general for Windows?
  • how do I fix each cause? can an app fix it?

SCOPE OF THIS QUESTION: For the scope of this StackOverflow question, the phenomenon happens with:

  • both native Win32 and managed .NET/WPF/Windows Forms apps
  • both normal Win32 windows and Win32 Dialog windows
  • Windows versions including XP, Vista, 7, 8, and 10 (but see below for the dark truth of multiple causes)

DO NOT FLAG AS BROAD OR DUP: Kindly read the full answer below before marking as a dup or flagging broad. We already went through a cycle of this and SO users subsquently re-opened the question after understanding the following: This question benefits the community by being the first Q&A to explain all the different causes of window resize jitter so that users can identify which of the causes is causing their problem and solve it. The answers below are long simply because it is tricky to explain why jitter occurs in general, not because the question covers many separate bugs/issues that would be better served by separate Q&A. As you will see, all the permutations above (native/managed, window/dialog, XP-10) all boil down to only two root causes, but identifying which you have is the tricky part. That is what this question is about. Splitting the question up would completely negate that benefit. We further limit the scope of the question in the next section...

NOT IN SCOPE OF THIS QUESTION:

  • If your app has one or more child windows (child HWNDs), the info in this question is useful to you (since the jerk-causing BitBlts we will describe are applied to your child windows along with the parent window), but during window resize you have an additional problem to handle that is beyond the scope of this question: you need to make all your child windows move atomically and in sync with the parent window. For this task, you will probably want BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos and you can find out about them here and here.

  • This question assumes that if your app draws to a window using GDI, DirectX, or OpenGL, then you have already implemented a WM_ERASEBKGND handler in your wndproc that simply returns 1. WM_ERASEBKGND is an arcane Windows remnant from Windows 3.1 that comes before WM_PAINT to give your app a chance to "erase the background" of your window before you draw your window...uh huh. If you let the WM_ERASEBKGND message go into DefWindowProc(), that will cause your entire window to get painted a solid color, usually white, on each redraw, including redraws that happen during live window resizing. The result is an ugly full-window flicker that is gross, but not the type of jitter/flicker/jumping we are talking about in this question. Intercepting WM_ERASEBKGND fixes this problem immediately.

  • This question is primarily about live-resize by dragging window borders with the mouse. However, much of what is written here also applies to ugly artifacts you can see when an app manually does a one-time window resize using SetWindowPos(). These are less visible though because they only flick on the screen for one instant, rather than over a long period of dragging.

  • This question is not about how to make your app-specific drawing code go faster, even though doing so may be a solution to the ugly resizing problem in many cases. If your app really does take huge amounts of time to redisplay its contents during live window resize, consider optimizing your drawing code in general or at least switching to a faster, lower-quality drawing mode during resize by intercepting the WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE messages to detect resize.

  • If your app fails to resize at all during app resizing (e.g. it "hangs" during resizing, especially if it is OpenGL using GLFW or other library), see these other questions which explain about Microsoft's hideous nested/modal event loop inside WM_SYSCOMMAND during dragging: here especially this good answer, here, here, here, and here.

INITIAL ANSWERS

To get things started, we have provided two initial answers which you should read in this order:

as well as a list of source material which may help others glean insights:

We hope people will contribute more answers with creative ways of avoiding the problems described in 2a and especially 2b.

解决方案

PART 2: Identifying and Fixing Windows Resize Problems

Note: you want to read PART 1 first for this answer to make sense.

This answer will not solve all your resizing problems.

It organizes the still-usable ideas from other posts and adds a few novel ideas.

None of this behavior is at all documented on Microsoft's MSDN, and what follows below is the result of my own experimentation and looking at other StackOverflow posts.

2a. Resize Problems from SetWindowPos() BitBlt and Background Fill

The following problems happen on all versions of Windows. They date back to the very first days of live-scrolling on the Windows platform (Windows XP) and are still present on Windows 10. On more recent Windows versions, other resize problems may layer on top of this problem, as we explain below.

Here are the Windows events associated with a typical session of clicking a window border and dragging that border. Indentation indicates nested wndproc (nested because of sent (not posted) messages or because of the hideous Windows modal event loop mentioned in "NOT IN SCOPE OF THIS QUESTION" in the question above):

msg=0xa1 (WM_NCLBUTTONDOWN)  [click mouse button on border]
  msg=0x112 (WM_SYSCOMMAND)  [window resize command: modal event loop]
    msg=0x24 (WM_GETMINMAXINFO)
    msg=0x24 (WM_GETMINMAXINFO) done
    msg=0x231 (WM_ENTERSIZEMOVE)      [starting to size/move window]
    msg=0x231 (WM_ENTERSIZEMOVE) done
    msg=0x2a2 (WM_NCMOUSELEAVE)
    msg=0x2a2 (WM_NCMOUSELEAVE) done

  loop:
    msg=0x214 (WM_SIZING)             [mouse dragged]
    msg=0x214 (WM_SIZING) done
    msg=0x46 (WM_WINDOWPOSCHANGING)
      msg=0x24 (WM_GETMINMAXINFO)
      msg=0x24 (WM_GETMINMAXINFO) done
    msg=0x46 (WM_WINDOWPOSCHANGING) done
    msg=0x83 (WM_NCCALCSIZE)
    msg=0x83 (WM_NCCALCSIZE) done
    msg=0x85 (WM_NCPAINT)
    msg=0x85 (WM_NCPAINT) done
    msg=0x14 (WM_ERASEBKGND)
    msg=0x14 (WM_ERASEBKGND) done
    msg=0x47 (WM_WINDOWPOSCHANGED)
      msg=0x3 (WM_MOVE)
      msg=0x3 (WM_MOVE) done
      msg=0x5 (WM_SIZE)
      msg=0x5 (WM_SIZE) done
    msg=0x47 (WM_WINDOWPOSCHANGED) done
    msg=0xf (WM_PAINT)                    [may or may not come: see below]
    msg=0xf (WM_PAINT) done
goto loop;

    msg=0x215 (WM_CAPTURECHANGED)       [mouse released]
    msg=0x215 (WM_CAPTURECHANGED) done
    msg=0x46 (WM_WINDOWPOSCHANGING)
      msg=0x24 (WM_GETMINMAXINFO)
      msg=0x24 (WM_GETMINMAXINFO) done
    msg=0x46 (WM_WINDOWPOSCHANGING) done
    msg=0x232 (WM_EXITSIZEMOVE)
    msg=0x232 (WM_EXITSIZEMOVE) done  [finished size/moving window]
  msg=0x112 (WM_SYSCOMMAND) done
msg=0xa1 (WM_NCLBUTTONDOWN) done

Each time you drag the mouse, Windows gives you the series of messages shown in the loop above. Most interestingly, you get WM_SIZING then WM_NCCALCSIZE then WM_MOVE/WM_SIZE, then you may (more on that below) receive WM_PAINT.

Remember we assume you have provided a WM_ERASEBKGND handler that returns 1 (see "NOT IN SCOPE OF THIS QUESTION" in the question above) so that message does nothing and we can ignore it.

During the processing of those messages (shortly after WM_WINDOWPOSCHANGING returns), Windows makes an internal call to SetWindowPos() to actually resize the window. That SetWindowPos() call first resizes the non-client area (e.g. the title bars and window border) then turns its attention to the client area (the main part of the window that you are responsible for).

During each sequence of messages from one drag, Microsoft gives you a certain amount of time to update the client area by yourself.

The clock for this deadline apparently starts ticking after WM_NCCALCSIZE returns. In the case of OpenGL windows, the deadline is apparently satisfied when you call SwapBuffers() to present a new buffer (not when your WM_PAINT is entered or returns). I do not use GDI or DirectX, so I don't know what the equavalent call to SwapBuffers() is, but you can probably make a good guess and you can verify by inserting Sleep(1000) at various points in your code to see when the behaviors below get triggered.

How much time do you have to meet your deadline? The number seems to be around 40-60 milliseconds by my experiments, but given the kinds of shenanigans Microsoft routinely pulls, I wouldn't be surprised if the number depends on your hardware config or even your app's previous behavior.

If you do update your client area by the deadline, then Microsoft will leave your client area beautifully unmolested. Your user will only see the pixels that you draw, and you will have the smoothest possible resizing.

If you do not update your client area by the deadline, then Microsoft will step in and "help" you by first showing some other pixels to your user, based on a combination of the "Fill in Some Background Color" technique (Section 1c3 of PART 1) and the "Cut off some Pixels" technique (Section 1c4 of PART 1). Exactly what pixels Microsoft shows your user is, well, complicated:

  • If your window has a WNDCLASS.style that includes the CS_HREDRAW|CS_VREDRAW bits (you pass the WNDCLASS structure to RegisterClassEx):

    • Something surprisingly reasonable happens. You get the logical behavior shown in Figures 1c3-1, 1c3-2, 1c4-1, and 1c4-2 of PART 1. When enlarging the client area, Windows will fill in newly exposed pixels with the "background color" (see below) on the same side of the window you are dragging. If needed (left and top border cases), Microsoft does a BitBlt to accomplish this. When shrinking the client area, Microsoft will chop off pixels on the same side of the window you are dragging. This means you avoid the truly heinous artifact that makes objects in your client area appear to move in one direction then move back in the other direction.

    • This may be good enough to give you passable resize behavior, unless you really want to push it and see if you can totally prevent Windows from molesting your client area before you have a chance to draw (see below).

    • Do not implement your own WM_NCCALCSIZE handler in this case, to avoid buggy Windows behavior described below.

  • If your window has a WNDCLASS.style that does not include the CS_HREDRAW|CS_VREDRAW bits (including Dialogs, where Windows does not let you set WNDCLASS.style):

    • Windows tries to "help" you by doing a BitBlt that makes a copy of a certain rectangle of pixels from your old client area and writes that rectangle to a certain place in your new client area. This BitBlt is 1:1 (it does not scale or zoom your pixels).

    • Then, Windows fills in the other parts of the new client area (the parts that Windows did not overwrite during the BitBlt operation) with the "background color."

    • The BitBlt operation is often the key reason why resize looks so bad. This is because Windows makes a bad guess about how your app is going to redraw the client area after the resize. Windows places your content in the wrong location. The net result is that when the user first sees the BitBlt pixels and then sees the real pixels drawn by your code, your content appears to first move in one direction, then jerk back in the other direction. As we explained in PART 1, this creates the most hideous type of resize artifact.

    • So, most solutions for fixing resize problems involve disabling the BitBlt.

    • If you implement a WM_NCCALCSIZE handler and that handler returns WVR_VALIDRECTS when wParam is 1, you can actually control which pixels Windows copies (BitBlts) from the old client area and where Windows places those pixels in the new client area. WM_NCCALCSIZE is just barely documented, but see the hints about WVR_VALIDRECTS and NCCALCSIZE_PARAMS.rgrc[1] and [2] in the MSDN pages for WM_NCCALCSIZE and NCCALCSIZE_PARAMS. You can even provide NCCALCSIZE_PARAMS.rgrc[1] and [2] return values that completely prevent Windows from BitBlting any of the pixels of the old client area to the new client area, or cause Windows to BitBlt one pixel from and to the same location, which is effectively the same thing since no on-screen pixels would get modified. Just set both NCCALCSIZE_PARAMS.rgrc[1] and [2] to the same 1-pixel rectangle. In combination with eliminating the "background color" (see below), this gives you a way to prevent Windows from molesting your window's pixels before you have time to draw them.

    • If you implement a WM_NCCALCSIZE handler and it returns anything other than WVR_VALIDRECTS when wParam is 1, then you get a behavior which (at least on Windows 10) does not at all resemble what MSDN says. Windows seems to ignore whatever left/right/top/bottom alignment flags you return. I advise you do not do this. In particular the popular StackOverflow article How do I force windows NOT to redraw anything in my dialog when the user is resizing my dialog? returns WVR_ALIGNLEFT|WVR_ALIGNTOP and this appears to be completely broken now at least on my Windows 10 test system. The code in that article might work if it is changed to return WVR_VALIDRECTS instead.

    • If you do not have your own custom WM_NCCALCSIZE handler, you get a pretty useless behavior that is probably best avoided:

      • If you shrink the client area, nothing happens (your app gets no WM_PAINT at all)! If you're using the top or left border, your client area contents will move along with the top left of the client area. In order to get any live resizing when shrinking the window, you have to manually draw from a wndproc message like WM_SIZE, or call InvalidateWindow() to trigger a later WM_PAINT.

      • If you enlarge the client area

        • If you drag the bottom or right window border, Microsoft fills in the new pixels with the "background color" (see below)

        • If you drag the top or left window border, Microsoft copies the existing pixels to the top left corner of the expanded window and leaves an old junk copy of old pixels in the newly opened space

So as you can see from this sordid tale, there appear to be two useful combinations:

  • 2a1. WNDCLASS.style with CS_HREDRAW|CS_VREDRAW gives you the behavior in Figures 1c3-1, 1c3-2, 1c4-1, and 1c4-2 of PART 1, which is not perfect but at least your client area content will not move one direction then jerk back in the other direction

  • 2a2. WNDCLASS.style without CS_HREDRAW|CS_VREDRAW plus a WM_NCCALCSIZE handler returning WVR_VALIDRECTS (when wParam is 1) that BitBlts nothing, plus disabling the "background color" (see below) may completely disable Windows' molestation of your client area.

There is apparently another way to achieve the effect of combination 2a2. Instead of implementing your own WM_NCCALCSIZE, you can intercept WM_WINDOWPOSCHANGING (first passing it onto DefWindowProc) and set WINDOWPOS.flags |= SWP_NOCOPYBITS, which disables the BitBlt inside the internal call to SetWindowPos() that Windows makes during window resizing. I have not tried this trick myself but many SO users reported it worked.

At several points above, we mentioned the "background color." This color is determined by the WNDCLASS.hbrBackground field that you passed to RegisterClassEx. This field contains an HBRUSH object. Most people set it using the following boilerplate code:

wndclass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);

The COLOR_WINDOW+1 incantation gives you a white background color. See MSDN dox for WNDCLASS for the +1 explanation and note there is a lot of wrong info about the +1 on StackOverflow and MS forums.

You can choose your own color like this:

wndclass.hbrBackground = CreateSolidBrush(RGB(255,200,122));

You can also disable the background fill-in using:

wndclass.hbrBackground = NULL;

which is another key ingredient of combination 2a2 above. But be aware that newly uncovered pixels will take on some essentially random color or pattern (whatever garbage happens to be in your graphics framebuffer) until your app catches up and draws new client area pixels, so it might actually be better to use combination 2a1 and choose a background color that goes with your app.

2b. Resize Problems from DWM Composition Fill

At a certain point during the development of Aero, Microsoft added another live resize jitter problem on top of the all-Windows-version problem described above.

Reading earlier StackOverflow posts, it is actually hard to tell when this problem was introduced, but we can say that:

  • this problem definitely occurs in Windows 10
  • this problem almost certainly occurs in Windows 8
  • this problem may have also occurred in Windows Vista with Aero enabled (many posts with resize problems under Vista do not say if they have Aero enabled or not).
  • this problem probably did not occur under Windows 7, even with Aero enabled.

The problem revolves around a major change of architecture that Microsoft introduced in Windows Vista called DWM Desktop Composition. Applications no longer draw directly to the graphics framebuffer. Instead, all applications are actually drawing into an off-screen framebuffer which is then composited with the output of other apps by the new, evil Desktop Window Manager (DWM) process of Windows.

So, because there is another process involved in displaying your pixels, there is another opportunity to mess up your pixels.

And Microsoft would never miss such an opportunity.

Here is what apparently happens with DWM Compostion:

  • The user clicks the mouse on a window border and begins to drag the mouse

  • Each time the user drags the mouse, this triggers the sequence of wndproc events in your application that we described in section 2a above.

  • But, at the same time, DWM (which remember is a separate process that is runnning asynchronously to your app) starts its own deadline timer.

  • Similarly to section 2a above, the timer apparently starts ticking after WM_NCCALCSIZE returns and is satisfied when your app draws and calls SwapBuffers().

  • If you do update your client area by the deadline, then DWM will leave your client area beautifully unmolested. There is still a definite chance that your client area could still get molested by the problem in section 2a, so be sure to read section 2a as well.

  • If you do not update your client area by the deadline, then Microsoft will do something truly hideous and unbelievably bad (didn't Microsoft learn their lesson?):

    • Suppose this is your client area before the resize, where A, B, C, and D represent pixel colors at the middle of your client area top, left, right, and bottom edges:

    --------------AAA-----------------
    |                                |
    B                                C
    B                                C
    B                                C
    |                                |
    --------------DDD-----------------
    

    • Suppose you are using the mouse to enlarge your client area in both dimensions. Genius Windows DWM (or perhaps Nvidia: more on that later) will always copy the pixels of your client area to the upper-left corner of the new client area (regardless of which window border you are dragging) and then do the most absurd thing imaginable to the rest of the client area. Windows will take whatever pixel values used to be along the bottom edge of your client area, stretch them out to the new client area width (a terrible idea we explored in Section 1c2 of PART 1, and replicate those pixels to fill in all the newly opened space at the bottom (see what happens to D). Then Windows will take whatever pixel values used to be along the right edge of your client area, stretch them out to the new client area height, and replicate them to fill in the newly opened space at the top-right:

    --------------AAA-----------------------------------------------
    |                                |                             |
    B                                C                             |
    B                                C                             |
    B                                CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    |                                |CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    --------------DDD-----------------CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    ------------------------------DDDDDDDDD-------------------------
    

    • I cannot even imagine what they were smoking. This behavior produces the worst possible result in many cases. First, it's almost guaranteed to generate the horrific back-and-forth motion we showed in Figure 1c3-3 and Figure 1c4-3 of PART 1 when dragging the left and top window borders, since the rectangle copied is always at the upper-left regardless of which window border you are dragging. Second, the even more ridulous thing that's happening with the edge pixels being replicated is going to produce ugly bars if you happen to have any pixels set there other than the background color. Notice how the bars of C and D created do not even line up with the original C and D from the copied old pixels. I can understand why they are replicating the edge, hoping to find background pixels there to "automate" the process of background color detection, but it seems the likelihood of this actually working is heavily outweighed by the hack factor and chance of failure. It would be better if DWM used the app's chosen "background color" (in WNDCLASS.hbrBackground), but I suspect DWM might not have access to that info since DWM is in a different process, hence the hack. Sigh.

But we haven't even gotten to the worst part yet:

  • What actually is the deadline that DWM gives you to draw your own client area before DWM corrupts it with this clumsy hack of a guess? Apparently (from my experiments) the deadline is on the order of 10-15 milliseconds! Given that 15 milliseconds is close to 1/60, I would guess that the deadline is actually the end of the current frame. And the vast majority of apps are unable to meet this deadline most of the time.

That is why, if you launch Windows Explorer on Windows 10 and drag the left border, you will most likely see the scroll bar on the right jitter/flicker/jump around erratically as if Windows were written by a fourth grader.

I cannot believe that Microsoft has released code like this and considers it "done." It is also possible that the responsible code is in the graphics driver (e.g. Nvidia, Intel, ...) but some StackOverflow posts led me to believe that this behavior is cross-device.

There is very little you can do to prevent this layer of incompetence from generating hideous jitter/flicker/jump when resizing using the left or top window border. That is because the rude, non-consentual modification of your client area is happening in another process.

I am really hoping that some StackOverflow user will come up with some magic DWM setting or flag in Windows 10 that we can make to either extend the deadline or disable the horrible behavior completely.

But in the meantime, I did come up with one hack that somewhat reduces the frequency of the hideous back-and-forth artifacts during window resize.

The hack, inspired by a comment in https://stackoverflow.com/a/25364123/1046167 , is to do a best-effort at synchronizing the app process with the vertical retrace that drives DWM's activity. Actually making this work in Windows is not trivial. The code for this hack should be the very last thing in your WM_NCCALCSIZE handler:

LARGE_INTEGER freq, now0, now1, now2;
QueryPerformanceFrequency(&freq); // hz

// this absurd code makes Sleep() more accurate
// - without it, Sleep() is not even +-10ms accurate
// - with it, Sleep is around +-1.5 ms accurate
TIMECAPS tc;
MMRESULT mmerr;
MMC(timeGetDevCaps(&tc, sizeof(tc)), {});
int ms_granularity = tc.wPeriodMin;
timeBeginPeriod(ms_granularity); // begin accurate Sleep() !

QueryPerformanceCounter(&now0);

// ask DWM where the vertical blank falls
DWM_TIMING_INFO dti;
memset(&dti, 0, sizeof(dti));
dti.cbSize = sizeof(dti);
HRESULT hrerr;
HRC(DwmGetCompositionTimingInfo(NULL, &dti), {});

QueryPerformanceCounter(&now1);

// - DWM told us about SOME vertical blank
//   - past or future, possibly many frames away
// - convert that into the NEXT vertical blank

__int64 period = (__int64)dti.qpcRefreshPeriod;

__int64 dt = (__int64)dti.qpcVBlank - (__int64)now1.QuadPart;

__int64 w, m;

if (dt >= 0)
{
    w = dt / period;
}
else // dt < 0
{
    // reach back to previous period
    // - so m represents consistent position within phase
    w = -1 + dt / period;
}

// uncomment this to see worst-case behavior
// dt += (sint_64_t)(0.5 * period);

m = dt - (period * w);

assert(m >= 0);
assert(m < period);

double m_ms = 1000.0 * m / (double)freq.QuadPart;

Sleep((int)round(m_ms));

timeEndPeriod(ms_granularity);

You can convince yourself that this hack is working by uncommenting the line that shows "worst-case" behavior by trying to schedule the drawing right in the middle of a frame rather than at vertical sync, and noticing how many more artifacts you have. You can also try varying the offset in that line slowly and you will see that artifacts abruptly disappear (but not completely) at about 90% of the period and come back again at about 5-10% of the period.

Since Windows is not a real-time OS, it is possible for your app to be preempted anywhere in this code, leading to inaccuracy in the pairing of now1 and dti.qpcVBlank. Preemption in this small code section is rare, but possible. If you want, you can compare now0 and now1 and loop around again if the bound is not tight enough. It is also possible for preemption to disrupt the timing of Sleep() or the code before or after Sleep(). There's not much you can do about this, but it turns out timing errors in this part of the code are swamped by the uncertian behavior of DWM; you are still going to get some window resize artifacts even if your timing is perfect. It's just a heuristic.

There is a second hack, and it is an incredibly creative one: as explained in the StackOverflow post Can't get rid of jitter while dragging the left border of a window, you can actually create two main windows in your app, and every time Windows would do SetWindowPos, you intecept that and instead hide one window and show the other! I haven't tried this yet but the OP reports that it bypasses the insane pixel DWM pixel copy described above.

There is a third hack, which might work depending on your application (especially in combination with the timing hack above). During live resizing (which you can detect by intercepting WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE), you could modify your drawing code to initially draw something much simpler that is much more likely to complete within the deadline imposed by problem 2a and 2b, and call SwapBuffers() to claim your prize: that will be enough to prevent Windows from doing the bad blit/fill described in section 2a and 2b. Then, immediately after the partial draw, do another draw that fully updates the window contents and call SwapBuffers() again. That might still look somewhat odd, since the user will see your window update in two parts, but it's likely to look much better than the hideous back-and-forth motion artifact from Windows.

One more tantalizing point: some apps in Windows 10, including the console (start cmd.exe), are rock-solid free of DWM Composition artifacts even when dragging the left border. So there is some way of bypassing the problem. Let's find it!

2c. How to Diagnose Your Problem

As you try to solve your particular resize problem, you may wonder which of the overlapping effects from Section 2a and Section 2b you are seeing.

One way to separate them is to debug on Windows 7 (with Aero disabled, just to be safe) for a bit.

Another way to quickly identify if you are seeing the problem in Section 2b is to modify your app to display the test pattern described in Section 2b, like this example (note the 1-pixel-thin colored lines on each of the four edges):

Then grab any window border and start resizing that border rapidly. If you see intermittent giant colored bars (blue or green bars in the case of this test pattern, since there is blue on the bottom edge and green on the right edge) then you know you are seeing the problem in Section 2b.

You can test if you are seeing the problem in Section 2a by setting WNDCLASS.hbrBackground to a distinct background color, like red. As you resize the window, newly exposed parts will show up with that color. But read through Section 2a to make sure your message handlers are not causing Windows to BitBlt the entire client area, which would cause Windows not to draw any background color.

Remember that the problems in Section 2a and 2b only show up if your app fails to draw by a certain deadline, and each problem has a different deadline.

So, without modification, your app might show the Section 2b problem only, but if you modify your app to draw more slowly (insert Sleep() in WM_PAINT before SwapBuffers() for example), you may miss the deadline for both Section 2a and Section 2b and start to see both problems simultaneously.

This may also happen when you change your app between a slower DEBUG build and a RELEASE build, which can make chasing these resize problems very frustrating. Knowing what's going on under the hood can help you deal with the confusing results.

这篇关于调整窗口大小时,如何平滑丑陋的抖动/闪烁/跳跃(尤其是拖动左/上边框(Win 7-10; bg,bitblt和DWM))?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆