Toon posts:

[Delphi] Panel streamen naar een nieuw panel

Pagina: 1
Acties:
  • 173 views sinds 30-01-2008
  • Reageer

Verwijderd

Topicstarter
N.a.v. een vraag van jelmervos heb ik me vastgebeten in het creeren/copieren van componenten via Delphi's streaming mechanisme.
(jelmervos, sorry dat ik je de voorbeelden van Mark Miller nog niet gestuurd heb, maar ik kan die CD nergens vinden... |:()

'k Kom een heel eind, maar stuit op wat probleempjes:

* De Owner van die componenten. TReader.ReadRootComponent zet die standaard op nil, ook al heb je de Owner van die TReader goed gezet. En Owner is een readonly property. :(
Ik heb 't nu "opgelost" door eerst het component in te lezen om achter z'n ComponentClass (en naam) te komen, dat component weg te gooien en opnieuw te creeren (met juiste Owner en naam), en 't component nogmaals te streamen.
Werkt, maar is niet erg netjes, en bovendien wordt de Owner van componenten die automatisch worden "meegestreamd" (ToolButtons van een ToolBar bv.) niet goed gezet.

* Alle classes die gestreamd worden, moeten expliciet via RegisterClass bekend gemaakt worden aan de ClassList, omdat anders de Reader ze niet herkent. Niet handig...

Oftewel, volgens mij doe ik iets niet goed...
Mocht iemand hier chocola van kunnen maken, dan houd ik me van harte aanbevolen! :)

Het voorbeeld appje bestaat uit een form met een ToolBar, 3 ToolButtons, een Memo en een Button. Die staan allemaal op Panel1.
Button1.OnClick voert CopyToForm2 uit, waarbij een nieuw Form wordt gemaakt, daar een NewPanel op gezet wordt, en vervolgens wordt de boel gestreamd.
code:
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ComCtrls, ToolWin, ExtCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    ToolBar1: TToolBar;
    ToolButton1: TToolButton;
    ToolButton2: TToolButton;
    ToolButton3: TToolButton;
    Button1: TButton;
    Memo1: TMemo;
    procedure CopyToForm2(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.CopyToForm2(Sender: TObject);
var
  Stream: TMemoryStream;
  Reader: TReader;
  Form2: TForm;
  NewPanel: TPanel;
  Comp: TComponent;
  CompClass: TComponentClass;
  i, iPosition: integer;
  sName: string;
begin
  Form2 := TForm.Create(Self);
  with Form2 do begin
    Visible := False;     // Necessary for ShowModal
    Left := Self.Left + 20;  // Form1 dimensions with a little offset
    Top := Self.Top + 20;
    Height := Self.Height;
    Width := Self.Width;
    NewPanel := TPanel.Create(Form2); // panel to stream to
  end;
  try
    Stream := TMemoryStream.Create;
    try
    Stream.WriteComponent(Panel1); // store the panel
    for i := 0 to ComponentCount-1 do begin
      // stream all controls on that panel
      Comp := Components[i];
      if (Comp is TControl) and (TControl(Comp).Parent = Panel1) then
        Stream.WriteComponent(Comp);
    end;
    Stream.Seek(0, soFromBeginning); // quite important... :)
    Reader := TReader.Create(Stream, Stream.Size);
    try
      // To prevent Memo's etc. to cause a
      // 'Control doesn't have a Parent' exception
      Reader.Parent := NewPanel.Owner;
      Reader.Owner := NewPanel.Owner; // Doesn't seem to do anything...
      // First read the panel
      Reader.ReadRootComponent(NewPanel);
      iPosition := Reader.Position;
      // get the rest of the controls
      while iPosition < Stream.Size do begin
        // Step 1: get the component class
        Comp := Reader.ReadRootComponent(nil);
        CompClass := TComponentClass(FindClass(Comp.ClassName));
        sName := Comp.Name;
        Comp.Free;
        // Step 2: create the control with the new Owner
        //     and re-stream it.
        Comp := CompClass.Create(NewPanel.Owner);
        Comp.Name := sName;
        Reader.Position := iPosition;
        Comp := Reader.ReadRootComponent(nil);
        // make the new Panel the parent
        if Comp is TControl then
        TControl(Comp).Parent := NewPanel;
        iPosition := Reader.Position;
      end;
    finally
      Reader.Free;
    end;
    finally
    Stream.Free;
    end;
    Form2.ShowModal;
  finally
    Form2.Free;
  end;
end;

initialization // so the reader wont choke on 'Class not found'...
  RegisterClass(TPanel);
  RegisterClass(TToolBar);
  RegisterClass(TToolButton);
  RegisterClass(TButton);
  RegisterClass(TMemo);
finalization
  // nothing to finalize
end.

  • Delphi32
  • Registratie: Juli 2001
  • Laatst online: 22-08 10:56

Delphi32

Heading for the gates of Eden

Ik begrijp wat je hebt gedaan...
maar waarom wil je dit dan? Er zijn toch tig andere manieren om dit veel beter (ik bedoel gemakkelijker en overzichtelijker) op te lossen? Denk aan TFrames en Packages.

Heel benieuwd naar je beweegreden.

Verwijderd

Volgens mij hebben de delphi-programmeurs de TReader en TWriter gemaakt met de bedoeling om TForm's weg te schrijven en te lezen; dit werkt ook gewoon goed, zul je ook wel geprobeerd hebben.

1 van de problemen die je krijgt als je een Panel wilt weg schrijven is dat de componenten die op die Panel staan de form als Owner hebben; dus worden deze componenten niet weggeschreven [Volgens mij]. Daarom zul je wel Stream.WriteComponent gebruikt hebben ipv Writer.WriteRootComponent.

Een verbetering zou misschien kunnen zijn om alle namen van de klasse van de componenten die je weg gaat schrijven aan het begin van de stream te zetten, zodat je die als eerste in kunt lezen, dan al die componenten creeeren etc..., en dan ook geen TReader gebruiken.

  • jelmervos
  • Registratie: Oktober 2000
  • Niet online

jelmervos

Simple user

Op donderdag 22 november 2001 23:01 schreef Afterlife het volgende:
N.a.v. een vraag van jelmervos heb ik me vastgebeten in het creeren/copieren van componenten via Delphi's streaming mechanisme.
(jelmervos, sorry dat ik je de voorbeelden van Mark Miller nog niet gestuurd heb, maar ik kan die CD nergens vinden... |:()
Geen probleem.

Ik ben wel benieuwd of je dit gaat lukken. Ik vind het zeer interessant.

"The shell stopped unexpectedly and Explorer.exe was restarted."


  • joepP
  • Registratie: Juni 1999
  • Niet online
Mijn complimenten voor het duidelijke & interessante topic! Hier kunnen veel mensen een voorbeeld aan nemen!
* Alle classes die gestreamd worden, moeten expliciet via RegisterClass bekend gemaakt worden aan de ClassList, omdat anders de Reader ze niet herkent. Niet handig...
Niet handig, wel noodzakelijk. Hier valt gewoon niets aan te doen. Om streaming mogelijk te maken moet namelijk de classname als STRING worden omgezet naar de werkelijke class. Zonder dat ergens te registreren (in dit geval in het globale object ClassList in Classes) kan dat niet. Je maakt trouwens zelf ook handig gebruik van deze mogelijkheid bij de aanroep van de functie ClassName.

Je code kan ook nog wat beter:

* Je hoeft geen gebruik te maken van het Reader object, je kan alles met de stream doen.

* Je 'trucje' voor het zetten van de Owner is noodzakelijk, maar het kan een stuk netter door alleen het type te lezen, en niet het complete object. Nadeel hiervan is dat je dan OF weer -wel- een Reader object nodig hebt, OF vervelende string parsing moet doen die in dit geval niet zoveel toevoegt. Ik zal dat achterwege laten.

* Als je alle kinderen van Panel1 wilt hebben, moet je over Panel1.Controls loopen. Je doet het nu nogal omslachtig via alle Components van het form.

* Als je de owner wilt inlezen moet je eerst een object creeeren van dat type met de juiste owner, en daarna streamen. Dat doe je ook, maar niet op de juiste manier. Je kent 2x toe aan Comp bij het inlezen, dat kan nooit goed zijn. Hoe moet het dan wel? Zie hier:
code:
1
2
3
4
5
6
7
  //omslachtig
  A := TLabel.Create(Owner);
  Comp := TLabel(Stream.ReadComponent(A));
  //NU GEEN Free(A) ofzoiets doen, A = Comp!!!
 
  //beter
  Comp := TLabel(Stream.ReadComponent(TLabel.Create(Owner)));

Hier een mooiere versie van je progje, ik heb voor de leesbaarheid alle try/except/finally meuk weggehaald. Er wordt een panel met daarop een button, checkbox en label gestreamed.
code:
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Label1: TLabel;
    Button2: TButton;
    Button1: TButton;
    CheckBox1: TCheckBox;
    procedure Button2Click(Sender: TObject);
  private
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button2Click(Sender: TObject);
var
  Stream: TMemoryStream;
  Comp: TComponent;
  CompClass: TComponentClass;
  NewForm: TForm;
  NewPanel: TPanel;
  i, iPos: Integer;
begin
  //create
  Stream := TMemoryStream.Create;

  //save Panel1 + children
  Stream.WriteComponent(Panel1);
  For i := 0 to Panel1.ControlCount-1 do
    Stream.WriteComponent(Panel1.Controls[i]);

  //create new form
  NewForm := TForm.Create(self);
  with NewForm do begin
    Visible := False;
    Left := Self.Left + 20;
    Top := Self.Top + 20;
    Height := Self.Height;
    Width := Self.Width;
  end;

  //load Panel1 + children
  Stream.Position := 0;
  NewPanel := TPanel(Stream.ReadComponent(TPanel.Create(NewForm)));
  NewPanel.Parent := NewForm;
  while Stream.Position < Stream.Size do begin
    //step 1: determine type (can be improved!)
    iPos := Stream.Position;
    Comp := Stream.ReadComponent(nil);
    CompClass := TComponentClass(FindClass(Comp.ClassName));
    Comp.Free;

    //step 2: read it
    Stream.Position := iPos;
    Comp := Stream.ReadComponent(CompClass.Create(NewForm));
    if Comp is TControl then TControl(Comp).Parent := NewPanel;
  end;

  //show!
  NewForm.ShowModal;

  //free
  Stream.Free;
end;

initialization
begin
  RegisterClasses([TForm1, TPanel, TLabel, TButton, TCheckBox]);
end;

Uiteraard kan dit allemaal nog veel mooier, aangezien objecten elkaar kunnen refereren, panels in panels kunnen staan (recursief), etc etc etc. Maar het is een mooi begin!

Verwijderd

Hoewel de owner property read-only is kun je 'm via een omweg toch veranderen, nl. door de InsertComponent en RemoveComponent methodes van die owner te gebruiken.

bv:
code:
1
2
3
4
5
procedure ChangeOwner(Component, NewOwner: TComponent);
begin
  Component.Owner.RemoveComponent(Component);
  NewOwner.InsertComponent(Component)
end;

bron: Mastering Delphi 5, Marco Cantu, ISBN 0-7821-2564-4
(zowieso een musthave dat boek)

  • joepP
  • Registratie: Juni 1999
  • Niet online
Op vrijdag 23 november 2001 13:50 schreef hezik het volgende:
Hoewel de owner property read-only is kun je 'm via een omweg toch veranderen, nl. door de InsertComponent en RemoveComponent methodes van die owner te gebruiken.

bv:
code:
1
2
3
4
5
procedure ChangeOwner(Component, NewOwner: TComponent);
begin
  Component.Owner.RemoveComponent(Component);
  NewOwner.InsertComponent(Component)
end;

bron: Mastering Delphi 5, Marco Cantu, ISBN 0-7821-2564-4
(zowieso een musthave dat boek)
Thnx, handige tip! En idd, dat boek is absoluut verplichte kost voor alle Delphi programmeurs

Verwijderd

Topicstarter
Op vrijdag 23 november 2001 13:41 schreef joepP het volgende:
Je code kan ook nog wat beter:
Weet ik, vandaar dit topic. :)
* Je hoeft geen gebruik te maken van het Reader object, je kan alles met de stream doen.
't Eerste wat Stream.ReadComponent doet, is een Reader aanmaken en diens ReadRootComponent aanroepen. Meestal is dat prima, maar bij bv. een Memo waar al tekst in staat krijg je dan 'Control doesn't have a Parent window' meldingen. Vandaar die Reader.
* Je 'trucje' voor het zetten van de Owner is noodzakelijk, maar het kan een stuk netter door alleen het type te lezen, en niet het complete object.
Was ook een eerste poging, en nogal lomp.
* Als je alle kinderen van Panel1 wilt hebben, moet je over Panel1.Controls loopen.
Goeie tip, die kende ik nog niet!
(Daar sta je dan, met 3 jaar Delphi ervaring... :) )
Hier een mooiere versie van je progje
Dank. Nog steeds niet helemaal netjes, maar ik heb weer een hoop bijgeleerd.

Verwijderd

Topicstarter
Op vrijdag 23 november 2001 00:41 schreef Delphi32 het volgende:
Heel benieuwd naar je beweegreden.
Twee redenen.
1: Ik was aan het experimenteren met het opslaan en inlezen van specifieke componenten uit een blob in een database, en dan is gewoon streaming handiger dan Frames of Packages.

2: We zijn aan het onderzoeken of we een gemeenschappelijke object structuur voor forms etc. kunnen ontwerpen voor zowel de Delphi applicaties als de ASP/JavaScript versies. De tekstversie van Delphi's DFM's lijkt een aardige basis te zijn, is goed te parsen en vrij eenvoudig om te zetten naar XML.

En nog een reden: ik wil dit gewoon onder de knie hebben... :+

Verwijderd

Oke, ik heb ook wat gefrutst:
code:
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
procedure TForm1.ReadComponents(AOwner: TComponent;
  AStream: TStream);

  function ReadInteger: Integer;
  begin
    AStream.Read(Result, SizeOf(Result));
  end;

  function ReadString: string;
  var
    Ch: Char;
  begin
    Result := '';
    AStream.Read(Ch, 1);
    while Ch <> #0 do
    begin
    Result := Result + Ch;
    AStream.Read(Ch, 1);
    end;
  end;

var
  CompClass: TComponentClass;
  Comp: TComponent;

  procedure ReadComponent(AComp: TComponent);
  var
    CtrlCount: Integer;
    I: Integer;
  begin
    AStream.ReadComponent(AComp);

    { Var. CtrlCount voor de leesbaarheid }
    CtrlCount := ReadInteger;
    for I := 0 to CtrlCount - 1 do
    begin
    CompClass := TComponentClass(FindClass(ReadString));
    Comp := CompClass.Create(AOwner);
    { Als CtrlCount > 0 dan is AComp een TWinControl;
      AComp wordt de parent van de child-componenten }
    if Comp is TControl then
      TControl(Comp).Parent := TWinControl(AComp);
    ReadComponent(Comp);
    end;
  end;

begin
  while AStream.Position < AStream.Size do
  begin
    CompClass := TComponentClass(FindClass(ReadString));
    Comp := CompClass.Create(AOwner);
    if (Comp is TControl) and (AOwner is TWinControl) then
    TControl(Comp).Parent := TWinControl(AOwner);
    ReadComponent(Comp);
  end;
end;

procedure TForm1.WriteComponents(AComp: TComponent; AStream: TStream);

  procedure WriteInteger(const AInt: Integer);
  begin
    AStream.Write(AInt, SizeOf(Integer));
  end;

  procedure WriteString(const S: string);
  begin
    AStream.Write(PChar(S)^, Length(S) + 1);
  end;

  procedure WriteComponent(AComp: TComponent);
  var
    I: Integer;
  begin
    RegisterClass(TPersistentClass(AComp.ClassType));
    AStream.WriteComponent(AComp);

    if AComp is TWinControl then
    with AComp as TWinControl do
    begin
      WriteInteger(ControlCount);
      for I := 0 to ControlCount - 1 do
      begin
        RegisterClass(TPersistentClass(Controls[I].ClassType));
        WriteString(Controls[I].ClassName);
        WriteComponent(Controls[I]);
      end;
    end
    else
    WriteInteger(0);
  end;

begin
  WriteString(AComp.ClassName);
  WriteComponent(AComp);
end;

Als je een TForm hebt met een Panel en GroupBox; en een nieuwe form [Form2] gemaakt hebt, kun je het vlgde doen:
code:
1
2
3
4
5
6
7
8
9
  S := TMemoryStream.Create;
  try
    WriteComponents(Panel1, S);
    WriteComponents(GroupBox1, S);
    S.Seek(0, soFromBeginning);
    ReadComponents(Form2, S);
  finally
    S.Free;
  end;

procedure ReadComponents heeft 2 argumenten
  • AOwner; alle componenten die gecreeerd worden, krijgen als owner AOwner; dit wordt ook de parent van de nieuwe componenten, behalve als het sub/child (?) componenten zijn. Dus een TButton op een TPanel krijgt als parent de panel.
  • En de stream
Opgeslagen vorm lijkt nu wat op de .dfm: published properties worden opgeslagen en parent wordt bepaald aan de hand van de structuur van de opgeslagen stream.

edit:

Overbodige code weggemikt :), en nu :Z

Verwijderd

Topicstarter
Op maandag 26 november 2001 02:09 schreef DiFool het volgende:
en nu :Z
En dat was verdiend! :)
Prima code, waarvan ik vandaag een hoop heb geleend in een CopyComponents(NewOwner: TComponent; Components: array of TComponent) procedure. Thanks!

Uiteindelijk zit ik nog met 1 probleem: de Writer registreert nu de classes (logisch, want op dat moment heeft 'ie ze al). Wanneer je de stream uit een database haalt of van een (COM-)DLL krijgt aangeleverd wordt dat wat lastiger. Hmm... is wel een uitdaging... :)

Verwijderd

Op maandag 26 november 2001 21:52 schreef Afterlife het volgende:

[..]
Uiteindelijk zit ik nog met 1 probleem: de Writer registreert nu de classes (logisch, want op dat moment heeft 'ie ze al). Wanneer je de stream uit een database haalt of van een (COM-)DLL krijgt aangeleverd wordt dat wat lastiger. Hmm... is wel een uitdaging... :)
Het probleem is dat je een string hebt [bv 'TComboBox'] die je moet omzetten naar een class. Dit kun je doen door de ClassList in Classes.pas te vullen [RegisterClass] en daarna weer in te lezen [FindClass].

Maar je zou ook een functie kunnen maken die werkt als FindClass, maar waar al de hele VCL in staat; of je zou een functie kunnen maken die ClassList vult met alle classes uit de VCL etc.
Pagina: 1