Lou*_*ini 25 windows directx winapi resize window-resize
问题:当我抓住我的Windows应用程序的调整大小边框,特别是顶部或左边框,并调整窗口大小时,窗口的内容会在我拖动时调整"实时"大小,但是它们会以一种看似像的丑陋方式调整大小即使是最新手的用户也是一个明显的错误:窗口相对边缘的内容从边缘拖动抖动/闪烁/来回跳跃.视情况而定,现象可能如下:
一旦我停止拖动,丑陋的现象就会停止,但在拖动过程中,它会使应用程序看起来很业余和不专业.
说这个Windows问题已经让成千上万的应用程序开发人员疯狂,这并不是轻描淡写的.
以下是现象,友好地准备了两个实例图片一个相关的问题由罗马Starkov:


另一个例子展示了Kenny Liu的邪恶"双重图像"现象(请注意快速闪光):

有关任务管理器现象的另一个示例视频就在这里.
这个问题的目的: 任何遇到这个问题的开发人员都会发现至少有30个StackOverflow问题,其中一些是最近的,有些是从2008年开始的,充满了很有希望的答案很少有效.现实情况是,这一个问题有很多原因,而现有的StackOverflow问题/答案从未使更广泛的背景清晰.这个问题试图回答:
本问题的范围:对于StackOverflow问题的范围,这种现象发生在:
请勿标记为广泛或DUP:在标记为复制或标记为广泛之前,请阅读以下完整答案.我们已经经历了这个循环,SO用户在理解了以下内容之后经常重新打开这个问题:这个问题有益于社区,是第一个解释窗口调整大小抖动的所有不同原因的问答,以便用户可以识别哪个原因导致他们的问题并解决它.下面的答案很长,只是因为解释为什么一般发生抖动是很棘手的,不是因为这个问题涵盖了许多单独的问题和问题,这些错误/问题可以通过单独的问答得到更好的服务.正如您将看到的,上面的所有排列(本机/托管,窗口/对话框,XP-10)都归结为只有两个根本原因,但识别您所拥有的是棘手的部分.这就是这个问题的意义所在.将问题分开将完全否定这一好处.我们在下一节中进一步限制了问题的范围......
不在本问题的范围内:
如果您的应用程序有一个或多个子窗口(子HWND),则此问题中的信息对您很有用(因为BitBlts我们将描述的引起混蛋的内容与父窗口一起应用于您的子窗口),但在窗口调整大小时还有另外一个问题需要处理,这超出了这个问题的范围:你需要让你的所有子窗口以原子方式移动并与父窗口同步.对于这项任务,你可能会想要 BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos,你可以在这里和这里找到它们.
这个问题假设如果你的应用程序使用GDI,DirectX或OpenGL绘制到一个窗口,那么你已经WM_ERASEBKGND在你的应用程序中实现了一个wndproc简单的返回1. WM_ERASEBKGND是来自Windows 3.1的一个神秘的Windows残余,以前WM_PAINT给你的应用程序一个机会在你画窗户之前"擦掉窗户的背景"......嗯.如果你让WM_ERASEBKGND消息进入DefWindowProc(),那将导致你的整个窗口在每次重绘时被涂成纯色,通常是白色,包括在实时窗口大小调整期间发生的重绘.结果是一个丑陋的全窗口闪烁是粗略的,但不是我们在这个问题中讨论的抖动/闪烁/跳跃的类型.拦截WM_ERASEBKGND立即解决了这个问题.
此问题主要是通过使用鼠标拖动窗口边框来实时调整大小.但是,此处编写的大部分内容也适用于当应用程序手动执行一次性窗口调整时可以看到的丑陋工件SetWindowPos().这些不太明显,因为它们只在屏幕上轻弹一瞬间,而不是长时间的拖动.
这个问题不是关于如何使你的特定于应用程序的绘图代码更快,即使这样做可能是许多情况下丑陋的调整大小问题的解决方案.如果您的应用确实需要花费大量时间在实时窗口调整大小期间重新显示其内容,请考虑优化绘图代码,或者至少在调整大小期间切换到更快,质量更低的绘图模式,方法是拦截WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE消息以检测调整大小.
如果您的应用程序在应用程序调整大小期间根本无法调整大小(例如,在调整大小期间它"挂起",特别是如果它是使用GLFW或其他库的OpenGL),请参阅这些其他问题,这些问题解释了WM_SYSCOMMAND在拖动过程中Microsoft内部隐藏的嵌套/模态事件循环:这里特别是这个好的答案,这里,这里,这里和这里.
初步答复
为了开始,我们提供了两个初始答案,您应该按此顺序阅读:
第1部分:什么使大小看起来好或坏?
第2部分:识别和修复Windows调整大小问题
SetWindowPos() BitBlt背景填充和背景填充的问题以及可能有助于其他人收集见解的源材料列表:
我们希望人们能够以创造性的方式提供更多答案,以避免2a,尤其是2b中描述的问题.
Cod*_*ray 12
因为这是一个复杂的、多方面的问题,我建议按以下顺序阅读答案:
第 1 部分:是什么让调整大小看起来好或坏?
第 2 部分:识别并解决 Windows 调整大小问题
SetWindowPos() BitBlt和背景填充以及可以帮助其他人收集见解的源材料列表:
请随时以创造性的方式贡献更多答案,以避免 2a 尤其是 2b 中描述的问题!
Lou*_*ini 11
StackOverflow 上关于平滑调整大小的问题有太多的歧义和不明确,我们需要建立一个通用词汇来帮助人们更清楚地回答。
这就是我们在本节中要做的。
为简单起见,我们将仅在水平维度上解释平滑调整大小的问题,但这里的所有内容都同样适用于垂直调整大小。
下面我们将参考一个窗口的
“非客户区”:Windows 管理的窗口部分,包括顶部的标题栏和所有边缘的窗口边框,以及
“客户区:”您负责的窗口的主要部分
假设您有一个应用程序:
无论窗口如何调整大小。
您的应用程序可能会自己绘制 L/R(例如在一个窗口内使用 GDI/OpenGL/DirectX),或者 L/R 可能是一些 Microsoft 控件(它有自己的 HWND 与您的主窗口 HWND 分开);没关系。
这是应用程序窗口客户区的简化表示。如您所见,我们在客户区最左侧有三列宽的 LLL,在客户区最右侧有三列宽的 RRR,其他各种客户区内容以“-”表示之间(请忽略 StackOverflow 坚持添加的灰色背景;L 和 R 位于客户区的最左侧和最右侧):
LLL-----------RRR
现在想象你抓住这个窗口的左边界或右边界并拖动它来使窗口变大或变小。
想象一下,您的应用程序绘制速度非常快,因此它始终可以在 1 毫秒内响应用户的拖动操作,而操作系统让您的应用程序可以快速绘制,而无需尝试在屏幕上绘制任何其他内容来“帮助”您。
当您拖动应用程序边框时,用户会在屏幕上看到以下内容(这些数字的每一行代表一个时刻):
向右拖动右边框(扩大宽度):
(图 1a-1) LLL-----------RRR(最初,当您单击鼠标时) LLL------------RRR(拖动鼠标时) LLL------------RRR(拖动鼠标时) LLL--------------RRR(当你松开鼠标时)
向左拖动右边框(缩小宽度):
(图 1a-2) LLL-----------RRR LLL----------RRR LLL---------RRR LLL--------RRR
向左拖动左边框(扩大宽度):
(图 1a-3) LLL-----------RRR LLL------------RRR LLL-------------RRR LLL--------------RRR
将左边框向右拖动(缩小宽度):
(图 1a-4) LLL-----------RRR LLL----------RRR LLL---------RRR LLL--------RRR
这些看起来都很好而且很流畅:
到现在为止还挺好。
现在,想象一下您的应用程序绘制速度如此之慢,以至于当您用鼠标拖动时,该应用程序无法跟上您的步伐。是的,最终,您的绘图会赶上来,但我们正在讨论在您用手拖动鼠标期间会发生什么。显然计算机无法伸手抓住你的手来减慢你的鼠标移动速度,所以关键问题是:
例如,向右拖动右边框时(扩大宽度):
(图 1b-1) LLL-----------RRR ?????????????????? (这里应该显示什么?) ????????????????????? (这里应该显示什么?) LLL--------------RRR(应用赶上)
再举一个例子,当向左拖动左边框时(缩小宽度):
(图 1b-2) LLL-----------RRR ?????????????????? (这里应该显示什么?) ??????????????? (这里应该显示什么?) LLL--------RRR(应用赶上)
这些是决定运动是否流畅的关键问题,也是整个 StackOverflow 问题所围绕的关键问题。
不同版本的 Windows 在不同的上下文中对这些问题提供了不同的答案,这意味着更平滑地调整大小的解决方案取决于您所处的情况。
在用户开始拖动鼠标以调整窗口大小之后,但在您的应用程序通过以新大小绘制窗口来赶上之前的时间段内,有多种选择。
屏幕可以保持原样,直到应用程序赶上(您的客户端像素甚至非客户端区域中的窗口边框都不会发生变化):
向右拖动右边框(扩大宽度)时的示例:
(图1c1-1) LLL-----------RRR LLL-----------RRR LLL-----------RRR LLL--------------RRR(应用赶上)
向左拖动左边框时的示例(缩小宽度):
(图1c1-2) LLL-----------RRR LLL-----------RRR LLL-----------RRR LLL--------RRR(应用赶上)
这种方法的明显缺点是,在相关期间,应用程序似乎“挂起”并且似乎对您的鼠标移动没有响应,因为 R 和“-”、L 和窗口边框都没有移动。
微软经常被指责为 Windows 是一个没有响应的操作系统(有时是他们的错,有时是应用程序开发人员的错),所以自从微软引入实时调整大小(Windows XP?)以来,微软从未使用过“什么都不做”的方法通过它自己。
“什么都不做”的方法对用户来说很烦人,而且看起来不专业,但事实证明(非常不明显)它并不总是最糟糕的选择。继续阅读...
另一种可能性是,Windows 总是可以让窗口边框立即跟随您的鼠标移动(因为 Windows 本身有足够的处理能力,至少可以及时绘制非客户区),并且在等待您的应用程序时,Windows 可以获取客户区域的旧像素并放大或缩小这些像素,就像放大/放大图像一样,以便它们“适合”更小或更大的空间。
这种技术通常比任何其他技术都差,因为它会导致原始内容的模糊图像很可能不成比例。所以在任何情况下都没有人应该这样做。除了,正如我们将在第 2 部分中看到的,有时微软会这样做。
放大窗口时可以使用的另一种技术如下:Windows 始终可以使窗口边框立即跟随您的鼠标移动,并且 Windows 可以用一些临时背景色 B 填充现在更大的客户区的新像素:
例如,向右拖动右边框时(扩大宽度):
(图1c3-1) LLL-----------RRR LLL-----------RRRB LLL-----------RRRBB LLL--------------RRR(应用赶上)
这种方法的优点是在所讨论的时间段内,至少您的窗口边框是移动的,因此应用程序感觉响应。
另一个不错的功能是在拖动过程中,L 保持静止,就像它应该的那样。
有点奇怪的是,您在拖动时创建的新空间被一些随机颜色填充,更奇怪的是 R 直到稍后才真正移动(注意 R 在最后一刻向右猛拉 3 列),但至少 R 只会朝着正确的方向移动。这是部分改进。
一个巨大而重要的问题是:新填充的背景色 B 应该是什么颜色?如果 B 恰好是黑色,而您的应用程序恰好具有大部分白色背景,反之亦然,则比 B 与您现有内容的背景颜色相匹配要丑得多。正如我们将在第 2 部分中看到的,Windows 已经部署了几种不同的策略来改进 B 的选择。
现在考虑相同的想法,但将其应用于我们将左边框向左拖动(扩大宽度)的情况。
合乎逻辑的事情是在窗口左侧填充新的背景颜色:
(图1c3-2) LLL-----------RRR BLLL-----------RRR BBLLL-----------RRR LLL--------------RRR(应用赶上)
这是合乎逻辑的,因为 R 会保持原状,就像它应该的那样。L 将具有与我们在上面的图 1c3-1 中描述的相同的怪异(L 会保持静止,然后在最后一刻突然向左猛拉 3 列),但至少 L 只会向正确的方向移动。
然而——这真的会让人感到震惊——在你必须处理的几个重要情况下,Windows 并没有做合乎逻辑的事情。
相反,即使您拖动左侧窗口边框,Windows 有时也会填充右侧的背景像素 B:
(图1c3-3) LLL-----------RRR LLL-----------RRRB LLL-----------RRRBB LLL--------------RRR(应用赶上)
是的,这太疯狂了。
考虑一下这对用户的看法:
L 看起来在一个方向上以恒定速度非常平稳地移动,所以这实际上很好,但是
看看R在做什么:
存款准备金率 存款准备金率 存款准备金率 RRR(应用赶上)
这看起来太可怕了,可怕的,糟糕的,恶心的,......甚至没有词来形容这看起来有多糟糕。
人眼对运动极其敏感,即使是发生在几帧时间内的运动。我们的眼睛立即注意到 R 的这种奇怪的来回运动,我们立即知道有些事情严重错误。
因此,在这里您可以开始了解为什么某些丑陋的调整大小问题仅在您拖动左侧(或顶部)边框而不是右侧(或底部)边框时发生。
实际上,这两种情况(图 1c3-2 与图 1c3-3)都做了一些奇怪的事情。在图 1c3-2 中,我们临时添加了一些不属于那里的背景像素 B。但是这种奇怪的行为远不如图 1c3-3 的来回运动那么明显。
这种来回运动是许多 StackOverflow 问题所涉及的抖动/闪烁/跳跃。
因此,平滑调整大小问题的任何解决方案都必须:
至少防止您客户区中的项目出现向一个方向跳跃然后返回另一个方向的情况。
如果可能的话,最好也避免添加背景像素 B
第 1c3 节涉及扩展窗口。如果我们查看缩小窗口,我们将看到一组完全类似的情况。
缩小窗口时可以使用的技术如下:Windows 始终可以使窗口边框立即跟随您的鼠标移动,并且 Windows 可以简单地切掉(裁剪)您现在更小的客户区的一些像素。
例如,当向左拖动右边框时(缩小宽度):
(图1c4-1) LLL-----------RRR LLL-----------RR LLL-----------R LLL--------RRR(应用赶上)
使用这种技术,L 保持原样,但右侧发生了奇怪的事情:R,无论窗口大小如何,它都应该保持齐平,似乎它的右边缘被右边缘逐渐切掉客户区,直到 R 消失,然后当应用程序赶上时,突然 R 重新出现在正确的位置。这很奇怪,但请记住,R 似乎在任何时候都不会向右移动。R 的左边缘似乎保持静止,直到所有 R 向左跳回 3 列的最后时刻。所以,就像我们在图 1c3-1 中看到的那样,R 只会在正确的方向上移动。
现在考虑当我们向右拖动左边框(缩小宽度)时会发生什么。
合乎逻辑的做法是将客户区左侧的像素刮掉:
(图1c4-2) LLL-----------RRR LL-----------RRR L------------RRR LLL--------RRR(应用赶上)
这将具有与图 1c4-1 相同的奇怪属性,只是左右角色颠倒了。L 似乎从 L 的左边缘逐渐被剃光,但 L 的右边缘将保持静止,直到最后时刻 L 似乎向右跳。所以 L 只会朝着正确的方向移动,尽管是突然的。
但是——是的,再次为完全震惊做好准备——在你必须处理的几个重要情况下,Windows 并没有做合乎逻辑的事情。
相反,即使您拖动左侧窗口边框,Windows 有时也会从右侧切掉像素:
(图1c4-3) LLL-----------RRR LLL-----------RR LLL-----------R LLL--------RRR(应用赶上)
考虑一下这对用户的看法:
L 看起来在一个方向上以恒定速度非常平稳地移动,所以这实际上很好,但是
看看R在做什么:
存款准备金率 RR 电阻 RRR(应用赶上)
正如您在阅读 1c3 部分后现在应该意识到的那样,这种来回运动看起来绝对可怕,并且比图 1c4-1 和图 1c4-2 公认的怪异行为要糟糕得多。
到目前为止,当用户开始拖动窗口边框但应用程序尚未重绘时,我们已经提出了不同的想法。
这些方法实际上可以组合使用。
暂时,试着从微软的角度思考这个问题。在用户开始拖动鼠标来调整窗口大小的那一刻,微软无法提前知道你的应用绘制需要多长时间。所以微软必须取得一个平衡:
如果您的应用程序要快速响应,那么 Microsoft 对屏幕所做的任何更改都会使您的应用程序看起来比 Microsoft 只是让您绘制真实内容更糟(请记住,上述所有技巧在不同程度上都很奇怪,并且会您的内容看起来很奇怪,所以不使用任何这些技巧肯定会更好)。
但是,如果 Microsoft 等待您绘制的时间太长,您的应用程序(以及扩展的 Windows)将看起来像我们在第 1c1 节中所解释的那样卡顿和无响应。这让微软失去面子,即使是你的错。
所以,另一种选择是先推迟任何屏幕变化,给应用程序一定的时间来绘制,如果应用程序未能按时完成,则使用上述方法之一暂时“填补空白”。 ”
这对你来说听起来很可怕吗?你猜怎么着?这就是 Windows 所做的,至少以 2 种不同的方式同时具有 2 个不同的截止时间。 第 2 部分将深入探讨这些案例......
Lou*_*ini 10
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.
SetWindowPos() BitBlt and Background FillThe 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
Run Code Online (Sandbox Code Playgroud)
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]到相同的1像素矩形.结合消除"背景颜色"(见下文),这为您提供了一种方法,可以防止Windows在您有时间绘制窗口之前骚扰窗口的像素.
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);
Run Code Online (Sandbox Code Playgroud)
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));
Run Code Online (Sandbox Code Playgroud)
You can also disable the background fill-in using:
wndclass.hbrBackground = NULL;
Run Code Online (Sandbox Code Playgroud)
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.
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:
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?):
--------------AAA----------------- | | B C B C B C | | --------------DDD-----------------
--------------AAA----------------------------------------------- | | | B C | B C | B CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC | |CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC --------------DDD-----------------CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC | DDDDDDDDD | | DDDDDDDDD | | DDDDDDDDD | | DDDDDDDDD | | DDDDDDDDD | ------------------------------DDDDDDDDD-------------------------
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:
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 /sf/answers/1775488641/ , 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);
Run Code Online (Sandbox Code Playgroud)
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
您可能可以通过查看源材料来收集我遗漏的想法:
2014 与 2017 更新:拖动窗口左边框时无法消除抖动:可能是最新的问题,但仍然缺乏上下文;建议有两个窗口并在实时调整大小期间交替取消隐藏它们的创造性但相当疯狂的技巧!也是我在答案中发现的唯一一个问题,其中提到了 DWM 中的竞争条件以及使用DwmGetCompositionTimingInfo().
2014为什么每次调整 WPF 窗口大小时都会出现黑色延迟?: 是的 WPF 也这样做。没有有用的答案
2009如何修复 WPF 表单调整大小 - 控件滞后和黑色背景?: 控件滞后和黑色背景?” 多 HWND 示例。提及WM_ERASEBKGND和背景画笔技巧,但没有现代答案。
2018使用WPF时有没有办法减少或防止表单闪烁?: 是的,截至 2018 年仍未修复。
2018使用 SetWindowPos 更改窗口左边缘时减少闪烁
:未回答的问题有许多过时的建议,例如WM_NCCALCSIZE
2012 OpenGL 闪烁/损坏,窗口调整大小和 DWM 处于活动状态:很好的问题陈述,回答者完全误解了上下文并提供了不适用的答案。
2012如何避免 GUI 调整大小中的瞬时更新?: 提到了拦截WM_WINDOWPOSCHANGING和设置的技巧WINDOWPOS.flags |= SWP_NOCOPYBITS。
2016 Unity 错误报告:“窗口大小调整非常不连贯和卡顿(边框不能顺滑地跟随鼠标)”在数百个应用程序中发现的典型错误报告部分是由于此错误报告中的问题,部分是由于某些应用程序具有画得慢。我发现的唯一一个文档实际上说 Windows 10 DWM 会限制并扩展旧窗口的外部像素,我可以确认这一点。
2014使用 Windows-8 之前的答案(包括和 )从左侧调整大小时在窗口上闪烁。CS_HREDRAW/CS_VREDRAWWM_NCCALCSIZE
2013 Resizing Window 导致右边框附近出现拖尾现象,老式的 Win-7-only 解决方案禁用 Aero。
2018 Flicker-free expand (resize) of a window to left一个多窗口(multi-HWND)案例的例子,没有真正的答案。
2013 WinAPI C++: Reprogramming Window Resize : 太含糊不清地询问是关于客户区闪烁(如这个问题)还是非客户区闪烁。
2018 GLFW 错误“在 Windows 10 上调整窗口大小会显示跳跃行为”是许多此类错误之一,它们从未解释上下文,就像许多 StackOverflow 帖子一样
2008 年“Flicker Free Main Frame Resizing” CodeProject实际上执行 StretchBlt 但在 Windows 8+ 世界中不起作用,当屏幕上显示不正确的像素时,应用程序无法控制。
2014在 Windows 中平滑调整窗口大小(使用 Direct2D 1.1)?:Windows 8+ DWM 副本的明确但未解决的问题
2010当用户调整我的对话框大小时,如何强制窗口不要在我的对话框中重绘任何内容?:WM_NCCALCSIZE 修复了禁用在 Windows 8+ 中不再有效的 bitblt,因为 DWM 在应用程序有机会显示之前破坏了屏幕。
2014移动/调整窗口大小时闪烁:以前在 Windows 8+ 中不起作用的修复的综述。
2007 WinXP时代“减少闪烁”CodeProject推荐WM_ERASEBKGND+SWP_NOCOPYBITS
2008 年早期的Google Bug报告新的 Vista DWM 问题
如果您使用 DXGI,则可以使用 DirectComposition + WS_EX_NOREDIRECTIONBITMAP 完全绕过重定向表面,并在从 WM_NCCALCSIZE 返回之前(即在任何截止时间计时器启动之前)以新大小渲染/呈现客户区域。这是使用 D3D11 的最小示例:
#include <Windows.h>
#include <d3d11.h>
#include <dcomp.h>
#include <dxgi1_2.h>
ID3D11Device* d3d;
ID3D11DeviceContext* ctx;
IDXGISwapChain1* sc;
/// <summary>
/// Crash if hr != S_OK.
/// </summary>
void hr_check(HRESULT hr)
{
if (hr == S_OK) return;
while (true) __debugbreak();
}
/// <summary>
/// Passthrough (t) if truthy. Crash otherwise.
/// </summary>
template<class T> T win32_check(T t)
{
if (t) return t;
// Debuggers are better at displaying HRESULTs than the raw DWORD returned by GetLastError().
HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
while (true) __debugbreak();
}
/// <summary>
/// Win32 message handler.
/// </summary>
LRESULT window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_CLOSE:
ExitProcess(0);
return 0;
case WM_NCCALCSIZE:
// Use the result of DefWindowProc's WM_NCCALCSIZE handler to get the upcoming client rect.
// Technically, when wparam is TRUE, lparam points to NCCALCSIZE_PARAMS, but its first
// member is a RECT with the same meaning as the one lparam points to when wparam is FALSE.
DefWindowProc(hwnd, message, wparam, lparam);
if (RECT* rect = (RECT*)lparam; rect->right > rect->left && rect->bottom > rect->top)
{
// A real app might want to compare these dimensions with the current swap chain
// dimensions and skip all this if they're unchanged.
UINT width = rect->right - rect->left;
UINT height = rect->bottom - rect->top;
hr_check(sc->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0));
// Do some minimal rendering to prove this works.
ID3D11Resource* buffer;
ID3D11RenderTargetView* rtv;
FLOAT color[] = { 0.0f, 0.2f, 0.4f, 1.0f };
hr_check(sc->GetBuffer(0, IID_PPV_ARGS(&buffer)));
hr_check(d3d->CreateRenderTargetView(buffer, NULL, &rtv));
ctx->ClearRenderTargetView(rtv, color);
buffer->Release();
rtv->Release();
// Discard outstanding queued presents and queue a frame with the new size ASAP.
hr_check(sc->Present(0, DXGI_PRESENT_RESTART));
// Wait for a vblank to really make sure our frame with the new size is ready before
// the window finishes resizing.
// TODO: Determine why this is necessary at all. Why isn't one Present() enough?
// TODO: Determine if there's a way to wait for vblank without calling Present().
// TODO: Determine if DO_NOT_SEQUENCE is safe to use with SWAP_EFFECT_FLIP_DISCARD.
hr_check(sc->Present(1, DXGI_PRESENT_DO_NOT_SEQUENCE));
}
// We're never preserving the client area so we always return 0.
return 0;
default:
return DefWindowProc(hwnd, message, wparam, lparam);
}
}
/// <summary>
/// The app entry point.
/// </summary>
int WinMain(HINSTANCE hinstance, HINSTANCE, LPSTR, int)
{
// Create the DXGI factory.
IDXGIFactory2* dxgi;
hr_check(CreateDXGIFactory1(IID_PPV_ARGS(&dxgi)));
// Create the D3D device.
hr_check(D3D11CreateDevice(
NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
NULL, 0, D3D11_SDK_VERSION, &d3d, NULL, &ctx));
// Create the swap chain.
DXGI_SWAP_CHAIN_DESC1 scd = {};
// Just use a minimal size for now. WM_NCCALCSIZE will resize when necessary.
scd.Width = 1;
scd.Height = 1;
scd.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
scd.SampleDesc.Count = 1;
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
scd.BufferCount = 2;
// TODO: Determine if PRESENT_DO_NOT_SEQUENCE is safe to use with SWAP_EFFECT_FLIP_DISCARD.
scd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
scd.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
hr_check(dxgi->CreateSwapChainForComposition(d3d, &scd, NULL, &sc));
// Register the window class.
WNDCLASS wc = {};
wc.lpfnWndProc = window_proc;
wc.hInstance = hinstance;
wc.hCursor = win32_check(LoadCursor(NULL, IDC_ARROW));
wc.lpszClassName = TEXT("D3DWindow");
win32_check(RegisterClass(&wc));
// Create the window. We can use WS_EX_NOREDIRECTIONBITMAP
// since all our presentation is happening through DirectComposition.
HWND hwnd = win32_check(CreateWindowEx(
WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, TEXT("D3D Window"),
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hinstance, NULL));
// Bind our swap chain to the window.
// TODO: Determine what DCompositionCreateDevice(NULL, ...) actually does.
// I assume it creates a minimal IDCompositionDevice for use with D3D that can't actually
// do any adapter-specific resource allocations itself, but I'm yet to verify this.
IDCompositionDevice* dcomp;
IDCompositionTarget* target;
IDCompositionVisual* visual;
hr_check(DCompositionCreateDevice(NULL, IID_PPV_ARGS(&dcomp)));
hr_check(dcomp->CreateTargetForHwnd(hwnd, FALSE, &target));
hr_check(dcomp->CreateVisual(&visual));
hr_check(target->SetRoot(visual));
hr_check(visual->SetContent(sc));
hr_check(dcomp->Commit());
// Show the window and enter the message loop.
ShowWindow(hwnd, SW_SHOWNORMAL);
while (true)
{
MSG msg;
win32_check(GetMessage(&msg, NULL, 0, 0) > 0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1964 次 |
| 最近记录: |