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)
|