PART 2: Identifying and Fixing Windows Resize Problems
Note: you want to read PART 1 first for this answer to make sense.
This answer will not solve all your resizing problems.
It organizes the still-usable ideas from other posts and adds a few novel ideas.
None of this behavior is at all documented on Microsoft's MSDN, and what follows below is the result of my own experimentation and looking at other StackOverflow posts.
2a. Resize Problems from SetWindowPos()
BitBlt
and Background Fill
The following problems happen on all versions of Windows. They date back to the very first days of live-scrolling on the Windows platform (Windows XP) and are still present on Windows 10. On more recent Windows versions, other resize problems may layer on top of this problem, as we explain below.
Here are the Windows events associated with a typical session of clicking a window border and dragging that border. Indentation indicates nested wndproc
(nested because of sent (not posted) messages or because of the hideous Windows modal event loop mentioned in "NOT IN SCOPE OF THIS QUESTION" in the question above):
msg=0xa1 (WM_NCLBUTTONDOWN) [click mouse button on border]
msg=0x112 (WM_SYSCOMMAND) [window resize command: modal event loop]
msg=0x24 (WM_GETMINMAXINFO)
msg=0x24 (WM_GETMINMAXINFO) done
msg=0x231 (WM_ENTERSIZEMOVE) [starting to size/move window]
msg=0x231 (WM_ENTERSIZEMOVE) done
msg=0x2a2 (WM_NCMOUSELEAVE)
msg=0x2a2 (WM_NCMOUSELEAVE) done
loop:
msg=0x214 (WM_SIZING) [mouse dragged]
msg=0x214 (WM_SIZING) done
msg=0x46 (WM_WINDOWPOSCHANGING)
msg=0x24 (WM_GETMINMAXINFO)
msg=0x24 (WM_GETMINMAXINFO) done
msg=0x46 (WM_WINDOWPOSCHANGING) done
msg=0x83 (WM_NCCALCSIZE)
msg=0x83 (WM_NCCALCSIZE) done
msg=0x85 (WM_NCPAINT)
msg=0x85 (WM_NCPAINT) done
msg=0x14 (WM_ERASEBKGND)
msg=0x14 (WM_ERASEBKGND) done
msg=0x47 (WM_WINDOWPOSCHANGED)
msg=0x3 (WM_MOVE)
msg=0x3 (WM_MOVE) done
msg=0x5 (WM_SIZE)
msg=0x5 (WM_SIZE) done
msg=0x47 (WM_WINDOWPOSCHANGED) done
msg=0xf (WM_PAINT) [may or may not come: see below]
msg=0xf (WM_PAINT) done
goto loop;
msg=0x215 (WM_CAPTURECHANGED) [mouse released]
msg=0x215 (WM_CAPTURECHANGED) done
msg=0x46 (WM_WINDOWPOSCHANGING)
msg=0x24 (WM_GETMINMAXINFO)
msg=0x24 (WM_GETMINMAXINFO) done
msg=0x46 (WM_WINDOWPOSCHANGING) done
msg=0x232 (WM_EXITSIZEMOVE)
msg=0x232 (WM_EXITSIZEMOVE) done [finished size/moving window]
msg=0x112 (WM_SYSCOMMAND) done
msg=0xa1 (WM_NCLBUTTONDOWN) done
Each time you drag the mouse, Windows gives you the series of messages shown in the loop above. Most interestingly, you get WM_SIZING
then WM_NCCALCSIZE
then WM_MOVE/WM_SIZE
, then you may (more on that below) receive WM_PAINT
.
Remember we assume you have provided a WM_ERASEBKGND
handler that returns 1 (see "NOT IN SCOPE OF THIS QUESTION" in the question above) so that message does nothing and we can ignore it.
During the processing of those messages (shortly after WM_WINDOWPOSCHANGING
returns), Windows makes an internal call to SetWindowPos()
to actually resize the window. That SetWindowPos()
call first resizes the non-client area (e.g. the title bars and window border) then turns its attention to the client area (the main part of the window that you are responsible for).
During each sequence of messages from one drag, Microsoft gives you a certain amount of time to update the client area by yourself.
The clock for this deadline apparently starts ticking after WM_NCCALCSIZE
returns. In the case of OpenGL windows, the deadline is apparently satisfied when you call SwapBuffers()
to present a new buffer (not when your WM_PAINT
is entered or returns). I do not use GDI or DirectX, so I don't know what the equavalent call to SwapBuffers()
is, but you can probably make a good guess and you can verify by inserting Sleep(1000)
at various points in your code to see when the behaviors below get triggered.
How much time do you have to meet your deadline? The number seems to be around 40-60 milliseconds by my experiments, but given the kinds of shenanigans Microsoft routinely pulls, I wouldn't be surprised if the number depends on your hardware config or even your app's previous behavior.
If you do update your client area by the deadline, then Microsoft will leave your client area beautifully unmolested. Your user will only see the pixels that you draw, and you will have the smoothest possible resizing.
If you do not update your client area by the deadline, then Microsoft will step in and "help" you by first showing some other pixels to your user, based on a combination of the "Fill in Some Background Color" technique (Section 1c3 of PART 1) and the "Cut off some Pixels" technique (Section 1c4 of PART 1). Exactly what pixels Microsoft shows your user is, well, complicated:
If your window has a WNDCLASS.style
that includes the CS_HREDRAW|CS_VREDRAW
bits (you pass the WNDCLASS structure to RegisterClassEx
):
Something surprisingly reasonable happens. You get the logical behavior shown in Figures 1c3-1, 1c3-2, 1c4-1, and 1c4-2 of PART 1. When enlarging the client area, Windows will fill in newly exposed pixels with the "background color" (see below) on the same side of the window you are dragging. If needed (left and top border cases), Microsoft does a BitBlt
to accomplish this. When shrinking the client area, Microsoft will chop off pixels on the same side of the window you are dragging. This means you avoid the truly heinous artifact that makes objects in your client area appear to move in one direction then move back in the other direction.
This may be good enough to give you passable resize behavior, unless you really want to push it and see if you can totally prevent Windows from molesting your client area before you have a chance to draw (see below).
Do not implement your own WM_NCCALCSIZE
handler in this case, to avoid buggy Windows behavior described below.
If your window has a WNDCLASS.style
that does not include the CS_HREDRAW|CS_VREDRAW
bits (including Dialogs, where Windows does not let you set WNDCLASS.style
):
Windows tries to "help" you by doing a BitBlt
that makes a copy of a certain rectangle of pixels from your old client area and writes that rectangle to a certain place in your new client area. This BitBlt
is 1:1 (it does not scale or zoom your pixels).
Then, Windows fills in the other parts of the new client area (the parts that Windows did not overwrite during the BitBlt
operation) with the "background color."
The BitBlt
operation is often the key reason why resize looks so bad. This is because Windows makes a bad guess about how your app is going to redraw the client area after the resize. Windows places your content in the wrong location. The net result is that when the user first sees the BitBlt
pixels and then sees the real pixels drawn by your code, your content appears to first move in one direction, then jerk back in the other direction. As we explained in PART 1, this creates the most hideous type of resize artifact.
So, most solutions for fixing resize problems involve disabling the BitBlt
.
If you implement a WM_NCCALCSIZE
handler and that handler returns WVR_VALIDRECTS
when wParam
is 1, you can actually control which pixels Windows copies (BitBlts
) from the old client area and where Windows places those pixels in the new client area. WM_NCCALCSIZE
is just barely documented, but see the hints about WVR_VALIDRECTS
and NCCALCSIZE_PARAMS.rgrc[1] and [2]
in the MSDN pages for WM_NCCALCSIZE
and NCCALCSIZE_PARAMS
. You can even provide NCCALCSIZE_PARAMS.rgrc[1] and [2]
return values that completely prevent Windows from BitBlting
any of the pixels of the old client area to the new client area, or cause Windows to BitBlt
one pixel from and to the same location, which is effectively the same thing since no on-screen pixels would get modified. Just set both NCCALCSIZE_PARAMS.rgrc[1] and [2]
to the same 1-pixel rectangle. In combination with eliminating the "background color" (see below), this gives you a way to prevent Windows from molesting your window's pixels before you have time to draw them.
If you implement a WM_NCCALCSIZE
handler and it returns anything other than WVR_VALIDRECTS
when wParam
is 1, then you get a behavior which (at least on Windows 10) does not at all resemble what MSDN says. Windows seems to ignore whatever left/right/top/bottom alignment flags you return. I advise you do not do this. In particular the popular StackOverflow article How do I force windows NOT to redraw anything in my dialog when the user is resizing my dialog? returns WVR_ALIGNLEFT|WVR_ALIGNTOP
and this appears to be completely broken now at least on my Windows 10 test system. The code in that article might work if it is changed to return WVR_VALIDRECTS
instead.
If you do not have your own custom WM_NCCALCSIZE
handler, you get a pretty useless behavior that is probably best avoided:
If you shrink the client area, nothing happens (your app gets no WM_PAINT
at all)! If you're using the top or left border, your client area contents will move along with the top left of the client area. In order to get any live resizing when shrinking the window, you have to manually draw from a wndproc
message like WM_SIZE
, or call InvalidateWindow()
to trigger a later WM_PAINT
.
If you enlarge the client area
If you drag the bottom or right window border, Microsoft fills in the new pixels with the "background color" (see below)
If you drag the top or left window border, Microsoft copies the existing pixels to the top left corner of the expanded window and leaves an old junk copy of old pixels in the newly opened space