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 .