Best way to do non-flickering, segmented graphics updates in Delphi?

Jon Lennart Aasenden picture Jon Lennart Aasenden · Jun 15, 2011 · Viewed 11.3k times · Source

I thought I could just throw this out there and just ask: I have seen Delphi controls that are flawless in terms of graphical effects. Meaning: no flickering, sectioned updates (only redraw the section of a control that is marked as dirty) and smooth scrolling.

I have coded a lot of graphical controls over the years, so I know about double buffering, dibs, bitblts and all the "common" stuff (I always use dibs to draw everything if possible, but there is an overhead). Also know about InvalidateRect and checking TCanvas.ClipRect for the actual rect that needs to be updated. Despite all these typical solutions, I find it very difficult to create the same quality components as say - Developer Express or Razed Components. If the graphics is smooth you can bet the scrollbars (native) flicker, and if the scrollbars and frame is smooth you can swear the background flickers during scrolling.

Is there a standard setup of code to handle this? A sort of best practises that ensures smooth redraws of the entire control -- including the non-client area of a control?

For instance, here is a "bare bone" control which take height for segmented updates (only redraw what is needed). If you create it on a form, try moving a window over it, and watch it replace the parts with colors (see paint method).

Does anyone have a similar base class that can handle non client area redraws without flickering?

type

TMyControl = Class(TCustomControl)
private
  (* TWinControl: Erase background prior to client-area paint *)
  procedure WMEraseBkgnd(var Message: TWmEraseBkgnd);message WM_ERASEBKGND;
Protected
  (* TCustomControl: Overrides client-area paint mechanism *)
  Procedure Paint;Override;

  (* TWinControl: Adjust Win32 parameters for CreateWindow *)
  procedure CreateParams(var Params: TCreateParams);override;
public
  Constructor Create(AOwner:TComponent);override;
End;


{ TMyControl }

Constructor TMyControl.Create(AOwner:TComponent);
Begin
  inherited Create(Aowner);
  ControlStyle:=ControlStyle - [csOpaque];
end;

procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);

  (* When a window has this style set, any areas that its
     child windows occupy are excluded from the update region. *)
  params.ExStyle:=params.ExStyle + WS_CLIPCHILDREN;

  (* Exclude VREDRAW & HREDRAW *)
  with Params.WindowClass do
  Begin
    (* When a window class has either of these two styles set,
       the window contents will be completely redrawn every time it is
       resized either vertically or horizontally (or both) *)
    style:=style - CS_VREDRAW;
    style:=style - CS_HREDRAW;
  end;
end;

procedure TMyControl.Paint;

  (* Inline proc: check if a rectangle is "empty" *)
  function isEmptyRect(const aRect:TRect):Boolean;
  Begin
    result:=(arect.Right=aRect.Left) and (aRect.Bottom=aRect.Top);
  end;

  (* Inline proc: Compare two rectangles *)
  function isSameRect(const aFirstRect:TRect;const aSecondRect:TRect):Boolean;
  Begin
    result:=sysutils.CompareMem(@aFirstRect,@aSecondRect,SizeOf(TRect))
  end;

  (* Inline proc: This fills the background completely *)
  Procedure FullRepaint;
  var
    mRect:TRect;
  Begin
    mRect:=getClientRect;
    AdjustClientRect(mRect);
    Canvas.Brush.Color:=clWhite;
    Canvas.Brush.Style:=bsSolid;
    Canvas.FillRect(mRect);
  end;

begin
  (* A full redraw is only issed if:
      1. the cliprect is empty
      2. the cliprect = clientrect *)
  if isEmptyRect(Canvas.ClipRect)
  or isSameRect(Canvas.ClipRect,Clientrect) then
  FullRepaint else
  Begin
    (* Randomize a color *)
    Randomize;
    Canvas.Brush.Color:=RGB(random(255),random(255),random(255));

    (* fill "dirty rectangle" *)
    Canvas.Brush.Style:=bsSolid;
    Canvas.FillRect(canvas.ClipRect);
  end;
end;

procedure TMyControl.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
  message.Result:=-1;
end;

Updated

I just wanted to add that what did the trick was a combination of:

  1. ExcludeClipRect() when drawing the non-clientarea, so you dont overlap with the graphics in the clientarea
  2. Catching the WMNCCalcSize message rather than just using the bordersize for measurements. I also had to take height for the edge sizes:

    XEdge := GetSystemMetrics(SM_CXEDGE);
    YEdge := GetSystemMetrics(SM_CYEDGE);
    
  3. Calling RedrawWindow() with the following flags whenever you have scrollbars that have moved or a resize:

    mRect:=ClientRect;
    mFlags:=rdw_Invalidate
      or RDW_NOERASE
      or RDW_FRAME
      or RDW_INTERNALPAINT
      or RDW_NOCHILDREN;
    RedrawWindow(windowhandle,@mRect,0,mFlags);
    
  4. When updating the background during the Paint() method, avoid drawing over possible child objects, like this (see the RDW_NOCHILDREN mentioned above):

    for x := 1 to ControlCount do
    begin
      mCtrl:=Controls[x-1];
      if mCtrl.Visible then
      Begin
        mRect:=mCtrl.BoundsRect;
        ExcludeClipRect(Canvas.Handle,
        mRect.Left,mRect.Top,
        mRect.Right,mRect.Bottom);
      end;
    end;
    

Thanks for the help guys!

Answer

dthorpe picture dthorpe · Jun 15, 2011

Double buffering and fancy drawing tactics are only half the story. The other half, some would argue the more critical half, is to limit how much of your control is invalidated.

In your comments, you mention that you use RedrawWindow(handle, @R, 0, rdw_Invalidate or rdw_Frame). What are you setting the R rectangle to? If you set it to your client area rect, then you are redrawing the entire client area of your control. When scrolling, only a small portion of your control needs to be redrawn - the slice at the "trailing edge" of the scroll direction. Windows will bitblit the rest of the client area screen to screen to move the existing pixels over in the scroll direction.

Also check whether you have set your window flags to require full redraw on scroll. I don't recall the flag names offhand, but you want them turned off so that scroll actions only invalidate a slice of your client area. I believe this is the Windows default.

Even with hardware accelerated graphics, less work is faster than more work. Get your invalidate rects down to the absolute minimum and reduce the number of pixels you're pushing across the system bus.