如何消除窗口调整大小时的闪烁/抖动/跳动,尤其是拖动左/上边框时(Win 7-10; bg、bitblt 和 DWM)?

78
问题:当我调整Windows应用程序的大小调整边框,特别是顶部或左侧边框,并调整窗口大小时,窗口的内容会“实时”地随着我的拖动而调整大小,但它们以一种丑陋的方式调整大小,即使对于最初学者用户也看起来像一个明显的错误:与我拖动的边缘相反的窗口边缘处的内容会疯狂地抖动/闪烁/跳动。根据情况,现象可能看起来像:
  • 当我们减速或停止拖动时,似乎走出窗口边缘并弹回的内容
  • 间歇性地被不同颜色的边框(通常是黑色或白色)替换的内容
  • 有两个重叠的内容副本,严重且随着我们拖动的速度和程度成比例地错开一定距离。

这种丑陋的现象在我停止拖动后就会停止,但在拖动过程中,它使应用程序看起来业余和不专业。

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

以下是两个现象的示例图片,由Roman Starkov相关问题准备:

抖动:
example 1: jitter

边框:
example 2: border

以下是Kenny Liu提供的另一个例子,展示了邪恶的“双影”现象(请注意快速闪烁):

example 2: double-image

这里有另一个关于任务管理器现象的视频链接

问题:任何经历过此问题的开发人员很快就会发现,至少有30个Stack Overflow问题,其中一些是最近的,一些则可以追溯到2008年,充满了听起来很有希望但很少奏效的答案。事实上,这个问题有许多原因,而现有的Stack Overflow问题/答案从未清楚地说明更广泛的背景。这个问题旨在回答以下问题:

  • 这种难看的抖动/闪烁/跳跃的最可能原因是什么?
  • 我如何确定我看到的是哪种原因?
  • 这个原因是特定于某些图形驱动程序还是Windows通用的?
  • 我如何修复每个原因?应用程序能否解决它?

(这旨在作为规范的问答,以解释窗口调整抖动的所有不同原因,以便用户可以确定造成其问题的原因并解决它。正如答案所解释的那样,以上所有排列组合(本机/托管,窗口/对话框,XP-10)归结为只有两个根本原因,但确定你的问题属于哪一个是棘手的。)

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

  • 既包括本地Win32应用程序,也包括托管的.NET/WPF/Windows Forms应用程序
  • 既包括普通的Win32窗口,也包括Win32对话框窗口
  • 支持Windows版本包括XP、Vista、7、8和10(但请参见下面关于多个原因的黑暗真相)
如果你的应用程序有一个或多个子窗口(子HWND),那么这个问题中的信息对你很有用(因为我们将描述的造成卡顿的BitBlts会应用于父窗口和子窗口),但在调整窗口大小期间,你还需要处理一个超出此问题范围的额外问题:你需要使所有子窗口与父窗口同时移动并同步。为了完成这项任务,你可能需要使用BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos,你可以在这里这里找到相关信息。
本问题假设如果你的应用程序使用GDI、DirectX或OpenGL向窗口绘制内容,则你已经在wndproc中实现了一个WM_ERASEBKGND处理程序,并且该处理程序只返回1。WM_ERASEBKGND是Windows 3.1中保留的一种晦涩的Windows消息,在WM_PAINT之前给您的应用程序提供了一个机会,让您在绘制窗口之前"擦除窗口背景",嗯哼。如果让WM_ERASEBKGND消息进入DefWindowProc(),那么每次重新绘制时,包括在实时窗口调整大小期间发生的重新绘制,整个窗口都会被涂成一个固定的颜色,通常是白色。结果是一个难看的全窗口闪烁,很恶心,但不是本问题中所讨论的抖动/闪烁/跳动类型。拦截WM_ERASEBKGND可以立即解决此问题。
本问题主要关于通过拖动窗口边框进行实时调整大小。然而,本文中撰写的大部分内容也适用于当应用程序使用SetWindowPos()进行一次性窗口调整大小时可能出现的难看伪影。这些伪影不太明显,因为它们只在屏幕上闪烁了一瞬间,而不是在拖动过程中持续存在。
本问题与如何使你的应用程序特定的绘图代码运行更快无关,尽管在许多情况下这样做可能是解决丑陋调整大小问题的方法。如果你的应用程序在实时窗口调整大小期间需要大量时间才能重新显示其内容,请考虑通常优化绘图代码或至少通过拦截WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE消息,在调整大小期间切换到更快、更低质量的绘图模式。
如果你的应用程序在应用程序调整大小期间完全无法调整大小(例如,在OpenGL使用GLFW或其他库时“挂起”),请参见以下其他问题,这些问题解释了Microsoft在拖动期间WM_SYSCOMMAND内部的可恶嵌套/模态事件循环:这里尤其是这个很好的答案这里这里这里这里
7个回答

36

第二部分:识别和解决Windows调整大小问题

注意:您需要先阅读第一部分,才能理解本答案。

本答案无法解决所有的调整大小问题。

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

这种行为在 Microsoft 的 MSDN 上根本没有记录,以下内容是我自己的实验和查看其他 StackOverflow 帖子得出的结果。

2a. 由 SetWindowPos()BitBlt 和背景填充引起的调整大小问题

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

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

test pattern

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.


我认为部分名称有些混乱。3. Resize Behaviors in Windows 8/10: DWM 应该改为 3. Resize Behaviors in Windows Vista/7: DWM,因为 DWM / Aero 是在 Windows Vista 中引入的。而下一部分应该是 4. Resize Behaviors in Windows 8/10: Direct Composition,因为 Direct Composition 是桌面组合演变的下一个重要步骤,而且在问题中根本没有提到。 - user7860670
@VTT 这是一个很好的观点;在阅读了许多描述不同调整大小问题的SO帖子之后,实际上并不清楚第4节中描述的不良行为是从Vista/7 Aero、8还是10开始的。问题在于,许多Vista/7用户可能已经禁用了Aero,但在他们的帖子中没有说明,因此数据被扭曲了。我认为最好的解决方案是根据行为本身而不是操作系统版本或DWM / Composition重命名这些部分。我会尽快努力使Q重新打开并解锁新答案(请参见我的评论)。 - Louis Semprini
@VTT 太好了!我的浏览器没有更新。白天的工作有点干扰,但我会在接下来的一两天里处理标题。 - Louis Semprini
感谢提供的解决方案,它们都应该在 Windows 策略的墙上重新绘制!另一个方法可能是通过一个定时器为窗体应用一个透明标志,该标志在最后调整大小事件之后的20-30毫秒内切换关闭(这是TranspTester未来的开发)。此外,对于代码片段迷而言,问答中一个不错的功能更新是提供更多不同场景可用的示例。还有@DaveyDaveDave提出的更多颜色和内容的想法(难道我们还没有SO中的电影片段?) - Laurie Stearn
设置 WINDOWPOS.flags |= SWP_NOCOPYBITS,这将禁用窗口调整大小期间 Windows 在内部调用 SetWindowPos() 中的 BitBlt。我实现了自己的窗口调整大小边框,也就是独立执行 SetWindowPos 函数调用。使用此标志并不能解决问题。我有 1c3 和 1c4 两种情况。 - D .Stark
显示剩余2条评论

20

第一部分:什么决定了Resize的好坏?

关于平滑Resize的StackOverflow问题存在很多模糊和不清晰的地方,我们需要建立一个共同的词汇来帮助人们更清晰地回答问题。

这就是本节要做的事情。

为了简单起见,我们只讲解水平方向上的平滑缩放问题,但下面介绍的内容同样适用于垂直缩放。

以下我们将提到一个窗口的:

  • "非客户区":Windows管理的窗口部分,包括顶部的标题栏和所有边缘周围的窗口边框;

  • "客户区":您负责的窗口的主要部分。

假设您有一个应用程序,其中:

  • 一个按钮或标签L,应保持靠左
  • 一个按钮或标签R,应保持靠右

无论窗口如何被调整大小。

您的应用程序可能会自己绘制L / R(例如,在一个窗口内使用GDI / OpenGL / DirectX绘制),或者L / R可能是某个 Microsoft 控件(该控件将具有自己的 HWND,与您的主窗口 HWND 分离);都没有关系。

下面是您的应用程序窗口客户区的简化表示。正如您所看到的,我们在客户区最左边有三列宽度为LLL,在客户区最右边有三列宽度为RRR,之间为各种其他客户区内容,用"-"代表(请忽略 StackOverflow 坚持添加的灰色背景;L 和 R 在客户区的左侧和右侧边缘):

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

现在想象一下,您抓住窗口的左侧或右侧边缘,并拖动以使窗口变大或变小。

1a. 简单情况:及时绘制

假设您的应用程序非常快,可以始终在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
  • 当调整右边界时,R 看起来以恒定速度向一个方向移动,而 L 则保持静止。
  • 当调整左边界时,L 看起来以恒定速度向一个方向移动,而 R 则保持静止。

目前为止还不错。

1b. 困难情况:绘制落后

现在,想象一下您的应用程序在绘制方面非常慢,以至于当您使用鼠标拖动时,应用程序无法跟上您的速度。是的,最终,您的绘图将赶上来,但我们谈论的是您用手拖动鼠标的时候发生的情况。显然,计算机不能伸出手来减缓您的鼠标移动速度,因此关键问题是:

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

例如,在向右拖动右边界(增大宽度)时:

(Figure 1b-1)
LLL-----------RRR
??????????????????    (这里应该显示什么?)
???????????????????   (这里应该显示什么?)
LLL--------------RRR  (应用程序追上了)

另一个例子是,在向左拖动左边界(缩小宽度)时:

(Figure 1b-2)
LLL-----------RRR
 ????????????????  (这里应该显示什么?)
  ???????????????  (这里应该显示什么?)
   LLL--------RRR  (应用程序追上了)

这些问题决定了运动是否看起来平滑,也是整个 StackOverflow 问题的核心。

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

1c. 等待应用程序绘制的临时解决方案

在用户开始使用鼠标调整窗口大小之后,但在您的应用程序通过绘制新大小的窗口赶上之前,有几种选择可以做。

1c1. 什么都不做

屏幕可能保持原样直到应用程序赶上 (您的客户端像素以及非客户端区域中的窗口边框都不会改变):

例如,在向右拖动右边界(增大宽度)时:

(Figure 1c1-1)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
LLL--------------RRR  (应用程序追上了)

例如,在向左拖动左边界(缩小宽度)时:

(Figure 1c1-2)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
   LLL--------RRR  (应用程序追上了)

这种方法的明显缺点是在此期间,应用程序似乎已经 "挂起" 并且似乎无法响应您的鼠标移动,因为 R、'-'、L 或窗口边框都没有移动。

Microsoft 经常因 Windows 是一个不响应的操作系统而受到批评(有时是他们自己的

"什么都不做"的方法对用户来说很烦人,看起来也不专业。但是(非常不明显的)事实证明,它并不总是最糟糕的选择。继续阅读...

1c2. 缩放内容

另一种可能性是,Windows可以始终使窗口边框紧随您的鼠标移动(因为Windows本身具有足够的处理能力,至少可以及时地绘制非客户区域),在等待您的应用程序时,Windows可以获取客户区域的旧像素并将这些像素缩放,就像当您缩放图像以适应更小或更大的空间时一样。

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

1c3. 在扩大时填充一些背景颜色

在扩大窗口时,另一种可能的技术如下:Windows始终可以使窗口边框紧随您的鼠标移动,并且Windows可以用一些临时背景颜色B填充现在更大的客户区域的新像素:

例如,将右边框向右拖动(增加宽度)时:

(Figure 1c3-1)
LLL-----------RRR
LLL-----------RRRB
LLL-----------RRRBB
LLL--------------RRR  (应用程序追赶上来了)

此方法的优点是,在这段时间内,至少您的窗口边框正在移动,因此该应用程序会感到响应灵敏。

另一个好处是,在拖动过程中,L保持不变,就像它应该的那样。

填充随着您拖动而创建的新空间的一些随机颜色有点奇怪,甚至更奇怪的是,在之后R实际上没有移动(请注意R在最后一瞬间向右抽搐了3列),但至少R只朝着正确的方向移动。这是一个部分改进。

一个重要的问题是:新填充的背景颜色B应该是什么颜色?如果B恰好是黑色,而您的应用程序恰好具有大多白色背景,或者反之亦然,则它将比B与现有内容的背景颜色匹配要丑陋得多。如我们将在PART 2中看到的那样,Windows已经部署了几种不同的策略来改进B的选择。

现在考虑相同的想法,但是将其应用于将左边框向左拖动的情况(增加宽度)。

逻辑上应该在窗口的左侧填充新的背景颜色:

(Figure 1c3-2)
   LLL-----------RRR
  BLLL-----------RRR
 BBLLL-----------RRR
LLL--------------RRR  (应用程序追赶上来了)

然而 - 这真的会让人震惊 - 在您需要处理的几个重要情况下,Windows并不做逻辑上正确的事情。有时候即使您在拖动左窗口边框,Windows也会填充右侧的背景像素B:

(图1c3-3) LLL------ ---- RRR LLL---------- RR RB LLL--------- RRRBB LLL-----------RRR(应用程序赶上) 是的,这很疯狂。
考虑一下这对用户来说是什么样子:
- L看起来以恒定的速度向一个方向平稳移动,所以这实际上是好的,但是 - 看看R在做什么:
RRR RRR RRR RRR(应用程序赶上)
- R首先向左移动了两列,它不应该这样做:R应始终保持右侧对齐 - 然后R又回到了右侧。 天啊!
这看起来非常可怕,可怕,可怜,恶心...甚至没有词语可以形容它有多糟糕。
人眼对运动的敏感性非常高,即使在短短几帧时间内发生的运动也能被立即察觉到。我们的眼睛立即注意到R的这种奇怪来回运动,我们立刻就知道有严重的问题。
因此,在拖动左侧(或上方)边框而不是右侧(或下方)边框时,一些难看的调整大小问题发生的原因开始清晰起来。
实际上,两种情况(图1c3-2与图1c3-3)都会做出一些奇怪的事情。在图1c3-2中,我们暂时添加了一些不应该存在的背景像素B。但是,这种奇怪的行为比1c3-3的来回运动明显要少得多。
这种来回运动就是许多StackOverflow问题所涉及的抖动/闪烁/跳动。
因此,任何解决平滑调整大小问题的解决方案都必须:
- 至少防止客户区域中的项看起来向一个方向跳动,然后向另一个方向跳动。 - 如果可能的话,最好也避免需要添加背景像素B。
1c4.当缩小时,剪切一些像素
第1c3节处理了扩展窗口。如果我们观察缩小窗口,我们将看到有一组类似的情况。
当缩小窗口时,可以使用以下技术:
Windows始终可以使窗口边框立即跟随您的鼠标移动,并简单地裁剪一些现在较小的客户区域像素。例如,当将右侧边框向左拖动(缩小宽度)时:使用这种技术,L会像应该的那样保持不动,但是在右边会发生一件奇怪的事情: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在做什么:

    RRR
     RR
      R
    RRR  (应用程序跟上)
    
    • R首先向右滑动两列。 R的左侧边缘似乎随着其余部分的移动而向右移动。
    • 然后R再次向左弹回。

如您在阅读1c3节后所知道的那样,这种来回运动看起来绝对是可怕的,并且比图1c4-1和图1c4-2的奇怪行为要糟得多。

1c5. 等待一会儿,然后尝试上述方法之一

到目前为止,我们已经为用户开始拖动窗口边框但应用程序尚未重绘时该怎么做提供了单独的想法。

这些方法实际上可以结合使用。

暂时从Microsoft的角度考虑这个问题。当用户开始拖动鼠标调整您的窗口大小时,Microsoft无法预先知道您的应用程序绘制需要多长

如果您的应用程序需要快速响应,那么 Microsoft 对屏幕所做的任何更改都会使您的应用程序看起来比让您绘制真实内容要糟糕得多(请记住,上述所有技巧在不同程度上都很奇怪,并且会使您的内容出现奇怪,因此不使用这些技巧中的任何一个肯定是更好的)。

但是,如果 Microsoft 等待您绘制的时间太长,您的应用程序(以及 Windows 扩展)将显示为卡顿和无响应,正如我们在第 1c1 节中所解释的那样。 这即使是您的问题,也会让 Microsoft 失去面子。

因此,另一个选择是首先暂停任何屏幕更改并给应用程序一定的绘制时间,如果应用程序未能满足截止日期,则使用上述方法之一暂时“填补空白”。

这听起来对您来说可怕而且不专业?猜猜怎么着?这就是 Windows 所做的事情,至少用两种不同的截止时间和两种不同的方式同时进行。 第二部分 将深入探讨这些情况......


可能有一些需要更正的单词。请看“(注意,在最后一瞬间,R向左抽搐了3列)”。实际上,R向右抽搐... - walterlv

14

第三部分:悲伤图库:相关链接的注释列表

您可以通过查看源材料来获取我忽略的想法:

2014年至2017年更新:无法在拖动窗口左边框时摆脱抖动:可能是最新的问题,但仍缺乏上下文;建议使用两个窗口并在实时调整大小期间交替取消隐藏它们的创意但相当疯狂的黑客!还是我发现的唯一一个回答中提到DWM中的竞争条件和部分定时修复的问题,使用DwmGetCompositionTimingInfo()

2014 为什么每次调整WPF窗口大小时都会有黑色滞后?:是的,WPF也会出现这种情况。没有有用的答案。

2009 如何修复WPF表单调整大小 - 控件滞后和黑色背景?:控件滞后和黑色背景?“多HWND示例。提到了WM_ERASEBKGND和背景画笔技巧,但没有现代答案。

2018 在使用WPF时有没有减少或防止窗体闪烁的方法?:是的,截至2018年仍未解决。

2018 当使用SetWindowPos更改窗口左边缘时如何减少闪烁:未回答的问题,得到了许多过时的建议,例如WM_NCCALCSIZE

2012 在窗口调整大小和DWM活动时避免OpenGL闪烁/损坏:问题陈述很好,回答者完全误解了上下文并提供了不适用的答案。

2012 如何避免GUI调整大小时的瞬态更新?:提到拦截WM_WINDOWPOSCHANGING并设置WINDOWPOS.flags |= SWP_NOCOPYBITS的技巧。

2016年Unity的Bug报告: "窗口调整大小很卡顿和抖动(边框不会平滑地跟随鼠标移动)",这是成百上千个应用程序中典型的bug报告,部分原因归咎于此错误报告的问题,部分原因则是由于某些应用程序绘制速度过慢。我找到唯一一个确实提到Windows 10 DWM夹紧并扩展旧窗口外部像素的文档,可以予以证实。

2014年从左侧调整大小时窗口闪烁的问题,包括CS_HREDRAW/CS_VREDRAWWM_NCCALCSIZE的预Win8的答案。

2013年调整窗口大小导致右侧边缘模糊的问题,提供了在旧式Win 7上禁用Aero的解决方案。

2018年窗口向左无闪烁扩展(调整大小)的多窗口(multi-HWND)情况示例,没有真正的答案。

2013年WinAPI C++:重新编程窗口调整大小 :请求含糊不清,无法确定它是否涉及客户区闪烁(如此问题)或非客户区闪烁。

2018 GLFW bug "在Windows 10上调整窗口大小会出现跳跃行为",这是许多类似问题中的一个,但并没有解释背景,就像许多StackOverflow帖子一样。

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+中不再起作用,因为DWM在应用程序有机会显示之前损坏屏幕。

2014 Flicker when moving/resizing window:总结以前的修复方法,在Windows 8+中不起作用。

2007年WinXP时代的"减少闪烁" CodeProject建议使用WM_ERASEBKGND+SWP_NOCOPYBITS

2008年早期Google Bug报告了新的Vista DWM问题


2
这太棒了。谢谢!我进行了大量的研究,并使其在XP上运行。但正如您所指出的,微软不断添加层,并且不发布描述“规则”的白皮书 - 这意味着制作新层的新开发人员经常会互相踩踏并制造迷宫而不是一致的设计。非常感谢您花时间为他人收集这个综合调查。干杯! - Mordachai
非常感谢您的“色彩评论”。人们似乎认为软件开发是没有任何人对他们的决定负责的,而微软和其他公司所做出的实现选择通常是令人遗憾的,需要像“四年级学生写的一样”被指出来!-- 确切地说! - Mordachai

12

目录

由于这是一个复杂而多面的问题,建议按照以下顺序阅读答案:

此外还有一些可能帮助其他人提取见解的源材料列表:

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


4

如果您正在使用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);
    }
}

2
请参阅博客文章平滑调整大小测试,其中包含一些分析和解决方案的指针。基本上有一种胜利策略,即在实时调整大小期间呈现到重定向表面,并在其他时间使用交换链。我不确定这是否修复了您特定的问题,因为您需要足够低级别的控制方式来实现它。此方法还假设您正在使用Direct2D(如我目前所做)或DirectX进行绘图。

嗨,谢谢!看起来非常有前途。由于博客文章主要是关于Mac的,我认为大多数人也会想要Windows代码的直接链接:https://github.com/xi-editor/xi-win/pull/21 - Louis Semprini
另外,据我了解StackOverflow的礼仪,我们应该尽可能在实际的StackOverflow内容中包含尽可能多的代码,而不是指向外部资源,因此将关键伪代码或完整代码直接粘贴到您的答案中可能会很有帮助。 - Louis Semprini
@LouisSemprini,我允许你将那个拉取请求中的代码进行适当修改并完善答案,这样你就可以获得 StackOverflow 上辉煌的声望分数了。 - Raph Levien

0
首先,非常感谢您将有用的信息收集在一个地方,可惜微软似乎不太关心。我很讨厌引用史蒂夫·乔布斯的话:

微软唯一的问题就是他们没有品味。他们绝对没有品味。

我正在使用C#创建一个WinForms程序,我只想要一个固定大小的面板,它始终停留在窗口的右上角。如果我只使用一个面板并设置其Anchor/Dock属性,我将得到这个丑陋的抖动效果,几乎每个Windows程序都会遭受此类问题。
经过多次尝试和沮丧的实验后,我最终放弃了,并选择采用两种窗体的解决方法。这种解决方法非常有限,但适合我所需:
  • 窗口右侧的面板是固定大小的,无需重绘 = 无撕裂。

  • 我没有在它上面放置任何按钮或其他标准交互控件。它们会激活第二个窗口,而我无法找到防止这种情况发生的方法。(我的自定义控件上的MouseDown事件可以正常工作而不激活。)

解决方法:

  1. 创建一个BaseForm,将除固定面板以外的所有内容放入其中。
  2. 为固定面板创建一个FloatForm,包括以下内容:
    protected override bool ShowWithoutActivation => true;

以一种方式对其进行样式设置,使其与BaseWindow融为一体(将FormBorderStyle设置为NoneShowInTaskbar设置为False等)。


BaseForm_Shown事件中初始化FloatForm,在此处可以计算从BaseForm(包括窗口框架,而不是客户端区域)的右上角到FloatForm的左上角的位置差(均为屏幕坐标)。
    private FloatForm _floatForm;
    private int _floatFormXtoRight;
    private int _floatFormYtoTop;

    private void BaseForm_Shown(object sender, EventArgs e)
    {
        _floatForm = new FloatForm();
        _floatForm.Location = PointToScreen(ClientRectangle.Location);
        _floatForm.Left += ClientRectangle.Width - _floatForm.Width;
        _floatFormXtoRight = Right - _floatForm.Right + _floatForm.Width;
        _floatFormYtoTop = _floatForm.Top - Top;
        _floatForm.Show(this);
    }

BaseForm中处理WM_WINDOWPOSCHANGING消息,根据在m.LParam中收到的WINDOWPOS(这些值包括窗口框架,因此上一步中的增量计算是必要的)计算并设置FloatForm的新位置。
    [StructLayout(LayoutKind.Sequential)]
    public struct WINDOWPOS
    {
        public IntPtr hwnd;
        public IntPtr hwndInsertAfter;
        public int x, y, cx, cy;
        public uint flags;
    }

    protected override void WndProc(ref Message m)
    {

        const int WM_WINDOWPOSCHANGING = 0x0046;

        switch (m.Msg) {
            case WM_WINDOWPOSCHANGING:
                var w = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
                //Console.WriteLine($"WM_WINDOWPOSCHANGING: x{w.x} y{w.y} w{w.cx} h{w.cy}");

                if (_floatForm != null && w.cx != 0) {
                    _floatForm.Left = w.x + w.cx - _floatFormXtoRight;
                    _floatForm.Top = w.y + _floatFormYtoTop;
                }
                break;
        }
        base.WndProc(ref m);
    }

我在这里浪费了很多时间,试图处理WM_NCCALCSIZE并使用自己的NCCALCSIZE_PARAMS返回WVR_VALIDRECTS,但没有成功...这促使我创建了这个解决方法。
  1. FloatForm 中处理 WM_MOUSEACTIVATE 消息,返回 MA_NOACTIVATE,这样当被点击时它不会被激活(但控件的鼠标事件如 MouseEnter 仍然有效)
    protected override void WndProc(ref Message m)
    {
        const int WM_MOUSEACTIVATE = 0x0021;

        switch (m.Msg) {
            case WM_MOUSEACTIVATE:
                const int MA_NOACTIVATE = 3;

                m.Result = (IntPtr)MA_NOACTIVATE;
                return;
        }
        base.WndProc(ref m);
    }

结果:

workaround_animation

(“灯泡”图标来自Led24的Led Icons Pack)

这是我能想到的最简单的解决方法。仍然有一些细节需要解决(例如,在BaseWindow从最小化动画中恢复之前,FloatWindow将首先弹出),但这些超出了本主题的范围。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接