sábado, 25 de agosto de 2007

Usando Coleções no Delphi

Bom dia. Como de costume, as vezes eu venho a este blog postar soluções envolvendo algum aspecto de programação que eu precisei usar, ou que me deu vontade de escrever. Como eu disse ontem, não estou tendo tempo suficiente para postar muitas coisas, então vou postando coisas que eu já escrevi ... Hoje falaremos sobre Coleções no Delphi.

Para entender inicialmente o comportamento de uma coleção, tente colocando um componente TStatusBar em um form, escolha a propriedade Panels e clique em Add na janela "Editing Panels".

Depois deste passo adicione um TStatusPanel no editor. Agora clique no TStatusPanel que apareceu na janela e veja as mudanças na Object Inspector. No lugar de ver as propriedades do componente TStatusBar, você está agora vendo StatusBar1.Panels[0] refletido na Object Inspector...

Complicado ?? Se você está lendo isso e acompanhando no Delphi, eu creio que não, pois é uma coisa aparentemente simples, não é ?? Tudo isso que você fez são operações envolvendo uma Coleção. Um descendente de TCollectionItem, um TCollection que gerencia a lista dos TCollectionItems e um componente que contém o TCollection como uma de suas propriedades. Em nosso exemplo com o TStatusBar, ele contém um descendente de TCollection chamado TPanels e TPanels gerencia a lista dos itens que são descendentes de TCollectionItem, no caso, TPanel.Veja que cada TCollectionItem contém uma ou mais propriedades; Neste exemplo, TPanel contém as propriedades Alignment, Bevel, Style, Text e Width. Esta lista muda dependendo da definição do descendente de TCollectionItem.


Implementando uma coleção com TCollection e TCollectionItem

Primeiramente, em uma nova unit, defina três novas classes descendentes de TCollectionItem, TCollection e TComponent:


  TVindeCollectionItem = class(TCollectionItem)

  TVindeCollection = class(TCollection)

  TVindeComponent = class(TComponent)

Para fazer a classe TVindeCollectionItem funcionar, você precisa definir uma ou mais propriedades que contenham as informações a serem "colecionadas" pelo mecanismo que estamos estudando. O nosso exemplo define uma propriedade Texto do tipo String e outra chamada Blah do tipo integer. Você ainda precisará fazer um override no método GetDisplayName para formar a string que será exibida para cada item no editor de propriedades da coleção:
  TVindeCollectionItem = class(TCollectionItem)
  private
    FTexto: String;
    FBlah: Longint;
    function GetDisplayName: String; override;
    procedure SetTexto(const Value: String);
    procedure SetBlah(const Value: Longint);
  published
    property Texto: String read FTexto write SetTexto;
    property Blah: Longnt read FBlah write SetBlah;
  end;

Agora, defina o descendente de TCollection. Esta classe irá manter a lista de Itens que pertencem a mesma. Faça um override do método GetOwner para permitir salvar e recuperar os itens via persistência e gerenciar um array dos descendentes de TCollectionItem.
Também será necessário definir um novo construtor. O parâmetro passado para este construtor será a referência ao componente que contém a Collection. Ainda no construtor, você deverá chamar o construtor da classe ancestral, informando a classe do Item a ser armazenado na Coleção. Você pode acessar o tipo da mesma usando a propriedade ItemClass, que retorna a classe (descendente de TCollectionInfo) dos itens que pertencem à coleção.
  TVindeCollection = class(TCollection)
  private
    FVindeComponent: TVindeComponent;
    function GetItem(Index: Integer): TVindeCollectionItem;
    procedure SetItem(Index: Integer; Value: TVindeCollectionItem);
  protected
    function GetOwner: TPersistent; override;
  public
    constructor Create(VindeComponent: TVindeComponent);
    function Add: TVindeCollectionItem;
    property Items[Index: Integer]: TVindeCollectionItem read GetItem write SetItem; default;
  end;

Finalmente, defina o componente que irá conter a coleção. O componente irá possuir uma propriedade que é descendente de TCollection, que é justamente a classe que definimos anteriormente. Como é de praxe na construção de componentes, você deverá ter um campo privado, métodos de acesso ao campo e ainda a criação do mesmo no Create do componente e finalizado no Destroy. Mais sobre criação de componentes vc encontrará no bom e velho Google, ou em nosso knal (#DelphiX) na rede Freenode.
  TVindeComponent = class(TComponent)
  private
    FItems: TVindeCollection;
    procedure SetItems(Value: TVindeCollection);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Items: TVindeCollection read FItems write SetItems;
  end;

E aqui vai a unit completa ...
{---------------------------------------------------------------}
unit Collec1;

interface

uses Classes;

type

  TVindeComponent = class;

  TVindeCollectionItem = class(TCollectionItem)
  private
    FTexto: String;
    FBlah: Longint;
    function GetDisplayName: String; override;
    procedure SetTexto(const Value: String);
    procedure SetBlah(const Value: Longint);
  public
  published
    property Texto: String read FTexto write SetTexto;
    property Blah: Longint read FBlah write SetBlah;
  end;

  TVindeCollection = class(TCollection)
  private
    FVindeComponent: TVindeComponent;
    function GetItem(Index: Integer): TVindeCollectionItem;
    procedure SetItem(Index: Integer; Value: TVindeCollectionItem);
  protected
    function GetOwner: TPersistent; override;
  public
    constructor Create(VindeComponent: TVindeComponent);
    function Add: TVindeCollectionItem;
    property Items[Index: Integer]: TVindeCollectionItem read GetItem write SetItem; default;
  end;

  TVindeComponent = class(TComponent)
  private
    FItems: TVindeCollection;
    procedure SetItems(Value: TVindeCollection);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Items: TVindeCollection read FItems write SetItems;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Sample', [TVindeComponent]);
end;


{ TVindeCollectionItem }


//Nota: O comportamento padrão do método GetDisplayName é retornar o classname do Item

function TVindeCollectionItem.GetDisplayName: string;
begin
  Result := Texto;
  if Result = '' then Result := inherited GetDisplayName;
end;

procedure TVindeCollectionItem.SetTexto(const Value: String);
begin
  if FTexto <> Value then
    FTexto := Value;
end;

procedure TVindeCollectionItem.SetBlah(const Value: Longint);
begin
  if FBlah <> Value then
    FBlah := Value;
end;


{ TVindeCollection }

constructor TVindeCollection.Create(VindeComponent: TVindeComponent);
begin
  inherited Create(TVindeCollectionItem);
  FVindeComponent := VindeComponent;
end;

function TVindeCollection.Add: TVindeCollectionItem;
begin
  Result := TVindeCollectionItem(inherited Add);
end;

function TVindeCollection.GetItem(Index: Integer): TVindeCollectionItem;
begin
  Result := TVindeCollectionItem(inherited GetItem(Index));
end;

procedure TVindeCollection.SetItem(Index: Integer; Value: TVindeCollectionItem);
begin
  inherited SetItem(Index, Value);
end;

function TVindeCollection.GetOwner: TPersistent;
begin
  Result := FVindeComponent;
end;


{ TVindeComponent }

constructor TVindeComponent.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FItems := TVindeCollection.Create(Self);
end;

destructor TVindeComponent.Destroy;
begin
  FItems.Free;
  inherited Destroy;
end;

procedure TVindeComponent.SetItems(Value: TVindeCollection);
begin
  FItems.Assign(Value);
end;

end.
{---------------------------------------------------------------}

Notas
-----

Em nosso exemplo, nós não reescrevendo o método Assign da classe TCollectionItem; no entanto, este método é essencial para permitir o carregamento e o salvamento das informações contidas nos campos da classe que acabamos de criar, TVindeCollectionItem. Aki vai um exemplo de como nós podemos reescrever o método Assign de forma a permitir o salvamento das informações:
procedure TVindeCollectionItem.Assign(Source: TPersistent);
begin
  if Source is TVindeCollectionItem then
  begin
    Texto := TVindeCollectionItem(Source).Texto;
    Blah := TVindeCollectionItem(Source).Blah;
    Exit;
  end;
  inherited Assign(Source);
end;

Não foi incluído em nosso pequeno projeto a lógica necessária para notificar à classe TCollection quando um dos itens na coleção foi alterado. Isto é particularmente importante em um controle visual, como o nosso citado TStatusBar. A classe TCollection nos fornece um método virtual chamado Update para permitir a notificação por parte dos itens da coleção. Veja as classes TStatusBar ou mesmo THeaderControl para mais detalhes ...

Só espero que este texto tenha ajudado um pouco, pois não é fácil falar de uma construção de classes que, em sua essência, é complexa de entender e trabalhar ... No mais, até logo ... Uma outra hora dessas posto mais coisas de programação ...