Scrollbars part 11: Towards an even deeper understanding of the WM_NCCALCSIZE message


The other form of the WM_NCCALCSIZE message is the complicated one, when the WPARAM is TRUE. In this case, the LPARAM is a pointer to a NCCALCSIZE_PARAMS structure. When Windows sends the WM_NCCALCSIZE message, the NCCALCSIZE_PARAMS structure is filled out like this:

  • rgrc[0] = new window rectangle (in parent coordinates)
  • rgrc[1] = old window rectangle (in parent coordinates)
  • rgrc[2] = old client rectangle (in parent coordinates)

Notice that the client rectangle is given in parent coordinates, not in client coordinates.

When your window procedure returns, Windows expects the NCCALCSIZE_PARAMS structure to be filled out like this:

  • rgrc[0] = new client rectangle (in parent coordinates)

The new client rectangle specifies where the client area of the window should be located, given the new window rectangle.

Furthermore, if you return anything other than 0, Windows expects the remaining two rectangles to be filled out like this:

  • rgrc[1] = destination rectangle (in parent coordinates)
  • rgrc[2] = source rectangle (in parent coordinates)

(If you return 0, then Windows assumes that the destination rectangle equals the new client rectangle and the source rectangle equals the old client rectangle.)

The source and destination rectangles specify which part of the old window corresponds to which part of the new window. Windows will copy the pixels from the source rectangle to the destination rectangle and preserve their validity. The return value of the WM_NCCALCSIZE message specifies how the bits should be matched up if the two rectangles are not the same size. The default behavior is to align them at the top and left edges.

Let’s demonstrate custom valid rectangles with a fresh scratch program. (We’ll come back to the scrollbar program.) First, a helper function that computers the “center” of a rectangle.

void GetRectCenter(LPCRECT prc, POINT *ppt)
{
    ppt->x = prc->left + (prc->right - prc->left)/2;
    ppt->y = prc->top + (prc->bottom - prc->top)/2;
}

Exercise: Why do we use the formula c = a + (b-a)/2 instead of the simpler c = (a+b)/2?

Here’s our new PaintContent function:

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
    //  For debugging flicker - fill with an annoying color for 1 second
    DWORD dwLimit = GdiSetBatchLimit(1);
    FillRect(pps->hdc, &pps->rcPaint,
             GetSysColorBrush(COLOR_HIGHLIGHT));
    Sleep(1000);
    FillRect(pps->hdc, &pps->rcPaint,
             GetSysColorBrush(COLOR_WINDOW));
    GdiSetBatchLimit(dwLimit);

    //  Draw "concentric" rectangles
    RECT rc;
    GetClientRect(hwnd, &rc);
    POINT ptCenter;
    GetRectCenter(&rc, &ptCenter);
    int limit = max(rc.bottom, rc.right) / 2;
    rc.left = rc.right = ptCenter.x;
    rc.top = rc.bottom = ptCenter.y;
    for (int i = 0; i < limit; i += 10) {
        InflateRect(&rc, 10, 10);
        FrameRect(pps->hdc, &rc, GetSysColorBrush(COLOR_WINDOWTEXT));
    }
}

When debugging flicker problems, it helps to insert intentionally ugly background painting and annoying pauses so you can see what you are painting. Note, though, that when you do this, you also need to call GdiSetBatchLimit to disable batching. Otherwise, GDI will optimize out the redundant fill and you won’t see anything special.

The real work happens inside our WM_NCCALCSIZE handler.

UINT OnNcCalcSize(HWND hwnd, BOOL fCalcValidRects,
                           NCCALCSIZE_PARAMS *pcsp)
{
    UINT uRc = (UINT)FORWARD_WM_NCCALCSIZE(hwnd, fCalcValidRects,
                                           pcsp, DefWindowProc);

    if (fCalcValidRects) {
        //  Give names to these things
        RECT *prcClientNew = &pcsp->rgrc[0];
        RECT *prcValidDst  = &pcsp->rgrc[1];
        RECT *prcValidSrc  = &pcsp->rgrc[2];

        // Compute the old and new center points
        POINT ptOldCenter, ptNewCenter;
        GetRectCenter(prcValidSrc, &ptOldCenter);
        GetRectCenter(prcClientNew, &ptNewCenter);

        //  Tell USER how the old and new client rectangles match up
        *prcValidDst = *prcClientNew; // use entire client area
        prcValidDst->left += ptNewCenter.x - ptOldCenter.x;
        prcValidDst->top += ptNewCenter.y - ptOldCenter.y;

        uRc = WVR_VALIDRECTS;
    }
    return uRc;
}

    /* Add to WndProc */
    HANDLE_MSG(hwnd, WM_NCCALCSIZE, OnNcCalcSize);

How does this work?

If fCalcValidRects, then we do extra work to compute our valid rectangles by seeing how much the window content needs to be shifted and shifting the valid destination rectangle by the same amount. USER copies the upper left corner of the valid source rectangle to the upper left corner of the destination rectangle, so shfiting the upper left corner of the destination rectangle lets us adjust where USER will copy the pixels.

Play with this program: Grab the window and resize it. Observe that the central portion of the window client area is copied from the original window and is not redrawn. This has two benefits: First, there is no flicker. Second, this improves redraw performance since you aren’t drawing pixels unnecessarily. This second remark is particularly important when using the Remote Desktop feature, since Remote Desktop has to transfer all drawn pixels over the network to the client. The fewer pixels you have to transfer, the more responsive your program will be.

Now that we have a better understanding of the WM_NCCALCSIZE message, we can use this knowledge to improve our scrollbars.

Comments (6)
  1. floyd says:

    Hi Raymond,

    I know, this comment is somewhat late — but it wasn’t until last week that I came across your page.

    Believe it or not (you probably already know…) but this is in fact the only valuable resource on handling the WM_NCCALCSIZE message — and I tried hard to find anything, really, anything. From occasional hints I take it you are in fact working at Microsoft. Now, a lot of people would probably go and question my sanity, but I do envy you for "making it". Anyway, if this is the case, you may be able to help me out with a related problem:

    First off, a short abstract of what I’m trying to achieve before I delve into where I failed to do so: Currently I’m writing a small tool that is supposed to have as little impact on the user as possible. I succeeded with that, in my opinion at least, but I couldn’t get a particular piece of visual feedback to work: Adding and removing a frame dynamically.

    To keep the impact on the user low I decided to make the main window translucent, transparent, topmost, and invisible in the task bar, i.e. WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW. So far, no big deal, except for receiving mouse messages, which I solved with a global mouse hook. If you have an alternative for the mouse hook I’m all ears, as the MSDN suggests to use hooks for debugging only.

    Since this part works sufficiently I’ll leave it at that for the moment. Moreso, since I need to get that frame-thingie to work. At any rate, here is a list of things I’ve already tried and how they failed:

    * SetWindowLong( hWnd, GWL_STYLE, dwNewStyle ); That’s the only part I know I need, and it’s almost working, too, except that the frame gets placed *inside* the window, not around it. I’m also flushing the style change by calling SetWindowPos with the respective flags.

    * so I thought to myself: well, easy, I’ll just have AdjustWindowRect and move/size the window in the call to SetWindowPos. The problem here is, that the window gets properly resized, but just won’t move. At least not on the screen, since GetClientRect/ClientToScreen returns the ‘correct’ position, i.e. the position and size I had attempted to move it to.

    * the next 2 incarnations are probably the way to go. Well, almost at least. The first one being: SetWindowLong( … ); SetWindowPos( …, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED ); SetWindowPost( hWnd, <adjusted rect coordinates>, … ); The other implementation just altered the order, i.e. the window first gets repositioned and the frame added after that.

    Those last 2 implementations pretty much work, but they expose ugly visual artifacts. In particular, the client area pops towards the top left and back to where it should be when adding the frame (towards the lower right and back, when removing it). It wasn’t until then that I realized the WM_NCCALCSIZE message may turn out to relief my pain. Maybe a bit overexcited I went right at it. And was promptly set back by a hard to follow documentation. So I finally came across this blog and it suddenly all made perfect sense to me. At least, that’s what I thought…

    My first attempt was to go with a single call to SetWindowPos and account for the extra space needed by altering the contents of rgrc[0] inside my WM_NCCALCSIZE message handler. To my astonishment, the application crashed badly when running a debug build (msvc6.0 sp5). In fact, it wasn’t only my application crashing, but the entire system. In short, I get a bluescreen. Nothing to get all puffy about, if I was running a Win9x system. However, I’m working on Win2k and I have been wondering all these years what Win2k users were talking about when referring to a BSOD… I can reproduce this behaviour in debug builds, no matter if a debugger is running or not. I *think* this doesn’t happen with Release builds, but I wasn’t really tempted to give it a thorough analysis.

    Anyway, here comes the question: I know that if I forward the message to DefWindowProc it does in fact alter the contents of rgrc[0]. Is this something that client code isn’t allowed to do? If this isn’t the way to go, how would I preserve the client area at it’s current location while inflating the window to make room for the frame, and if I’m inflating the window, will there be any WM_PAINT messages send? This piece of information would be vital, since the drawing code relies on the client coordinates. Plus, I’m having a feeling that I still don’t fully understand how to work with the rgrc[]’s to achieve what I want, be it because I can’t write what I mean or because the system is doing something I didn’t quite anticipate. I’d be greatful if you could enlighten me.

    Wow, this turned out to be longer than I had anticipated. I hope you don’t mind. If nothing else, look at it as some appreciation for your programming blogs, in case you are ever wondering again whether people are interested in it or not.

    Regards,

    .f

  2. Raymond Chen says:

    Don’t use WS_EX_TRANSPARENT on toplevel windows; it doesn’t do what you think. I’ll have to write about that more later.

    rcrc[0] is legal to change; that’s sort of the whole point of the WM_NCCALCSIZE message. though of course the rectangle you return has to fit inside the window rectangle.

  3. floyd says:

    Thanks a lot for responding. I feared that this would happen: You answer my questions and thereby raise new ones…

    First off, I was under the impression that WS_EX_TRANSPARENT would cause a window to be invisible to mouse messages, and my application appears to be working according to this assumption (I don’t really remember where I got that ‘information’ from; the MSDN doesn’t seem to suggest anything along those lines). If I’m totally wrong, I can hardly wait for your take on this one.

    I figured that changing the rgrc[0] would be a Good Idea ™, but it just doesn’t seem get me anywhere. I tried doing the same that DefWindowProc does and my trusted machine BSOD’s on me. I sincerely hope I’m doing something utterly stupid, rather than having to fear that my system components are teaming up against me…

    With all that said, I’ll have to take the risk of sounding like a total fool, since your explanations should be sufficient already: The way I understand it, in my example of a simple toplevel window with no controls contained, all coordinates in the rgrc’s will always be screen-relative coordinates, both on entry as well as on exit.

    Damn, I feel stupid already bothering you with this. It’s frightening to see what a sudden OS shutdown can do to a man…

    .f

  4. Raymond Chen says:

    Yes I don’t see any obvious mistakes in your description. Maybe the world is just out to get you…

  5. Guest post by Joe Castro, WPF product team developer This document covers the design and some implementation

Comments are closed.