Jump to content
Mike Torrettinni

Is interposer class really best to customize TPanel.Paint?

Recommended Posts

I have interposer class that draws custom border around TPanel. But I only want this for selected panel on the form, not all.

So, I create a list of panels to be customized in xCustomizedPanels, on FormCreate.

 

A very simple implementation:

  TPanel = class(Vcl.ExtCtrls.TPanel)
  protected
    procedure Paint; override;
  end;
  
var xCustomizedPanels: TArray<TPanel>;  
  
procedure TPanel.Paint;
var
  vRect: TRect;
  vPanel: TPanel;
begin
  for vPanel in xCustomizedPanels do
  if vPanel = Self then
  begin
    vRect := GetClientRect;

    // Frame panel
    Canvas.Brush.Color := clSilver;
    Canvas.FrameRect(vRect);
  end;
end;

 

But! Isn't there a better solution so I assign customized Paint/OnPaint method only to selected panels, something like this:

 

procedure TfrmMainForm.FormCreate(Sender: TObject);
begin
  // assign customization 
  Panel1.OnPaint/Paint := CustomPaint;
  Panel2.OnPaint/Paint := CustomPaint;
  ...
end;

Thanks for any suggestion!

Share this post


Link to post

I usually add a Boolean value to the interposer and set it to True when the form is created, only for those components with the specialised behaviour.

  • Like 1

Share this post


Link to post
3 minutes ago, Dave Nottage said:

only for those components with the specialised behaviour.

How do you do that, how do you limit which controls the interposer applies to? Like I did, with a list of controls or similar?

Share this post


Link to post
2 minutes ago, Mike Torrettinni said:

How do you do that

 

9 minutes ago, Dave Nottage said:

I usually add a Boolean value to the interposer

For your case:

TPanel = class(Vcl.ExtCtrls.TPanel)
private
  FSpecialised: Boolean;
protected
  procedure Paint; override;
  property Specialised: Boolean read FSpecialised write FSpecialised;
end;

When the form is created, set the Specialised property to True for the TPanels where they need the special treatment. In the Paint method, check the FSpecialised flag.

Share this post


Link to post
Just now, Dave Nottage said:

 

For your case:


TPanel = class(Vcl.ExtCtrls.TPanel)
private
  FSpecialised: Boolean;
protected
  procedure Paint; override;
  property Specialised: Boolean read FSpecialised write FSpecialised;
end;

When the form is created, set the Specialised property to True for the TPanels where they need the special treatment. In the Paint method, check the FSpecialised flag.

This looks good, the boolean flag. But this applies to all panels on the form, right? Do you always do that, or you have a way to limit to which panel this applies to?

Share this post


Link to post

Instead of an interposer class, I would simply create a new component inheriting from TPanel. The component code is what @DaveNottage shown except for the class name. Then this component is added to a package (Actually two: a run time and a design time package) so that it can be installed in the IDE and available where you need: you drop your specialized component on the form/frame instead of the standard panel.

 

If you don't know how to create your component, there are a lot of tutorials on the Internet showing how to create your own TMyButton. It's the same process with a panel Google is your friend.

Share this post


Link to post
4 minutes ago, FPiette said:

I would simply create a new component inheriting from TPanel.

Aha, I see now. I kind am trying to avoid creating components, I would really like to not do that, if possible.

Share this post


Link to post
55 minutes ago, Mike Torrettinni said:

But this applies to all panels on the form, right?

The property applies to all of them, yes.. but you set it to True only for the ones you want the specialised behaviour on.

Share this post


Link to post

Another option is to use a layered window to add the additional painting. This is very flexible, not limited to panels, doesn't require any changes to the implementation of the controls. 

Share this post


Link to post
1 hour ago, FPiette said:

Instead of an interposer class, I would simply create a new component inheriting from TPanel.

This means all the visual component mess: separate package, installation, registration, replacing existing panels, bundling it with an app etc.

4 hours ago, Mike Torrettinni said:

Isn't there a better solution

  TPanel = class(Vcl.ExtCtrls.TPanel)
  protected
    procedure Paint; override;
  public
    OnCustomPaint: TNotifyEvent;
  end;
  
procedure TPanel.Paint;
begin
  if Assigned(OnCustomPaint) then
    OnCustomPaint(Self)
  else
    inherited;
end;

I would do something alike

  • Like 2
  • Thanks 1

Share this post


Link to post
3 hours ago, Fr0sT.Brutal said:

This means all the visual component mess: separate package, installation, registration, replacing existing panels, bundling it with an app etc.


  TPanel = class(Vcl.ExtCtrls.TPanel)
  protected
    procedure Paint; override;
  public
    OnCustomPaint: TNotifyEvent;
  end;
  
procedure TPanel.Paint;
begin
  if Assigned(OnCustomPaint) then
    OnCustomPaint(Self)
  else
    inherited;
end;

I would do something alike

Great!

 

It works:

 

TPanel = class(Vcl.ExtCtrls.TPanel)
  protected
    procedure Paint; override;
  public
  var
    OnCustomPaint: TNotifyEvent;
    procedure CustomPaint(Sender: TObject); // Custom Paint
  end;
  
procedure TPanel.Paint;
begin
  if Assigned(OnCustomPaint) then
    OnCustomPaint(Self)
  else
    inherited;
end;

procedure TPanel.CustomPaint(Sender: TObject);
var
  vRect: TRect;
  vPanel: TPanel;
begin
  vRect := GetClientRect;
  
    // Frame panel
  Canvas.Brush.Color := clSilver;
  Canvas.FrameRect(vRect);
end;

procedure TfrmMainForm.Initialize;
begin
  Panel1.OnCustomPaint := Panel1.CustomPaint;
  Panel2.OnCustomPaint := Panel2.CustomPaint;
end;  

 

 

Share this post


Link to post

And now I have CustomPaint in its own unit, and I just use it on the Form/Frame when needed, and assign only panels that I need customized.

It's a little thing, but I used to have double panels behind Edit, to make Edit look like vertical centered with grayish border (not black). Then I started used TAdvPanel, which has a single colored border, but it flickers a lot more than double TPanels.

 

So, now I have this simple implementation. This just made my day @Fr0sT.Brutal 🙂

 

This is the wanted end result:

image.thumb.png.2250dd713382089eb6412c9fa657ca5b.png

Edited by Mike Torrettinni

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

It works:

Well, it's somewhat an overhead.

My solution was to assign an event handler like with any component:

Panel1.OnPaint := Form1.Panel1Paint

Thus you can have special paint for every of your panels

 

Alternatively, if you prefer storing methods in the object itself, you can just use a flag and add the method:

TPanel.Paint

begin

  if DoCustomPaint then CustomPaint;

end;

 

Panel1.CustomPaint := True

 

but then all panels will share the same painting method

 

Share this post


Link to post
1 minute ago, Fr0sT.Brutal said:

My solution was to assign an event handler like with any component:

Panel1.OnPaint := Form1.Panel1Paint

But then it's not designed for any form, but just on Form1, right?  I need this to be used at any form I need.

 

5 minutes ago, Fr0sT.Brutal said:

Alternatively, if you prefer storing methods in the object itself, you can just use a flag and add the method:

TPanel.Paint

begin

  if DoCustomPaint then CustomPaint;

end;

 

Panel1.CustomPaint := True

OK, I like this, I will just name it Pane1.CustomBorder := True; or similar.

 

This is still in finalizing screens the new project, so it will be changed, improved many times. I can be very picky with my UI design.

Share this post


Link to post
2 hours ago, Mike Torrettinni said:

But then it's not designed for any form, but just on Form1, right?  I need this to be used at any form I need.

It's just a common practice. You can easily define a standalone class with such method if you want. But overriding and inheriting is more powerful solution. Everything depends on your needs

Share this post


Link to post
14 minutes ago, Fr0sT.Brutal said:

It's just a common practice. You can easily define a standalone class with such method if you want. But overriding and inheriting is more powerful solution. Everything depends on your needs

I can agree to that, since I use interposer heavily.

Some years ago I have heard that some people have the philosophy to wrap everything into interposers.

 

I'm not going that far yet, but close.

I can understand well to have a "safety" layer between the framework and your app.

That serves a lot of useful purposes: 

  • to easily manage bugfixes at a centralized place for each component
  • to make workarounds around some conceptual limitations of some components
  • to enhance the components with new, custom features
  • they don't interfear with normal designer process, so no need to install custom components

That are enough reasons for me to consider interposers everywhere 🙂

 

 

 

Edited by Rollo62
  • Like 1

Share this post


Link to post
10 hours ago, FPiette said:

Then this component is added to a package (Actually two: a run time and a design time package) so that it can be installed in the IDE and available where you need: you drop your specialized component on the form/frame instead of the standard panel.

If the component does not have extra design-time functionality that directly interacts with IDE APIs (property/component editors, OpenTools plugin, etc), then you do not need to make a separate design-time package.  You can create 1 package and mark it as being BOTH a runtime package AND a design-time package.  That will work just fine.

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
1 hour ago, Remy Lebeau said:

You can create 1 package and mark it as being BOTH a runtime package AND a design-time package.  That will work just fine.

...or just mark it as a design-time package. That will work equally fine.

Share this post


Link to post
3 hours ago, Anders Melander said:

...or just mark it as a design-time package. That will work equally fine.

Um no, because designtime-only packages are not used in runtime executables.  The component needs to be in a runtime package, which in this particular example can ALSO be used as a design-time package.

Share this post


Link to post
14 hours ago, David Heffernan said:

Another option is to use a layered window to add the additional painting. This is very flexible, not limited to panels, doesn't require any changes to the implementation of the controls. 

If you make the layered window be a child of the panel, then that will only work on Windows 8+.  On earlier Windows, you would have to make the layered window be a top-level overlay window that follows the panel onscreen.

  • Like 1

Share this post


Link to post
1 minute ago, Remy Lebeau said:

Um no, because designtime-only packages are not used in runtime executables.  The component needs to be in a runtime package, which in this particular example can ALSO be used as a design-time package.

Um yes.

As you know we've had this discussion before. I can accept that in your opinion it is best to have both design- and run-time packages (or both in one), but it's not a requirement and never have been.

Share this post


Link to post
1 hour ago, Anders Melander said:

As you know we've had this discussion before. I can accept that in your opinion it is best to have both design- and run-time packages (or both in one), but it's not a requirement and never have been.

http://docwiki.embarcadero.com/RADStudio/Sydney/en/Package-specific_Compiler_Directives

Quote

Packages compiled with the {$DESIGNONLY ON} directive should not ordinarily be used in applications, since they contain extra code required by the IDE.

https://www.oreilly.com/library/view/delphi-in-a/1565926595/re414.html

Quote

Use the $DesignOnly directive in a package’s .dpk file to mark the package as being a design-time package. You cannot link the package with an application or library. You can only install the package in the IDE of Delphi or C++ Builder.

 

Share this post


Link to post
21 hours ago, Fr0sT.Brutal said:

This means all the visual component mess: separate package, installation, registration, replacing existing panels, bundling it with an app etc.

I really don't agree with this sentence!

 

Creating a separate package helps having clean code which is reused easily.

Registration is just one procedure call in the design time package. You write it one and forget it.

You don't replace an existing panel: you use your specialized one when needed instead of the standard. You have both the standard panel and your specialized available in the component toolbar. You can even use both in the same form without worrying.

There is no bundling with app required. Simply don't add that package in the run time package list. The component code will automatically bundled in the application. Nothing to deploy.

 

Building component is the best solution. You do it once and them you use it.

 

If you have an existing application and want to use your specialized panel, it is easy to edit the .dfm and .pas files using an external editor (NotePad or NotePad++) to do a simple search and replace to change TPanel by TMyPanel where is is needed. It is always easier to do that edit than opening the form, deleting the standard panel, dropping the specialized panel and reassigning properties and events!

 

Share this post


Link to post
7 hours ago, Remy Lebeau said:

make the layered window be a top-level overlay window that follows the panel onscreen.

Yes, that's what I envisage

Share this post


Link to post
4 hours ago, FPiette said:

If you have an existing application and want to use your specialized panel, it is easy to edit the .dfm and .pas files using an external editor (NotePad or NotePad++) to do a simple search and replace to change TPanel by TMyPanel where is is needed. It is always easier to do that edit than opening the form, deleting the standard panel, dropping the specialized panel and reassigning properties and events!

If you use interposers then you don't need all that. You just declare the interposer and use the unit where it's declared (or declare it in the unit where you use it). That's the point of it: You don't need to change the existing forms and often not even the code.

When I use interposers to fix bugs in existing components (mostly VCL), or alter their behavior, I place the interposer in a separate unit and use that. When I need to alter a few controls on a single form I just declare the interposer in that forms unit. Problem solved, next case.

 

I'm not saying one shouldn't use proper design-time registered components, I still consider interposers somewhat of a hack, but given the hassle of maintaining design-time packages across multiple projects and versions, interposers are a blessing.

 

In order to remove the need for interposer classes Delphi would need:

  • A package management system that allowed us the define per project design-time packages.
    The current project package configuration doesn't work. It still operate on a global package list.
  • A mechanism to redirect the class resolver when forms are streamed.
    Since it already uses a global class registry when streaming a form it shouldn't be that hard to implement.

I do wish the VCL had an official mechanism to solve the same problems that interposer does - but it doesn't.

  • Like 3

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×