Entwickler-Ecke

Grafische Benutzeroberflächen (VCL & FireMonkey) - Komponenteneigenschaften zur Laufzeit


j0hnny - Sa 30.10.10 15:37
Titel: Komponenteneigenschaften zur Laufzeit
Ich habe eine eigene Komponente geschrieben, die auf TEdit basiert.
Jetzt will ich die Eigenschaft Color erst auf eine bestimmte Farbe setzten und danach wieder zurück in den Ausgangszustand (also die Farbe, die in der Entwicklungsumgebung angegeben wurde).

Jetzt habe ich den Create constructor überschrieben und versucht die eingestellte Farbe auszulesen und als NormalColor zu speichern. Leider klappt das nicht wirklich, weil anscheinend die Eigenschaft Color erst nach dem Create gesetzt wird.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
constructor TEditCustom.Create(AOwner: TComponent);
begin

  ErrorColor:=clRed;
  NormalColor:=Color;
end;


Weiß jemand Hilfe? Grüße


jaenicke - Sa 30.10.10 15:50

Die in der IDE eingestellten Werte werden automatisch erst nach dem Konstruktor gesetzt. Du kannst dort also Standardwerte setzen, die dann durch die eingestellten ersetzt werden, sofern vorhanden. Das gilt sowohl für die IDE selbst als auch zur Laufzeit.


j0hnny - Sa 30.10.10 20:44

Achso, das erklärt das Ganze. Aber wie kann ich denn nun die in der IDE eingestellten Werte auslesen? Ich würde halt gerne die Werte zwischenspeichern. Und das direkt nach dem Create oder so.

Grüße


jaenicke - Sa 30.10.10 20:48

Die stehen doch dann in den echten Eigenschaften.

Es gibt AfterConstruction, das kannst du überschreiben glaube ich, da sollten die Werte gesetzt sein (habs nie benutzt, aber ich vermute es ist so).


j0hnny - Sa 30.10.10 21:00

Also ich will halt sowas in der Art wie:


Delphi-Quelltext
1:
2:
3:
4:
5:
ColorTemp:=Color; //Color aus der IDE
...
Color:=ColorNeu; //Color Überschreiben
...
Color:=ColorTemp; //Color zurücksetzten


Das Ganze steht in verschiedenen Prozeduren.


Das mit dem AfterCreate werd ich mal testen.


jaenicke - Sa 30.10.10 21:09

Das sieht irgendwie seltsam aus, bist du sicher, dass das nicht besser geht? :gruebel:

Und davon abgesehen: Wenn du ohnehin woanders erst die werte überschreibst, reicht es doch die da auch zu sichern, oder?


j0hnny - Sa 30.10.10 22:26

Das Problem ist, dass die Prozeduren verschiedene Events sind, die der Nutzer auslöst. Also ist die Reihenfolge der Aufrufe nicht bekannt. So ähnlich sieht es aus:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
procedure OnExit;
begin
  if error then
    Color:=ColorError
  else
    Color:=ColorNormal;
end;

procedure OnChange;
begin
    Color:=ColorNormal;
end;


Das heißt, ich brauche irgendwo her ColorNormal, was der Einstellung im IDE entsprechen soll. Natürlich könnte ich so eine Abfrage machen "if Color<>ColorError then..." oder so. Aber irgendwie müsste das doch auch anders gehen oder?


j0hnny - Sa 30.10.10 22:35

Es funktioniert jetzt mittels


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
procedure OnExit;
begin
  if Color<>ColorError then ColorNormal:=Color;
  if error then
    Color:=ColorError
  else
    Color:=ColorNormal;
end;

procedure OnChange;
begin
  if Color<>ColorError then ColorNormal:=Color;
  Color:=ColorNormal;
end;


Aber so sauber finde ich das nicht.


jaenicke - Sa 30.10.10 22:43

So, ich habe es ausprobiert, nach dem inherited im Konstruktor und wie von mir vermutet in AfterConstruction ist der eingestellte Wert gesetzt. :nixweiss:

Du hast in deinem ersten Quelltext schlicht das inherited vergessen. ;-)


j0hnny - Sa 30.10.10 22:51

Komisch. Das "Inherited" habe ich drin, nur im ersten Post vergessen.
Demnach müsste doch folgender Code funktionieren, oder?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
constructor TMeinEdit.Create(AOwner: TComponent);
begin
  Inherited;
  ColorError:=clRed;
  ColorNormal:=Color;
end;

procedure TMeinEdit.Change;
begin
  if error then
    Color:=ColorNormal
  else
    Color:=ColorError;
end;


Tut er aber nicht. In ColorNormal wird immer nur Weiß gespeichert, aber nicht die Farbe aus dem IDE. Was mach ich falsch?^^

PS: Mit

Delphi-Quelltext
1:
2:
3:
4:
5:
procedure TMeinEdit.AfterConstruction;
begin
  Inherited;
  ColorNormal:=Color;
end;

klappts auch nicht.


jaenicke - Sa 30.10.10 23:05

Ok, mein Fehler, ich installiere fast nie Komponenten, jetzt habe ich es mal gemacht. Getestet hatte ich das Formular selbst. Da läuft das natürlich anders. Bei einer Komponente werden die Werte ja nach deren Erzeugung vom Formular zugewiesen.

Ich habe aber eine ganz andere Idee: Benutze einfach den Setter von deiner Property Color. ;-)
Wenn du dann intern die Farbe setzt, darfst du dann natürlich nicht den auch benutzen (sondern z.b. inherited Color setzen).

Denn das Problem ist doch ansonsten, dass deine Komponente nicht mit späteren Zuweisungen an Color klarkommt. ;-)


j0hnny - Sa 30.10.10 23:24

Ich kenn mich halt garnicht mit Komponenten aus.

Wie meist du das mit dem Color setzen. Was genau muss ich tun? ;) Woher kriege ich denn die SetColor Funktion.


jaenicke - Sa 30.10.10 23:33


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
  published
    property Color: TColor read GetColor write SetColor;
  end;

// ...

function TEditX.GetColor: TColor;
begin
  Result := inherited Color;
end;

procedure TEditX.SetColor(const Value: TColor);
begin
  inherited Color := Value;
  FMyColor := Value;
end;


j0hnny - Sa 30.10.10 23:51

Also für ColorNormal "Color:=ColorNormal;" und für ColorError "inherited Color:=ColorError;" ?

Ich werd das mal morgen ausprobieren. Vielen Dank für die all die Hilfe :)


HelgeLange - So 31.10.10 00:29

Es gibt ein Event bei Komponenten, welches Dir Bescheid sagt, wann di Eigenschaften aus dem DFM file geladen wurde. Da kannst Du einfach die in der Designzeit angegebenen Werte auslesen, sichern und überschreiben. Einfach die Methode Loaded dazu überschrieben.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
Type TMyComponent = class(TEdit)
     public
       procedure Loaded; override;
     end;

implementation

procedure TMyComponent.Loaded;
begin
  OldColor := Self.Color;
end;


Hab jetzt die Variablen nicht eingebaut, aber darum ging es ja nicht ;)


jaenicke - So 31.10.10 01:30

Aber dann kann die Eigenschaft eben nicht mehr später gesetzt werden, da dann die Automatik nicht greift. Deshalb ist hier die eigene Property die sinnvollere Variante. ;-)


bummi - So 31.10.10 10:48

@jaenicke
ich denke auch eine 2. Property macht mehr Sinn
aber wenn es in OnCreate zwischenspeichert und in Loaded hat er ja auch alle Infod die er in seinem Fall benötigt, oder?


jaenicke - So 31.10.10 10:50

Der Setter wird auch ausgelöst, wenn die Eigenschaft beim Laden gesetzt wird. (Geht ja auch gar nicht anders.)

Deshalb wird der Rest dann gar nicht mehr gebraucht.


j0hnny - So 31.10.10 13:27

Vielen Dank Jungs! Jetzt klappts!

Jetzt muss ich euch aber leider mit noch einer Frage nerven. Ich will, dass die Eigenschaft ColorError im IDE unter den Objekt-Eigenschaften erscheint.
Dafür muss ich ja ne eigene Property definieren. Muss ich wirklich für jede Property eine Set-Prozedur und eine Get-Funktion schreiben und dann noch in OnCreate einen Standartwert setzten? Oder geht das auch einfacher? Sowas wie

Delphi-Quelltext
1:
2:
published
    ColorError: TColor default clRed;

wäre perfekt ;)


jaenicke - So 31.10.10 13:37

Du musst dann unter private auch noch ein Feld deklarieren, in dem der Wert gespeichert wird.

Delphi-Quelltext
1:
2:
3:
4:
private
  FColorError: TColor;
published
  property ColorError: TColor read FColorError write FColorError;
Das wird aber auch automatisch gemacht, wenn du die Property eingibst und Strg + Shift + C drückst. ;-)
Das nennt sich Klassenvervollständigung.


j0hnny - So 31.10.10 14:05

Cool, danke. Funktioniert super. Bei mir macht der allerdings mit Strg+Shift+C noch zusätzlich eine Set-Funktion "property ColorError: TColor read FColorError write SetColorError;". Aber von Hand eingetragen klappts.

Jetzt bin ich schon fast fertig mit der Komponente. Allerdings würde ich gerne noch eine Liste von Strings mit dazugehörigen Floats definieren. Also jeweils Variablenname und Wert. Für die Liste der Strings kann ich ja eine StringList als property definieren, die über das IDE definiert werden kann. Aber wie mache ich das mit dem array of Double? Ohne property würde ich einfach einen dynamischen array definieren. Aber wie mache ich eine Liste von Zahlen, die per IDE eingetragen werden kann? Gibt es eine Art StringList für Zahlen oder die Möglichkeit auch dynamische Arrays als property zu benutzen? Oder fällt euch noch ne andere Möglichkeit ein?


jaenicke - So 31.10.10 14:10

user profile iconj0hnny hat folgendes geschrieben Zum zitierten Posting springen:
Bei mir macht der allerdings mit Strg+Shift+C noch zusätzlich eine Set-Funktion "property ColorError: TColor read FColorError write SetColorError;". Aber von Hand eingetragen klappts.
Ich mache das so:
Ich schreibe "property XY", drücke dann Strg + Left, kopiere den Namen, schreibe "read F", Strg + V, "write F", Strg + V, dann Strg + Shift + C. Schon ist alles fertig. ;-)

Und für die zweite Frage: Das müsste mit einer eigenen Klasse gehen, aber da müsste ich auch mal schauen welche Methoden du implementieren musst.

// EDIT:
Auf den ersten Blick würde ich sagen du musst ReadData und WriteData implementieren.


j0hnny - Mo 01.11.10 16:08

Dank eurer Hilfe bin ich nun soweit Fertig mit meiner Komponente. Allerdings gibts noch eine Sache, die ich nicht hinbekomme. Und zwar habe ich zwei Komponenten miteinander verknüpft. Also in Comp1 ist Comp2 eingebunden als:


Delphi-Quelltext
1:
2:
published
  property Comp2: TComp2 read FComp2 write FComp2;


Wie teile ich Comp1 mit, dass sich bei Comp2 eine Variable geändert hat? Muss ich das irgendwie ein procedure von Comp1 in Comp2 überschreiben oder so?

Ein weiteres Problem ist, dass ich wenn ich bei Load von Comp1 auf Comp2 zugreifen will, ist Comp2 noch NIL. Wie kriege ich hin, dass Comp2 vor Comp1 geladen wird?

Grüße


jaenicke - Mo 01.11.10 16:23

Du kannst z.B. einen Setter für deine Eigenschaft benutzen. Dann kannst du beim zuweisen von comp2 ein Event dort setzen, das dir dann eine Änderung mitteilt.

Ja, und wegen nil: wenn du das im Konstruktor erzeugst, klappt das. Wenn das erst später zugewiesen wird, kannst du auch keine Eigenschaften der Unterklasse im OI zuweisen.


j0hnny - Mo 01.11.10 17:02

Comp2 ist ja quasi schon der Comp1 zugewiesen. Die Änderung einer Eigenschaft von Comp2 soll der Comp1 mitgeteilt werden. Hier weiß ich jetzt nicht, wie ich zwischen Comp1 und Comp2 kommunizieren kann, wenn Comp2 schon zugewiesen wurde.


jaenicke - Mo 01.11.10 17:33

Das kannst du wie gesagt beim Zuweisen einrichten. Indem du einen Setter benutzt, der dafür ein Event bei der untergeordneten Komponente setzt.
Dann wird die entsprechende Methode in deiner äußeren Komponente ausgelöst, wenn die innere das Event feuert.


j0hnny - Mo 01.11.10 22:15

Sorry, aber irgendwie raff ich das nicht so ganz. Also die untergeordnete Comp2 soll der übergeordneten Comp1 sagen, dass es bei ihr was neues gibt^^ Also hab ich irgendwie das Problem wie ich aus Comp2 auf Comp1 zugreifen, weil ja Comp1 in Comp2 integriert ist und nicht umgekehrt.


jaenicke - Di 02.11.10 00:08

ok, mal so im Browser runtergetippselt, Tippfehler kannst du behalten: :D

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
type
  TChangedValue = (chvFirst, chvSecond);

  TOnValueChanged = procedure(Sender: TObject; ChangedValue: TChangedValue) of object;

  TComp2 = class
  private
    FOnValueChanged: TOnValueChanged;
    FValue1, FValue2: Integer;
    procedure DoValueChanged(ChangedValue: TChangedValue);
    procedure SetValue1(const Value: Integer);
    procedure SetValue2(const Value: Integer);
  public
    property OnValueChanged: TOnValueChanged read FOnValueChanged write FOnValueChanged;
    property Value1: Integer read FValue1 write SetValue1;
    property Value2: Integer read FValue2 write SetValue2;
  end;

  TComp1 = class
  private
    FComp2: TComp2;
    procedure Comp2ValueChanged(Sender: TObject; ChangedValue: TChangedValue);
  public
    constructor Create();
    destructor Destroy(); override;
    property Comp2: TComp2 read FComp2;
  end;

implementation

procedure TComp2.DoValueChanged(ChangedValue: TChangedValue);
begin
  if Assigned(FOnValueChanged) then
    FOnValueChanged(Self, ChangedValue);
end;

procedure TComp2.SetValue1(const Value: Integer);
begin
  FValue1 := Value;
  DoValueChanged(chvFirst);
end;

procedure TComp2.SetValue2(const Value: Integer);
begin
  FValue2 := Value;
  DoValueChanged(chvSecond);
end;

procedure TComp1.Comp2ValueChanged(Sender: TObject; ChangedValue: TChangedValue);
begin
  if ChangedValue = chvFirst then
    ShowMessage('Erster Wert geändert')
  else
    ShowMessage('Anderer Wert geändert');
end;

constructor TComp1.Create();
begin
  FComp2 := TComp2.Create();
  FComp2.OnValueChanged := Comp2ValueChanged;
end;

destructor Destroy(); override;
begin
  FComp2.Free();
end;


j0hnny - Mi 03.11.10 12:32

Vielen Dank! Wusste ich garnicht, dass man das so mit den Events machen kann. Sehr schön.

Jetzt habe ich aber schonwieder ein Problem. Irgendwie komme ich mit der Reihenfolge der Funktionsaufrufe bei Erzeugung mehrerer Komponenten absolut nicht klar.

Also ich verknüpfe zwei Komponenten miteinander. Comp2 ist quasi ne Unterkomposition von Comp1. In Comp2 gibts eine StringList als published property, die man in der IDE verändern kann. Nun gibt es einen Setter für diese Stringlist, der mir aus den Strings einen array von Zahlen macht. Das klappt soweit ganz gut. Allerdings ist dieser array nach ausführen des Programms wieder gelöscht, obwohl er zur Designtime noch gefüllt existiert.
Naja jetzt will ich über Comp1 auf diesen array von Comp2 zugreifen. Habe jetzt verschiedene Dinge ausprobiert. Beispielsweise den Setter der Stringlist auch beim Laden von Comp2 zu aktivieren um den array zu füllen.
Dieser array ist aber noch nicht gefüllt, wenn ich ihn in Comp1 aufrufen will.
Wenn ich jetzt einen Getter für den array nutze, existiert zu den Zeitpunkt des Aufrufs in Comp1 aber noch nicht die Stringlist.
Wenn ich beim Create von Comp2 auf die Stringlist zugreifen will ist diese noch leer. Wenn ich in Comp2 beim Loaded auf die Stringlist zugreife um den array zu füllen klappt dies zwar, aber das Loaded von Comp2 wird erst nach dem Loaded von Comp1 ausgeführt.

Gibt es nicht irgendeine Möglichkeit auf die generierten Daten aus der Designtime in der Runtime zuzugreifen? Bzw. wie kriege ich es hin, dass Loaded von Comp2 vor Comp1 ausgeführt wird? Oder gibt es vielleicht auch noch eine weiter Funktion die ich überschreiben kann, die nach Loaded ausgeführt wird?


j0hnny - Mi 03.11.10 16:55

Hat sich erledigt. Habe es jetzt anders gelöst. Aber vielen Dank nochmal für eure Hilfe.

Jetzt habe ich aber immer noch eine Frage. Ich habe zwei fast identische Komponenten. Die einen leitet sich von TEdit (TComp1) ab und die andere von TLabeledEdit (TComp2). Beide haben die selbe property Prop1. Jetzt möchte ich beide miteinander kompatibel machen. Die Komponenten kommunizieren unter sich über:


Delphi-Quelltext
1:
2:
if Owner.Components[i].ClassType=TComp1 then
 temp:=TComp1(Owner.Components[i]).Prop1;


Jetzt möchte ich aber, dass diese Abfrage für Comp1 und Comp2 funktioniert:


Delphi-Quelltext
1:
2:
3:
Comp:=Owner.Components[i];
if (Comp.ClassType=TComp1) or (Comp.ClassType=TComp2) then
 temp:=TComp1(Owner.Components[i]).Prop1;


Das liefert mir aber leider ne Zugriffsverletzung. Kann man irgendwie auf Prop1 zugreifen, ohne vorher abzufragen, ob es sich um TComp1 oder TComp2 handelt?

EDIT: Habe ein weiteres Problem. Und zwar benutze ich einen Setter für TStrings. Wenn die Strings über die Designumgebung geändert werden, wird die Setter Prozedur ausgelöst. Wenn ich aber zur Runtime Strings editieren, wird die Setter Prozedur nicht ausgelöst. Gibts da eine einfache Methode auf Änderungen zu reagieren?

Grüße


jaenicke - Mi 03.11.10 17:48

user profile iconj0hnny hat folgendes geschrieben Zum zitierten Posting springen:
Kann man irgendwie auf Prop1 zugreifen, ohne vorher abzufragen, ob es sich um TComp1 oder TComp2 handelt?
Packe die gemeinsamen Eigenschaften in ein interface und lasse das beide implementieren. ;-)


j0hnny - Mi 03.11.10 18:00

Wie kann man denn beide in ein Interface packen?^^


jaenicke - Mi 03.11.10 18:08


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
type
  IMyInterface = interface
    property XY read Get...
  end;

  TComp1 = class(T..., IMyInterface)
    property XY read Get...
  end;

  TComp2 = class(T..., IMyInterface)
    property XY read Get...
  end;

var
  a: IMyInterface;
begin
  a := MyComp1;
  a.XY ...
  a := MyComp2;
  a.XY ...
  // usw.
Casten geht natürlich auch.


j0hnny - Mi 03.11.10 18:37

Danke! Habs jetzt in ein Interface gepackt. Nur leider will er jetzt nicht


Delphi-Quelltext
1:
2:
Comp:=Owner.Components[i];
IMyInterface(Comp).Prop1;


fressen :(


jaenicke - Mi 03.11.10 20:02

Das geht nicht so einfach. Hier musst du erst nachfragen ob das Objekt das Interface implementiert. Beispiel (sehr zusammengedrängt und ungetestet^^):

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
var
  MyInterface: IMyInterface;
  MyObject: TComponent;
begin
  MyObject := TComp1.Create(nil);
  TComp1(MyObject).XY := 100;
  if Succeeded(IInterface(MyObject).QueryInterface(IMyInterface, MyInterface)) then
    ShowMessage(IntToStr(MyInterface.XY));


j0hnny - Mi 03.11.10 23:28

Wenn ich eh eine Abfrage brauche, dann mache ich es einfach ähnlich wie oben beschrieben. Aber die Sache mit dem Interface ist schonmal gut um das Ganze besser zu ordnen. Wieder was dazu gelernt ;)

Hast du vielleicht auch noch eine Antwort auf meine andere Frage :) Hatte die nur via Edit hinzugefügt.


jaenicke - Do 04.11.10 06:00

Der Unterschied ist, dass zur Designzeit das TStrings Objekt neu zugewiesen wird, während du dir das vermutlich holst und darin etwas änderst. Und das löst den Setter nicht aus.

Du kannst aber eine eigene Klasse von TStringList ableiten und dort Changed überschreiben. Oder eine normale TStringList nehmen und OnChanged zuweisen.

Dann bekommst du darüber die Änderungen mit.