第二部分:识别和解决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);
TIMECAPS tc;
MMRESULT mmerr;
MMC(timeGetDevCaps(&tc, sizeof(tc)), {});
int ms_granularity = tc.wPeriodMin;
timeBeginPeriod(ms_granularity);
QueryPerformanceCounter(&now0);
DWM_TIMING_INFO dti;
memset(&dti, 0, sizeof(dti));
dti.cbSize = sizeof(dti);
HRESULT hrerr;
HRC(DwmGetCompositionTimingInfo(NULL, &dti), {});
QueryPerformanceCounter(&now1);
__int64 period = (__int64)dti.qpcRefreshPeriod;
__int64 dt = (__int64)dti.qpcVBlank - (__int64)now1.QuadPart;
__int64 w, m;
if (dt >= 0)
{
w = dt / period;
}
else
{
w = -1 + dt / period;
}
m = dt - (period * w);
assert(m >= 0);
assert(m < period);
double m_ms = 1000.0 * m / (double)freq.QuadPart;
Sleep((int)round(m_ms));
timeEndPeriod(ms_granularity);
You can convince yourself that this hack is working by uncommenting the line that shows "worst-case" behavior by trying to schedule the drawing right in the middle of a frame rather than at vertical sync, and noticing how many more artifacts you have. You can also try varying the offset in that line slowly and you will see that artifacts abruptly disappear (but not completely) at about 90% of the period and come back again at about 5-10% of the period.
Since Windows is not a real-time OS, it is possible for your app to be
preempted anywhere in this code, leading to inaccuracy in the pairing of now1
and dti.qpcVBlank
. Preemption in this small code section is rare, but possible. If you want, you can compare now0
and now1
and loop around again if the bound is not tight enough. It is also possible for preemption to disrupt the timing of Sleep()
or the code before or after Sleep()
. There's not much you can do about this, but it turns out timing errors in this part of the code are swamped by the uncertian behavior of DWM; you are still going to get some window resize artifacts even if your timing is perfect. It's just a heuristic.
There is a second hack, and it is an incredibly creative one: as explained in the StackOverflow post Can't get rid of jitter while dragging the left border of a window, you can actually create two main windows in your app, and every time Windows would do SetWindowPos
, you intecept that and instead hide one window and show the other! I haven't tried this yet but the OP reports that it bypasses the insane pixel DWM pixel copy described above.
There is a third hack, which might work depending on your application (especially in combination with the timing hack above). During live resizing (which you can detect by intercepting WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE
), you could modify your drawing code to initially draw something much simpler that is much more likely to complete within the deadline imposed by problem 2a and 2b, and call SwapBuffers()
to claim your prize: that will be enough to prevent Windows from doing the bad blit/fill described in section 2a and 2b. Then, immediately after the partial draw, do another draw that fully updates the window contents and call SwapBuffers()
again. That might still look somewhat odd, since the user will see your window update in two parts, but it's likely to look much better than the hideous back-and-forth motion artifact from Windows.
One more tantalizing point: some apps in Windows 10, including the console (start cmd.exe
), are rock-solid free of DWM Composition artifacts even when dragging the left border. So there is some way of bypassing the problem. Let's find it!
2c. How to Diagnose Your Problem
As you try to solve your particular resize problem, you may wonder which of the overlapping effects from Section 2a and Section 2b you are seeing.
One way to separate them is to debug on Windows 7 (with Aero disabled, just to be safe) for a bit.
Another way to quickly identify if you are seeing the problem in Section 2b is to modify your app to display the test pattern described in Section 2b, like this example (note the 1-pixel-thin colored lines on each of the four edges):
Then grab any window border and start resizing that border rapidly. If you see intermittent giant colored bars (blue or green bars in the case of this test pattern, since there is blue on the bottom edge and green on the right edge) then you know you are seeing the problem in Section 2b.
You can test if you are seeing the problem in Section 2a by setting WNDCLASS.hbrBackground
to a distinct background color, like red. As you resize the window, newly exposed parts will show up with that color. But read through Section 2a to make sure your message handlers are not causing Windows to BitBlt
the entire client area, which would cause Windows not to draw any background color.
Remember that the problems in Section 2a and 2b only show up if your app fails to draw by a certain deadline, and each problem has a different deadline.
So, without modification, your app might show the Section 2b problem only, but if you modify your app to draw more slowly (insert Sleep()
in WM_PAINT
before SwapBuffers()
for example), you may miss the deadline for both Section 2a and Section 2b and start to see both problems simultaneously.
This may also happen when you change your app between a slower DEBUG
build and a RELEASE
build, which can make chasing these resize problems very frustrating. Knowing what's going on under the hood can help you deal with the confusing results.
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 是桌面组合演变的下一个重要步骤,而且在问题中根本没有提到。 - user7860670WINDOWPOS.flags |= SWP_NOCOPYBITS
,这将禁用窗口调整大小期间 Windows 在内部调用SetWindowPos()
中的 BitBlt。我实现了自己的窗口调整大小边框,也就是独立执行SetWindowPos
函数调用。使用此标志并不能解决问题。我有 1c3 和 1c4 两种情况。 - D .Stark