Сергей Осколков дата публикации 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
Doc: IHtmlDocument2;
DocSink: TDocSink;
function DocOnDblClick(const pEvtObj: IDispatch): WordBool;
В обработчике OnCreate формы напишем
procedure TMainForm.FormCreate(Sender: TObject);
begin
WB.Navigate('about:blank');
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.
Код тестового приложения прилагается.
Ссылки:
К материалу прилагаются файлы:
Обсуждение материала [ 26-05-2008 10:00 ] 27 сообщений |