如何在调整 windows 大小时平滑丑陋的 jitter/flicker/jumping,尤其是拖动 left/top 边框(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)?

问题: 当我抓取 Windows 应用程序的调整大小边框,尤其是顶部或左侧边框,并调整 window 的大小时, window 的内容会在我拖动时“实时”调整大小,但它们会以一种可怕的方式调整大小,即使对于大多数新手用户来说,这看起来也像是一个明显的错误:window 相对边缘的内容我从边缘疯狂地来回拖着jitter/flicker/jump。根据情况,现象可能如下所示:

当我停止拖动时丑陋的现象就停止了,但是在拖动过程中它让应用程序看起来很业余和不专业。

毫不夸张地说,这个 Windows 问题已经 使成千上万的应用程序开发人员疯狂

这里有两张该现象的示例图片,为a related question by Roman Starkov精心准备:

抖动:

边框:

Kenny Liu:

中邪恶的“重影”现象(注意快速闪现)的另一个例子

任务管理器现象的另一个示例视频是 here

问题: 任何遇到此问题的开发人员都会很快发现至少有 30 个 Stack Overflow 问题,有些是最近的,有些是 2008 年的,充满了 promising-sounding 很少有用的答案。事实上,这个问题有 多种原因 ,而现有的 Stack Overflow questions/answers 从未使更广泛的背景变得清晰。本题旨在回答:

(这是一个规范的问答,用于解释 window 调整大小抖动的所有不同原因,以便用户可以识别导致他们的问题的原因并解决它。正如答案所解释的那样,所有上面的排列(native/managed、window/dialog、XP-10)归结为只有两个根本原因,但确定你有哪个是棘手的部分。)

这个问题的范围:对于这个问题的范围,现象发生在:

不在这个问题的范围内:

第 2 部分:识别和修复 Windows 调整大小问题

注意:您需要先阅读 PART 1 才能理解此答案。

此答案不会解决您所有的大小调整问题。

它整理了其他 post 的 still-usable 想法,并添加了一些新颖的想法。

None Microsoft 的 MSDN 上完全记录了此行为,下面是我自己的实验和查看其他 Whosebug posts.

的结果

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

的问题

Windows的所有版本都会出现以下问题。它们可以追溯到 Windows 平台(Windows XP)上 live-scrolling 的最初几天,并且仍然存在于 Windows 10。在最近的 Windows版本,其他调整大小问题可能会叠加在这个问题之上,我们将在下面解释。

这里是与单击 window 边框并拖动该边框的典型 session 关联的 Windows 事件。缩进表示嵌套 wndproc(嵌套是因为已发送(不是 posted)消息或因为上面问题中“不在本问题的范围内”中提到的可怕的 Windows 模态事件循环):

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

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

请记住,我们假设您已经提供了一个 WM_ERASEBKGND 处理程序 returns 1(请参阅上面问题中的“不在此问题的范围内”),以便该消息不执行任何操作,我们可以忽略它。

在处理这些消息期间(在 WM_WINDOWPOSCHANGING return 秒后不久),Windows 对 SetWindowPos() 进行内部调用以实际调整 window. SetWindowPos() 调用首先调整 non-client 区域的大小(例如标题栏和 window 边框),然后将注意力转移到客户区(window 的主要部分负责)。

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

这个截止日期的时钟显然在 WM_NCCALCSIZE return 秒后开始滴答作响。在 OpenGL windows 的情况下,当您调用 SwapBuffers() 以显示新缓冲区时(不是在输入 WM_PAINT 或 returns 时)显然已满足截止日期。我不使用 GDI 或 DirectX,所以我不知道对 SwapBuffers() 的等效调用是什么,但您可能会做出一个很好的猜测,并且您可以通过在您的不同位置插入 Sleep(1000) 来验证用于查看以下行为何时被触发的代码。

你有多少时间赶在最后期限前完成?根据我的实验,这个数字似乎在 40-60 毫秒左右,但考虑到微软经常使用的各种恶作剧,如果这个数字取决于你的硬件配置甚至你的应用程序以前的行为,我也不会感到惊讶。

如果您在截止日期前更新了您的客户区,那么 Microsoft 将使您的客户区完好无损。您的用户将看到您绘制的像素,并且您将尽可能平滑地调整大小。

如果您没有在截止日期前更新您的客户区,那么 Microsoft 将介入并“帮助”您,首先根据“填充一些背景颜色”技术(PART 1 的第 1c3 节)和“切断一些像素”技术(PART 1 的第 1c4 节)的组合。 Microsoft 向您的用户显示的确切像素非常复杂:

  • 如果您的 window 有一个包含 CS_HREDRAW|CS_VREDRAW 位的 WNDCLASS.style(您将 WNDCLASS 结构传递给 RegisterClassEx) :

    • 发生了一件出奇合理的事情。您会得到 PART 1 的图 1c3-1、1c3-2、1c4-1 和 1c4-2 中所示的逻辑行为。放大客户区时,Windows 将在您拖动的 window 同一侧用“背景颜色”(见下文)填充新暴露的像素。如果需要(左边框和上边框),Microsoft 会执行 BitBlt 来完成此操作。缩小客户区时,Microsoft 会切断您拖动的 window 同一侧的像素。这意味着您可以避免真正令人发指的伪像,这种伪像会使您的客户区中的 object 看起来向一个方向移动然后向另一个方向移动。

    • 这可能足以让您可以通过调整大小行为,除非您真的想推动它并看看您是否可以完全防止 Windows 在您拥有之前骚扰您的客户区有机会抽签(见下文)。

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

  • 如果您的 window 有一个 WNDCLASS.style 不包含 CS_HREDRAW|CS_VREDRAW 位(包括对话框,其中 Windows 不允许您设置 WNDCLASS.style):

    • Windows 试图通过 BitBlt 来“帮助”您,从您的旧客户区复制某个像素矩形并将该矩形写入新客户区的某个地方。此 BitBlt 是 1:1(它不会缩放或缩放您的像素)。

    • 然后,Windows用“背景颜色。”

    • BitBlt 操作通常是调整大小看起来很糟糕的关键原因。这是因为 Windows 对您的应用程序在调整大小后将如何重绘客户区做出了错误的猜测。 Windows 将您的内容放在错误的位置。最终结果是,当用户首先看到 BitBlt 像素,然后看到您的代码绘制的真实像素时,您的内容似乎首先向一个方向移动,然后又向另一个方向猛拉回来。正如我们在 PART 1 中解释的那样,这会创建最可怕的调整大小工件类型。

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

    • 如果你实现一个 WM_NCCALCSIZE 处理程序,并且当 wParam 为 1 时,该处理程序 returns WVR_VALIDRECTS,你实际上可以控制哪些像素 Windows 从旧客户区复制 (BitBlts),Windows 将这些像素放置在新客户区。 WM_NCCALCSIZE 几乎没有记录,但请参阅 WM_NCCALCSIZE and NCCALCSIZE_PARAMS 的 MSDN 页面中有关 WVR_VALIDRECTSNCCALCSIZE_PARAMS.rgrc[1] and [2] 的提示。您甚至可以提供 NCCALCSIZE_PARAMS.rgrc[1] and [2] return 值来完全阻止 Windows 从 BitBlting 旧客户区的任何像素到新客户区,或者导致 Windows 到 BitBlt 一个像素从和到同一位置,这实际上是同一件事,因为没有 on-screen 像素会被修改。只需将两个 NCCALCSIZE_PARAMS.rgrc[1] and [2] 设置为相同的 1 像素矩形。结合消除“背景颜色”(见下文),这为您提供了一种方法来防止 Windows 在您有时间绘制它们之前骚扰您的 window 的像素。

    • 如果您实现了一个 WM_NCCALCSIZE 处理程序并且当 wParam 为 1 时它 return 是 WVR_VALIDRECTS 以外的任何东西,那么您会得到一个行为这(至少在 Windows 10)根本不像 MSDN 所说的那样。 Windows 似乎忽略了任何 left/right/top/bottom 对齐标志你 return。我建议你不要这样做。特别是流行的 Whosebug 文章 How do I force windows NOT to redraw anything in my dialog when the user is resizing my dialog? returns WVR_ALIGNLEFT|WVR_ALIGNTOP 并且这似乎已完全损坏,至少在我的 Windows 10 测试系统上是这样。如果改为 return WVR_VALIDRECTS,那篇文章中的代码可能会起作用。

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

      • 如果您缩小客户区,则什么也不会发生(您的应用根本不会 WM_PAINT)!如果您使用的是上边框或左边框,您的客户区内容将与客户区的左上角一起移动。为了在缩小 window 时获得任何实时调整大小,您必须手动从 wndproc 消息中绘制 WM_SIZE,或调用 InvalidateWindow() 触发稍后的 [=21] =].

      • 如果扩大客户区

        • 如果拖动底部或右侧 window 边框,Microsoft 会用“背景色”填充新像素(见下文)

        • 如果拖动顶部或左侧 window 边框,Microsoft 会将现有像素复制到展开后 window 的左上角,并留下旧的旧垃圾副本新打开的像素 space

正如您从这个肮脏的故事中看到的那样,似乎有两个有用的组合:

  • 2a1。 WNDCLASS.styleCS_HREDRAW|CS_VREDRAW 给出了 PART 1 的图 1c3-1、1c3-2、1c4-1 和 1c4-2 中的行为,这并不完美,但至少是您的客户区内容不会向一个方向移动然后向另一个方向猛拉回来

  • 2a2。 WNDCLASS.style 没有 CS_HREDRAW|CS_VREDRAW 加一个 WM_NCCALCSIZE 处理程序 returning WVR_VALIDRECTS (当 wParam 为 1 时) BitBlts 什么都没有,加上禁用“背景颜色”(见下文)可能会完全禁止 Windows' 骚扰您的客户区。

显然还有另一种方法可以达到组合2a2的效果。您可以拦截 WM_WINDOWPOSCHANGING(首先将其传递给 DefWindowProc)并设置 WINDOWPOS.flags |= SWP_NOCOPYBITS,从而禁用内部调用中的 BitBlt,而不是实现您自己的 WM_NCCALCSIZE SetWindowPos() Windows 在 window 调整大小时进行。我自己没有尝试过这个技巧,但许多 SO 用户报告说它有效。

在上面的几个地方,我们提到了“背景色”。此颜色由您传递给 RegisterClassExWNDCLASS.hbrBackground 字段决定。该字段包含一个 HBRUSH object。大多数人使用以下 boi 设置它错误代码:

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

COLOR_WINDOW+1 咒语给你一个白色的背景颜色。有关 +1 的解释,请参阅 MSDN dox for WNDCLASS,并注意 Whosebug 和 MS 论坛上有很多关于 +1 的错误信息。

您可以这样选择自己的颜色:

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

您还可以禁用背景 fill-in 使用:

wndclass.hbrBackground = NULL;

这是上述组合 2a2 的另一个关键成分。但请注意,新发现的像素将呈现出一些本质上随机的颜色或图案(无论图形帧缓冲区中碰巧有什么垃圾),直到您的应用赶上并绘制新的客户区像素,因此实际上使用组合 2a1 和选择适合您的应用的背景颜色。

2b。从 DWM 合成填充调整大小问题

在 Aero 开发过程中的某个时刻,Microsoft 在上述 all-Windows-version 问题之上添加了 另一个 实时调整大小抖动问题。

阅读早期的 Whosebug posts,实际上很难判断这个问题是什么时候引入的,但我们可以说:

  • 这个问题肯定出现在Windows10
  • 这个问题几乎肯定发生在Windows8
  • 此问题也可能发生在 Windows 启用了 Aero 的 Vista 中(许多 post 在 Vista 下出现调整大小问题的人都没有说明是否启用了 Aero)。
  • 在 Windows 7 下可能不会出现此问题,即使启用了 Aero。

问题围绕着微软在 Windows Vista 中引入的架构的重大变化,称为 DWM Desktop Composition。应用程序不再直接绘制到图形帧缓冲区。相反,所有应用程序实际上都绘制到 off-screen 帧缓冲区中,然后由 Windows 的新的邪恶桌面 Window 管理器 (DWM) 进程与其他应用程序的输出合成。

因此,由于显示像素涉及另一个过程,因此还有另一个机会弄乱您的像素。

微软绝不会放过这样的机会。

这显然是 DWM Compostion 发生的事情:

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

  • 每次用户拖动鼠标时,都会触发我们在上面第 2a 节中描述的应用程序中的 wndproc 事件序列。

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

  • 与上面的第 2a 部分类似,计时器显然在 WM_NCCALCSIZE return 秒后开始计时,并在您的应用绘制和调用 SwapBuffers() 时满足。

  • 如果您在截止日期前更新了您的客户区,那么 DWM 将让您的客户区完好无损。您的客户区仍有一定的机会受到第 2a 节中的问题的困扰,因此请务必阅读第 2a 节。

  • 如果你在截止日期前更新你的客户区,那么微软将做一些非常可怕和令人难以置信的坏事(微软没有吸取教训吗?):

    • 假设这是调整大小之前的客户区,其中 A、B、C 和 D 代表客户区顶部、左侧、右侧和底部边缘中间的像素颜色:
      --------------AAA-----------------
      |                                |
      B                                C
      B                                C
      B                                C
      |                                |
      --------------DDD-----------------
      
    • 假设您正在使用鼠标在两个维度上扩大您的客户区。 Genius Windows DWM(或者 Nvidia:稍后会详细介绍)将始终将您的客户区像素复制到新客户区的 upper-left 角(无论您 window 边界是哪个正在拖动)然后对客户区的其余部分做最荒谬的事情。 Windows 将采用沿客户区底部边缘的任何像素值,将它们拉伸到新的客户区宽度(我们在 PART 1 的第 1c2 节中探讨了一个糟糕的想法,并复制那些像素来填充底部所有新打开的 space(看看 D 会发生什么)。然后 Windows 将采用沿客户区域右边缘的任何像素值,将它们展开到新的客户区高度,并复制它们填入新开的space处top-right:
      --------------AAA-----------------------------------------------
      |                                |                             |
      B                                C                             |
      B                                C                             |
      B                                CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
      |                                |CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
      --------------DDD-----------------CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
      |                             DDDDDDDDD                        |
      |                             DDDDDDDDD                        |
      |                             DDDDDDDDD                        |
      |                             DDDDDDDDD                        |
      |                             DDDDDDDDD                        |
      ------------------------------DDDDDDDDD-------------------------
      
    • 我什至无法想象他们抽的是什么。在许多情况下,这种行为会产生最坏的结果。首先,当拖动左侧和顶部 window 边框时,几乎可以保证生成我们在 PART 1 的图 1c3-3 和图 1c4-3 中显示的可怕 back-and-forth 运动,因为矩形被复制始终位于 upper-left,无论您拖动哪个 window 边框。其次,复制边缘像素时发生的更荒谬的事情将产生 ugl如果你碰巧在那里设置了除背景颜色以外的任何像素。请注意,创建的 C 和 D 条甚至不与复制的旧像素中的原始 C 和 D 对齐。我能理解他们为什么要复制边缘,希望在那里找到背景像素以“自动化”背景颜色检测过程,但似乎黑客因素和失败的可能性大大超过了这种实际工作的可能性。如果 DWM 使用应用程序选择的“背景颜色”(在 WNDCLASS.hbrBackground 中)会更好,但我怀疑 DWM 可能无法访问该信息,因为 DWM 处于不同的进程中,因此被黑客攻击。唉。

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

  • 在 DWM 用这种笨拙的猜测破坏它之前,DWM 给你绘制自己的客户区的最后期限到底是什么时候?显然(根据我的实验)截止日期是 大约 10-15 毫秒 !鉴于 15 毫秒接近 1/60,我猜测截止日期实际上是当前帧的结尾。大多数应用程序 无法 满足此截止日期。

这就是为什么,如果您在 Windows 10 上启动 Windows Explorer 并拖动左边框,您很可能会看到 jitter/flicker/jump 右侧的滚动条不规则地围绕如果 Windows 是四年级学生写的。

我无法相信 Microsoft 发布了这样的代码并认为它​​“完成了”。也有可能负责的代码在图形中 driver(例如 Nvidia、Intel 等),但一些 Whosebug post 让我相信这种行为是 cross-device。

在使用左边框或上边框 window 调整大小时,您几乎无法阻止这一层无能生成可怕的 jitter/flicker/jump。那是因为粗鲁地,non-consentual 对您的客户区的修改发生在另一个进程中。

我真的希望一些 Whosebug 用户会在 Windows 10 中提出一些神奇的 DWM 设置或标志,我们可以用来延长截止日期或完全禁用可怕的行为。

但与此同时,我确实想出了一个技巧,可以在一定程度上降低 window 调整大小期间可怕的 back-and-forth 伪影的频率。

https://whosebug.com/a/25364123/1046167 中评论的启发,该 hack 是 best-effort 将应用程序进程与驱动 DWM activity 的垂直回溯同步。实际上,在 Windows 中完成这项工作并非易事。这个 hack 的代码应该是你的 WM_NCCALCSIZE 处理程序中的最后一件事:

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);

你可以通过取消注释显示“worst-case”行为的行来说服自己这个 hack 正在工作,方法是尝试将绘图安排在帧的中间而不是垂直同步,并注意如何你有更多的文物。您也可以尝试慢慢改变该行中的偏移量,您会看到伪像在大约 90% 的周期突然消失(但不是完全消失),并在大约 5-10% 的周期再次出现。

由于 Windows 不是 real-time OS,因此您的应用可能是 在此代码中的任何地方被抢占,导致 now1dti.qpcVBlank 的配对不准确。这个小代码段中的抢占很少见,但有可能。如果需要,您可以比较 now0now1 并在边界不够紧的情况下再次循环。抢占也有可能打乱Sleep()的时序或Sleep()前后的代码。您对此无能为力,但事实证明这部分代码中的计时错误被 DWM 的不确定行为淹没了;你仍然会得到一些 window 调整大小的工件,即使你的时机是完美的。这只是一种启发式方法。

还有第二个 hack,这是一个非常有创意的 hack:正如 Whosebug post Can't get rid of jitter while dragging the left border of a window 中所解释的,您实际上可以在您的应用程序中创建两个主要的 windows,每次 Windows 会做 SetWindowPos,你就会拦截它,而不是隐藏一个 window 并显示另一个!我还没有尝试过,但是 OP 报告说它绕过了上面描述的疯狂像素 DWM 像素复制。

还有第三种 hack,它可能会起作用,具体取决于您的应用程序(尤其是与上面的计时 hack 结合使用)。在实时调整大小期间(您可以通过拦截 WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE 检测到),您可以修改绘图代码以最初绘制更简单的东西,更有可能在问题 2a 和 2b 规定的截止日期内完成,然后调用 SwapBuffers() 领取您的奖品:这足以防止 Windows 做出第 2a 和 2b 节中描述的坏事 blit/fill。然后,在部分绘制之后,立即进行另一次完全更新 window 内容的绘制并再次调用 SwapBuffers()。这看起来可能仍然有些奇怪,因为用户会看到您的 window 更新 i两部分,但它看起来可能比 Windows.

中可怕的 back-and-forth 运动伪影要好得多

还有一个诱人的地方:Windows 10 中的一些应用程序,包括控制台(启动 cmd.exe),rock-solid 即使拖动左边框也没有 DWM 组合工件。所以有 某种方法 可以绕过这个问题。让我们找到它!

2c。如何诊断您的问题

当您尝试解决特定的调整大小问题时,您可能想知道您看到的是第 2a 节和第 2b 节中的哪些重叠效果。

分离它们的一种方法是在 Windows 7(禁用 Aero,只是为了安全)上调试一下。

另一种快速确定您是否遇到第 2b 节中的问题的方法是修改您的应用以显示第 2b 节中描述的测试模式,就像这个例子(注意 1-pixel-thin 彩色线条四个边中的每一个):

然后抓住任何 window 边框并开始快速调整该边框的大小。如果您看到断断续续的巨大彩色条(在此测试模式中为蓝色或绿色条,因为底部边缘为蓝色而右侧边缘为绿色)那么您就知道您遇到了第 2b 节中的问题。

您可以通过将 WNDCLASS.hbrBackground 设置为不同的背景颜色(例如红色)来测试您是否看到第 2a 部分中的问题。当您调整 window 的大小时,新曝光的部分将以该颜色显示。但是通读第 2a 节以确保您的消息处理程序不会导致 Windows 到 BitBlt 整个客户区,这会导致 Windows 不绘制任何背景颜色。

请记住,第 2a 和 2b 节中的问题仅在您的应用未能在特定截止日期之前绘制时才会出现,并且每个问题都有不同的截止日期。

因此,如果不进行修改,您的应用程序可能只会显示第 2b 部分的问题,但是如果您修改应用程序以使其绘制速度更慢(在 SwapBuffers() 之前的 WM_PAINT 中插入 Sleep()例如),您可能会错过第 2a 节和第 2b 节的截止日期,并开始同时看到这两个问题。

当您在较慢的 DEBUG 构建和 RELEASE 构建之间更改您的应用程序时,也可能会发生这种情况,这会使追逐这些调整大小的问题变得非常令人沮丧。了解引擎盖下发生的事情可以帮助您处理令人困惑的结果。

第 1 部分:是什么让调整大小看起来好还是坏?

在 Whosebug 中关于平滑调整大小的问题有太多的歧义和不明确之处,我们需要建立一个通用的词汇表来帮助人们更清楚地回答他们。

这就是我们将在本节中做的事情。

为简单起见,我们将仅在水平维度上解释平滑调整大小的问题,但这里的所有内容都适用于垂直调整大小。

下面我们会参考一个window的

  • "non-client area:" window 中由 Windows 管理的部分,包括顶部的标题栏和所有边缘的 window 边框,和

  • "client area:"您负责window的主要部分

假设您有一个应用:

  • 应该保留的按钮或标签 L flush-left
  • 应该保留的按钮或标签 R flush-right

无论 window 如何调整大小。

您的应用程序可能会绘制 L/R 本身(例如,在 window 中使用 GDI/OpenGL/DirectX)或者 L/R 可能是某个 Microsoft 控件(它有自己的 HWND与您的主要 window HWND 分开);没关系。

这是您的应用程序客户区的简化表示 window。可以看到,我们在客户区最左边有three-column-wide LLL,在客户区最右边有three-column-wide RRR,其他各种客户区内容用“-”表示between(请忽略 Whosebug 坚持添加的灰色背景;L 和 R 位于您的客户区的最左边缘和最右边缘):

LLL-----------RRR

现在想象一下,您抓住 window 的左边框或右边框并拖动它以使 window 变大或变小。

1a。简单案例:按时绘图

假设您的应用程序绘制速度非常快,因此它始终可以在 1 毫秒内响应用户的拖动操作,并且 OS 让您的应用程序能够快速绘制而无需尝试在屏幕给 "help" 你。

当您拖动应用程序边框时,用户会看到以下内容 on-screen(这些数字的每一行代表一个瞬间):

向右拖动右边框(扩大宽度):

(Figure 1a-1)
LLL-----------RRR     (initially, when you click the mouse)
LLL------------RRR    (as you drag the mouse)
LLL-------------RRR   (as you drag the mouse)
LLL--------------RRR  (when you release the mouse)

将右边框向左拖动(缩小宽度):

(Figure 1a-2)
LLL-----------RRR
LLL----------RRR
LLL---------RRR
LLL--------RRR

向左拖动左边框(扩大宽度):

(Figure 1a-3)
   LLL-----------RRR
  LLL------------RRR
 LLL-------------RRR
LLL--------------RRR

向右拖动左边框(缩小宽度):

(Figure 1a-4)
LLL-----------RRR
 LLL----------RRR
  LLL---------RRR
   LLL--------RRR

这些看起来都很好很光滑:

  • 调整右边框时,R似乎在一个方向上以恒定速度移动,而L保持不动。
  • 调整左边框时,L似乎在一个方向上以恒定速度移动,而R保持不动。

到目前为止一切顺利。

1b。硬壳:绘图落后

现在,假设您的应用程序绘图速度太慢,以致于您用鼠标拖动时应用程序跟不上您的速度。是的,最终,您的绘图会赶上,但我们正在谈论的是在您用手拖动鼠标期间发生的情况。显然电脑无法伸手抓住你的手来减慢你的鼠标移动速度,所以关键问题是:

  • 在此期间屏幕上应该显示什么,以及
  • 谁来决定应该显示什么?

例如右边框向右拖动时(放大宽度):

(Figure 1b-1)
LLL-----------RRR
??????????????????    (what should show here?)
???????????????????   (what should show here?)
LLL--------------RRR  (app catches up)

再举个例子,当左边框向左拖动时(缩小宽度):

(Figure 1b-2)
LLL-----------RRR
 ????????????????  (what should show here?)
  ???????????????  (what should show here?)
   LLL--------RRR  (app catches up)

事实证明,这些是决定运动是否流畅的关键问题,也是整个 Whosebug 问题所围绕的关键问题。

Windows 的不同版本在不同的上下文中对这些问题提供了不同的答案,这意味着更平滑地调整大小的解决方案取决于您所处的情况。

1c。等待应用绘制时的临时解决方案

在用户开始拖动鼠标调整 window 大小之后,但在您的应用赶上绘制 window 之前的时间段内,有多种选择新尺寸。

1c1。什么都不做

屏幕可以保持原样,直到应用程序赶上(您的客户端像素甚至 non-client 区域中的 window 边框都不会改变):

向右拖动右边框(放大宽度)示例:

(Figure 1c1-1)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
LLL--------------RRR  (app catches up)

向左拖动左边框(缩小宽度)时的示例:

(Figure 1c1-2)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
   LLL--------RRR  (app catches up)

此方法的明显缺点是,在相关期间,应用程序似乎有 "hung" 并且似乎对您的鼠标移动没有反应s,因为 R、'-'、L 和 window 边界都没有移动。

Microsoft 经常被指责为 Windows 反应迟钝 OS(这有时是他们的错,有时是应用程序开发人员的错),所以自从 Microsoft 推出 live-resize (Windows XP?),微软从来不单独使用"do nothing"方法。

"do nothing" 方法对用户来说很烦人而且看起来不专业,但事实证明(非常non-obviously)它并不总是最糟糕的选择。继续阅读...

1c2。缩放内容

另一种可能性是Windows总能使window边框立即跟随你的鼠标移动(因为Windows本身有足够的处理能力至少绘制non-client 区域),并且在等待您的应用程序时,Windows 可以获取客户区的旧像素并向上或向下缩放这些像素,就像 zoom/blow 放大图像一样这样他们 "fit" 在更小或更大的 space.

这种技术通常比任何其他技术都差,因为它会导致原始内容的图像模糊,很可能不成比例。所以在任何情况下都不应该这样做。除了,正如我们将在 PART 2 中看到的,有时 Microsoft 会这样做。

1c3。放大时,填充一些背景颜色

另一种在放大 window 时可以使用的技术如下:Windows 总是可以使 window 边框立即跟随您的鼠标移动,而 Windows 可以用一些临时背景色填充 now-larger 客户区的新像素 B:

例如右边框向右拖动时(放大宽度):

(Figure 1c3-1)
LLL-----------RRR
LLL-----------RRRB
LLL-----------RRRBB
LLL--------------RRR  (app catches up)

此方法的优点是,在相关期间,至少您的 window 边框 移动的,因此应用感觉响应迅速。

另一个不错的功能是在拖动过程中,L 保持静止,就像它应该的那样。

有点奇怪的是,您在拖动时创建的新 space 填充了一些随机颜色,更奇怪的是 R 直到后来才真正移动(注意 R 向右猛拉在最后一刻减少了 3 列),但至少 R 只在正确的方向上移动。这是部分改进。

一个巨大而重要的问题是:新的 filled-in 背景色 B 应该是什么颜色?如果 B 碰巧是黑色的,而你的应用恰好有一个大部分是白色的背景,反之亦然,它会比 B 与你现有内容的背景颜色相匹配更难看。正如我们将在 PART 2 中看到的,Windows 部署了几种不同的策略来改进 B 的选择。

现在考虑同样的想法,但是将其应用于我们将左边框向左拖动(扩大宽度)的情况。

合乎逻辑的做法是在 window:

的左侧填充新的背景颜色
(Figure 1c3-2)
   LLL-----------RRR
  BLLL-----------RRR
 BBLLL-----------RRR
LLL--------------RRR  (app catches up)

这是合乎逻辑的,因为 R 会保持原样,就像它应该的那样。 L 将具有与我们在上面的图 1c3-1 中描述的相同的怪异(L 将保持静止,然后在最后一瞬间突然向左猛拉 3 列),但至少 L 只会朝正确的方向移动。

然而——这真的会让人震惊——在你必须处理的几个重要案例中,Windows 并没有做合乎逻辑的事情。

相反,Windows 有时会填充右侧的背景像素 B,即使您正在拖动左侧 window 边框:

(Figure 1c3-3)
   LLL-----------RRR
  LLL-----------RRRB
 LLL-----------RRRBB
LLL--------------RRR  (app catches up)

是的,这太疯狂了。

考虑用户的看法:

  • L看起来在一个方向上以恒定的速度非常平稳地移动,所以这实际上很好,但是

  • 看看R在做什么:

      RRR
     RRR
    RRR  
      RRR  (app catches up)
    
    • R 首先向左移动两列,它应该 这样做:R 应该始终保持flush-right
    • R 然后再次回到右边。天哪!

这看起来太可怕了,可怕的,糟糕透顶的,令人作呕的,...甚至无法用语言来形容这看起来有多糟糕。

人眼对运动极其敏感,即使是在几帧时间内发生的运动也是如此。我们的眼睛立即注意到 R 的这种奇怪的 back-and-forth 运动,我们立即知道出现了严重错误。

所以在这里您可以开始了解为什么只有当您拖动左(或上)边框而不是右(或下)边框时才会出现这些丑陋的调整大小问题。

实际上,这两种情况(图 1c3-2 与图 1c3-3)都做了一些奇怪的事情。在图 1c3-2 中,我们临时添加了一些不属于那里的背景像素 B。但是这种奇怪的行为比图 1c3-3 的 back-and-forth 运动更不引人注意。

这个back-and-forth动议 许多 Whosebug 问题都是关于 jitter/flicker/jumping 的。

因此,平滑调整大小问题的任何解决方案都必须:

  • 至少防止客户区中的项目出现向一个方向跳跃然后向另一个方向返回。

  • 如果可能的话最好也避免添加背景像素 B

1c4。缩小时,切掉一些像素

第 1c3 节涉及扩展 window。如果我们查看缩小 window,我们将看到一组完全相似的情况。

缩小 window 时可以使用的技术如下:Windows 总是可以使 window 边框立即跟随您的鼠标移动,而 Windows 可以简单地砍掉(裁剪)您的 now-smaller 客户区的一些像素。

例如,将右边框向左拖动(缩小宽度)时:

(Figure 1c4-1)
LLL-----------RRR
LLL-----------RR
LLL-----------R
LLL--------RRR     (app catches up)

使用这种技术,L 保持原样,但右侧发生了一件奇怪的事情:R,无论 window 大小如何,都应该保持 flush-right,看起来使其右边缘逐渐被客户区的右边缘切掉,直到 R 消失,然后当应用程序赶上时,R 突然重新出现在正确的位置。这很奇怪,但请记住,R 似乎从来没有向右移动。 R 的左边缘似乎保持静止,直到所有 R 向左跳回 3 列的最后一刻。所以,就像我们在图 1c3-1 中看到的那样,R 只向正确的方向移动。

现在考虑当我们将左边框向右拖动(缩小宽度)时会发生什么。

合乎逻辑的做法是去除客户区左侧的像素:

(Figure 1c4-2)
LLL-----------RRR
 LL-----------RRR
  L-----------RRR
   LLL--------RRR  (app catches up)

这将具有与图 1c4-1 相同的怪异特性,只是左右角色互换了。 L 似乎逐渐从 L 的左边缘被削掉,但 L 的右边缘将保持静止,直到最后一刻 L 似乎跳到右边。所以 L 只会朝着正确的方向移动,尽管是突然的。

但是——是的,准备再次彻底震惊——在你必须处理的几个重要案例中,Windows 没有做合乎逻辑的事情。

相反,Windows 有时会切断右侧的像素,即使您拖动左侧 window 边框也是如此:

(Figure 1c4-3)
LLL-----------RRR
 LLL-----------RR
  LLL-----------R
   LLL--------RRR  (app catches up)

考虑用户的看法:

  • L看起来在一个方向上以恒定的速度非常平稳地移动,所以这实际上很好,但是

  • 看看R在做什么:

    RRR
     RR
      R
    RRR  (app catches up)
    
    • R先向右滑动两列。 R 的左边缘似乎与 R 的其余部分一起向右移动。
    • R 然后再次回到左边

正如您在阅读第 1c3 节后现在应该意识到的那样,这个 back-and-forth 动作看起来绝对可怕,并且比图 1c4-1 和图 1c4-2 公认的怪异行为要糟糕得多。

1c5。稍等一下,然后尝试以上之一

到目前为止,当用户开始拖动 window 边框但应用尚未重绘时,我们已经提出了不同的想法。

这些方法其实可以结合使用

暂时,试着从微软的角度来思考这个问题。在用户开始拖动鼠标调整 window 大小的那一刻,Microsoft 无法提前知道您的应用程序需要多长时间才能绘制。所以微软必须取得平衡:

  • 如果您的应用程序要快速响应,那么 Microsoft 对屏幕所做的任何更改都会使您的应用程序看起来比 Microsoft 只让您绘制真实内容更糟糕(请记住,所有上面的技巧在不同程度上很奇怪,会让你的内容看起来很奇怪,所以不用这些技巧肯定更好。

  • 但如果 Microsoft 等待您绘制的时间过长,您的应用程序(以及 Windows 的扩展)将看起来像我们在第 1c1 节中解释的那样无响应。这让微软即使是你的错也很没面子

所以,另一种选择是先推迟任何屏幕变化,给应用程序一定的时间来绘制,如果应用程序未能在截止日期前完成,则采用上述方法之一暂时"fill in the gap."

你觉得这听起来很可怕吗?你猜怎么了?这就是 Windows 所做的,至少同时以 2 种不同的方式和 2 个不同的截止时间。 PART 2 将深入研究这些案例...

第 3 部分:悲伤画廊:相关链接的注释列表

您可以通过查看源代码来收集我遗漏的想法 material:

2014 年和 2017 年更新:Can't get rid of jitter while dragging the left border of a window:可能是最新的问题,但仍然缺乏上下文;建议有两个 windows 并在实时调整大小时交替取消隐藏它们的创意但相当疯狂的技巧!这也是我发现的唯一一个问题,答案提到了 DWM 中的竞争条件和 DwmGetCompositionTimingInfo().

的部分时序修复

2014 Why is there a black lag every time a WPF window is resized? :是的,WPF 也这样做。没有有用的答案

2009 How to fix the WPF form resize - controls lagging behind and black background?:控件滞后和黑色背景?”多 HWND 示例。提到 WM_ERASEBKGND 和背景刷技巧,但没有现代答案。

2018 Is there a way to reduce or prevent form flickering when using WPF? : 是的,截至 2018 年仍未修复。

2018 Reduce flickering when using SetWindowPos to change the left edge of a window : 未回答的问题有许多过时的建议,例如 WM_NCCALCSIZE

2012 OpenGL flickering/damaged with window resize and DWM active : 很好的问题陈述,回答者完全误解了上下文并提供了不适用的答案。

2012 How to avoid transient updates in a GUI resize? : 提到拦截 WM_WINDOWPOSCHANGING 和设置 WINDOWPOS.flags |= SWP_NOCOPYBITS.

的技巧

2016 Unity bug report:"Window resizing is very choppy and stutters (border does not smoothly follow the mouse)" 在数百个应用程序中发现的典型错误报告部分是由于此错误报告中的问题,部分是由于某些应用程序的绘图速度较慢。我发现的唯一一个文档实际上说 Windows 10 DWM 夹紧并扩展了旧 window 的外部像素,我可以确认这一点。

2014 Flickering on window when resizing from left side 前 Windows-8 答案包括 CS_HREDRAW/CS_VREDRAWWM_NCCALCSIZE.

2013 Resizing Window causes smearing near the right border 使用老式的仅限 Win-7 的解决方案来禁用 Aero。

2018 Flicker-free expansion (resize) of a window to the left 多 window(多 HWND)案例的示例,没有真正的答案。

2013 WinAPI C++: Reprogramming Window Resize : 太模棱两可的问是客户区闪烁(像这个问题)还是非客户区闪烁

2018 GLFW bug "Resizing windows on Windows 10 shows jumpy behaviour" 像许多 Whosebug 帖子一样,许多此类错误中的一个从未解释上下文

2008 "Flicker Free Main Frame Resizing" CodeProject 实际上执行了 StretchBlt,但在 Windows 8+ 世界中不起作用,其中当屏幕上显示不正确的像素时应用程序无法控制。

2014 Smooth window resizing in Windows (using Direct2D 1.1)?:关于 Windows 8+ DWM 副本

的问题陈述得很好但没有得到解决

2010 How do I force windows NOT to redraw anything in my dialog when the user is resizing my dialog?:WM_NCCALCSIZE 修复禁用不再适用于 Windows 8+ 的 bitblt,因为 DWM 在应用程序有机会显示之前损坏屏幕。

2014 Flicker when moving/resizing window:以前在 Windows 8+ 中不起作用的修复程序汇总。

2007 WinXP时代"reducing flicker" CodeProject推荐WM_ERASEBKGND+SWP_NOCOPYBITS

2008 年初 Google Bug 报告新的 Vista DWM 问题

请参阅博客 post The smooth resize test,其中有一些分析和解决方案的指针。基本上有一个获胜策略,即在实时调整大小时渲染到重定向表面,并在其他时间使用交换链。我不确定这是否可以解决您的具体问题,因为您需要对演示文稿的工作方式进行足够的 low-level 控制才能实现它。这种方法还假设您正在使用 Direct2D(正如我目前正在做的那样)或 DirectX 进行绘图。

如果您使用的是 DXGI,则可以使用 DirectComposition + WS_EX_NOREDIRECTIONBITMAP 完全绕过重定向表面和 render/present 具有新大小的客户区,甚至在从 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);
    }
}

Table 个目录

因为这是一个复杂的、多方面的问题,我建议按以下顺序阅读答案:

  • 第 1 部分:

  • 第 2 部分:

    • 2a:调整来自 SetWindowPos() BitBlt 和背景填充的问题
    • 2b:调整 DWM 合成填充的问题
    • 2c:如何诊断您的问题

以及可能有助于其他人收集见解的来源列表 material:

  • 第 3 部分:

请随时以创造性的方式提供更多答案,以避免 2a,尤其是 2b 中描述的问题!