Search code examples
delphivcl

delphi component property: TObjectList<TPicture>


I'm trying to create a VCL component, that lets you insert multiple TImages of different sizes as properties. I was told to best use a TObjectList ( Delphi component with a variable amount of TPictures ), but now I'm struggling to make the single TPictures assignable in the Property editor.

What i have at the moment: (it compiles)

unit ImageMultiStates;

interface

uses
  Vcl.Graphics, Vcl.StdCtrls, System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Forms, Generics.Collections;

type

  TImageMultiStates = class(TImage)
  private
    FPictures: TObjectList<TPicture>;
    procedure SetPicture(Which: Integer; APicture: TPicture);
    function GetPicture(Which: Integer): TPicture;
  public
    Count: integer;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Activate(Which: Integer);
  published
    // property Pictures: TObjectList<TPicture> read GetPicture write SetPicture;
    // property Pictures[Index: Integer]: TObjectList<TPicture> read GetPicture write SetPicture;
    property Pictures: TObjectList<TPicture> read FPictures write FPictures;
  end;

procedure Register;

implementation

constructor TImageMultiStates.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FPictures := TObjectList<TPicture>.Create;
end;

destructor TImageMultiStates.Destroy;
begin
  FPictures.Free;
  inherited Destroy;
end;

procedure TImageMultiStates.SetPicture(Which: Integer; APicture: TPicture);
begin
  FPictures[Which] := APicture;
  if Which=0 then
    Picture.Assign(APicture);
end;

function TImageMultiStates.GetPicture(Which: Integer): TPicture;
begin
  Result := FPictures[Which];
end;

procedure TImageMultiStates.Activate(Which: Integer);
begin
  Picture.Assign(FPictures[Which]);
end;

procedure Register;
begin
  RegisterComponents('Standard', [TImageMultiStates]);
end;

end.

What doesn't work is the final result in the PropertyEditor. It shows one single item named "Pictures", with the value "(TObjectList)". Clicking it doesn't do anything, i don't get a proper editor. Other ideas for the line in question have been commented out, they bring other errors: The first one throws the compiler error "E2008 Incompatible Types", The second one throws "Published property 'Pictures' can not be of type ARRAY".


Solution

  • The IDE has no idea how to edit a TObjectList at design-time, and the DFM streaming system has no idea how to stream a TObjectList. You would have to implement a custom property editor and custom streaming logic. While that is certainly possible, it is a LOT of work.

    What you are attempting to do is better handled by using System.Classes.TCollection instead. Both the IDE and the DFM streaming system have built-in support for handling TCollection editing and streaming automatically for you.

    Try something more like this:

    unit ImageMultiStates;
    
    interface
    
    uses
      System.Classes, Vcl.Controls, Vcl.ExtCtrls, Vcl.Graphics;
    
    type
      TImagePictureItem = class(TCollectionItem)
      private
        FPicture: TPicture;
        procedure PictureChanged(Sender: TObject);
        procedure SetPicture(Value: TPicture);
      public
        constructor Create(Collection: TCollection); override;
        destructor Destroy; override;
      published
        property Picture: TPicture read FPicture write SetPicture;
      end;
    
      TImagePictureEvent = procedure(Sender: TObject; Index: Integer) of object; 
    
      TImagePictures = class(TOwnedCollection)
      private
        FOnPictureChange: TImagePictureEvent;
        function GetPicture(Index: Integer): TImagePictureItem;
        procedure SetPicture(Index: Integer; Value: TImagePictureItem);
      protected
        procedure Update(Item: TCollectionItem); override;
      public
        constructor Create(Owner: TComponent); reintroduce;
        property Pictures[Index: Integer]: TImagePictureItem read GetPicture write SetPicture; default;
        property OnPictureChange: TImagePictureEvent read FOnPictureChange write FOnPictureChange;
      end;
    
      TImageMultiStates = class(TImage)
      private
        FActivePicture: Integer;
        FPictures: TImagePictures;
        function GetPicture(Index: Integer): TPicture;
        procedure PictureChanged(Sender: TObject; Index: Integer);
        procedure SetActivePicture(Index: Integer);
        procedure SetPicture(Index: Integer; Value: TPicture);
        procedure SetPictures(Value: TImagePictures);
      protected
        procedure Loaded; override;
      public
        constructor Create(Owner: TComponent); override;
        function Count: integer;
        property Pictures[Index: Integer]: TPicture read GetPicture write SetPicture;
      published
        property ActivePicture: Integer read FActivePicture write SetActivePicture default -1;
        property Picture stored False;
        property Pictures: TImagePictures read FPictures write SetPictures;
      end;
    
    procedure Register;
    
    implementation
    
    { TImagePictureItem }
    
    constructor TImagePictureItem.Create(Collection: TCollection);
    begin
      inherited Create(Collection);
      FPicture := TPicture.Create;
      FPicture.OnChange := PictureChanged;
    end;
    
    destructor TImagePictureItem.Destroy;
    begin
      FPicture.Free;
      inherited;
    end;
    
    procedure TImagePictureItem.PictureChanged(Sender: TObject);
    begin
      Changed(False);
    end;
    
    procedure TImagePictureItem.SetPicture(Value: TPicture);
    begin
      FPicture.Assign(Value);
    end;
    
    { TImagePictures }
    
    constructor TImagePictures.Create(Owner: TComponent);
    begin
      inherited Create(Owner, TImagePictureItem);
    end;
    
    function TImagePictures.GetPicture(Index: Integer): TImagePictureItem;
    begin
      Result := TImagePictureItem(inherited GetItem(Index));
    end;
    
    procedure TImagePictures.SetPicture(Index: Integer; Value: TImagePictureItem);
    begin
      inherited SetItem(Index, Value);
    end;
    
    procedure TImagePictures.Update(Item: TCollectionItem);
    begin
      if Assigned(FOnPictureChange) then
      begin
        if Item <> nil then
          FOnPictureChange(Self, Item.Index)
        else
          FOnPictureChange(Self, -1);
      end;
    end;
    
    { TImageMultiStates }
    
    constructor TImageMultiStates.Create(Owner: TComponent);
    begin
      inherited Create(Owner);
      FPictures := TImagePictures.Create(Self);
      FPictures.OnPictureChange := PictureChanged;
      FActivePicture := -1;
    end;
    
    procedure TImageMultiStates.Loaded;
    begin
      inherited;
      PictureChanged(nil, FActivePicture);
    end;
    
    function TImageMultiStates.Count: Integer;
    begin
      Result := FPictures.Count;
    end;
    
    procedure TImageMultiStates.PictureChanged(Sender: TObject; Index: Integer);
    begin
      if (FActivePicture <> -1) and ((Index = -1) or (Index = FActivePicture)) then
        Picture.Assign(GetPicture(FActivePicture));
    end;
    
    function TImageMultiStates.GetPicture(Index: Integer): TPicture;
    begin
      Result := FPictures[Index].Picture;
    end;
    
    procedure TImageMultiStates.SetPicture(Index: Integer; Value: TPicture);
    begin
      FPictures[Index].Picture.Assign(Value);
    end;
    
    procedure TImageMultiStates.SetActivatePicture(Value: Integer);
    begin
      if FActivePicture <> Value then
      begin
        if ComponentState * [csLoading, csReading] = [] then
          Picture.Assign(GetPicture(Value));
        FActivePicture := Value;
      end;
    end;
    
    procedure Register;
    begin
      RegisterComponents('Standard', [TImageMultiStates]);
    
      // the inherited TImage.Picture property is published, and you cannot
      // decrease the visibility of an existing property.  However, if you move
      // this procedure into a separate design-time package, you can then use
      // DesignIntf.UnlistPublishedProperty() to hide the inherited
      // Picture property at design-time, at least:
      //
      // UnlistPublishedProperty(TImageMultiStates, 'Picture');
      //
      // Thus, users are forced to use the TImageMultiStates.Pictures and
      // TImageMultiStates.ActivePicture at design-time.  The inherited
      // Picture property will still be accessible in code at runtime, though...
    end;
    
    end.