Board index » delphi » PaintBox InvalidateRect

PaintBox InvalidateRect


2006-08-29 12:21:06 AM
delphi119
I'm trying to optimize a paintBox's Paint procedure. It draws a series of
moveto lineto's (a stock market bar chart). I want the app to be able to
Invalidate just part of the paintBox then have the Paint procedure just draw
those affected lines. This would be the style defined by Petzold in
Progamming Windows (win32).
It looks like I can use pb1.Canvas.ClipRect on the Paint side, which is
good, but I can not figure out a way to just invalidate part of the paintBox.
It looks like its all (Invalidate) or nothing.
According to the docs it says use the win32 function SelectClipRgn. So to
verify that I am invalidating a specific region and the Paint 'sees' this
region I have tried the following code which does not work (doing this
directly on a Form does work using the win32 InvalidateRect).
Should I even be using a TPaintBox for this?
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls;
type
TForm1 = class(TForm)
pb1: TPaintBox;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure pb1Paint(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
rec: TRect;
region: hRGN;
begin
//rec := Rect(50,50,100,100);
//InvalidateRect(pb1.Canvas.Handle, @rec, true); //This doesn't seem to
work.
region := CreateRectRgn(50,50,100,100);
SelectClipRgn(pb1.Canvas.Handle, region); //This doesn't seem to work
either.
pb1.Update;
end;
procedure TForm1.pb1Paint(Sender: TObject);
begin
pb1.Canvas.Rectangle(pb1.Canvas.ClipRect); //Identify ClipRect location
end;
end.
 
 

Re:PaintBox InvalidateRect

"Shawn Jones" <XXXX@XXXXX.COM>writes:
Quote

I'm trying to optimize a paintBox's Paint procedure. It
draws a series of moveto lineto's [...]
You'll get better performance drawing to an in-memory bitmap
and then painting it with TCanvas::CopyRect.
Quote
I want the app to be able to Invalidate just part of the
paintBox then have the Paint procedure just draw those
affected lines. [...] It looks like its all (Invalidate) or
nothing.
You can use the win32 API InvalidateRect but then you don't
want to use the OnPaint event. you will need to handle WM_PAINT
directly which isn't that hard to do.
The sample is C++ because my Delphi is poor but you can
easily read it and convert it. Everything preceeded with
double colons is a win32 API call and Handle is the HWND
of the control that you're working with:
MESSAGE void __fastcall TMyPaintBox::WMPaint(TWMPaint &Message)
{
if( ::GetUpdateRect(Handle, NULL, false) )
{
PAINTSTRUCT ps = { 0 }; // defines and zeros structure
::BeginPaint( Handle, &ps );
TRect R = ps.rcPaint; // save a copy of the update rect
::ValidateRect( Handle, &ps.rcPaint ); // pass the address of ps.rcPaint
// add your custom drawing here
::EndPaint( Handle, &ps );
}
else inherited::Dispatch(&Message);
}
Quote
Should I even be using a TPaintBox for this?
You will still have the same issues with other controls because
you're doing custom drawing so you'd need to handle WM_PAINT
in any case.
~ JD
 

Re:PaintBox InvalidateRect

Did you disable background erase? If not, the background gets "erased"
(filled with white) anyway on each paint.
Nils
 

Re:PaintBox InvalidateRect

Upon further investigation, it appears that the line pb1.Update does not
cause a paint message to the PaintBox unless the whole Paintbox is
invalidated with pb1.Invalidate. Invalidating only part of the PaintBox
with either InvalidateRect or SelectClipRgn does not cause a paint message
with pb1.Update.
I tried using the OnPaint for the PaintBox and I also tried JD's suggestion
of handling the PaintBox's WM_PAINT message.
Quote
procedure TForm1.Button1Click(Sender: TObject);
var
rec: TRect;
region: hRGN;
begin
//rec := Rect(50,50,100,100);
//InvalidateRect(pb1.Canvas.Handle, @rec, true); //This doesn't
//seem to work.

region := CreateRectRgn(50,50,100,100);
SelectClipRgn(pb1.Canvas.Handle, region); //This doesn't //seem to work
either.

pb1.Update;
end;

procedure TForm1.pb1Paint(Sender: TObject);
begin
pb1.Canvas.Rectangle(pb1.Canvas.ClipRect); //Identify ClipRect
//location end;

end.
 

Re:PaintBox InvalidateRect

"Shawn Jones" <XXXX@XXXXX.COM>writes:
Quote

Upon further investigation,
How silly of me. TPaintBox descends from TGraphicControl which
means that it doesn't have a Window's Handle so you can't
(directly) use InvalidateRect with it.
To continue using the TPaintBox (and InvalidateRect), you'll
need to handle all of it is painting from the Parent of the
TPaintBox (just be sure to traverse far enough up the Parent
chain until you get to a control that descends from
TWinControl). It would be this control for which you would
want to catch WM_PAINT but the logic would be different.
You'll need to get the update rect and calculate any portion
of the TPaintBox that needs painting and then use a custom
message to send that Rect to the TPaintBox for painting. This
also means that when you calculate the Rect to invalidate, it
has to be relative to the Parent control and you would need to
use that control's Handle (Yuk).
Your other choice is to switch from using a TPaintBox to a
TWinControl descendent - like a TPanel so that you can use
InvalidateRect and the WM_PAINT handler that I posted.
~ JD
 

Re:PaintBox InvalidateRect

Thanks JD that was very helpful.
So it looks like TPaintBox has the nice high level Canvas but gives up a
windows handle (I just assumed Canvas.Handle was a windows handle) and with
a TPanel you gain a windows handle but give up Canvas. TForm looks to have
both.
I've also been looking at the Graphics32 library's TPaintBox32 which has
Flush(rect). Looks like they've also implemented a RepaintMode called
Optimized that may do alot of what I was going to do by hand. It includes a
bitmap as a back buffer just as you suggested in your original reply as
being more efficient.
JD writes:
Quote
"Shawn Jones" <XXXX@XXXXX.COM>writes:
>
>Upon further investigation,

How silly of me. TPaintBox descends from TGraphicControl which
means that it doesn't have a Window's Handle so you can't
(directly) use InvalidateRect with it.

To continue using the TPaintBox (and InvalidateRect), you'll
need to handle all of it is painting from the Parent of the
TPaintBox (just be sure to traverse far enough up the Parent
chain until you get to a control that descends from
TWinControl). It would be this control for which you would
want to catch WM_PAINT but the logic would be different.

You'll need to get the update rect and calculate any portion
of the TPaintBox that needs painting and then use a custom
message to send that Rect to the TPaintBox for painting. This
also means that when you calculate the Rect to invalidate, it
has to be relative to the Parent control and you would need to
use that control's Handle (Yuk).

Your other choice is to switch from using a TPaintBox to a
TWinControl descendent - like a TPanel so that you can use
InvalidateRect and the WM_PAINT handler that I posted.

~ JD
 

Re:PaintBox InvalidateRect

Quote
So it looks like TPaintBox has the nice high level Canvas but gives up a
windows handle (I just assumed Canvas.Handle was a windows handle) and with
a TPanel you gain a windows handle but give up Canvas. TForm looks to have
both.
You don't need a form or a 3rd party component:
you can also add a canvas (TConrtolCanvas) to a TWinControl descendant.
For example TCustomControl does this - so basically all you need to do
is descend from TCustomControl and you're done.
Since TPanel inherits from TCustomControl, it also has a Canvas,
it's just a protected property rather than public which means
you need to create a descendant that accesses the protected property.
Gerrit Beuze
ModelMaker Tools
 

Re:PaintBox InvalidateRect

"Shawn Jones" <XXXX@XXXXX.COM>writes:
Quote

Please trim your posts.
Quote
[...] a TPanel you gain a windows handle but give up Canvas.
As Gerrit noted, TPanel::Canvas is there. it is just protected
instead of published. Since you need to add a message handler
for WM_PAINT anyway, I'd suggest that you derive from
TPanel and the Canvas will be accessable from within the new
class.
Of course you could always use a 'cracker' class to expose
the Canvas. For example, add the the following declaration
in your unit:
TCrackedPanel = class(TPanel);
Then to use it:
with TCrackedPanel(Panel1).Canvas do
begin
MoveTo( 0, 0 );
Lineto( 10, 10 );
end;
Another option is to allocate a TControlCanvas and assign the
TPanel to the TControlCanvas::Control property.
Note that both the cracker class and the TControlCanvas options
would require you to subclass the TPanel's WndProc method:
var
OldWndProc: TWndMethod = nil;
function TForm1.Create(AOwner: TComponent);
begin
OldWndProc := Panel1.WindowProc;
Panel1.WindowProc := PanelWndProc;
end;
function TForm1.PanelWndProc(var Message: TMessage);
var
R : TRect;
PS : PAINTSTRUCT;
begin
if Message.Msg = WM_PAINT then
begin
if GetUpdateRect(Panel1.Handle, nil, false) then
begin
BeginPaint( Panel1.Handle, PS );
R := PS.rcPaint;
ValidateRect( Panel1.Handle, PS.rcPaint );
// add your custom drawing here using R
::EndPaint( Panel1.Handle, PS );
end else OldWndProc( Message );
end else OldWndProc( Message );
end;
Quote
TForm looks to have both.
That's true but it would complicate the painting exponetially
because the WM_PAINT would be sent for everything - not just
your drawing.
~ JD
 

Re:PaintBox InvalidateRect

I'm getting the behavior I wanted handling WndProc on a TPanel and using
JD's WM_PAINT handler. I can call InvalidateRect with a sub rectangle of
the clientRect and the paint handler is recognizing it property.
Then I created a PanelCanvas component that derives from TPanel and exposes
Canvas as follows:
unit PanelCanvas;
interface
uses
SysUtils, Classes, Controls, ExtCtrls;
type
TPanelCanvas = class(TPanel)
private
protected
public
property Canvas;
published
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('My Components', [TPanelCanvas]);
end;
end.
However using this component along with handling WM_PAINT via WndProc the
paint message again is not called whenever I call InvaidateRect - ugh.
 

Re:PaintBox InvalidateRect

Quote
However using this component along with handling WM_PAINT via WndProc the
paint message again is not called whenever I call InvaidateRect - ugh.
Just to make sure it is the right handle: which handle are you using when
calling InvalidateRect?
--
Jens Gruschel
www.pegtop.net
 

Re:PaintBox InvalidateRect

Jens Gruschel writes:
Quote
>However using this component along with handling WM_PAINT via
>WndProc the paint message again is not called whenever I call
>InvaidateRect - ugh.

Just to make sure it is the right handle: which handle are you using
when calling InvalidateRect?
Thanks Jens,
That was the problem. I was using:
InvalidateRect(PanelCanvas1.Canvas.Handle,@rec,true);//Doesn't work.
instead of:
InvalidateRect(PanelCanvas1.Handle,@rec,true);//Does work.
Any suggestions on how to setup the PanelCanvas component to expose an
OnPaint event instead of having the client override WindowProc?
 

Re:PaintBox InvalidateRect

"Shawn Jones" <XXXX@XXXXX.COM>writes:
Quote

InvalidateRect(PanelCanvas1.Handle,@rec,true);
Note that by using true as the final parameter, you're causing
the background to be erased before you do you custom drawing.
Do you really want to double the overhead?
Quote
Any suggestions on how to setup the PanelCanvas component to
expose an OnPaint event instead of having the client
override WindowProc?
To be clear, it is not the 'client' or even nessaseraly the
Owner or the Parent and it is subclassing the WndProc method
not overriding it as the sample below does. It may sound like
nit-picking but it is an important distinction because they
require 2 seperate methods to implement.
Since you've derived from TPanel, you also have the option of
using a message handler *instead* of overriding the WndProc
method (sorry, no sample because I don't do delphi well
enough). The 2 differences are that a message handler is
interested in only one specific message while the WndProc gets
them all and you have to manually filter for the message that
you want. The other difference is that a message handler sees
the message earlier in the process of processing the message
than when the WndProc sees it.
Add the following to your PanelCanvas unit. Note that since
you'll be doing all of the drawing from with the unit, you no
longer need to promote the Canvas from protected to public
because all protected members and methods will be available
in the unit. You may want to change it is name now.
protected
Procedure WndProc(var Message: TMessage); override;
procedure TPanelCanvas.WndProc(var Message: TMessage);
var
R : TRect;
PS : PAINTSTRUCT;
begin
if Message.Msg = WM_PAINT then
begin
if GetUpdateRect( Handle, nil, false ) then
begin
BeginPaint( Handle, PS );
R := PS.rcPaint;
ValidateRect( Handle, PS.rcPaint );
// add your custom drawing here using R
EndPaint( Handle, PS );
end else inherited;
end else inherited;
end;
~ JD
 

Re:PaintBox InvalidateRect

Thanks JD,
I'm still getting my Delphi legs under me so I may not have been clear in
what I was asking.
My goal was to make a standalone component that a user (client) could drag
from the tools pallet and place on a Form then from the Events tab double
click beside OnPaint and have OnPaint handler code shelled out in the editor
where the user could key in specific paint code.
Since TPanel doesn't include this, I was looking for help in coding this
into my PanelCanvas component.
 

Re:PaintBox InvalidateRect

"Shawn Jones" <XXXX@XXXXX.COM>writes:
Quote

My goal was to make a standalone component that a user
(client) could drag from the tools pallet and place on a
Form then from the Events tab double click beside OnPaint
and have OnPaint handler code shelled out in the editor
where the user could key in specific paint code.
I know how to do it with CBuilder but not with Delphi so
I searched the archives and found some delphi code that I
adapted so if it doesn't work, you will need to post to
components.writing.general or the components.writing.win32
group after you have nailed down your design.
Quote
Since TPanel doesn't include this, I was looking for help in
coding this into my PanelCanvas component.
Adding an event is simple enough but it is unclear to me
*exactly* what you want to accomplish. For example, what
information do you want to include in the parameter?
If the only parameter is the Sender (TObject), then a simple
TNotifyEvent will do:
private
FOnPaint: TNotifyEvent;
published
property OnPaint: TNotifyEvent read FOnPaint write FOnPaint;
However, I suspect that you want to do something with the
TRect that you gathered when you captured WM_PAINT and in
that case, you need to define the event first:
TPaintEvent = procedure (Sender : TObject; ARect : TRect ) of Object;
private
FOnPaint : TPaintEvent;
published
property OnPaint: TPaintEvent read FOnPaint write FOnPaint;
The next question is when to call the event. If you will be
passing in that TRect from WM_PAINT, then you have no choice
but to override the WndProc method (or use a message handler)
and call it from there:
protected
Procedure WndProc(var Message: TMessage); override;
procedure TPanelCanvas.WndProc(var Message: TMessage);
var
R : TRect;
PS : PAINTSTRUCT;
begin
if Message.Msg = WM_PAINT then
begin
if GetUpdateRect( Handle, nil, false ) then
begin
BeginPaint( Handle, PS );
R := PS.rcPaint;
ValidateRect( Handle, PS.rcPaint );
if Assigned( FOnPaint ) then FOnPaint( Self, R );
else DoPaint( R ); // DoPaint not shown
EndPaint( Handle, PS );
end else inherited;
end else inherited;
end;
If you're just going to use a TNotifyEvent, then I would
suggest that you override the protected PaintWindow method:
protected
Procedure PaintWindow(var DC: HDC); override;
procedure TPanelCanvas.PaintWindow(var DC: HDC);
begin
if Assigned( FOnPaint ) then FOnPaint( Self );
else inherited;
end;
~ JD
 

Re:PaintBox InvalidateRect

Thanks JD, that hit the spot :).
Just a note about the call to ValidateRect. According to the ms docs:
"The BeginPaint function automatically validates the entire client area.
Neither the ValidateRect nor ValidateRgn function should be called if a
portion of the update region must be validated before the next WM_PAINT
message is generated.
The system continues to generate WM_PAINT messages until the current update
region is validated. "
So is the call to ValidateRect necessary?
Thanks very much for your help.
shawn
JD writes:
Quote
TPaintEvent = procedure (Sender : TObject; ARect : TRect ) of
Object;

private
FOnPaint : TPaintEvent;
published
property OnPaint: TPaintEvent read FOnPaint write FOnPaint;

The next question is when to call the event. If you will be
passing in that TRect from WM_PAINT, then you have no choice
but to override the WndProc method (or use a message handler)
and call it from there:

protected
Procedure WndProc(var Message: TMessage); override;

procedure TPanelCanvas.WndProc(var Message: TMessage);
var
R : TRect;
PS : PAINTSTRUCT;
begin
if Message.Msg = WM_PAINT then
begin
if GetUpdateRect( Handle, nil, false ) then
begin
BeginPaint( Handle, PS );
R := PS.rcPaint;
ValidateRect( Handle, PS.rcPaint );
if Assigned( FOnPaint ) then FOnPaint( Self, R );
else DoPaint( R ); // DoPaint not shown
EndPaint( Handle, PS );
end else inherited;
end else inherited;
end;