Escrever componente, parte 2

Este artigo irá cobrir como escrever as propriedades avançadas, como escrever streaming personalizado para essas propriedades, e sub-unidades.


Este artigo apareceu originalmente em Delphi Developer 

Copyright Pinnacle Publishing, Inc. Todos os direitos reservados. 


Este artigo é a parte dois de um artigo de três partes sobre componentes. A parte um cobre a criação de componentes básicos, a segunda parte irá cobrir como escrever as propriedades avançadas, como escrever streaming personalizado para essas propriedades, e sub-unidades. A parte final irá abranger editores de propriedade / componente, como escrever editores dedicados para o seu componente / propriedade, e como escrever componentes "escondidos".

Muitas vezes é necessário escrever componentes que desempenham funções mais avançadas. Estes componentes necessitam frequentemente de referência para qualquer um dos outros componentes, possuem propriedades personalizadas de formatos de dados, ou têm uma propriedade que possui uma lista de valores, em vez de um único valor. Nesta parte, iremos explorar vários exemplos relativos a esses vários assuntos, começando com o mais simples.

Referências de Componente

Alguns componentes precisam referênciar outros componentes. TLabel por exemplo, tem uma propriedade "FocusControl". Quando você incluir uma ampersand na propriedade "Caption",  ela sublinha a próxima letra (e &Hello se torna Hello), pressionando a tecla de atalho ALT-H no seu teclado irá desencadear um evento em seu rótulo. Se a propriedade "FocusControl" tiver sido definida, o foco será passado para o controle especificado.

Fazer essa propriedade no seu próprio componente é bastante simples. Tudo que você deve fazer é declarar uma nova propriedade, e definir o tipo de propriedade mais baixo para a classe base que pode aceitar (TWinControl permitirá que qualquer descendente de TWinControl seja utilizado), mas, existem implicações.

type

  TSimpleExample = class(TComponent)

  private

    FFocusControl: TWinControl;

  protected

    procedure SetFocusControl(const Value: TWinControl); virtual;

  public

  published

    property FocusControl: TWinControl read FFocusControl write SetFocusControl;

  end;



procedure TSimpleExample.SetFocusControl(const Value: TWinControl);

begin

  FFocusControl := Value;

end;



Pegue o exemplo acima. Este é um exemplo simples (daí o nome do componente) de como escrever um componente que faça referência a outro componente. Se você tem essa propriedade em seu componente o Object Inspector irá mostrar um combobox com uma lista de elementos que correspondem aos critérios (todos os componentes descendem de TWinControl).

O nosso componente pode fazer algo do tipo

procedure TSimpleExample.DoSomething;

begin

  if (Assigned(FocusControl)) and

     (FocusControl.Enabled) then

    FocusControl.Setfocus;

end;


Primeiro vamos verificar se a propriedade foi atribuída, se assim for vamos definir o foco para ela, mas existem situações em que a propriedade não é Nula, ainda assim o componente que aponta para isso não é mais válido. Isso ocorre com freqüência quando uma propriedade faz referência a um componente que foi destruído.

Felizmente o Delphi fornece-nos uma solução. Toda vez que um componente é destruído ele notifica o proprietário (o nosso formulário) que está sendo destruído. Nesse ponto, todos os componentes possuídos da mesma forma são  notificado deste evento também. Para burlar este evento nós devemos reescrever sobre um método padrão de TComponent chamado "Notificação".

type

  TSimpleExample = class(TComponent)

  private

    FFocusControl: TWinControl;

  protected

    procedure Notification(AComponent: TComponent;

      Operation: TOperation); override;

    procedure SetFocusControl(const Value: TWinControl); virtual;

  public

  published

    property FocusControl: TWinControl read FFocusControl write SetFocusControl;

  end;



procedure TSimpleExample.SetFocusControl(const Value: TWinControl);

begin

  FFocusControl := Value;

end;



procedure TSimpleExample.Notification(AComponent :TComponent;

  Operation : TOperation);

begin

  inherited; //Never forget to call this

  if (Operation = opRemove) and

     (AComponent = FocusControl) then

    FFocusControl := nil;

end;


Agora, quando o nosso componente referenciado é destruído, nós somos notificados, ao ponto em que podemos definir a nossa referência para Nulo (Nil). Note, contudo, que eu disse "cada componente possuído da mesma forma é notificado sobre este evento também"

Isto nos introduz um outro problema. Nós apenas somos notificados de que o componente está sendo destruído se for possuído pelo mesmo formulário. É possível ter o nosso ponto de propriedade aos componentes em outros formulários (ou mesmo sem um dono), e quando estes componentes são destruídos não somos notificados. Mais uma vez, há uma solução.

TComponent introduz um método chamado "FreeNotification". O objetivo do FreeNotification é dizer ao componente (FocusControl) para nos manter avisados quando for destruído.

Uma aplicação seria mais ou menos assim

 type

  TSimpleExample = class(TComponent)

  private

    FFocusControl: TWinControl;

  protected

    procedure Notification(AComponent: TComponent;

      Operation: TOperation); override;

    procedure SetFocusControl(const Value: TWinControl); virtual;

  public

  published

    property FocusControl: TWinControl read FFocusControl write SetFocusControl;

  end;



procedure TSimpleExample.SetFocusControl(const Value: TWinControl);

begin

  if Value <> FFocusControl then

  begin

    if Assigned(FFocusControl) then

      FFocusControl.RemoveFreeNotification(Self);



    FFocusControl := Value;



    if Assigned(FFocusControl) then

      FFocusControl.FreeNotification(Self);

  end;

end;



procedure TSimpleExample.Notification(AComponent: TComponent;

  Operation : TOperation);

begin

  if (Operation = opRemove) and

     (AComponent = FocusControl) then

    FFocusControl := nil;

end;


Quando definir a nossa propriedade FocusControl, em primeiro lugar verifique se ela já está definida para um componente. Se já estiver definida, precisamos dizer ao componente original que já não precisamos mais saber quando é destruído. Depois que nossa propriedade foi definida para o novo valor, nós informamos o novo componente que exige uma notificação quando for libertado. O resto do nosso código continua o mesmo, uma vez que o componente referenciado ainda chama o nosso método de Notificação.

Definição

Esta seção é realmente bastante simples e não vai demorar muito para cobrir. Não duvido que você já esteja familiarizado com a criação dos seus próprios tipos ordinais.

type

  TComponentOption = (coDrawLines,

                      coDrawSolid,

                      coDrawBackground);


Propriedades deste tipo irão mostrar um combobox com uma lista de todos os valores possíveis, mas às vezes será necessário definir uma combinação de muitos (ou todos) destes valores. É aqui que as definições entram.

Type

  TComponentOption = (coDrawLines,

                      coDrawSolid,

                      coDrawBackground);

  TComponentOptions = set of TComponentOption;


A publicação de uma propriedade do tipo TComponentOptions resultaria em um [+] aparecendo ao lado do nome da nossa propriedade. Quando você clica para ampliar a propriedade, você verá uma lista de opções. Para cada elemento em TComponentOption você verá uma propriedade Boolean, você pode incluir / excluir os elementos do seu conjunto definindo o seu valor de verdadeiro / falso.

É simples verificar / alterar elementos de um conjunto de dentro do nosso componente desse jeito.


 if coDrawLines in OurComponentOptions

  then DrawTheLines;


ou

procedure TSomeComponent.SetOurComponentOptions(const Value: TComponentOptions);

begin

  if (coDrawSolid in Value) and

  (coDrawBackground in value) then

    {raise an exception}



  FOurComponentOptions := Value;

  Invalidate;

end;


Propriedades binárias

Às vezes é necessário escrever as suas próprias rotinas streaming para ler e escrever tipos de propriedade personalizada (Isto é como o Delphi lê / escreve as propriedades do Topo e Esquerda para os componentes não visíveis sem realmente publicar estas propriedades no objeto inspetor).

Por exemplo, eu escrevi uma vez um componente para moldar um formulário baseado em uma imagem bitmap. O meu código na hora de converter um bitmap de uma janela-região ficou extremamente lento e não poderia ser de nenhuma utilidade no tempo de execução. Minha solução foi a de converter os dados em desenho, e stream os dados binários que resultaram da conversão. Criar um propriedades binárias é um processo de três etapas.

1. Escreva um método para gravar os dados.
2. Escrever um método para ler os dados.
3. Diga Delphi que temos uma propriedade binária, e passar os nossos métodos de leitura / escrita.

type

  TBinaryComponent = class(TComponent)

  private

    FBinaryData : Pointer;

    FBinaryDataSize : DWord;

    procedure WriteData(S : TStream);

    procedure ReadData(S : TStream);

  protected

    procedure DefineProperties(Filer : TFiler); override;

  public

    constructor Create(AOwner : TComponent); override;

  end;


DefineProperties é chamado pelo Delphi quando ele precisa de stream o nosso componente. Tudo o que precisamos de fazer é substituir este método, e adicionar uma propriedade usando ou o TFiler.DefineProperty ou TFiler.DefineBinaryProperty.

procedure TFiler.DefineBinaryProperty(const Name: string;

  ReadData, WriteData: TStreamProc; HasData: Boolean);



constructor TBinaryComponent.Create(AOwner: TComponent);

begin

  inherited;

  FBinaryDataSize := 0;

end;



procedure TBinaryComponent.DefineProperties(Filer: TFiler);

var

  HasData : Boolean;

begin

  inherited;

  HasData := FBinaryDataSize <> 0;

  Filer.DefineBinaryProperty('BinaryData',ReadData,

    WriteData, HasData );

end;



procedure TBinaryComponent.ReadData(S: TStream);

begin

  S.Read(FBinaryDataSize, SizeOf(DWord));

  if FBinaryDataSize > 0 then begin

    GetMem(FBinaryData, FBinaryDataSize);

    S.Read(FBinaryData^, FBinaryDataSize);

  end;

end;



procedure TBinaryComponent.WriteData(S: TStream);

begin

  //This will not be called if FBinaryDataSize = 0



  S.Write(FBinaryDataSize, Sizeof(DWord));

  S.Write(FBinaryData^, FBinaryDataSize);

end;


Em primeiro lugar nós substituimos DefineProperties. Uma vez que fizemos, isso vamos definir uma propriedade binária com os valores --

-BinaryData: O nome invisível da propriedade a ser utilizado.
-ReadData: O procedimento responsável pela leitura dos dados.
-WriteData: O procedimento responsável por escrever os dados.
-HasData: Se isto é falso, o procedimento WriteData nem sequer é chamado.

Persistência

Uma breve explicação da persistência está na ordem que nós devemos fazer referência a ela nas seções seguintes. Persistência é o que o torna possível para o Delphi ler e escrever as propriedades de todos os seus componentes. TComponent deriva de uma classe denominada TPersistent. TPersistent Delphi é simplesmente uma classe capaz de ter as suas propriedades lidas e escritas pelo Delphi, o que significa que os descendentes de qualquer TPersistent também têm essa mesma capacidade.

Coleções

À medida que nós progredimos neste artigo nós cobrimos propriedades de componente de maior complexidade. As coleções são um dos mais complexos tipos de propriedade "standard" Delphi. Se você jogar um TDBGrid em um formulário e olhar para as suas propriedades no Object Inspector, você verá uma propriedade chamada "Colunas".

Colunas é uma propriedade de coleção, quando você clica no botão [..] verá uma pequena janela pop-up. Esta janela é um editor de propriedade padrão para propriedades TCollection (e descendentes de TCollection).

Sempre que você clica no botão "Novo" você verá um novo item adicionado (um item TColumn), clicar sobre o item irá selecionar o objeto Inspector para que você possa alterar as suas propriedades e eventos. Como isso é feito?

As propriedades das Colunas descendem de TCollection. TCollection é semelhante a um array, que contém uma lista do TCollectionItem. Pelo fato de o TCollection ser descendente de TPersistent, ele é capaz de fazer o stream dessa lista de itens, igualmente, o TCollectionItem também é descendente de TPersistent e também pode transmitir as suas propriedades. Então o que temos é um item tipo-array capaz de fazer streaming de todos os seus elementos e suas propriedades.

A primeira coisa a fazer ao criar a nossa própria estrutura baseada em TCollection / TCollectionItem consiste em definir a nossa CollectionItem.

(Veja OurCollection.pas)

type

  TOurCollectionItem = class(TCollectionItem)

  private

    FSomeValue : String;

  protected

    function GetDisplayName : String; override;

  public

    procedure Assign(Source: TPersistent); override;

  published

    property SomeValue : String

      read FSomeValue

      write FSomeValue;

  end;


O que fizemos aqui foi criar um descendente de TCollectionItem. Nós adicionamos uma propriedade token denominada "SomeValue", sobrepondo a função GetDisplayName (para alterar o texto que é mostrado no editor padrão), e finalmente sobrepondo o método Assign a fim de permitir o TOurCollectionItem a ser atribuído a outro TOurCollectionItem. Se vamos omitir o passo final, então, o método Assign da nossa coleção de classe não funcionará!

procedure TOurCollectionItem.Assign(Source: TPersistent);

begin

  if Source is TOurCollectionItem then

    SomeValue := TOurCollectionItem(Source).SomeValue

  else

    inherited; //raises an exception

end;



function TOurCollectionItem.GetDisplayName: String;

begin

  Result := Format('Item %d',[Index]);

end;


A implementação de TOurCollection é muito mais complexa, e exige que façamos mais um pouco de trabalho.

 TOurCollection = class(TCollection)

  private

    FOwner : TComponent;

  protected

    function GetOwner : TPersistent; override;

    function GetItem(Index: Integer): TOurCollectionItem;

    procedure SetItem(Index: Integer; Value:

      TOurCollectionItem);

    procedure Update(Item: TOurCollectionItem);

  public

    constructor Create(AOwner : TComponent);



    function Add : TOurCollectionItem;

    function Insert(Index: Integer): TOurCollectionItem;



    property Items[Index: Integer]: TOurCollectionItem

      read GetItem

      write SetItem;

  end;


Há uma série de itens para cobrir baseados na declaração de classe acima, por isso vamos iniciar de cima e cobrir um de cada vez.

GetOwner É um método virtual introduzido no TPersistent. Isto precisa ser substituído, uma vez que o código padrão para esse método retorna Nulo. Na nossa aplicação vamos alterar o construtor para receber apenas um parâmetro (AOwner: TComponent). Nós armazenamos esse parâmetro em FOwner, que é então passado como resultado de GetOwner (TComponent descende de TPersistent, por isso, portanto, é um tipo de resultado válido).

constructor TOurCollection.Create(AOwner: TComponent);

begin

  inherited Create(TOurCollectionItem);

  FOwner := AOwner;

end;


function TOurCollection.GetOwner: TPersistent;

begin

  Result := FOwner;

end;


O Create não só armazena o proprietário (que é exigido para o Object Inspector para funcionar corretamente), também diz ao Delphi qual classe o nosso CollectionItem é chamando "inherited Create(TOurCollectionItem)".

GetItem / SetItem são declarados da mesma forma que eles estão em TCollection, mas em vez de trabalhar no TCollectionItem eles trabalham na nossa nova classe TOurCollectionItem. Estes são utilizados em nossas propriedades "itens" mais adiante.

Atualizar como acima é um substituto direto do original, trabalhando em nossa nova classe CollectionItem em vez disso.

Adicionar / Inserir ambos são responsáveis por adicionar itens à lista, estes dois foram substituídos para retornar objetos da classe adequada.

Por último, uma propriedade "itens" é introduzida para substituir a propriedade Items original, novamente, para que sejamos devolvidos com um resultado TOurCollectionItem ao invés de um TCollectionItem nos economizando dos problemas desnecessários de typecasting o resultado a cada vez.

Finalmente um exemplo da implantação desse tipo de propriedade em um componente nosso.

 TCollectionComponent = class(TComponent)

  private

    FOurCollection : TOurCollection;

    procedure SetOurCollection(const Value:

      TOurCollection);

  public

    constructor Create(AOwner : TComponent); override;

    destructor Destroy; override;

  published

    property OurCollection : TOurCollection

      read FOurCollection

      write SetOurCollection;

  end;


É simples assim. Depois que a nossa classe TCollection estiver escrita, todo o trabalho duro estará feito. O nosso construtor cria a classe coleção, o destruidor o destrói, e SetOurCollection faz isso.

constructor TCollectionComponent.Create(AOwner: TComponent);

begin

  inherited;

  FOurCollection := TOurCollection.Create(Self);

end;



destructor TCollectionComponent.Destroy;

begin

  FOurCollection.Free;

  inherited;

end;



procedure TCollectionComponent.SetOurCollection(

  const Value: TOurCollection);

begin

  FOurCollection.Assign(Value);

end;


Como mencionado anteriormente, o (Self) passou para o TOurCollectionItem. Create é armazenado em TOurCollection's FOwner variável, que é passada como o resultado de GetOwner. Um ponto a notar aqui é que no SetOurCollection nós não definimos FOurCollection: = valor enquanto você está substituindo o objeto (os objetos são simplesmente ponteiros), nós Atribuimos nossa propriedade ao valor.

As versões posteriores do Delphi tornam tudo isso mais simples ainda. Em vez de ter que substituir o GetOwner na nossa classe Coleção, podemos agora derivar a nossa coleção de TOwnedCollection. TOwnedCollection é um wrapper para TCollection com este trabalho feito para nós.

Sub-propriedades

Anteriormente neste artigo nós vimos como era possível criar uma propriedade expansível. A limitação da técnica anterior era de que cada sub-item apareceu como uma propriedade Boolean. Esta seção seguinte irá demonstrar como criar propriedades expansíveis que podem conter qualquer tipo de propriedade.

Se um componente requer uma propriedade que é um tipo record, esta poderia ser facilmente implementada, expondo cada propriedade separadamente. Se, no entanto, nosso componente precisa introduzir duas ou mais unidades de um mesmo complexo, nosso visualizador Object Inspector subitamente se torna muito complicado.

A resposta é criar uma estrutura complexa (semelhante a um registro ou objeto) e publicar essa estrutura como uma propriedade quando necessário. O problema óbvio é que o Delphi não sabe como exibir esta propriedade, a menos que nós digamos isso. Criar um editor de propriedade full blown (com diálogos etc) seria excesso, então felizmente o Delphi forneceu uma solução. Como mencionado anteriormente, o streaming interno do Delphi é baseado em torno da classe TPersistent. O primeiro passo é, portanto, derivar nossa estrutura complexa a partir desta classe.

type

  TExpandingRecord = class(TPersistent)

  private

    FIntegerProp : Integer;

    FStringProp : String;

    FCollectionProp : TOurCollection;

    procedure SetCollectionProp(const Value:

      TOurCollection);

  public

    constructor Create(AOwner : TComponent);

    destructor Destroy; override;



    procedure Assign(Source : TPersistent); override;

  published

    property IntegerProp : Integer

      read FIntegerProp

      write FIntegerProp;

    property StringProp : String

      read FStringProp

      write FStringProp;

    property CollectionProp : TOurCollection

      read FCollectionProp

      write SetCollectionProp;

  end;


Na estrutura acima nós criamos um descendente de TPersistent e dado três propriedades exemplo, um integer, uma string, e a coleção que criamos no início deste artigo TOurCollection.

O construtor e destruidor simplesmente cuida da criação e destruindo o objeto CollectionProp, e o SetCollectionProp é implantado para impedir que este objeto de referência seja perdido (nós Atribuimos (valor), ao invés de FCollectionProp: = valor). Considerando que Atribuir é executado de forma a permitir-nos a atribuir as propriedades de TExpandableRecord para outro TExpandableRecord (novamente, isso é necessário, pois vamos precisar para Atribuir-la quando ela for finalmente implementada como uma propriedade de um componente).

procedure TExpandingRecord.Assign(Source: TPersistent);

begin

  if Source is TExpandingRecord then

    with TExpandingRecord(Source) do begin

      Self.IntegerProp := IntegerProp;

      Self.StringProp := StringProp;



      //This actually assigns

      Self.CollectionProp := CollectionProp;

    end else

      inherited; //raises an exception

end;



constructor TExpandingRecord.Create(AOwner : TComponent);

begin

  inherited Create;

  FCollectionProp := TOurCollection.Create(AOwner);

end;



destructor TExpandingRecord.Destroy;

begin

  FCollectionProp.Free;

  inherited;

end;



procedure TExpandingRecord.SetCollectionProp(const Value: TOurCollection);

begin

  FCollectionProp.Assign(Value);

end;


Uma vez que este código foi implementado, todo o trabalho duro está feito. Para implementar esta classe como um objeto agora é totalmente em frente.

(Veja ExpandingComponent.pas)

TExpandingComponent = class(TComponent)

  private

    FProperty1,

    FProperty2,

    FProperty3 : TExpandingRecord;

  protected

    procedure SetProperty1(const Value :

      TExpandingRecord);

    procedure SetProperty2(const Value :

      TExpandingRecord);

    procedure SetProperty3(const Value :

      TExpandingRecord);

  public

    constructor Create(AOwner : TComponent); override;

    destructor Destroy; override;

  published

    property Property1 : TExpandingRecord

      read FProperty1

      write SetProperty1;

    property Property2 : TExpandingRecord

      read FProperty2

      write SetProperty2;

    property Property3 : TExpandingRecord

      read FProperty3

      write SetProperty3;

  end;


O construtor terá que criar as três propriedades como objetos usados, o destruidor obviamente vai ter que destruí-los. Os procedimentos serão SetPropertyX vão Atribuir (Valor) para o objeto correto.

Compile esse componente em um pacote e coloque TExpandingComponent no seu formulário. Olhando para o objeto inspector você irá notar que os nossos TExpandingRecord propertys todos têm um [+] ao lado deles, clicar neste botão irá expandir nossa propriedade para revelar todas as suas sub-propriedades.

À primeira vista tudo parece bem, as nossas propriedades são mostradas com um sub-conjunto de propriedades expansível, mas nem tudo é tão perfeito como parece. Clicando no Botão [..] de nossa "CollectionProp" não invoque o editor padrão TCollection, na verdade, ele não faz nada.

Você pode perguntar-se "O que fizemos errado?", A resposta é "nada!". O erro aqui não é da nossa parte, a culpa recai sobre o desenvolvedores do Delphi, Borland. Embora eu goste de pensar nos desenvolvedores de Delphi como infalíveis, eles não o são, e às vezes cometem erros, e este é um perfeito exemplo de um.


Quando você registrar um editor de propriedade você pode limitar o alcance do componente que se aplica. Você pode especificar que ele deve funcionar apenas em determinados nomes de propriedade, ou só em certos componentes, e é isso que eles fizeram. Apesar da arquitetura do Delphi que define a menor forma-objeto ser capaz de streaming TPersistent, alguém da Borland registrou o editor de propriedade para trabalhar apenas em objetos que devem descender de TComponent. Um lapso, estou certo, mas que na verdade não nos ajuda em nada.

A resposta para este problema é descer a nossa TExpandableRecord de TComponent em vez de TPersistent, então editor de propriedade será invocado. O problema é que o padrão do editor de propriedade do Delphi para propriedades do tipo TComponent (e descendentes), mostra um combobox em vez de mostrar uma visão expansível de sub-propriedades.

A solução para todo este dilema reside nos editores de propriedade, e será coberta na parte final deste artigo.