Answer:
A lot of programmers complain about flicker when they draw on a TPaintBox control, when they draw on the form’s Canvas, or when they paint on the Canvas of a control that they have written. One newsgroup post contained a subject line that read: “TPaintBox wicked flicker, can it be stopped.” The answer is yes, and the solution is simple, once you understand what is going on.
Explanation of flicker
An example of flicker
Flicker in TControl and TGraphicControl objects
The wrong way to use TPaintBox or TImage
Removing flicker
Notes
Explanation of flicker:
Before you eliminate flicker, it’s helpful to know what it is. Windows sends your program WM_PAINT messages to notify you that some part of the screen needs to be repainted. This happens when your program first starts, when you restore or maximize the program, and when the program is uncovered from beneath another program. You can also ask Windows to send a WM_PAINT message by calling the InvalidateRect API call.
Any window that responds to WM_PAINT messages must call the BeginPaint and EndPaint API functions. These API functions might sound foreign to you if you’ve been programming with OWL, the VCL, or MFC because these frameworks call the functions for you (see TWinControl::PaintHandler in CONTROLS.PAS). Among other things, BeginPaint will send your window a WM_ERASEBKGND if the window is marked for erasing. The window will almost always be marked for erasing if Windows sent the WM_PAINT message on its own. If the WM_PAINT message was sent because you called InvalidateRect, then the window will be marked for erasing if the last argument to InvalidateRect was true.
The flicker that you see is caused by the default handler for the WM_ERASEBKGND message. Whenever the DefWindowProc receives a WM_ERASEBKGND message, it erases the contents of the window by filling the window with the background color of the form. The default handler is is equivalent to executing this code:
Canvas->Brush->Color = Color;
Canvas->FillRect(ClientRect);
Realize that this handler executes when your window calls BeginPaint after receiving a WM_PAINT message. Your entire window will be erased before BeginPaint returns. Your window will remain erased until you paint over it. Generally, you paint the window in your WM_PAINT handler sometime after BeginPaint has been called. The flicker that you see is caused by BeginPaint erasing the background of your window just before your WM_PAINT handler paints the window. In fact, you can change the color of the flicker by changing the Color property of the form.
An example of flicker:
To demonstrate the flicker phenomena, it’s beneficial to slow down the message handling process so the flicker becomes more apparent. The following code example does just that. Create a new project and and add a button to the main form (put it near the bottom). Set the form’s Color property to clBlue. Then modify the header and source files as shown below (create the OnClick and OnPaint handler’s using the Object Inspector).
// Header file
#ifndef MAINFORMH
#define MAINFORMH
//—————————
#include
#include
#include
#include
//—————————
class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *Button1;
void __fastcall Button1Click(TObject *Sender);
void __fastcall FormPaint(TObject *Sender);
private: // User declarations
void __fastcall WMEraseBkgnd(TWMEraseBkgnd &Message);
void __fastcall WMPaint(TWMPaint &Message);
public: // User declarations
__fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_ERASEBKGND,TWMEraseBkgnd,WMEraseBkgnd)
MESSAGE_HANDLER(WM_PAINT, TWMPaint, WMPaint);
END_MESSAGE_MAP(TForm)
};
// CPP file
//———————–
#include
#pragma hdrstop
#include “MAINFORM.h”
//———————–
#pragma resource “*.dfm”
TForm1 *Form1;
//———————–
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//—————————————–
void __fastcall TForm1::WMEraseBkgnd(TWMEraseBkgnd &Message)
{
OutputDebugString (“Inside WM_ERASEBKGND handler”);
TForm::Dispatch(&Message); // pass message to default handler
Sleep(5000); // delay so flicker becomes obvious
OutputDebugString (“Leaving WM_ERASEBKGND handler”);
}
//—————————————–
void __fastcall TForm1::WMPaint(TWMPaint &Message)
{
OutputDebugString (“Just received WM_PAINT message. Before BeginPaint”);
TForm::Dispatch(&Message);
OutputDebugString (“Finished handling WM_PAINT message.”);
}
//—————————————–
void __fastcall TForm1::Button1Click(TObject *Sender)
{
OutputDebugString (“About to call invalidate”);
::InvalidateRect(Handle,NULL,TRUE);
OutputDebugString (“Just returned from Invalidate call”);
}
//—————————————–
void __fastcall TForm1::FormPaint(TObject *Sender)
{
OutputDebugString (“Just entered form’s OnPaint. BeginPaint already called.”);
Canvas->Brush->Color = clBlack;
Canvas->FillRect(ClientRect);
OutputDebugString (“Leaving OnPaint handler.”);
}
Run the program. Push the button a few times and observe how the background is erased before the OnPaint handler executes. Close the program and then look at the OutDbg1.TXT file. Locate the point where the first InvalidateRect call was made. You should see this sequence of events:
About to call invalidate
Just returned from Invalidate call
Just received WM_PAINT message. Before BeginPaint
Inside WM_ERASEBKGND handler
Leaving WM_ERASEBKGND handler
Just entered form’s OnPaint. BeginPaint already called.
Leaving OnPaint handler.
Finished handling WM_PAINT message.
Flicker in TControl and TGraphicControl objects:
The explanation of WM_ERASEBKGND only applies to descendents of TWinControl because only TWinControl descendents have window handles. TGraphicControl and TControl don’t have window handles, and as such, they have no concept of DefWindowProc, BeginPaint, and WM_ERASEBKGND. However, descendent’s of these two classes can still suffer from flicker (note that TPaintBox is derived from TGraphicControl). The key lies within the InvalidateControl method of TControl. This function calls InvalidateRect for the parent window:
procedure TControl.InvalidateControl(IsVisible, IsOpaque: Boolean);
begin
…
…
InvalidateRect(Parent.Handle, @Rect, not (IsOpaque or
(csOpaque in Parent.ControlStyle) or BackgroundClipped));
…
end
Parent->Handle is the HWND of the form or panel that the control is placed on. Rect is equal to the BoundsRect of the control. The InvalidateRect call is telling Windows to invalidate the region of the parent window where the control is located. Notice that since InvalidateRect is being called, the same WM_PAINT ==> BeginPaint ==> WM_ERASEBKGND sequence of events looms on the horizon. The last argument to InvalidateRect controls whether BeginPaint will send the WM_ERASEBKGND message. IsOpaque will be true if the control itself has the csOpaque control style. csOpaque means that a control completely paints its client area. By default, controls do not contain this style. The logic above tells the Windows not to erase the region if either the form or the control completely paint themselves. Since csOpaque is not set, InvalidRect is instructed to erase the background region of the control, which causes flicker.
The wrong way to use TPaintBox or TImage:
Now you know that flicker is caused by the WM_ERASEBKGND message. However, it may be difficult to track down why this message is being sent to your program. Imagine that you have a program that needs to frequently update the display. The following code example shows one way of updating the screen via a PaintBox control. This code uses a PaintBox control to show the water level in an imaginary storage tank. The program has a timer that reads the water level (in a real system, it would read an I/O port, but in our test program it reads the value from a TrackBar). If the water level has changed, the program updates the water level display in the PaintBox.
The program contains an OnPaint handler for the PaintBox control that completely paints the contents of the PaintBox based on the current water level. The timer event calls the VCL Repaint method when the water level changes.
//———————————————————————–
// To compile this code, place a TrackBar, a Timer, and a PaintBox control
// onto a form. The PaintBox control should be 200 pixels high. The TrackBar
// should range from -200 to 0 and should be a vertical TrackBar. Set the
// Timer interval to 10. Then create an OnTimer event and an OnPaint handler
// for the PaintBox control. Add an int member to the form class called
// WaterLevel.
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
// Note: The TrackBar ranges from -200 at the top to 0 at the bottom. The
// new water level is the trackbar position multiplied by -1. The -1
// inverts -200 into a positive 100. This way, the water levels
// range from 0 to 200.
int NewWaterLevel = TrackBar1->Position * -1;
if (NewWaterLevel != WaterLevel) // has the water level changed
{ // if so, store the new value
WaterLevel = NewWaterLevel; // and Repaint the control
PaintBox1->Repaint();
}
}
//—————————————————————————
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
// Competely repaint the PaintBox to display the correct water level
// Use a memory bitmap to do off screen drawing. Then draw the bitmap
// to the Canvas of the PaintBox.
//
Graphics::TBitmap *MemBitmap = new Graphics::TBitmap;
MemBitmap->Width = PaintBox1->Width;
MemBitmap->Height = PaintBox1->Height;
// Fill in the water tank with white.
MemBitmap->Canvas->Brush->Color = clWhite;
TRect rect = PaintBox1->ClientRect;
MemBitmap->Canvas->FillRect(rect);
// Now find the pixel that represents the water level. This code
// assumes that the paintbox is 200 pixels high, and that the max
// water level is 200. Your water level meter should be more robust.
// Fill in the water portion of the tank with blue.
rect.Top += 200 – (WaterLevel);
MemBitmap->Canvas->Brush->Color = clBlue;
MemBitmap->Canvas->FillRect(rect);
// Copy off screen image to the canvas
PaintBox1->Canvas->Draw(0,0,MemBitmap);
delete MemBitmap;
}
Removing the flicker:
Many programmers like to place all of their painting code in one location. In the previous example, the painting code resides in an OnPaint handler. Unfortunately, the PaintBox flickers whenever the water level changes because the Repaint call results in a call to the API InvalidateRect function. You could replace Repaint with Refresh or with a combination of Invalidate and Update, but the flicker would persist. (Note that early versions of the VCL help file claimed that Repaint and Refresh served different purposes. This is not true. Refresh simply calls Repaint, so the two are the same. Borland has updated the help files to reflect this).
There are several ways to eliminate the flicker in the PaintBox control.
Add a WM_ERASEBKND handler and block the erase action
Add csOpaque to the ControlStyle of the PaintBox.
Don’t call Repaint, Refresh, or Invalidate.
Since the PaintBox is erased via a WM_ERASEBKGND message that is sent to its parent (in this case, the main form), you can intercept the WM_ERASEBKGND message. You can prevent flicker by not passing this message on for default processing. Unfortunately, trapping WM_ERASEBKGND might impact the appearance of other controls on the form.
To use the csOpaqe method, add this line of code to the form’s constructor. Do this for all controls that need to be painted frequently.
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
WaterLevel = 0;
PaintBox1->ControlStyle = PaintBox1->ControlStyle << csOpaque;
}
Of the three choices listed, I prefer the last. The PaintBox control provides a Canvas property that allows you to paint on the control whenever you feel like it. There is no reason why you have to put all of your painting code inside of an OnPaint handler. Whenever you need to update the screen, just do it, plain and simple. Don't ask Repaint to do it for you. By avoiding Repaint and its evil cohorts, you bypass the WM_PAINT ==> BeginPaint ==> WM_ERASEBKND chain of events that causes the flicker in the first place. Additionally, you significantly reduce the amount of code that executes. Here is a modified version of the water level program that avoids flicker by moving the painting code into a reusable function.
void TForm1::PaintWaterLevel(void)
{
Graphics::TBitmap *MemBitmap = new Graphics::TBitmap;
MemBitmap->Width = PaintBox1->Width;
MemBitmap->Height = PaintBox1->Height;
MemBitmap->Canvas->Brush->Color = clWhite;
TRect rect = PaintBox1->ClientRect;
MemBitmap->Canvas->FillRect(rect);
rect.Top += 200 – (WaterLevel);
MemBitmap->Canvas->Brush->Color = clBlue;
MemBitmap->Canvas->FillRect(rect);
PaintBox1->Canvas->Draw(0,0,MemBitmap);
delete MemBitmap;
}
//—————————————————————————
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
int NewWaterLevel = TrackBar1->Position * -1;
if (NewWaterLevel != WaterLevel) // has the water level changed
{ // if so, store the new value
WaterLevel = NewWaterLevel; // and Repaint the control
PaintWaterLevel();
}
}
//—————————————————————————
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
PaintWaterLevel();
}
For this strategy to work, the PaintWaterLevel function must paint the entire client area of the PaintBox control. This is accomplished by creating an offscreen bitmap that matches the size of the PaintBox. The function first paints the background of the offscreen bitmap, and then paints the water level. Notice that you still need an OnPaint handler that will repaint the water level when the main window is uncovered from beneath another window on the desktop.
Note: If you are designing a component that is derived from TGraphicControl or TCustomControl, you can put your painting code inside the virtual Paint method of the class. The VCL calls Paint to allow the control to redraw itself. However, you can call Paint from your own code to update the control’s appearance on the screen.
Note: The implementation of the PaintWaterLevel function eliminates flicker caused by updates from code. It does not remove flicker that is caused when the program is uncovered from beneath another window, because this action still causes the WM_PAINT ==> BeginPaint ==> WM_ERASEBKGND sequence of events. Normally, this flicker isn’t noticed by users, but if it is a problem in your program, consider using the csOpaque strategy.
博主友情提示:
如您在评论中需要提及如QQ号、电子邮件地址或其他隐私敏感信息,欢迎使用>>博主专用加密工具v3<<处理后发布,原文只有博主可以看到。