14 de maio de 2010

Criando componentes com Delphi - parte IV

Para completar o componente que iniciei no último post faltaram alguns detalhes. Ao criar o componente, usei como classe base o TWinControl, que introduz algumas propriedades úteis, tais como as relacionadas a posicionamento e tamanho.

No entanto, eu preciso de outras que não estão publicadas no Object Inspector - é o caso do Enabled e do Visible. Como elas já existem, publicá-las no Object Inspector é uma questão de redeclará-las na classe no nosso componente:
TWSelecionaArq = class(TWinControl)
{ ... }
published
property Constraints;
property Enabled;
property Visible;
{ ... }
Como já afirmei em outro post, a secção published funciona exatamente igual ao public, isto é, tudo que está nessa área pode ser acessado sem restrições por qualquer outra área do programa. A única diferença é que o que aparece sob o published é também exibido no Object Inspector na fase de desenvolvimento.

Mas nosso componente é composto por outros (uma caixa de edição e um botão) e o valor dessas propriedades são aplicadas apenas no componente principal, não sendo repassadas aos internos. Desse modo, os componentes internos não refletem visualmente sua condição de desabilitados, por exemplo. Para resolver isso no caso do Enabled, basta sobrescrever o método SetEnabled, que é virtual na classe TControl e ajustar manualmente os componentes internos:
procedure TWSelecionaArq.SetEnabled(Value: Boolean);
begin
inherited;

_Edit.Enabled := Value;
_Btn.Enabled := Value;
end;
Falta criar as propriedades específicas do nosso componente. Por exemplo, o nome padrão para o arquivo - que é o texto que deve aparecer na caixa de edição quando a tela é aberta. É através dela também que conseguiremos saber o nome do arquivo informado pelo usuário:
TWSelecionaArq = class(TWinControl)
{ ... }
function GetNomeArquivo : String;
procedure SetNomeArquivo (ANome: String);

published
property NomeArquivo : String read GetNomeArquivo write SetNomeArquivo;
{ ... }
function TWSelecionaArq.GetNomeArquivo : String;
begin
Result := '';
if _Edit <> Nil then
Result := _Edit.Text;
end;

procedure TWSelecionaArq.SetNomeArquivo (ANome: String);
begin
if _Edit <> Nil then
_Edit.Text := ANome;
end;
Veja que a propriedade foi inserida na área published para aparecer no Object Inspector. Note também que não há um membro interno para armazenar o valor dela - o próprio Text da caixa de edição interna ao componente fica responsável por isso.

Também teremos uma propriedade para indicar se o arquivo informado deve ou não existir, disparando uma validação quando o componente perder o foco. A declaração da propriedade em si é simples, apenas lê e grava o valor boolean informado pelo programador. O interesse aqui é outro : planejar um novo evento no componente, que levará em conta a opção do programador em relação à existência prévia do arquivo. O mesmo ponto do programa dará ao programador ainda a chance de fazer sua própria validação, através da resposta a um evento.

Há uma mensagem do Windows chamada WM_KILLFOCUS que é enviada para uma janela (componentes TWinControl também são janelas) quando ela está prestes a perder o foco do teclado. Portanto, vou interceptar essa mensagem e introduzir todo o tratamento de validação nesse ponto. O detalhe é que o componente principal não recebe foco, de modo que o tratamento terá que ser feito com a caixa de edição. A chave pra resolver esse problema é a propriedade WindowProc, existente em todos os controles. Ela armazena um método que é ativado pra tratar todas as mensagens recebidas pelo controle.
TWValidarArquivo = procedure (Sender: TObject;var AValido: Boolean) of object;

TWSelecionaArq = class(TWinControl)
private
_OnValidarArquivo : TWValidarArquivo;
_OldEditProc : TWndMethod;

protected
procedure EditWinProc(var Msg: TMessage);

published
property OnValidarArquivo : TWValidarArquivo read _OnValidarArquivo write _OnValidarArquivo;
end;

{ ... }

constructor TWSelecionaArq.Create(AOwner: TComponent);
begin
inherited;

_OnValidarArquivo := Nil;

_Edit := TEdit.Create (Self);
_Edit.Parent := Self;
_OldEditProc := _Edit.WindowProc;
_Edit.WindowProc := EditWinProc;

{ ... }

procedure TWSelecionaArq.EditWinProc(var Msg: TMessage);
var lValido : boolean;
begin
if Assigned (_OldEditProc) then
_OldEditProc (Msg);

if Msg.Msg = WM_KILLFOCUS then begin
lValido := true;
if DeveExistir and (Trim (NomeArquivo) <> '') then begin
lValido := FileExists(Trim (NomeArquivo));
if not lValido then
MessageBox (Handle, PChar ('Arquivo não existe : ' + NomeArquivo),
'Erro', MB_OK or MB_ICONERROR);

end;

if Assigned (_OnValidarArquivo) and lValido then
_OnValidarArquivo (Self, lValido);

{ Qualquer erro força o foco de volta ao Edit }
if (not lValido) And _Edit.CanFocus then
_Edit.Setfocus;
end;
end;
Duas coisas em especial chamam a atenção nos trechos de código acima. Primeiro, foi criado um novo tipo de dado para representar o evento de validação e uma propriedade com esse tipo foi inserida na área published para que o evento possa ser tratado pelo programador através do Object Inspector. Veja em EditWinProc que essa propriedade representa uma função mas ela só é chamada se o programador associou-lhe um código, isto é, se ele providenciou uma resposta ao evento através do Object Inspector. O programador poderá fazer isso separadamente cada vez que incluir o componente numa tela.

O segundo fato é a forma com que tratamos a propriedade WindowProc. É possível que ela já esteja alimentada; então, antes de sobrescrever o valor dela eu salvo o valor anterior para que a rotina não se perca. Por isso, as primeiras linhas do EditWinProc fazem a chamada da função que existia previamente, se for necessário.

Um tratamento semelhante ao do WindowProc é aplicado ao evento de clique no botão interno. A diferença aqui é que esse evento já existe e precisa apenas ser respondido manualmente:
No construtor :
constructor TWSelecionaArq.Create(AOwner: TComponent);
begin
{ ... }
_Btn := TBitBtn.Create (Self);
_Btn.Parent := Self;
_Btn.OnClick := SelecionarArquivo;

{ ... }

{ Resposta interna do evento de clique: }
procedure TWSelecionaArq.SelecionarArquivo(Sender: TObject);
begin
_OpenDialog.FileName := NomeArquivo;
if _OpenDialog.Execute then begin
NomeArquivo := _OpenDialog.FileName;
if _Edit.CanFocus then
_Edit.SetFocus;
end;
end;
Ao contrário do evento de validação, o OnClick para abrir o diálogo de seleção de arquivo é exclusivamente interno, não podendo ser manipulado pelo programador que está usando o componente.

O download dos fontes desse exemplo pode ser feito clicando aqui. O exemplo foi construído com Delphi 2005.

Mais Informações
Criando componentes com Delphi - parte I, parte II e parte III.

Nenhum comentário :

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.