Версия для печати


События на web-странице
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1319

Сергей Осколков
дата публикации 07-06-2007 09:55

События на web-странице

События на веб-странице

Поводом для написания этой статьи послужил один вопрос на Круглом Столе. В нём автор хотел, чтобы по щелчку на изображении на странице TWebBrowser он мог бы как-то получать адрес (URL) этого изображения. Подобные вопросы были и раньше, в более общей форме их можно сформулировать так: как получить сообщение о событии, произошедшем с каким-нибудь из элементов страницы, загруженной в TwebBrowser? Как получить данные, связанные с этим событием?

Для обычных элементов управления Windows задача решается известным способом - события (events) в рамках VCL, сообщения (messages) - в Windows вообще. Но кнопки, выпадающие списки, поля ввода, изображения и т.д. на веб-страницах в Internet Explorer или WebBrowser не являются элементами управления Windows. И к ним этот подход не применим.

Решение - через события COM. Как известно, элементы веб-страницы представляются браузером в виде иерархии объектов так называемой объектной модели документа, DOM (document object model). В DOM у объектов есть и события. "Верхний" элемент иерарахии - объект "окно", среди его членов есть объект "документ", через который мы можем работать с элементами веб-страницы.

Доступ к этим объектам возможен, например, из сценариев JavaScript в рамках самого html-документа. Также мы имеем доступ к ним из Дельфи c помощью механизма COM, через интерфейсы, описанные в MSHTML_TLB.pas. Этот файл получается в результате импорта библиотеки типов Microsoft HTML object library. В этой библиотеке типов описаны и интерфейсы событий. Интерфейс, представляющий объект документ в целом - IHtmlDocumеnt2. Обратимся к интерфейсу событий этого объекта, а именно HtmlDocumentEvents2. (Есть еще интерфейс HtmlDocumentEvents, но я решил сразу обратиться к этому, по мнемоническому правилу - раз там Document2, то и здесь попробую Events2. :) ).

Для пробы, а именно задачу "попробовать" я ставил, выберем событие OnDoubleClick - если мы добавим обработчик этого события (двойного щелчка), то он не помешает нам "одинарно" щелкать (кликать) мышкой по документу в целях навигации. В интерфейсе HtmlDocumentEvents2 этому событию соответствует метод

function ondblclick(const pEvtObj: IHTMLEventObj): WordBool; dispid -601;

Реализация обработки события

События COM реализуются через интерфейсы обратного вызова, так называемые "стоки" (sink) событий. Клиенты, желающие получать оповещения о событиях на сервере, должны реализовать интерфейс IDispatch и передать серверу ссылку на него (передача осуществляется через метод Advise интерфейса IConnectionPoint сервера). Теперь, при возникновении соответствующего события на COM-сервере, сервер будет обращаться к этому интерфейсу клиента, при этом также передавая параметры произошедшего события. Нам остается только сделать так, чтобы при обращении сервера на клиенте выполнялись нужные нам процедуры обработки событий. Я не настолько большой знаток этой темы (события COM), чтобы излагать её в целом и подробно, поэтому приведу только практическое решение задачи для данного случая. В статье Анатолия Тенцера "Создание модулей расширения Microsoft Office" приводится код (правда, не полный) класса TBaseSink, служащего базовым классом стока, от которого можно наследовать классы, реализующие стоки для конкретных интерфейсов и событий и позаимствованного автором по его словам у Бина Ли (ссылки на статью и на сайт Бина Ли - внизу страницы). В статье также есть пример реализации наследника этого базового класса, в частности метода DoInvoke. Я в свое время основывался на этой статье, только добавил реализацию методов базового класса, отсутствовавших в ней и для данной задачи использовал этот класс.

В классе-потомке нужно переопределить защищенный метод DoInvoke, а именно, позволить в нем серверу вызывать обработчик нужного события и передавать в него параметры события. В общем случае метод должен вызывать соответствующий обработчик события по ID этого события, указанному в интерфейсе событий. Если отвлечься от передачи параметров в обработчики событий, то код в методе DoInvoke мог бы выглядеть примерно так:

case DispId of
   1: if Assigned(FOnEvent1)
    begin
      FOnEvent1;
      Result := S_OK;
    end;
   2:  if Assigned(FOnEvent2)
    begin
      FOnEvent2;
      Result := S_OK;
    end;
   3: if Assigned(FOnEvent3)
    begin
       FOnEvent3;
       Result := S_OK;
    end;
...
   end;
где вместо 1,2,3 и т.д. - ID соответствующих событий, взятые из объявления интерфейса в библиотеке типов.

В нашем конкретном случае это выглядит так:
function TDocSink.DoInvoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
  Flags: Word; var dps: TDispParams; pDispIds: PDispIdList; VarResult,
  ExcepInfo, ArgErr: Pointer): HResult;
type
  POleVariant = ^OleVariant;
begin
  Result := DISP_E_MEMBERNOTFOUND;
  try
  case DispId of
    -601: if Assigned(FOnDblClick)
    then begin
      FOnDblClick(IDispatch(dps.rgvarg^[pDispIds^[0]].dispVal));
      Result := S_OK;
    end;
  end;
  except
    Result := E_UNEXPECTED;
  end;
end;

Если посмотреть интерфейс HtmlDocumentEvents2, то можно увидеть, что событию OnDoubleClick соответствует ID=-601. В методе - это параметр функции DispId. Вся реализация сводится к тому, что к классу добавляется процедурное свойство OnDoubleClick следующего типа:

TSimpleEvent = function (const pEvtObj: IDispatch): WordBool of object;
и в случае DispID=-601, вызывается эта процедура (точнее - функция).

Откуда взялся этот тип? Из интерфейса событий, см. выше. Отличие в том, что я передаю параметр не типа IHTMLEventObj, а его предка IDispatch. Почему? Потому, что я знаю, как передать параметр этого типа в обработчик события.

Рассмотрим передачу параметров в обработчик события. Параметры передаются через параметр (извините повторение) метода DoInvoke dps: TDispParams;- это вариантный массив. Значения его элементов представлены типом TVariantArg (описан в модуле ActiveX), который представляет из себя запись с вариантами, из которой нужно извлечь значение нужного нам типа. В данном случае первый и единственный параметр, передаваемый в обработчик получается как

IDispatch(dps.rgvarg^[pDispIds^[0]].dispVal)

Если бы было несколько параметров разных типов, то они получались бы заменой индекса 0 на 1, 2 и т.д. и соответствующим типом. Например, если бы был ещё второй var параметр булевского типа, то мы могли бы получить его как

dps.rgvarg^[pDispIds^[1]].pbool^

При тестировании программы я столкнулся с тем, что при возникновении ошибки в обработчике события в дальнейшем этот обработчик уже начинал вызываться не при двойном, но и при одинарном щелчке на странице. Я решил, что это связано с тем, что функция в такой ситуации в результате исключительной ситуации не возвращает серверу нужный результат или что-то подобное. Добавление обработчика исключения и возврашение в это случае результата E_UNEXPECTED решило проблему.

Также я добавил в класс конструктор CreateConnected(pSource: IInterface), чтобы не вызывать сначала конструктор, а потом метод Connect класса, а сразу создавать его, подключенным к нужному экземпляру IHtmlDocument2. Код базового класса TBaseSink, а также класса TDocSink, реализующего сток для события двойного щелчка по документу - в модуле sink.pas тестового примера.

Наполняем обработчик события содержанием

Теперь создадим проект с одной формой, поместим на неё TWebBrowser, добавим в uses ссылку на модуль с классом-стоком, в секцию private формы добавим
private
 { Private declarations }
 Doc: IHtmlDocument2;
 DocSink: TDocSink; //класс, реализующий сток
 function DocOnDblClick(const pEvtObj: IDispatch): WordBool; //обработчик события
В обработчике OnCreate формы напишем
procedure TMainForm.FormCreate(Sender: TObject);
begin
  WB.Navigate('about:blank'); //чтобы свойство WB.Document было проинициализировано
  Doc := WB.Document as IHTMLDocument2;
  DocSink := TDocSink.CreateConnected(Doc);
  DocSink.OnDblClick := DocOnDblClick;
end;

Теперь давайте напишем что-то в обработчике события документа DocOnDoubleClick. Как мы видим, у функции есть параметр типа IDispatch, который на самом деле имеет тип IHTMLEventObj, к какому мы его и приведем. Посмотрим, что мы можем получить из этого параметра.И здесь нас ждёт приятный сюрприз: спасибо разработчикам HTML DOM, в этом параметре (Объект события) содержится уйма информации, в том числе ссылка на конкретный объект страницы, в котором произошло событие - свойство srcElement. Кого это интересует в практическом плане, можно посмотреть интерфейс IHTMLEventObj в модуле MSHTML_TLB.pas. Например, давайте напишем такой обработчик :

function TMainForm.DocOnDblClick(const pEvtObj: IDispatch): WordBool;
var EvtObject: IHTMLEventObj;
    Elt: IHtmlElement;
    TagName: string;
begin
  Result := True;
  if not pEvtObj.QueryInterface(IHtmlEventObj, EvtObject) = S_OK
  then exit;
  Memo.Lines.Add('x=' + IntToStr(EvtObject.clientX));
  Memo.Lines.Add('y=' + IntToStr(EvtObject.clientY));
  if (EvtObject.srcElement.QueryInterface(IHtmlElement, Elt) = S_OK)
  and (LowerCase(Elt.tagName) = 'img')
  then Memo.Lines.Add((Elt as IHTMLImgElement).src);
end;

По двойному щелчку мыши на веб-странице мы получаем в Memo клиентские координаты курсора мыши в этот момент и, если элемент, над которым произошел щелчок - картинка, то получаем её адрес. Всё, задача выполнена.

Понятно, что если мы хотим обработать другие события, то нужно в модуле стока объявить процедурный тип, соответствующий нужному методу интерфейса HtmlDocumentEvents2, добавить соответствующее свойство в класс, реализующий сток, в методе DoInvoke класса добавить в оператор case случай c соответствующим событию ID, и написать нужный код в обработчике события в форме, содержащей WebBrowser. В библиотеке типов MSHTML_TLB.pas описаны и событийные интерфейсы для других объектов, кроме документа, но поскольку через параметр pEvtObj мы получаем ссылку на конкретный объект, в котором произошло событие, то для обработки тех событий элементов страницы, которые входят в HtmlDocumentEvents2, можно использовать этот интерфейс. Для специфических событий каких-то элементов, нужно проделать аналогичное описанному здесь применительно к соответствующему интерфейсу, например IHtmlElement и его интерфейсу событий - HtmlElementEvents.

Код тестового приложения прилагается. Ссылки:
Сергей Осколков,
Специально для Королевства Delphi


К материалу прилагаются файлы: