18 de abril de 2011

Criando um editor de HTML com o TWebBrowser - parte III

Um problema clássico enfrentado por quem cria um editor HTML com o TWebBrowser está em como relatar ao usuário informações a respeito das formatações existentes no ponto do editor onde o cursor está atualmente ou sobre alterações introduzidas no documento editado. Isto porque o componente TWebBrowser não disponibiliza um evento apropriado que possa ser interceptado para exibir tais informações. Não há, por exemplo, eventos específicos para mudanças do documento ou que reflita mudanças na posição atual do cursor.

O problema, portanto, pode ser desmembrado em dois: como descobrir que houve uma mudança no texto ou que houve uma alteração na posição do cursor e como recuperar a formatação existente na posição atual do cursor.

Para a primeira questão, podemos usar um evento definido na interface IHtmlDocument2. Apenas para relembrar: a propriedade Document do TWebBrowser implementa essa interface, bastando fazer um cast para ter acesso às propriedades, métodos e eventos dela:
function TForm1.DOMInterface: IHtmlDocument2;
begin
Result := WebBrowser1.Document As IHtmlDocument2;
end;

Um bom evento do IHtmlDocument2 para obtermos notificações das manutenções que precisamos é o onkeydown. Ele é disparado sempre que uma tecla é pressionada quando o componente está com o foco, não importa se é uma letra, número ou tecla de controle. Portanto, isso inclui as setas de navegação, page-up/page-down, as combinações de tecla, etc.

Para recebermos as notificações, temos que alimentar a propriedade onkeydown com a instância de uma classe que implemente a interface básica IDispatch. Quando o evento ocorre, a função Invoke definida por esta interface é executada, permitindo-nos particularizar uma resposta ao evento. Conforme eu disse no post sobre uso de interfaces em Delphi, podemos criar uma herança de TInterfacedObject e implementar nela os métodos introduzidos pela IDispatch:
TWMSHTMLEvent = procedure(Sender: TObject; Event: IHTMLEventObj) of object;
TWMSHTMLEventConnector = class(TInterfacedObject, IDispatch)
private
FDoc: IHtmlDocument2;
FOnEvent: TMSHTMLEvent;

{ Métodos do IDispatch }
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;

public
constructor Create(ADoc: IHtmlDocument2; Handler: TWMSHTMLEvent);
end;

{ ... }

function TWMSHTMLEventConnector.Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
begin
Result := S_OK;

if Assigned(FOnEvent) then
FOnEvent(Self, FDoc.parentWindow.event);
end;

No trecho de código acima, criei também um tipo chamado TWMSHTMLEvent para representar um evento externo. Este tipo enviará como parâmetros o Sender (componente que gerou o evento) e uma instância de IHTMLEventObj, interface que traz as informações de um evento HTML, tais como o código da tecla pressionada e o elemento HTML existente no ponto onde o evento ocorreu.

Embora sejam obrigatórias para o IDispatch, as demais funções não são relevantes no contexto de um evento HTML, podendo ser implementadas apenas ajustando o valor de retorno como E_NOTIMPL.

Com a classe implementadora do IDispatch definida como mostrado acima, podemos manter a resposta ao evento HTML dentro do próprio Form onde o TWebBrowser está inserido, e, portanto, mantendo separadas as responsabilidades de cada classe. Note também que o Document é passado no construtor. Precisamos dele para obter as informações do último evento ocorrido, dado que é repassado para o Form através do parâmetro que criamos no nosso próprio evento.

Na verdade, nossa classe pode ser utilizada para responder a qualquer um dos eventos definidos em IHtmlDocument2, incluindo aqueles associados aos movimentos do mouse e a cliques em qualquer ponto da página HTML.

E qual é o melhor lugar para fazer a associação entre a instância de nossa classe e o evento onkeydown ? Nós só teremos um Document montado depois que ao menos uma página HTML tenha sido completamente carregada. Portanto, o evento OnNavigateComplete2 do componente TWebBrowser é o local mais apropriado:
procedure TForm1.FormCreate(Sender: TObject);
begin
_KeyDownEvt := Nil;
WebBrowser1.Navigate('http://balaiotecnologico.blogspot.com/');
end;

procedure TForm1.WebBrowser1NavigateComplete2(ASender: TObject; const pDisp: IDispatch; var URL: OleVariant);
begin
if (_KeyDownEvt = Nil) then
_KeyDownEvt :=
TWMSHTMLEventConnector.Create (DomInterface, DoOnWebKeyDown);
DomInterface.onkeydown := _KeyDownEvt As IDispatch;
end;

Veja que a criação da instância da classe TWMSHTMLEventConnector pra conectar os eventos ocorre um única vez. Já a atribuição ao onkeydown acontece toda vez que uma nova página for carregada. Isso se dá por que o documento é criado uma única vez mas suas propriedades são sempre reajustadas para refletir a configuração da nova página. A função DoOnWebKeyDown passada ao construtor é implementada no próprio Form e é ela a responsável por responder ao evento em questão.

Agora, vamos à segunda parte do problema: obter informações sobre a tag HTML existente na posição atual do cursor no editor. Para isso, vamos usar a propriedade selection do IHtmlDocument2, que pode conter tanto o atual ponto de inserção do editor quanto todo o texto selecionado, se houver uma seleção.
function TForm1.GetHtmlAtCurPos: string;
var Element, Rng: OleVariant;
begin
Result := '';
try
Rng := DOMInterface.selection.createRange;

if (DOMInterface.selection.Type_ = 'None')
or (DOMInterface.selection.Type_ = 'Text') then
Element := Rng.parentElement
else if (DOMInterface.selection.Type_ = 'Control') then
Element := Rng.commonParentElement;
except
end;

if (VarType(Element) <> varEmpty) then
begin
Result := Element.innerHtml;
if Length(Result) = 0 then
Result := Element.outerHtml;
end else
Result := '';
end; end;

Basicamente, a função acima recupera a seleção atual (ou o ponto de inserção), descobre quem é o elemento HTML onde ele está inserido e retorna o texto HTML correspondente a este elemento. De posse do elemento HTML, é possível criar rotinas mais sofisticadas para capturar detalhes da formatação, como uso de negrito ou cores.

Embora seja estranho usar um OleVariant para ter acesso a propriedades do elemento HTML, este é um recurso bastante usado quando se trabalha com COM; ainda mais quando o tipo exato retornado depende do contexto em que uma função é chamada. Isso não dá erro de compilação no Delphi porque a ligação com a função real é feita somente em tempo de execução. Neste caso, se tentar usar uma função que não existe, um erro será reportado na execução do programa.

Agora, podemos implementar no Form uma resposta ao evento de tecla pressionada:
procedure TForm1.DoOnWebKeyDown(Sender: TObject; EventObj: IHTMLEventObj);
var code : integer;
begin
EventObj.cancelBubble := True;

{ Recupera a tecla que foi pressionada }
code := EventObj.keyCode;

if not (code in [VK_PRIOR, VK_NEXT, VK_END, VK_HOME, VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN])
then _Modified := True;

_Painel.Text := GetHtmlAtCurPos;
end;

A assinatura da função retratada no quadro acima tem que ser exatamente igual à do evento externo criado por nós pois ela é passada para a classe que implementa o IDispatch, através de seu construtor .

Mais Informações
Criando um editor de HTML com o TWebBrowser: conceitos básicos, disponibilizando interações para o usuário, Interface IHtmlDocument2.

2 comentários :

edulamy disse...

Por que quando utilizo windows 7 (IE 9) meu webbrowser fica bloqueado não deixa eu escrever nada nele e quando utilizo win xp fica tudo normal? Teria a solução para tal?

Luís Gustavo Fabbro disse...

Edu

Isso acontece mesmo se você não atribuir valor para o DomInterface.onkeydown ?

O Windows 7 é mais chato com aspectos de segurança do que o XP era; tente executar seu programa como Administrador pra ver se assim funciona.

[]s

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.