Creating a component with named sub-components?
Asked Answered
I

3

6

I need to know the basics behind making a component produce and manage sub-components. I originally tried this by creating a TCollection, and tried to put a name on each TCollectionItem. But I learned it's not that easy as I had hoped.

So now I am going to start this project from scratch again, and I'd like to get it right this time. These sub-components are not visual components, and should not have any display or window, just based off of TComponent. The main component holding these sub-components will also be based off of TComponent. So nothing here is visual at all, and I don't want a little icon on my form (in design time) for each of these sub-components.

I would like to be able to maintain and manage these sub-components in a collection-like fashion. The important thing is that these sub-components should be created, named and added to the form source, just like menu items are for example. This is the whole point of the idea in the first place, if they cannot be named, then this whole idea is kaput.

Oh, another important thing: the main component being the parent of all the sub-components needs to be able to save these sub-components to the DFM file.

EXAMPLE:

Instead of accessing one of these sub items like:

MyForm.MyItems[1].DoSomething();

I would instead like to do something like:

MyForm.MyItem2.DoSomething();

So I do not have to rely on knowing the ID of each sub item.

EDIT:

I felt it a little necessary to include my original code so it can be seen how the original collection works. Here's just the server side collection and collection item stripped from the full unit:

//  Command Collections
//  Goal: Allow entering pre-set commands with unique Name and ID
//  Each command has its own event which is triggered when command is received
//  TODO: Name each collection item as a named component in owner form

  //Determines how commands are displayed in collection editor in design-time
  TJDCmdDisplay = (cdName, cdID, cdCaption, cdIDName, cdIDCaption);

  TJDScktSvrCmdEvent = procedure(Sender: TObject; Socket: TJDServerClientSocket;
    const Data: TStrings) of object;

  TSvrCommands = class(TCollection)
  private
    fOwner: TPersistent;
    fOnUnknownCommand: TJDScktSvrCmdEvent;
    fDisplay: TJDCmdDisplay;
    function GetItem(Index: Integer): TSvrCommand;
    procedure SetItem(Index: Integer; Value: TSvrCommand);
    procedure SetDisplay(const Value: TJDCmdDisplay);
  protected
    function GetOwner: TPersistent; override;
  public
    constructor Create(AOwner: TPersistent);
    destructor Destroy;
    procedure DoCommand(const Socket: TJDServerClientSocket;
      const Cmd: Integer; const Data: TStrings);
    function Add: TSvrCommand;
    property Items[Index: Integer]: TSvrCommand read GetItem write SetItem;
  published
    property Display: TJDCmdDisplay read fDisplay write SetDisplay;
    property OnUnknownCommand: TJDScktSvrCmdEvent
      read fOnUnknownCommand write fOnUnknownCommand;
  end;

  TSvrCommand = class(TCollectionItem)
  private
    fID: Integer;
    fOnCommand: TJDScktSvrCmdEvent;
    fName: String;
    fParamCount: Integer;
    fCollection: TSvrCommands;
    fCaption: String;
    procedure SetID(Value: Integer);
    procedure SetName(Value: String);
    procedure SetCaption(const Value: String);
  protected
    function GetDisplayName: String; override;
  public
    procedure Assign(Source: TPersistent); override;
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
  published
    property ID: Integer read fID write SetID;
    property Name: String read fName write SetName;
    property Caption: String read fCaption write SetCaption;
    property ParamCount: Integer read fParamCount write fParamCount;
    property OnCommand: TJDScktSvrCmdEvent read fOnCommand write fOnCommand;
  end;

////////////////////////////////////////////////////////////////////////////////
implementation
////////////////////////////////////////////////////////////////////////////////

{ TSvrCommands }

function TSvrCommands.Add: TSvrCommand;
begin
  Result:= inherited Add as TSvrCommand;
end;

constructor TSvrCommands.Create(AOwner: TPersistent);
begin
  inherited Create(TSvrCommand);
  Self.fOwner:= AOwner;
end;

destructor TSvrCommands.Destroy;
begin
  inherited Destroy;
end;

procedure TSvrCommands.DoCommand(const Socket: TJDServerClientSocket;
  const Cmd: Integer; const Data: TStrings);
var
  X: Integer;
  C: TSvrCommand;
  F: Bool;
begin
  F:= False;
  for X:= 0 to Self.Count - 1 do begin
    C:= GetItem(X);
    if C.ID = Cmd then begin
      F:= True;
      try
        if assigned(C.fOnCommand) then
          C.fOnCommand(Self, Socket, Data);
      except
        on e: exception do begin
          raise Exception.Create(
            'Failed to execute command '+IntToStr(Cmd)+': '+#10+e.Message);
        end;
      end;
      Break;
    end;
  end;
  if not F then begin
    //Command not found

  end;
end;

function TSvrCommands.GetItem(Index: Integer): TSvrCommand;
begin
  Result:= TSvrCommand(inherited GetItem(Index));
end;

function TSvrCommands.GetOwner: TPersistent;
begin
  Result:= fOwner;
end;

procedure TSvrCommands.SetDisplay(const Value: TJDCmdDisplay);
begin
  fDisplay := Value;
end;

procedure TSvrCommands.SetItem(Index: Integer; Value: TSvrCommand);
begin
  inherited SetItem(Index, Value);
end;

{ TSvrCommand }

procedure TSvrCommand.Assign(Source: TPersistent);
begin
  inherited;

end;

constructor TSvrCommand.Create(Collection: TCollection);
begin
  inherited Create(Collection);
  fCollection:= TSvrCommands(Collection);
end;

destructor TSvrCommand.Destroy;
begin
  inherited Destroy;
end;

function TSvrCommand.GetDisplayName: String;
begin        
  case Self.fCollection.fDisplay of
    cdName: begin
      Result:= fName;
    end;
    cdID: begin
      Result:= '['+IntToStr(fID)+']';
    end;
    cdCaption: begin
      Result:= fCaption;
    end;
    cdIDName: begin
      Result:= '['+IntToStr(fID)+'] '+fName;
    end;
    cdIDCaption: begin
      Result:= '['+IntToStr(fID)+'] '+fCaption;
    end;
  end;
end;

procedure TSvrCommand.SetCaption(const Value: String);
begin
  fCaption := Value;
end;

procedure TSvrCommand.SetID(Value: Integer);
begin
  fID:= Value;
end;

procedure TSvrCommand.SetName(Value: String);
begin
  fName:= Value;
end;
Integumentary answered 6/12, 2011 at 20:43 Comment(14)
You could study the source code for the three "similar existing things" that you cited. How did the Delphi developers implement this?Wroth
Now after your edit it is clear what exactly it is that you want: No, this is not possible. But I doubt this is really your wish. Why do you want to address the collection item by name in code?Faust
Maybe not the actual collection item being referenced from code, but create some invisible component behind each collection item which has a name to be saved in the DFM. However as mentioned in another comment below on your answer, I'm not sure how to show that component's properties in the object inspector when it's already showing the properties of the TCollectionItem?Integumentary
I have a feeling this will require me to make a property editor, or at least try to use the same property editor as the TPopupMenu.Integumentary
Yes, you need a property editor. Sometimes is the finding the right term the key to the answer.Thain
That seems to be my problem in this website, I never went to any school for programming, so I don't know many terms, but everyone here expects you to know everything.Integumentary
No, I don't think you need a property editor. If you make a TCollection property, then double clicking on that property will open the default collection editor in the designer. Subsequently, when you select a collection item in that collection editor (a separate window), then that collection item with all its properties will be shown in the object inspector. Now, your sub component is one of these properties. Open it with [+], ét voila.Faust
Nobody here expects you to know everything (or much of anything, for that matter - this site caters very much to beginners). What we do expect, though, is for you to be able to actually describe the problem you're trying to solve and provide enough information in order for people to be able to help you. :)Naca
@Faust That makes sense and might work, but I'd hate to require you to click the [+] first to access everything. Since each collection item and component will correspond with each other anyway, I may as well wrap the component using the collection item. Read/Write of the collection item's properties will be redirected to the properties of the component that item represents. Problem solved. Now I just need to get on Delphi and make sure this will work...Integumentary
@NGLN, my thought was that collection editor is the kind of a property editor. Jerry, just for making that clear, so you want the collection editor, the one on the screenshot is from the VirtualStringTree column editor. (+1 for compensation)Thain
And also to clarify as I didn't mention (although irrelevant to the question) - The reason for this is I have an old pair of custom Server/Client sockets. They work by using a 'Command number' with parameters passed back and forth. Each side has a pre-defined list of Commands in a collection (TSvrCommand[s] and TCliCommand[s]) - Each command I'd rather call like MyCommand.Send(['abc', '123']); instead of MySocket.Commands[765].Send(['abc', '123']);Integumentary
One issue I just realized - suppose I get this thing working (as discussed towards the end of the chat session) and each TCollectionItem has one of my 'invisible named components' behind it. Then suppose for whatever reason the user deletes the declaration of this sub-component from the form's class. Then my collection item won't have anything to reference to anymore. How to handle the collection item in this case? Delete it? Recreate the sub-component? Raise an error? (this is just a note to know what to discuss tomorrow when this question is picked back up in chat)Integumentary
Look at my answer. I edited the source of the second unit to create some temporary colleciton to use the built-in CollectionEditor to edit the items. It is still not ideal because you have to publish every property of TChildComponent to the TChildComponentCollectionItem.Depreciable
@Faust Thanks for the edit, by the way :DIntegumentary
D
9

This Thread helped me creating something as we discussed yesterday. I took the package posted there and modified it a bit. Here is the source:

TestComponents.pas

unit TestComponents;

interface

uses
  Classes;

type
  TParentComponent = class;

  TChildComponent = class(TComponent)
  private
    FParent: TParentComponent;
    procedure SetParent(const Value: TParentComponent);
  protected
    procedure SetParentComponent(AParent: TComponent); override;
  public
    destructor Destroy; override;
    function GetParentComponent: TComponent; override;
    function HasParent: Boolean; override;
    property Parent: TParentComponent read FParent write SetParent;
  end;

  TParentComponent = class(TComponent)
  private
    FChilds: TList;
  protected
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property Childs: TList read FChilds;
  end;

implementation

{ TChildComponent }

destructor TChildComponent.Destroy;
begin
  Parent := nil;
  inherited;
end;

function TChildComponent.GetParentComponent: TComponent;
begin
  Result := FParent;
end;

function TChildComponent.HasParent: Boolean;
begin
  Result := Assigned(FParent);
end;

procedure TChildComponent.SetParent(const Value: TParentComponent);
begin
  if FParent <> Value then
  begin
    if Assigned(FParent) then
      FParent.FChilds.Remove(Self);
    FParent := Value;
    if Assigned(FParent) then
      FParent.FChilds.Add(Self);
  end;
end;

procedure TChildComponent.SetParentComponent(AParent: TComponent);
begin
  if AParent is TParentComponent then
    SetParent(AParent as TParentComponent);
end;

{ TParentComponent }

constructor TParentComponent.Create(AOwner: TComponent);
begin
  inherited;
  FChilds := TList.Create;
end;

destructor TParentComponent.Destroy;
var
  I: Integer;
begin
  for I := 0 to FChilds.Count - 1 do
    FChilds[0].Free;
  FChilds.Free;
  inherited;
end;

procedure TParentComponent.GetChildren(Proc: TGetChildProc; Root: TComponent);
var
  i: Integer;
begin
  for i := 0 to FChilds.Count - 1 do
    Proc(TComponent(FChilds[i]));
end;

end.

TestComponentsReg.pas

unit TestComponentsReg;

interface

uses
  Classes,
  DesignEditors,
  DesignIntf,
  TestComponents;

type
  TParentComponentEditor = class(TComponentEditor)
    procedure ExecuteVerb(Index: Integer); override;
    function GetVerb(Index: Integer): string; override;
    function GetVerbCount: Integer; override;
  end;

procedure Register;

implementation

uses
  ColnEdit;

type
  TChildComponentCollectionItem = class(TCollectionItem)
  private
    FChildComponent: TChildComponent;
    function GetName: string;
    procedure SetName(const Value: string);
  protected
    property ChildComponent: TChildComponent read FChildComponent write FChildComponent;
    function GetDisplayName: string; override;
  public
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
  published
    property Name: string read GetName write SetName;
  end;

  TChildComponentCollection = class(TOwnedCollection)
  private
    FDesigner: IDesigner;
  public
    property Designer: IDesigner read FDesigner write FDesigner;
  end;

procedure Register;
begin
  RegisterClass(TChildComponent);
  RegisterNoIcon([TChildComponent]);
  RegisterComponents('Test', [TParentComponent]);
  RegisterComponentEditor(TParentComponent, TParentComponentEditor);
end;

{ TParentComponentEditor }

procedure TParentComponentEditor.ExecuteVerb(Index: Integer);
var
  LCollection: TChildComponentCollection;
  i: Integer;
begin
  LCollection := TChildComponentCollection.Create(Component, TChildComponentCollectionItem);
  LCollection.Designer := Designer;
  for i := 0 to TParentComponent(Component).Childs.Count - 1 do
    with TChildComponentCollectionItem.Create(nil) do
    begin
      ChildComponent := TChildComponent(TParentComponent(Component).Childs[i]);
      Collection := LCollection;
    end;
  ShowCollectionEditorClass(Designer, TCollectionEditor, Component, LCollection, 'Childs');
end;

function TParentComponentEditor.GetVerb(Index: Integer): string;
begin
  Result := 'Edit Childs...';
end;

function TParentComponentEditor.GetVerbCount: Integer;
begin
  Result := 1;
end;

{ TChildComponentCollectionItem }

constructor TChildComponentCollectionItem.Create(Collection: TCollection);
begin
  inherited;
  if Assigned(Collection) then
  begin
    FChildComponent := TChildComponent.Create(TComponent(TOwnedCollection(Collection).Owner).Owner);
    FChildComponent.Name := TChildComponentCollection(Collection).Designer.UniqueName(TChildComponent.ClassName);
    FChildComponent.Parent := TParentComponent(TComponent(TOwnedCollection(Collection).Owner));
  end;
end;

destructor TChildComponentCollectionItem.Destroy;
begin
  FChildComponent.Free;
  inherited;
end;

function TChildComponentCollectionItem.GetDisplayName: string;
begin
  Result := FChildComponent.Name;
end;

function TChildComponentCollectionItem.GetName: string;
begin
  Result := FChildComponent.Name;
end;

procedure TChildComponentCollectionItem.SetName(const Value: string);
begin
  FChildComponent.Name := Value;
end;

end.

The most important thing is the RegisterNoIcon which prevents showing the component on the form when you create it. The overridden methods in TChildComponent are causing them to be nested inside the TParentComponent.

Edit: I added a temporary collection to edit the items in the built-in TCollectionEditor instead of having to write an own one. The only disadvantage is that the TChildComponentCollectionItem has to publish every property that TChildComponent has published to be able to edit them inside the OI.

Depreciable answered 7/12, 2011 at 12:11 Comment(5)
+1 Very nice. I suspected the need to use Designer.CreateComponent for adding the element to the source file, but Designer.Modified takes care pretty nicely. Good find!Faust
@Jerry Add DesignIDE to the requires section of your package source file. See What ever happened to Proxies.pas?.Faust
New question related to this answer: #8773469Integumentary
Thank you very much for posting this! I've been searching for days on how to do pretty-much exactly this :)Leipzig
I've edited this solution to fix a bug where deleting a TParentComponent instance leaves the TChildComponent instances registered on the Form.Leipzig
F
1

Implement TCollectionItem.GetDisplayName to "name" the collection items.

And concerning the collection: when this is a published property, the collection will automatically be named as the property name.

Be careful to implement GetOwner when you create properties of TPersistent.

Faust answered 6/12, 2011 at 21:21 Comment(3)
No, I don't mean 'Display Name' I'm more than aware of that. I need to create another invisible component to represent each item, a named component.Integumentary
What is the reason it needs to inherit from TComponent?Depreciable
I can inherit it from whatever, so long as it's not anything visual, and it can have a name in the form's class - I assume components are the most fit, but please correct me if I'm wrong.Integumentary
F
1

Use the TComponent.SetSubComponent routine:

type
  TComponent1 = class(TComponent)
  private
    FSubComponent: TComponent;
    procedure SetSubComponent(Value: TComponent);
  public
    constructor Create(AOwner: TComponent); override;
  published
    property SubComponent: TComponent read FSubComponent write SetSubComponent;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TComponent1]);
end;

{ TComponent1 }

constructor TComponent1.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FSubComponent := TComponent.Create(Self);  // Nót AOwner as owner here !!
  FSubComponent.Name := 'MyName';
  FSubComponent.SetSubComponent(True);
end;

procedure TComponent1.SetSubComponent(Value: TComponent);
begin
  FSubComponent.Assign(Value);
end;

I now understand this sub component would be part of a collection item. In that case: no difference, use this method.

Faust answered 6/12, 2011 at 21:45 Comment(7)
That might work, on a much larger scale of course. The example is using one single sub-component without a collection, but it would probably work using it from a TCollectionItem. But will this actually create a new component, saved inside the form's class?Integumentary
Yes. A quote from my link: Unless such a component calls SetSubComponent with IsSubComponent set to True, its published properties will not be saved to the form file.Faust
Actually, I don't see how I can make the properties of this sub-component visible in the Object Inspector because what's really selected to show in the Object Inspector would be the item in the collection editor (TCollectionItem)Integumentary
What's the problem? This sub component of a collection item would exactly look the same as the Title property of a TColumn in a TDBGrid.Columns. Just click on the [+] in the object inspector to expand the sub component with all its properties.Faust
That would be a TPersistent, which is also not what I want. If I wanted only 1 instance of this sub-component, then yes. But I need one of these behind each and every collection item (or in general need to manage a list of these instances).Integumentary
Of course TColumnTitle is of type TPersistent, but I said "will look like" and also mean "will behave like". Just like there are multiple titles, each for every column in a TDBGrid, you can have multiple sub components, each one in every single collection item.Faust
let us continue this discussion in chatFaust

© 2022 - 2024 — McMap. All rights reserved.