Sample Project:  Double-buffering owner-drawn controls
By: Damon Chandler
 

   The VCL and Windows allows great customization with the owner-draw style.  This style allows the developer to create and manage the shape and appearance of the control.  Unfortunately, if implemented incorrectly, specifying this style oftentimes leads to irritating flicker.  Sources of flicker can vary from control to control, and also depends on the specific implementation of the drawing routine.

    In this project, we'll use the TStringGrid control as an example.  Since this control can have an enourmous amount of cells, it is especially prone to flicker.  In general, any control that is drawn with enough repetition will flicker.  Some common examples include owner-drawn buttons, ProgressBars, ListBoxes, ListViews, TreeViews, and frequently updated TImage controls, among many others.  The drawing of owner-drawn button controls, for example, is bottled-necked by the WM_DRAWITEM message.  This is the message that the button sends to its Parent window telling it that it needs to be drawn.  The VCL's implementation,  the BitBtn control, is even more prone to flicker since this message is bounced back to the BitnBtn before any drawing takes place.  The component model uses "echo" messages (usually of the same name with the prefix replaced by CN_  "component notification").  The VCL's OnDrawItem event is simply a wrapper of this WM_DRAWITEM message.  For an owner-drawn ListBox, for example, the OnDrawItem event is fired very frequently, once for every item in the list that needs repainting.  If the event handler is slow, the delay will add up and the result is flicker.

    Oftentimes, an owner-drawn control is also "over-drawn."  More specifically, the control is drawn twice -- once in response to the WM_ERASEBKGND message, and again in response to the WM_PAINT message.  The WM_ERASEBKGND message is most useful for artifact elimination.  However, in cases where these possible artifacts will be painted over in response to the WM_PAINT message, erasing the background does nothing but extra work.

    Two rules of thumb to keep in mind when experiencing flicker: supress background erasing and double-buffer.  The StringGrid component will be prone to flicker unless you implement the OnDrawCell function efficiently.  Further, avoid VCL
drawing routines whenever possible.   Althought the overhead is minimal, for controls that draw repeatedly (like TStringGrid), this overhead can add up. 

    The first task is to trap the WM_ERASEBKGND Windows message to supress background erasing.  Since we draw the StringGrid ourselves, there's no need to erase the background.  To do this, we'll subclass the Window procedure of the
StringGrid.  Next, we'll use a method of double-buffering to speed up the drawing.  The idea is to do all the drawing to a memory bitmap, then block transfer the bits to the StringGrid's device context (Canvas).  Be sure to set the DefaultDrawing to false, and in the Options property, set goHorzLine, goVertLine, goFixedVertLine, and goFixedHorzLine to false...
 

 

//in header...
    Graphics::TBitmap *MemBitmap;

    Controls::TWndMethod OldStringGridWP;
    void __fastcall NewStringGridWP(TMessage &Msg);

 


 
 
 

//---------------------------------------------------------------------------
//in source...
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
    OldStringGridWP = StringGrid1->WindowProc;
    StringGrid1->WindowProc = NewStringGridWP;

    MemBitmap = new Graphics::TBitmap();
    MemBitmap->Width = StringGrid1->ClientWidth;
    MemBitmap->Height = StringGrid1->ClientHeight;
}

//new window procedure -- supress background erasing
void __fastcall TForm1::NewStringGridWP(TMessage &Msg)
{
    if (Msg.Msg == WM_ERASEBKGND) Msg.Result = false;
    else OldStringGridWP(Msg);
}

//OnDrawCell event handler
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, long Col, long Row,
        TRect &Rect, TGridDrawState State)
{
    //if it's a fixed row (headers)
    if (State.Contains(gdFixed))
    {
        MemBitmap->Canvas->Brush->Color = clBtnFace;
        MemBitmap->Canvas->Font->Color = clWindowText;
        ::Rectangle(MemBitmap->Canvas->Handle, Rect.Left,
                    Rect.Top, Rect.Right, Rect.Bottom);
        Frame3D(MemBitmap->Canvas, Rect, clBtnHighlight, clBtnShadow, 1);
    }

    //if the cell is selected
    else if (State.Contains(gdSelected))
    {
        MemBitmap->Canvas->Brush->Color = clHighlight;
        MemBitmap->Canvas->Font->Color = clHighlightText;
        ::Rectangle(MemBitmap->Canvas->Handle, Rect.Left,
                    Rect.Top, Rect.Right, Rect.Bottom);
    }

    //if normal
    else
    {
        MemBitmap->Canvas->Brush->Color = StringGrid1->Color;
        MemBitmap->Canvas->Font->Color = StringGrid1->Font->Color;
        ::Rectangle(MemBitmap->Canvas->Handle, Rect.Left,
                    Rect.Top, Rect.Right + 1, Rect.Bottom + 1);
    }

    RECT R = RECT(Rect);
    UINT format = DT_LEFT | DT_SINGLELINE;
    AnsiString text = StringGrid1->Cells[Col][Row];

    ::DrawText(MemBitmap->Canvas->Handle, text.c_str(),
               text.Length(), &R, format);

    ::BitBlt(StringGrid1->Canvas->Handle, Rect.Left - 1, Rect.Top - 1,
             Rect.Right - Rect.Left + 2, Rect.Bottom - Rect.Top + 2,
             MemBitmap->Canvas->Handle, Rect.Left - 1, Rect.Top - 1, SRCCOPY);

}

//restore the window procedure
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
    delete MemBitmap;
    StringGrid1->WindowProc = OldStringGridWP;
}
 

 


 
 

    As you can see, I used as little VCL drawing routines as possible.  The Frame3D function was the exception.  It is simply a wrapper for the API DrawEdge() function, but much easier to use.  The result is a StringGrid with just as fast a display (scrolling / selecting) as if the DefaultDrawing property were true.  Download the sample project as see for yourself.
 
 

Download sample project: Sample3.zip (29.8 KB)