📄 faq34.htm
字号:
the parent window where the control is located. Notice that since <TT>InvalidateRect</TT>
is being called, the same WM_PAINT ==> BeginPaint ==> WM_ERASEBKGND sequence of
events looms on the horizon. The last argument to <TT>InvalidateRect</TT> controls
whether <TT>BeginPaint</TT> will send the <TT>WM_ERASEBKGND</TT> message. <TT>IsOpaque</TT>
will be true if the control itself has the <TT>csOpaque</TT> control style. <TT>csOpaque</TT>
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 <TT>csOpaque</TT>
is not set, <TT>InvalidRect</TT> is instructed to erase the background region of
the control, which causes flicker.
</P>
<H3>
<A NAME="tpaintbox">The wrong way to use <TT>TPaintBox</TT> or <TT>TImage</TT>:
</H3>
<P>
Now you know that flicker is caused by the <TT>WM_ERASEBKGND</TT> 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 <TT>PaintBox</TT> control. This code uses a <TT>PaintBox</TT> 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 <TT>TrackBar</TT>). If the water level has
changed, the program updates the water level display in the <TT>PaintBox</TT>.
</P>
<P>
The program contains an <TT>OnPaint</TT> handler for the <TT>PaintBox</TT> control
that completely paints the contents of the <TT>PaintBox</TT> based on the current
water level. The timer event calls the VCL <TT>Repaint</TT> method when the
water level changes.
</P>
<pre>
<font color="navy">//-----------------------------------------------------------------------</font>
<font color="navy">// To compile this code, place a TrackBar, a Timer, and a PaintBox control</font>
<font color="navy">// onto a form. The PaintBox control should be 200 pixels high. The TrackBar</font>
<font color="navy">// should range from -200 to 0 and should be a vertical TrackBar. Set the</font>
<font color="navy">// Timer interval to 10. Then create an OnTimer event and an OnPaint handler</font>
<font color="navy">// for the PaintBox control. Add an int member to the form class called</font>
<font color="navy">// WaterLevel.</font>
<b>void</b> <b>__fastcall</b> TForm1<b>:</b><b>:</b>Timer1Timer<b>(</b>TObject <b>*</b>Sender<b>)</b>
<b>{</b>
<font color="navy">// Note: The TrackBar ranges from -200 at the top to 0 at the bottom. The</font>
<font color="navy">// new water level is the trackbar position multiplied by -1. The -1</font>
<font color="navy">// inverts -200 into a positive 100. This way, the water levels</font>
<font color="navy">// range from 0 to 200.</font>
<b>int</b> NewWaterLevel <b>=</b> TrackBar1<b>-></b>Position <b>*</b> <b>-</b><font color="blue">1</font><b>;</b>
<b>if</b> <b>(</b>NewWaterLevel <b>!=</b> WaterLevel<b>)</b> <font color="navy">// has the water level changed</font>
<b>{</b> <font color="navy">// if so, store the new value</font>
WaterLevel <b>=</b> NewWaterLevel<b>;</b> <font color="navy">// and Repaint the control</font>
PaintBox1<b>-></b>Repaint<b>(</b><b>)</b><b>;</b>
<b>}</b>
<b>}</b>
<font color="navy">//---------------------------------------------------------------------------</font>
<b>void</b> <b>__fastcall</b> TForm1<b>:</b><b>:</b>PaintBox1Paint<b>(</b>TObject <b>*</b>Sender<b>)</b>
<b>{</b>
<font color="navy">// Competely repaint the PaintBox to display the correct water level</font>
<font color="navy">// Use a memory bitmap to do off screen drawing. Then draw the bitmap</font>
<font color="navy">// to the Canvas of the PaintBox.</font>
<font color="navy">//</font>
Graphics<b>:</b><b>:</b>TBitmap <b>*</b>MemBitmap <b>=</b> <b>new</b> Graphics<b>:</b><b>:</b>TBitmap<b>;</b>
MemBitmap<b>-></b>Width <b>=</b> PaintBox1<b>-></b>Width<b>;</b>
MemBitmap<b>-></b>Height <b>=</b> PaintBox1<b>-></b>Height<b>;</b>
<font color="navy">// Fill in the water tank with white.</font>
MemBitmap<b>-></b>Canvas<b>-></b>Brush<b>-></b>Color <b>=</b> clWhite<b>;</b>
TRect rect <b>=</b> PaintBox1<b>-></b>ClientRect<b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>FillRect<b>(</b>rect<b>)</b><b>;</b>
<font color="navy">// Now find the pixel that represents the water level. This code</font>
<font color="navy">// assumes that the paintbox is 200 pixels high, and that the max</font>
<font color="navy">// water level is 200. Your water level meter should be more robust.</font>
<font color="navy">// Fill in the water portion of the tank with blue.</font>
rect<b>.</b>Top <b>+</b><b>=</b> <font color="blue">200</font> <b>-</b> <b>(</b>WaterLevel<b>)</b><b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>Brush<b>-></b>Color <b>=</b> clBlue<b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>FillRect<b>(</b>rect<b>)</b><b>;</b>
<font color="navy">// Copy off screen image to the canvas</font>
PaintBox1<b>-></b>Canvas<b>-></b>Draw<b>(</b><font color="blue">0</font><b>,</b><font color="blue">0</font><b>,</b>MemBitmap<b>)</b><b>;</b>
<b>delete</b> MemBitmap<b>;</b>
<b>}</b>
</pre>
<H3>
<A NAME="remove">Removing the flicker:</A>
</H3>
<P>
Many programmers like to place all of their painting code in one location. In
the previous example, the painting code resides in an <TT>OnPaint</TT> handler.
Unfortunately, the <TT>PaintBox</TT> flickers whenever the water level changes
because the <TT>Repaint</TT> call results in a call to the API <TT>InvalidateRect</TT>
function. You could replace <TT>Repaint</TT> with <TT>Refresh</TT> or with a
combination of <TT>Invalidate</TT> and <TT>Update</TT>, but the flicker would
persist. (Note that early versions of the VCL help file claimed that <TT>Repaint</TT>
and <TT>Refresh</TT> served different purposes. This is not true. <TT>Refresh</TT>
simply calls <TT>Repaint</TT>, so the two are the same. Borland has updated the
help files to reflect this).
</P>
<P>
There are several ways to eliminate the flicker in the <TT>PaintBox</TT> control.
<UL>
<LI> Add a <TT>WM_ERASEBKND</TT> handler and block the erase action
<LI> Add <TT>csOpaque</TT> to the <TT>ControlStyle</TT> of the <TT>PaintBox</TT>.
<LI> Don't call <TT>Repaint</TT>, <TT>Refresh</TT>, or <TT>Invalidate</TT>.
</UL>
Since the <TT>PaintBox</TT> is erased via a <TT>WM_ERASEBKGND</TT> message that
is sent to its parent (in this case, the main form), you can intercept the
<TT>WM_ERASEBKGND</TT> message. You can prevent flicker by not passing this
message on for default processing. Unfortunately, trapping <TT>WM_ERASEBKGND</TT>
might impact the appearance of other controls on the form.
</P>
<P>
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.
</P>
<pre>
<b>__fastcall</b> TForm1<b>:</b><b>:</b>TForm1<b>(</b>TComponent<b>*</b> Owner<b>)</b>
<b>:</b> TForm<b>(</b>Owner<b>)</b>
<b>{</b>
WaterLevel <b>=</b> <font color="blue">0</font><b>;</b>
PaintBox1<b>-></b>ControlStyle <b>=</b> PaintBox1<b>-></b>ControlStyle <b><<</b> csOpaque<b>;</b>
<b>}</b>
</pre>
<P>
Of the three choices listed, I prefer the last. The <TT>PaintBox</TT> control
provides a <TT>Canvas</TT> 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 <TT>OnPaint</TT> handler. Whenever you need to update
the screen, just do it, plain and simple. Don't ask <TT>Repaint</TT> to do it
for you. By avoiding <TT>Repaint</TT> and its evil cohorts, you bypass the
<TT>WM_PAINT ==> BeginPaint ==> WM_ERASEBKND</TT> 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.
</P>
<pre>
<b>void</b> TForm1<b>:</b><b>:</b>PaintWaterLevel<b>(</b><b>void</b><b>)</b>
<b>{</b>
Graphics<b>:</b><b>:</b>TBitmap <b>*</b>MemBitmap <b>=</b> <b>new</b> Graphics<b>:</b><b>:</b>TBitmap<b>;</b>
MemBitmap<b>-></b>Width <b>=</b> PaintBox1<b>-></b>Width<b>;</b>
MemBitmap<b>-></b>Height <b>=</b> PaintBox1<b>-></b>Height<b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>Brush<b>-></b>Color <b>=</b> clWhite<b>;</b>
TRect rect <b>=</b> PaintBox1<b>-></b>ClientRect<b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>FillRect<b>(</b>rect<b>)</b><b>;</b>
rect<b>.</b>Top <b>+</b><b>=</b> <font color="blue">200</font> <b>-</b> <b>(</b>WaterLevel<b>)</b><b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>Brush<b>-></b>Color <b>=</b> clBlue<b>;</b>
MemBitmap<b>-></b>Canvas<b>-></b>FillRect<b>(</b>rect<b>)</b><b>;</b>
PaintBox1<b>-></b>Canvas<b>-></b>Draw<b>(</b><font color="blue">0</font><b>,</b><font color="blue">0</font><b>,</b>MemBitmap<b>)</b><b>;</b>
<b>delete</b> MemBitmap<b>;</b>
<b>}</b>
<font color="navy">//---------------------------------------------------------------------------</font>
<b>void</b> <b>__fastcall</b> TForm1<b>:</b><b>:</b>Timer1Timer<b>(</b>TObject <b>*</b>Sender<b>)</b>
<b>{</b>
<b>int</b> NewWaterLevel <b>=</b> TrackBar1<b>-></b>Position <b>*</b> <b>-</b><font color="blue">1</font><b>;</b>
<b>if</b> <b>(</b>NewWaterLevel <b>!=</b> WaterLevel<b>)</b> <font color="navy">// has the water level changed</font>
<b>{</b> <font color="navy">// if so, store the new value</font>
WaterLevel <b>=</b> NewWaterLevel<b>;</b> <font color="navy">// and Repaint the control</font>
PaintWaterLevel<b>(</b><b>)</b><b>;</b>
<b>}</b>
<b>}</b>
<font color="navy">//---------------------------------------------------------------------------</font>
<b>void</b> <b>__fastcall</b> TForm1<b>:</b><b>:</b>PaintBox1Paint<b>(</b>TObject <b>*</b>Sender<b>)</b>
<b>{</b>
PaintWaterLevel<b>(</b><b>)</b><b>;</b>
<b>}</b>
</pre>
<P>
For this strategy to work, the <TT>PaintWaterLevel</TT> function must paint the
entire client area of the <TT>PaintBox</TT> control. This is accomplished by
creating an offscreen bitmap that matches the size of the <TT>PaintBox</TT>.
The function first paints the background of the offscreen bitmap, and then
paints the water level. Notice that you still need an <TT>OnPaint</TT> handler
that will repaint the water level when the main window is uncovered from beneath
another window on the desktop.
</P>
<P>
<A NAME="notes"></A>
<B>Note:</B> If you are designing a component that is derived from
<TT>TGraphicControl</TT> or <TT>TCustomControl</TT>, you can put your painting
code inside the virtual <TT>Paint</TT> method of the class. The VCL calls
<TT>Paint</TT> to allow the control to redraw itself. However, you can call
<TT>Paint</TT> from your own code to update the control's appearance on the
screen.
</P>
<P>
<B>Note:</B> The implementation of the <TT>PaintWaterLevel</TT> 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 <TT>WM_PAINT ==> BeginPaint ==> WM_ERASEBKGND</TT> sequence of events.
Normally, this flicker isn't noticed by users, but if it is a problem in your
program, consider using the <TT>csOpaque</TT> strategy.
</P>
</TD> </TR>
</TABLE>
</CENTER>
</BODY>
</HTML>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -