Using WM_SETREDRAW to speed up adding a lot of elements to a control


Today's Little Program shows one way you can implement a better version of WM_SET­REDRAW. Our first version doesn't use WM_SET­REDRAW at all.

Start with the scratch program and make the following changes:

HFONT g_hfList;
int g_yOrigin;
int g_cyLine;
int g_cLinesPerPage;
int g_cItems = 100;

// GetTrackPos incorporated by reference
// ScrollTo incorporated by reference
// ScrollDelta incorporated by reference
// OnSize incorporated by reference
// OnVscroll incorporated by reference + modifications
// OnCreate incorporated by reference
// OnDestroy incorporated by reference

// This is the same as the earlier version of PaintSimpleContent
// with one tiny change: Draw the items in reverse order so the effect
// is more noticeable.

void
PaintSimpleContent(HWND hwnd, PAINTSTRUCT *pps)
{
 HFONT hfPrev = SelectFont(pps->hdc, g_hfList);  /* Use the right font */

 int iMin = max(pps->rcPaint.top / g_cyLine, 0);
 int iMax = min((pps->rcPaint.bottom + g_cyLine - 1) / g_cyLine, g_cItems);

 for (int i = iMin; i < iMax; i++) {
  char szLine[256];
  int cch = wsprintf(szLine, "This is line %d", g_cItems - i);
  TextOut(pps->hdc, 0, i * g_cyLine, szLine, cch);
 }

 SelectFont(pps->hdc, hfPrev);
}

// PaintContent incorporated by reference

void AddItem(HWND hwnd)
{
 g_cItems++;
 InvalidateRect(hwnd, 0, TRUE);
 ScrollDelta(hwnd, 0);
}

void OnChar(HWND hwnd, TCHAR ch, int cRepeat)
{
 switch (ch) {
 case TEXT('1'):
  AddItem(hwnd);
  break;

 case TEXT('2'):
  for (int i = 0; i < 10000; i++) {
   AddItem(hwnd);
  }
  break;

 case TEXT('3'):
  SetWindowRedraw(hwnd, FALSE);
  for (int i = 0; i < 10000; i++) {
   AddItem(hwnd);
  }
  SetWindowRedraw(hwnd, TRUE);
  InvalidateRect(hwnd, nullptr, TRUE);
 }
}

 HANDLE_MSG(hwnd, WM_VSCROLL, OnVscroll);
 HANDLE_MSG(hwnd, WM_CHAR, OnChar);

Most of this program was stolen from my scroll bar series. The interesting new bits are that you can add one new item by hitting 1, or you can add ten thousand items by hitting 2, or you can add ten thousand items with redraw disabled by hitting 3.

I drew the items in reverse order so that adding an item forces everything to change position, so that the effect of the redraw is more noticeable.

Observe that adding one item is fast, but adding ten thousand items with redraw enabled is slow; you can watch the scroll bar as it slowly shrinks. But adding ten thousand items with redraw disabled is not too bad.

But we can do better.

BOOL g_fRedrawEnabled = TRUE;

void AddItem(HWND hwnd)
{
 g_cItems++;
 if (g_fRedrawEnabled) {
  InvalidateRect(hwnd, 0, TRUE);
  ScrollDelta(hwnd, 0);
 }
}

void OnSetRedraw(HWND hwnd, BOOL fRedraw)
{
 g_fRedrawEnabled = fRedraw;
 if (fRedraw) {
  InvalidateRect(hwnd, 0, TRUE);
  ScrollDelta(hwnd, 0);
 }
}

void
OnPaint(HWND hwnd)
{
 if (g_RedrawEnabled) {
  PAINTSTRUCT ps;
  BeginPaint(hwnd, &ps);
  PaintContent(hwnd, &ps);
  EndPaint(hwnd, &ps);
 } else {
  ValidateRect(hwnd, nullptr);
 }
}

 HANDLE_MSG(hwnd, WM_SETREDRAW, OnSetRedraw);

We have a custom handler for the WM_SET­REDRAW message that updates a flag that indicates whether redraw is enabled. When adding an item, we do the visual recalculations (updating the scroll bar, mostly) only if redraw is enabled. If a paint message comes in while redraw is disabled, we merely validate the window to say "It's all good, don't worry!" When redraw is re-enabled, we ask for a fresh repaint and update the scroll bars.

With this version of the program, adding ten thousand items with redraw disabled is lightning fast.

Notice that g_fRedrawEnabled is not reference-counted. It's a simply BOOL. In other words, if you send the WM_SET­REDRAW message twice to disable redraw, you still only need to enable it once. Disabling redraw on a window where redraw is already disabled has no effect.

Exercise: Compare the behavior of WM_SET­REDRAW with (the incorrect) Lock­Window­Update for this program.

Comments (14)
  1. WndSks says:

    Why do most controls seem to store the WM_SET­REDRAW state as a bool when a reference counted UINT model would cause a lot less grief for people using a UI toolkit that sits between their code and user32/comctrl. Is it a bitfield internally?

    [WM_SET­REDRAW is spec'd as non-refcounted, so implementations must not refcount it. Another thing to add to the list of things to do once that time machine is ready. -Raymond]
  2. 12BitSlab says:

    There are some similar items to WM_SETREDRAW in managed code.  I believe those are booleans as well.

  3. Hans says:

    Ray, why do you in your OnSetRedraw handler invalidate the window if redraw is re-enabled?

    On every Windows control, I had to do this manually.

    Also on your case TEXT('3'): code, you invalidate manually the window after calling SetWindowRedraw(hwnd, TRUE). This would match my experience.

    [True, technically it is the caller's responsibility to invalidate the window, but I just do it internally out of habit. -Raymond]
  4. Joshua says:

    [WM_SET­REDRAW is spec'd as non-refcounted, so implementations must not refcount it. Another thing to add to the list of things to do once that time machine is ready. -Raymond]

    I found it convenient to refcount it for my own controls anyway but present non-refcount by deduplication when delegating to system controls. He who uses my controls needs to read my manual. If you read this blog you probably don't use them.

    These days I don't use WM_SETREDRAW much anymore preferring to defer the calculations to the next WM_PAINT.

  5. Adrian says:

    In case anyone else was confused and tried to find MSDN documentation for SetWindowRedraw:

    SetWindowRedraw() is a macro from <windowsx.h>.  It is a convenient way of sending the WM_SETREDRAW message.

  6. .dan.g. says:

    I thought that disabling redraw would obviate the need for the extra handling in OnPaint.

    Where are the paint messages coming from if redraw is already disabled?

    [Perhaps somebody moved a window that had been obscuring our window. Use your imagination. -Raymond]
  7. @12BitSlab says:

    BeginUpdate and EndUpdate on the list and tree views and various other. They just use WM_SETREDRAW and are reference counted internally.

  8. .dan.g. says:

    @Raymond: But that doesn't explain why _you_ added  the extra code since your comment seems to bear no relation to the matter being discussed.

  9. Gabe says:

    .dan.g.: I think you understand what WM_SETREDRAW does. It just tells the window not to tell itself to redraw. It does not prevent the window from getting WM_PAINT messages.

    In other words, ordinarily what happens when you add an item to a listbox is that it adds the item to its internal data structures and tells itself to redraw. When you turn off redraw, that just prevents the listbox from telling itself to redraw. Windows will still tell the listbox to redraw for all the usual reasons.

  10. Gabe says:

    Oops, I meant to say that I think .dan.g. MISunderstands what WM_SETREDRAW does.

  11. .dan.g. says:

    @Gabe: Since you have decided to speak on behalf of Raymond, perhaps you can answer my question properly then.

    The subtext was: How to make a better version of WM_REDRAW, by adding code to the WM_PAINT handler to not draw if redraw is already disabled.

    But if redraw is already disabled why is the extra code necessary?

    [You might get paint messages when redraw is disabled. For example, somebody might do WM_SETREDRAW(FALSE), then call MessageBox, then re-enable redraw after MessageBox returns. MessageBox will pump messages, including paint messages. -Raymond]
  12. .dan.g. says:

    Thx Raymond.

    But I still don't see why it is relevant to your post. Your post, if I may remind you, was all about adding items and drawing them, not about calling MessageBox.

    Therefore it seems to me that for the duration of adding new items, and in the absence of any other interference, WM_SETREDRAW(FALSE) ought to have withheld _all_ WM_PAINT messages from needing to be handled. Otherwise it's not doing its job.

    [There are two sides to this demonstration. The control and the client. In this particular demonstration, the client does not pump messages while redraw is disabled, but some other client might, so the control needs to be ready for that. You are confusing the example with the general principle, like saying "Why do you check whether the length is zero? This program never passes a length of zero." Imagine, for example, the client did WM_SETREDRAW(FALSE), then issues asynchronous I/O to get the new items. While waiting for the I/O to complete, the code is pumping messages while the control has redraw disabled. The control will receive paint messages, and it had better not crash when that happens. -Raymond]
  13. Hans says:

    THX, Raymond for answering my question, but another thing in question:

    Before I had to experience I must call InvalidateRect(), my assumption was if redraw is off for a control the control still accumulates the dirty region, and after re-enabling redraw, only the dirty region will be repainted.

    But as I have to call InvalidateRect() myself I invalidate the whole window.

    Imagine a list control where mass items are added, so I disable redraw before and re-enable after adding the items. But none of the added ones is visible (they are not in visible range). What I get is one redundant redraw, as I have no other chance than invalidating the whole window. This may be less efficient than not to disabling redraw, and at least the control flickers.

    Is there some chance to optimize for this scenario (besides doing dirty region calculation myself)?

    [If you want to accumulate dirty regions, then you have to do it yourself. If you have scenarios where somebody makes a lot of changes to offscreen items, then you can opt to add dirty region accumulation to your control as an optimization. -Raymond]
  14. Gabe says:

    .dan.g.: WM_SETREDRAW(FALSE) tells the *target window* not to send *itself* paint messages. It sounds like you want it to tell *Windows* not to send paint messages to the target window.

    In other words, you're wondering why Windows doesn't perform the LockWindowUpdate behavior for you. That might be nice, but it may not always be what you want. What if you want to make a list box that shows a progress bar while the updates are happening?

    Don't be fooled by the name. WM_SETREDRAW(FALSE) just means "I'm going to be sending you a batch of updates" and WM_SETREDRAW(TRUE) just means "I'm done sending you updates". The most likely visible side-effect of this is that the display will not redraw itself, but that's just one possibility.

Comments are closed.