Aufklappbare Eigenschaft im Objektinspektor

Rund um die LCL und andere Komponenten
Antworten
siro
Beiträge: 730
Registriert: Di 23. Aug 2016, 14:25
OS, Lazarus, FPC: Windows 11
CPU-Target: 64Bit
Wohnort: Berlin

Aufklappbare Eigenschaft im Objektinspektor

Beitrag von siro »

Hallo zusammen,
ich hab mal wieder ein "kleines" Problemchen, was mehr und mehr zu einem Großen wird.

Eigentlich nur eine Schönheitskorrektur 8) für meiner Komponente.
Ich möchte lediglich 4 Werte einstellen, für 4 Abstände um einen Text herum.
Das funktioniert auch alles schon, sieht aber unschön aus im Objektinspektor
und so dachte ich mir, ich könnte das so machen, wie bei der Eigenschaft "Constraints":
Pfeil aufklappen und da sind die 4 Werte einzustellen. So, die Idee. :roll:

Wie man das macht, keine Ahnung, also habe ich herumprobiert. Man lernt ja nie aus.
Mit jedem neuen Ergebnis kamen leider immer neuere Probleme.
Vermutlich bin ich hier völlig auf dem Holzweg:

Um es zu verdeutlichen, habe ich mal eine komplett abgespekte Komponente dafür gebaut,
um lediglich die Funktionalität zu prüfen.

So habe ich mir eine Komponente TTextSpace erstellt und in die Komponente mit aufgenommen.
Bei create meiner Komponente rufe ich dann
FTextSpace := TTextSpace.create(self) auf.
Somit wird sie ja auch automatisch zerstört bei Komponente.free.

Im Prinzip scheint das "vorerst" zu laufen, aber:
merkwürdigerweise nur so lange ich noch in der Designphase bin.
Beim Starten des Programms geht es nicht mehr, bzw. hat die Komponente anscheinend die Werte vergessen...
Zudem erscheinen im Objektinspektor die Eigenschaften "Name" und "Tag" die ich garnicht haben möchte.

Ich habe mal die minimalistische Komponente angefügt:

Code: Alles auswählen

{ Siro 25.04.2017 }
{ lediglich zum prüfen der Funktionalität von TextSpace }
{ im Objektinspektor TextSpace.Left einstellen, das geht }
{ wenn das Programm aber gestartet wurde, geht es nicht mehr }
 
unit LAZ_TestLabel;
 
interface
 
uses
  LCLIntf,
  Classes,
  Graphics,
  Controls,   { TControl }
  StdCtrls;   { TCustomLabel }
 
Type TTextSpace = class(TComponent)
  private
    FLeft   : Integer;
    FRight  : Integer;
    FTop    : Integer;
    FBottom : Integer;
  protected
    procedure SetLeftSpace  (space:Integer);
    procedure SetTopSpace   (space:Integer);
    procedure SetRightSpace (space:Integer);
    procedure SetBottomSpace(space:Integer);
  public
    constructor create(aOwner:TComponent); override;
  published
    property Left   : Integer read FLeft   write SetLeftSpace;
    property Right  : Integer read FRight  write SetRightSpace;
    property Top    : Integer read FTop    write SetTopSpace;
    property Bottom : Integer read FBottom write SetBottomSpace;
end;
 
Type TTestLabel = class(TCustomLabel)
  private
    FTextSpace : TTextSpace;  // das ist der neue Knackpunkt :-)
  public
    constructor create(aOwner:TComponent); override;
    procedure paint; override;
  published
    property Caption;
    property AutoSize;
    property TextSpace : TTextSpace read FTextSpace write FTextSpace;
end;
 
 
procedure register;
 
Implementation
 
constructor TTextSpace.create(aOwner:TComponent);
begin
  inherited;
  // rundrum sollen 2 Pixel Platz bleiben
  (*
  Fleft   :=2;
  FTop    :=2;
  FRight  :=2;
  FBottom :=2;
  *)

end;
 
 
procedure TTextSpace.SetLeftSpace(space:Integer);
begin
  if Left = space then exit;
  FLeft:=space;
  // ohne die Abfrage Assigned gibts Chaos.....
  if Assigned(owner) then TControl(Owner).invalidate;
end;
 
procedure TTextSpace.SetTopSpace(space:Integer);
begin
  if Top = space then exit;
  FTop:=space;
  if Assigned(owner) then TControl(Owner).invalidate;
end;
 
procedure TTextSpace.SetRightSpace(space:Integer);
begin
  if Right = space then exit;
  FRight:=space;
  if Assigned(owner) then TControl(Owner).invalidate;
end;
 
procedure TTextSpace.SetBottomSpace(space:Integer);
begin
  if Bottom = space then exit;
  FBottom:=space;
  if Assigned(owner) then TControl(Owner).invalidate;
end;                           
 
constructor TTestLabel.create(aOwner:TComponent);
begin
  inherited;
  AutoSize:=FALSE// lediglich zum Testen
 
  // die TextSpace Komponente erstellen
  FTextSpace:=TTextSpace.create(self);
 
end;
 
procedure TTestLabel.paint;
begin
  // lediglich einen schwarzen Rahmen aussen
  canvas.brush.color:=clBlack;    // FrameRect wird immer mit brush gezeichnet
  canvas.FrameRect(ClientRect);
 
  canvas.brush.color:=clWhite;   // Texthintergrund in weiss
 
  // und den Text ausgeben
  canvas.TextOut(FTextSpace.left,    // Textposition X
                 FTextSpace.Top,     // Textposition Y
                 caption);
end;
 
{******************************************************************************}
{ alle Komponenten registrieren }
 
procedure Register;
begin
//  RegisterComponents('LAZ_Siro', [TTextSpace]); ich dachte ich muss es evtl. registrieren..
  RegisterComponents('LAZ_Siro', [TTestLabel]);
end;
 
Initialization
 
Finalization
 
end.
 

Warum geht es nicht mehr zur Laufzeit ?

Jetzt sehe ich grade, die Daten der TTextSpace Komponente landen garnicht in der .lfm Datei
dann kann das ja auch nicht funktionieren. Hmmmm

--------------
Nächster Versuch: Ich habe mir dann mal die Komponente "Constraints" angesehen,
die ist abgeleitet von TPersistent, das habe ich dann auch mal probiert.
Vorteil: Die Komponenteneigenschaften "Name" und Tag sind jetzt verschwunden. Supi genau das wollte ich ja :D
Aber es gibt gar keinen Constructor den ich überschreiben könnte. :(
Habe die Komponente dann trotzdem mittels create erzeugt, das ging wohl auch....
Nun wollte ich aber meine Hauptkomponente auch informieren, wenn sich einer der 4 Werte ändert.
Da kommt das nächste Problem, wo ist der Besitzer "owner" habe es dann mit GetOwner von TPersistent und Invalidate probiert.
Hier gibt es dann Zugriffsveletzungen.

Nun bin ich an dem Punkt, doch mal nachzufragen :P

Eigentlich brauche ich doch "NUR" eine Record mit 4 Integern die ich im Objektinspektor einstelle
und dann soll natürlich Invalidate ausgeführt werden, damit die Komponente auch neu gezeichnet wird.

Könnt Ihr mich auf den "richtigen" Weg bringen ?
Siro
Grüße von Siro
Bevor ich "C" ertragen muß, nehm ich lieber Lazarus...

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: Aufklappbare Eigenschaft im Objektinspektor

Beitrag von wp_xyz »

Nein, einen Record kannst du nicht nehmen, der wird vom Streaming-Mechanismus nicht unterstützt. TPersistent ist völlig richtig.

TPersistent hat keinen eigenen Constructor, aber natürlich den von TObject geerbten, und der ist noch nicht virtuell. Das hat den Vorteil, dass du dir die Parameterübergabe zusammenstricken kannst, wie du sie brauchst. Ich würde den Constructor von TTextSpace als "Owner" das zugehörige TTextLabel übergeben. Dann kannst du dir in TTextSpace eine Methode "Changed" schreiben, die das Invalidate des Labels aufruft. Changed wird immer aufgerufen, wenn sicher einer der "Spaces" ändert.

Da dieser "Owner" anders zu verstehen ist als der übliche von TComponent verwendete, funktioniert das automatische Freigeben der Klasse nicht. Du musst also im Destruktor des TTextLabels den TTextSpace explizit selbst freigeben.

Ein Trick noch: die vier Properties LeftSpace, TopSpace, RightSpace, BottomSpace benötigen jeweils einen eigenen Setter. Das kann man umgehen, indem sie in einem Array gespeichert werden, und man dem Setter den Array-Index mitgibt. Allerdings muss man auch einen Getter mit dem Index schreiben, was die Ersparnis wieder auffrisst. Aber trotzdem finde ich das sauberer, weil der Setter, in dem ja vielleicht noch einiges passiert, nur einmal vorhanden ist so dass man nichts mehr vergessen kann.

Was ich so an TPersistent-Abkömmlingen gesehen habe, ist mir aufgefallen, dass sie auch die Methode Assign überschreiben. Ich weiß nicht, ob das unbedingt für das Streaming nötig ist, aber es kann auf jeden Fall nicht schaden, wenn die neue Klasse komplett implementiert ist.

Und das wär's auch schon...

Code: Alles auswählen

 
const
  DEFAULT_TEXT_SPACE = 4;
 
type
  TTextSpace = class(TPersistent)
  private
    FOwner: TTextLabel;
    FMargins: array[0..3] of Integer;
    function GetSpace(AIndex: Integer): Integer;
    procedure SetSpace(AIndex: Integer; AValue: Integer);
  protected
    procedure Changed;
  public
    constructor Create(AOwner: TTextLabel);
    procedure Assign(ASource: TPersistent); override;
  published
    property LeftSpace: Integer index 0 read GetSpace write SetSpace default DEFAULT_TEXT_SPACE;
    property TopSpace: Integer index 1 read GetSpace wirte SetSpace default DEFAULT_TEXT_SPACE;
    property RightSpace: Integer index 2 read GetSpace write SetSpace default DEFAULT_TEXT_SPACE;
    property BottomSpace: Integer index 3 read GetSpace write SetSpace default DEFAULT_TEXT_SPACE;
  end;
 
  ...
constructor TTextSpace.Create(AOwner: TTextLabel);
var
  i: Integer;
begin
  inherited;
  FOwner := AOwner;
  for i:=0 to 3 do FMargins[i] := DEFAULT_TEXT_SPACE;
end;
 
procedure TTextSpace.Assign(ASource: TPersistent);
var
  i: Integer;
begin
  if ASource is TTextSpace then
    for i:=0 to 3 do FMargins[0] := ASource.FMargins[i];
  inherited;
end;
 
procedure TTextSpace.Change;
begin
  FOwner.Invalidate;
begin
 
function TTextSpace.GetSpace(AIndex: Integer): integer;
begin
  Result := FMargins[AIndex];
end;
 
procedure TTextSpace.SetSpace(AIndex: Integer; AValue: Integer);
begin
  if AValue = FMargins[AIndex] then
    exit;
  FMargins[AIndex] := AValue;
  Change;  // besser als direkter Aufruf von Invalidate - man weiß nie, was noch dazukommt...
end;
...
constructor TTextLabel.Create(AOwner: TComponent);
begin
  inherited;
  FTextSpace := TTextSpace.Create(self);
end;
 
destructor TTextlabel.Destroy;
begin
  FTextSpace.Free;
  inherited;
end;
 
procedure TTextLabel.Assign(ASource: TPersistent);
begin
  if ASource is TTextlabel then begin
    FTextSpace.Assign(ASource.TextSpace);
    // andere in TTextLabel eingeführte Properties
  end;
  inherited;
end;
 

siro
Beiträge: 730
Registriert: Di 23. Aug 2016, 14:25
OS, Lazarus, FPC: Windows 11
CPU-Target: 64Bit
Wohnort: Berlin

Re: Aufklappbare Eigenschaft im Objektinspektor

Beitrag von siro »

Guten Morgen wp_xyz

Das ist ja mal wieder SUPER Info. Hab vielen Dank dafür.

Ich habe die erste Variante grade am laufen und experimentiere noch daran rum.
Erstmal ohne Assign und mit 4 separaten Gettern und Settern.
In der Formulardatei erscheinen jetzt auch dei gesetzten Werte vonn TTextSpace.
Sieht alles absolut okay aus.

Habe grad mal einen Setter entfernt und es läuft trotzdem noch richtig
dann auch noch den Getter, auch das läuft noch.

So habe ich alle Setter und Getter entfernt und siehe da, es funktioniert immer noch richtig.
Die Werte werden auch ordnungsgemäß in der .lfm Datei gespeichert.

Die Setter benötige ich aber sowieso, weil ich dort ja "change" vom Owner aufrufe.

Dann lag ich ja garnicht so verkehrt mit der Ableitung von TPersistent, nur
mit dem Construktor war mir rätselhaft. Sowie den Owner in der TTextSpace Komponente zu speichern.

Die Idee mit dem Array für die Werte macht das Vorgehen natürlich noch etwas eleganter,
das werde ich gleich noch ausprobieren.

Hier mal der momentane Code mit ausgeklammerten Settern und Gettern.
scheint einwandfrei zu laufen.

Code: Alles auswählen

 
{ Siro 26.04.2017 }
{ Änderungen nach wp_xyz }
{ Getter und Setter testweise ausgeklammert }
{ funktioniert trotzdem scheinbar einwandfrei }
 
unit LAZ_TestLabel;
 
interface
 
uses
  LCLIntf,
  Classes,
  Graphics,
  Controls,   { TControl }
  StdCtrls;   { TCustomLabel }
 
Type TTextSpace = class(TPersistent)
  private
    FOwner  : TComponent;
    FLeft   : Integer;
    FRight  : Integer;
    FTop    : Integer;
    FBottom : Integer;
  protected
//    procedure SetLeftSpace  (space:Integer);
//    procedure SetTopSpace   (space:Integer);
//    procedure SetRightSpace (space:Integer);
//    procedure SetBottomSpace(space:Integer);
//    function  GetLeftSpace   : Integer;
//    function  GetTopSpace    : Integer;
//    function  GetRightSpace  : Integer;
//    function  GetBottomSpace : Integer;
    procedure Change;
  public
    constructor create(aOwner:TComponent);
  published
    property Left   : Integer read FLeft   write FLeft;   //read GetLeftSpace   write SetLeftSpace;
    property Right  : Integer read FRight  write FRight;  // read GetRightSpace write SetRightSpace;
    property Top    : Integer read FTop    write FTop;    // read GetTopSpace   write SetTopSpace;
    property Bottom : Integer read FBottom write FBottom; //read GetBottomSpace write SetBottomSpace;
end;
 
Type TTestLabel = class(TCustomLabel)
  private
    FTextSpace : TTextSpace;
  public
    constructor create(aOwner:TComponent); override;
    destructor destroy; override;
    procedure paint; override;
  published
    property Caption;
    property AutoSize;
    property TextSpace : TTextSpace read FTextSpace write FTextSpace;
end;
 
 
procedure register;
 
Implementation
 
constructor TTextSpace.create(aOwner:TComponent);
begin
  inherited create;   // das müsste dann TPersistent create sein
  FOwner:=aOwner;     // den Besitzer merken
end;
 
procedure TTextSpace.Change;
begin
  if Assigned(fOwner) then TTestLabel(fOwner).Invalidate;
end;
(*
procedure TTextSpace.SetLeftSpace(space:Integer);
begin
  if Left = space then exit;
  FLeft:=space;
  Change;
end;
 
procedure TTextSpace.SetTopSpace(space:Integer);
begin
  if Top = space then exit;
  FTop:=space;
  Change;
end;
 
procedure TTextSpace.SetRightSpace(space:Integer);
begin
  if Right = space then exit;
  FRight:=space;
  Change;
end;
 
procedure TTextSpace.SetBottomSpace(space:Integer);
begin
  if Bottom = space then exit;
  FBottom:=space;
  Change;
end;
 
function  TTextSpace.GetLeftSpace   : Integer;
begin
  result:=fLeft;
end;
 
function  TTextSpace.GetTopSpace    : Integer;
begin
  result:=fTop;
end;
 
function  TTextSpace.GetRightSpace  : Integer;
begin
  result:=fRight;
end;
 
function  TTextSpace.GetBottomSpace : Integer;
begin
  result:=fBottom;
end;
*)

{---------- Implemenatation TTestLabel -------------------}
 
constructor TTestLabel.create(aOwner:TComponent);
begin
  inherited;
  AutoSize:=FALSE// lediglich zum Testen
 
  // die TextSpace Komponente erstellen
  FTextSpace:=TTextSpace.create(self);
end;
 
destructor TTestLabel.Destroy;
begin
  // die TTextSpace Komponente wieder entfernen
  if Assigned(fTextSpace) then fTextSpace.free;
  inherited;
end;
 
procedure TTestLabel.paint;
begin
  // lediglich einen schwarzen Rahmen aussen
  canvas.brush.color:=clBlack;    // FrameRect wird immer mit brush gezeichnet
  canvas.FrameRect(ClientRect);
 
  canvas.brush.color:=clWhite;   // Texthintergrund in weiss
 
  // und den Text ausgeben
  canvas.TextOut(FTextSpace.left,    // Textposition X
                 FTextSpace.Top,     // Textposition Y
                 caption);
end;
 
{******************************************************************************}
{ alle Komponenten registrieren }
 
procedure Register;
begin
  RegisterComponents('LAZ_Siro', [TTestLabel]);
end;
 
Initialization
 
Finalization
 
end.


Hab nochmal vielen Dank für die tolle Unterstützung.

Siro
Grüße von Siro
Bevor ich "C" ertragen muß, nehm ich lieber Lazarus...

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: Aufklappbare Eigenschaft im Objektinspektor

Beitrag von wp_xyz »

In TTestLabel.Destroy kannst du dir noch das "if Assigned(fTextSpace)" sparen - das wird von "fTextSpace.Free" sowieso geprüft.

Antworten