The Unfortunate Effect of WM_SETREDRAW

… or why simply replacing LockWindowUpdate by WM_SETREDRAW is not that straightforward.

As you know, when you want to avoid flickering or multiple partial redraws of your Forms, during a flurry of updates for instance, even though it is very tempting, you must not use LockWindowUpdate. The Windows documentation has been updated from the days when it practically enticed people into using this API call for this wrong purpose and Raymond Chen has explained in detail why:
What does LockWindowUpdate do?
With what operations is LockWindowUpdate meant to be used?
With what operations is LockWindowUpdate not meant to be used?
Final remarks on LockWindowUpdate

Now, just replacing LockWindowUpdate(MyForm.Handle) by SendMessage(MyForm.Handle, WM_SETREDRAW, 0, 0) on the whole Form is not the solution either. Even though I’ve seen it recommended here or there.
Oh, it does a good job at preventing any painting on the Form, and if drawing is re-enabled quickly enough, there is little chance to cause the problem we’ll see below.

Simulate a long calculation that coincides with some visible UI changes.

Create a new VCL Forms application and put 2 Buttons on the Main Form.
In the 1st ButtonClick handler, paste the following code:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Width := Button1.Width*2;
  Repaint;
  sleep(2000);
  Button1.Width := Button1.Width div 2;
  Repaint;
end;

If you click the button, its size changes, the application stays unresponsive for 2 s (lengthy processing without a background thread) then everything is back to normal. If you click on the Form while it is “frozen” nothing happens unless you have an onClick event, or try to move the Form for instance, and in that case the event action is executed after the Form is unfrozen, when message processing can resume and handle the backlog.

Now, if you don’t want the UI changes to be visible.

You can wrap all that code with WM_SETREDRAW (like you’d be tempted to do with LockWindoUpdate):

procedure TForm1.Button1Click(Sender: TObject);
begin
  SendMessage(Handle, WM_SETREDRAW, Ord(False), 0);
  try
    Button1.Width := Button1.Width*2;
    Repaint;
    sleep(2000);
    Button1.Width := Button1.Width div 2;
    Repaint;
  finally
    SendMessage(Handle, WM_SETREDRAW, Ord(True), 0);
  end;
end;

Try it again. Seems good, the button size does not appear to change. But, wait!
Try to click anywhere on the form while it is “busy”.
Oops! Clicked through it, like it did not even exist!
It’s even more obvious when the form is above the Delphi editor: as soon as you click the button, the cursor changes from the regular Arrow pointer to the text IBeam.

Try with Notepad.

In case you’d believe it is some defect special to the VCL Forms, you can try and paste the following code in the 2nd Button OnClick handler. It opens Notepad for a new text document, prevents redrawing on it for 3s then re-enables it.
Note: If the window title is different (non English Windows…), you may have to adapt the 2nd FindWindow parameter or use an empty string.

procedure TForm1.Button2Click(Sender: TObject);
var
  h: THandle;
begin
  ShellExecute(Handle, nil, 'Notepad.exe', '','', SW_SHOWNORMAL);
  sleep(100);
  // you may have to change the default title in non English Win7 or put ''
  h := FindWindow('Notepad', 'Untitled - Notepad');
  if IsWindow(h) then begin
    SendMessage(h, WM_SETREDRAW, Ord(False), 0);
    try
      sleep(3000);
    finally
      SendMessage(h, WM_SETREDRAW, Ord(True), 0);
    end;
  end;
end;

Try to click anywhere on the Notepad window when it is “locked”, you’ll get right through it just as well!

What if you need to prevent drawing on the Form while doing some processing?

As it is very rare that the UI controls subject to unwanted drawing are directly placed on the Form, the easiest solution is to prevent drawing for the utmost parent, very often a Panel or a Tab/PageControl (the ClientWindow would not work).
And if you don’t have a master parent, you can always insert a Panel with alClient as a main container between the Form and the rest of the controls.
Useful tip: Contrary to DisableControls/EnableControls for the DataSets, WM_SETREDRAW does not care about nested calls, it does not matter how many times you call WM_SETREDRAW False (although avoiding sending unnecessary messages flying around is always desirable). You just have to guarantee that you’ll call WM_SETREDRAW True when you are done with your updates.

Why do we have this strange behavior?

Practically, WM_SETREDRAW default behavior is for the Window receiving it to disappear, but without causing the screen to be refreshed to show what’s behind. We are left with a “ghost” painting of the current Window, and is truly very static indeed.
It has been explained, sort of, by Raymond Chen in a very recent post:
There’s a default implementation for WM_SETREDRAW, but you might be able to do better

This entry was posted in Delphi, User Interface, Windows and tagged , , , , . Bookmark the permalink.

One Response to The Unfortunate Effect of WM_SETREDRAW

  1. Moritz Beutel says:

    Excellent post, thanks.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>