Подключение к событиям объектной модели DHTML при использовании WebBrowser-control

Автор: Тимофей Чадов
The RSDN Group

Источник: RSDN Magazine #0
Введение
Немного теории
События в Dynamic HTML
Связывание кода с событиями
Подключение к наборам событий. Механизм Connection Point
Подключение к событиям с использованием MFC
Подключение к событиям с использованием ATL
Подключение к индивидуальным событиям. Обработчики IHTMLElement::onXXX
Выбор момента для регистрации
Получение информации о событии
Распространение событий
Полезные свойства объекта event
Использование из C++
CHtmlEventSink - удобное подключение к событиям
Как это работает и как это использовать
Заключение

DHTMLSpy.zip - 48 KB


Рис. 1 Демонстрационное приложение DHTMLSpy

Введение

Предлагаемая вашему вниманию статья посвящена одному из интересных аспектов написания программ, использующих ActiveX-элемент WebBrowser. В ней будет рассмотрено, как можно подключиться к событиям объектной модели браузера, а значит, позволить приложению использовать те преимущества, которые дает DHTML. Я попытаюсь рассказать, как непосредственно обрабатывать события, возбуждаемые объектной моделью браузера в процессе работы пользователя со страницей.

Немного теории

Объектную модель html-документа можно представить как способ связи между html-страницами и браузером. Объектная модель - это представление объектов, методов, свойств и событий браузера в виде, удобном для работы с ними из кода программы или скрипта.

События в Dynamic HTML

Одна из сильных сторон Dynamic HTML - это события. Как вы, наверное, знаете, Windows - операционная система, управляемая событиями. Когда вы выполняете некоторое действие в среде Windows, например, щелкаете на окне мышью, операционная система формирует сообщение о событии. Получая сообщения Windows, WebBrowser формирует на их основе события своей объектной модели. Например, когда вы щелкаете мышью внутри страницы, браузер получает это событие и пропускает его через всю иерархию объектов страницы, инициируя, таким образом, множество событий onmousedown, onmouseup и onclick для всех элементов страницы, которых это касается. Если нужно обработать событие, возвратив некоторый результат, то инструкции идут от кода обратно к браузеру с помощью все той же объектной модели.

Связывание кода с событиями

Как и большинство ActiveX-элементов, WebBrowser является источником событий, подключаемых через стандартный механизм Connection Point. К числу таких событий относятся: BeforeNavigate, DocumentComplete и т.п. Несомненно, они важны для управления приложением в целом, однако, зачастую их возможностей явно недостаточно. Например, если вы захотите узнать о перемещении мыши над элементами страницы или о нажатии клавиши на клавиатуре, или вообще быть в курсе всех событий, которые можно использовать в скриптах DHTML, придется использовать DOM DHTML.

Существует несколько способов подключения к событиям объектной модели DHTML.

Способ 1. Connection Point

Оказывается, что не только сам WebBrowser, но и практически каждый элемент объектной модели DHTML является источником собственных событий. Эти события поддерживаются через исходящий (outgoing) интерфейс HTMLElementEvents (обратите внимание, буква I в названии интерфейса отсутствует). Этот интерфейс определяет набор событий, которые могут происходить с html-элементом в процессе работы пользователя со страницей. Некоторые элементы могут инициировать собственные (специфичные только для них) события. Как правило, это отражается в названии соответствующего интерфейса. Например, объект document поддерживает событийный интерфейс HTMLDocumentEvents.

Способ 2. Обработчики put_onXXX

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

<SCRIPT LANGUAGE="jscript">
function mousedownhandler()
{
    // функция обработчик
}

function afterPageLoads()
{
    someElement.onmousedown = mousedownhandler;
}
</SCRIPT>

Возникает вопрос. А можно ли получить нечто подобное из клиента на С++? Оказывается можно, причем похожим образом. Достаточно создать свою функцию обработки и зарегистрировать ее. Как это сделать, будет показано ниже.

Способ 3. Расширение объектной модели браузера. Механизм window.external

Наконец, еще один способ заставить приложения реагировать на события - это использовать механизм window.external. Напомню, что приложение, использующее WebBrowser, может расширить стандартную объектную модель документа WebBrowser, добавляя в нее собственные объекты, методы и свойства. Для этого в приложении нужно реализовать интерфейс IDocHostUIHandler, в методе GetExternal которого возвращать указатель на IDispatch объекта, расширяющего объектную модель WebBrowser.

Доступ к этому объекту из скрипта возможен через объект window.external. В этом случае соответствующий скрипт может выглядеть, например, так:

function mousedownhandler()
{
    // функция обработки
    window.external.onmousedown();
}

Думаю, идея понятна. Однако этот способ удобен, если вы сами формируете страницу или уже используете объект external. В других случаях применение этого подхода нецелесообразно. В этой статье будет описаны другие подходы.

Подключение к наборам событий. Механизм Connection Point

Начиная с IE 4.0, практически каждый элемент объектной модели DHTML поддерживает событийный интерфейс HTMLElementEvents2. Этот интерфейс определяет набор событий, которые могут происходить с html-элементом в процессе работы пользователя со страницей. WebBrowser использует стандартный механизм подключения к подобным событиям - Connection Points (рис. 2).


Рис. 2 Подключение к событиям DOM DHTML через точки соединения

Несмотря на то, что механизм подключения к событиям через точки соединения описан в специальной литературе достаточно подробно, не будет лишним еще раз напомнить принцип его работы, особенно в контексте WebBrowser'а. Чтобы подключиться к событийному интерфейсу элемента, необходимо проделать следующее.

Реализовать соответствующий disp-интерфейс обратного вызова. Все события, возбуждаемые элементом, будут направляться в IDispatch:Invoke этой реализации. При этом параметр dispMember будет содержать DISPID-события.

С точки зрения реализации IDispatch ситуация значительно упрощается тем, что функции-обработчики не требуют ни входных, ни выходных параметров. Кроме того, все уведомления о событиях будут приходить сразу в IDispatch::Invoke, а значит, реализация событийного интерфейса становится до безобразия простой.

Таким образом, в простейшем случае реализация disp-интерфейса может выглядеть так:

Листинг 1.
STDMETHODIMP CEventSink::Invoke(DISPID dispidMember,
    REFIID riid,
    LCID lcid,
    WORD wFlags,
    DISPPARAMS* pdispparams,
    VARIANT* pvarResult,
    EXCEPINFO* pexcepinfo,
    UINT* puArgErr)
{
    switch (dispidMember)
    {
        case DISPID_HTMLELEMENTEVENTS2_ONCLICK:
            OnClick();
            break;

        // Другие события
        ...

        default:
            break;
    }
    return S_OK;
}

STDMETHODIMP CEventSink::OnClick()
// Обрабатываем событие IHTMLElementEvetns2::OnClick
{
    ...
}
ПРИМЕЧАНИЕ
Идентификаторы, подобные DISPID_HTMLELEMENTEVENTS2_ONCLICK, определены в файле mshtmdid.h. Несмотря на то, что интерфейсы вида HTMLXXXEvents работают в Internet Explorer, начиная с версии 4.0, соответствующие заголовочные файлы появились только в Internet Client SDK (INetSDK) для IE 5.0 и выше. Поэтому, прежде чем компилировать примеры к этой статье, убедитесь, что у вас присутствуют свежие версии заголовочных файлов. Кстати, Platform SDK (содержащий InetSDK) за ноябрь 2001 можно найти на CD к этому номеру журнала.

Половина дела сделана, осталось подключиться к html-элементу, события которого нужно отслеживать.

Для этого необходимо:

  • Получить интерфейс IConnectionPointContainer требуемого элемента.
  • Через вызов метода IConnectionPointContainer::FindConnectionPoint получить точку соединения (интерфейс IConnectionPoint) элемента. В параметре REFIID riid этого метода следует передать идентификатор интересующего нас событийного интерфейса. Для событий html-элемента это, скорее всего, будет HTMLElementEvents2.
  • Ну и, наконец, через вызов IConnectionPoint::Advise подключиться к событиям элемента. После того, как уведомления о событиях станут не нужны, отсоединиться от точки соединения, вызвав IConnectionPoint::Unadvise().
  • Сделать это можно, например, так:

    HRESULT hr;
    IConnectionPointContainer* pCPC = NULL;
    IConnectionPoint* pCP = NULL;
    DWORD dwCookie;
    
    // Объект поддерживает точки соединения?
    hr = pElem->QueryInterface(IID_IConnectionPointContainer, (void**)&pCPC);
    
    if (SUCCEEDED(hr))
    {
        // Находим точку соединения для HTMLElementEvents2
        hr = pCPC->FindConnectionPoint(DIID_HTMLElementEvents2, &pCP);
    
        if (SUCCEEDED(hr))
        {
            // Подключаемся
            // pUnk указатель на IUnknown объекта - приемника событий
            hr = pCP->Advise(pUnk, &dwCookie);
    
            if (SUCCEEDED(hr))
            {
                // Подключились. Можно принимать события.
            }
            pCP->Release();
        }
        pCPC->Release();
    }

    Как можно заметить из предыдущего листинга, процесс подключения к событиям - дело хоть и не сложное, но нудное и однообразное. Посмотрим, что предлагают нам MFC и АTL для облегчения этого процесса.

    Подключение к событиям с использованием MFC

    Класс CCmdTarget содержит встроенную реализацию интерфейса IDispatch, поэтому, чтобы реализовать приемник событий, достаточно унаследовать от него свой класс . Не забудьте только вызвать метод CCmdTarget::EnableAutomation в конструкторе. Макросы DECLARE_DISPATCH_MAP, BEGIN_DISPATCH_MAP, DISP_FUNCTION, DISP_FUNCTION_ID и END_DISPATCH_MAP могут быть использованы для задания карты событий элемента. Таким образом, каждое событие можно связать с соответствующей функцией-обработчиком.

    BEGIN_DISPATCH_MAP(CHTMLEventSink, CCmdTarget)
        //{{AFX_DISPATCH_MAP(CHTMLEventSink)
            // NOTE - the ClassWizard will add and remove mapping macros here.
            DISP_FUNCTION_ID(CHTMLEventSink, \
                "HTMLELEMENTEVENTS2_ONCLICK", DISPID_HTMLELEMENTEVENTS2_ONCLICK, \
                OnClick, VT_EMPTY, VTS_DISPATCH)
            ...
        //}}AFX_DISPATCH_MAP
    END_DISPATCH_MAP()

    Процесс подключения к требуемому источнику событий можно упростить применением пары глобальных функции AfxConnectionAdvise и AfxConnectionUnadvise.

    //CMySink - наследник CCmdTarget с поддержкой автоматизации
    m_pSink = new CMySink();
    
    // Получаем указатель на IDispatch. 
    LPUNKNOWN pUnkSink = m_pSink->GetIDispatch(FALSE);
    
    // Подключаемся
    // m_pUnkSrc - указатель на IUnknown объекта, события которого собираемся наблюдать
    AfxConnectionAdvise(m_pUnkSrc, IID_MYEVENT, pUnkSink, FALSE,  &m_dwCookie);
       
    ...
    // Получаем события
    ...
    // Отключаемся
    AfxConnectionUnadvise(m_pUnkSrc, IID_MYEVENT, pUnkSink, FALSE,  m_dwCookie);

    Когда требуется реализовать подключение к событиям элемента управления ActiveX, встраиваемого в MFC-приложение (например, для WebBrowser), следует использовать набор макросов DECLARE_EVENTSINK_MAP, BEGIN_EVENTSINK_MAP, ON_EVENT и END_EVENTSINK_MAP. Пример их использования можно увидеть в реализации класса CHtmlView.

    Подключение к событиям с использованием ATL

    Как и следовало ожидать, ATL содержит собственный набор макросов для упрощения процесса подключения к событиям. Как-то неудобно рассказывать людям, пишущим на ATL, про реализацию disp-интерфейса, но на всякий случай напомню, что для этого необходимо создать класс, унаследованный от IDispatchImpl.

    ATL содержит две вспомогательные функции AtlAdvise и AtlUnadvise, упрощающих процесс подключения к событиям. Класс smart-указателя CComQIPtr также содержит метод Advise (который всего лишь вызывает AtlAdvise), правда при этом Unadvise почему-то нет. Вообще, использование CComQIPtr часто приводит к проблемам, и, возможно, лучше его не использовать в реальных приложениях.

    Помимо этого, для реализации приемника событий можно использовать классы ATL IDispEventImpl и IDispEventSimpleImpl, а также макросы BEGIN_SINK_MAP, SINK_ENTRY, SINK_ENTRY_EX и END_SINK_MAP.

    Подключение к индивидуальным событиям. Обработчики IHTMLElement::onXXX

    Выше уже говорилось, что возможно подключение к событиям не только через точки соединения, но и с помощью назначения индивидуальных обработчиков для конкретных событий. Рассмотрим последний механизм подробнее.

    В целом подход остается тем же. Как и в первом случае, необходимо создать COM-объект, реализующий disp-интерфейс обратного вызова.

    При подключении cсылка на этот объект присваивается соответствующему свойству элемента, событие которого мы хотим обработать. Для этого в каждом интерфейсе IHTMLXXXElement присутствуют свойства вида onXXX, отвечающие за различные события. Процесс подключения показан ниже.

    IHTMLDocument2* pHtmlDoc; // Указатель на документ
    LPDISPATCH dispFO; // Указатель на IDispatch объекта-обработчика
    ...
    VARIANT vIn;                            
    V_VT(&vIn) = VT_DISPATCH; 
    V_DISPATCH(&vIn) = dispFO; 
    
    // Регистрируем обработчик события document.onkeydown
    hr = pHtmlDoc->put_onkeydown( vIn );

    При возникновении события браузер просто вызовет IDispatch::Invoke объекта со значением DISPID = DISPID_VALUE (0). Как видите, с точки зрения реализации приемника событий, просматривается некоторая аналогия с подключением через Connection Point. Отличие в том, что в первом случае в реализацию IDispatch::Invoke будут приходит уведомления о всех событиях, а при использовании свойств onXXX - только об одном.

    Выбор момента для регистрации

    При описании обоих способов подразумевалось, что объект документа уже загружен. В противном случае GetHtmlDocument() вернет NULL, и не удастся получить указатели на требуемые элементы страницы, а значит, зарегистрировать обработчик. Чтобы не пропустить этот момент, можно обрабатывать событие DocumentComplete. Это гарантирует, что документ уже полностью загружен, а значит, можно обращаться к любому из его элементов.

    Получение информации о событии

    Настало время поговорить об объекте event, играющем важную роль в объектной модели браузера. Объект event является дочерним объектом для window и позволяет получить информацию о любом событии, происходящем на html-странице.

    Распространение событий

    Когда на странице происходит событие, объект event первым получает эту информацию. Для примера рассмотрим ситуацию:

    <BODY>
    ...
      <DIV ID=MyDiv>
        <H3 ID=MyTitle>Щелкните для возникновения события</H3>
      </DIV>
    ...
    </BODY>

    Когда браузер обрабатывает события от мыши, он выясняет, какой элемент находится под курсором мыши (если там находятся несколько элементов один над другим, он выбирает верхний элемент), ищет процедуру, связанную с этим объектом и выполняет ее. Предположим, это строка заголовка.

    Sub MyTitle_onclick()
    ...
    End Sub

    Далее браузер выясняет, какой объект является контейнером тега заголовка. В данном случае - это тег <DIV> с уникальным id=MyDiv. Так как для него имеется обработчик, управление передается ему:

    Sub MyDiv_onclick()
    ...
    End Sub

    Этот процесс продолжается до тех пор, пока есть доступные контейнеры. В нашем случае оставшимся контейнером является собственно документ, поэтому браузер ищет соответствующую процедуру и для него.

    Sub document_onclick()
    ...
    End Sub

    Таким образом, даже в таком простом случае один щелчок мыши послужил поводом сразу для трех событий (рис 3).


    Рис 3. Всплывание событий и объект event

    Данная модель "всплывания" событий (bubble - так этот процесс называется) достаточно проста и удобна. Она позволяет легко сгруппировать обработчики для нескольких элементов в одной процедуре обработки, позволяя работать на уровне контейнеров. Тем самым уменьшается количество кода.

    Полезные свойства объекта event

    Поиск источника

    Итак, можно заставить код реагировать на события с помощью функции, обрабатывающей целый контейнер. Практически всегда хочется узнать, какой элемент послужил источником данного события. Для этого у объекта event существует свойство srcElement. Оно позволяет узнать, для какого объекта было сгенерировано событие. Например, если пользователь щелкнет на теге заголовка на странице, таким элементом будет тег заголовка. Если пользователь щелкнет на пустом месте страницы, событие будет перенаправлено непосредственно к объекту document.

    Еще два свойства fromElement и toElement могут быть полезны при обработке событий onmouseout и onmouseover. Свойство fromElement возвращает ссылку на элемент, с которого ушел курсор мыши, а свойство toElement - на котором появился.

    Свойство cancelBubble. Разрушение цепочки событий

    Ранее было показано, как один щелчок мыши послужил поводом сразу для 3-х однотипных событий. Однако иногда хочется самим выбирать путь прохождения события. Свойство cancelBubble позволяет разрушить цепную реакцию возникновения событий. Смысл сказанного проще понять из рисунка 4.


    Рис 4. Разрушение цепочки событий

    Установив свойство cancelBubble в true, мы запретим его "всплывание" вверх по иерархии событий.

    Свойство returnValue. Действие по умолчанию

    Установка свойства returnValue в false позволяет отменить реакцию браузера на событие. Важно понимать отличие данного свойства от предыдущего. cancelBubble прерывает обработку событий только для элементов объектной модели документа. Воздействие же на свойство returnValue позволяет отменить всю дальнейшую обработку событий браузером. Манипулируя двумя этими свойствами, можно очень гибко управлять процессом обработки событий в документе.

    Действие браузера по умолчанию определяется индивидуально для каждого события. Например, в событии onsubmit можно отменить отправку неправильно заполненной формы, а в onсlick на тэге <A> запретить переход по ссылке.

    Координаты курсора

    Объект event предлагает четыре пары свойств, которые позволяют получить координаты курсора мыши: screenX и screenY, clientX и clientY, offsetX и offsetY и, наконец, просто x и y.

    Информация о мыши

    Свойство button возвращает информацию о том, какая из кнопок мыши была нажата. Возможные значения:

    0x00Ничего не нажато.
    0x01Левая кнопка.
    0x02Правая кнопка.
    0x04Средняя кнопка

    Значения данных флагов можно комбинировать. Например, при одновременном нажатии левой и правой клавиши в свойстве button будет содержаться 0x03 = (0x01 + 0x02). При обработке событий от мыши также можно запрашивать информацию о координатах курсора и состоянии клавиш SHIFT, CTRL и ALT.

    Информация о клавиатуре

    Четыре свойства объекта event содержат информацию о нажатых клавишах.

    Свойство type

    В завершение разговора об объекте event, рассмотрим еще одно полезное свойство - type. Нет, это не тип события, как могло бы показаться из названия свойства. Это название самого события без приставки "on". Например, для события onclick значением type будет строка "click".

    Использование из C++

    Итак, когда вызывается обработчик, никакой дополнительной информации о событии в функцию не передается. Чтобы узнать, что произошло, необходимо воспользоваться объектом event (интерфейс IHTMLEventObj), доступным через объект window (IHTMLWindow2) текущего документа. Посредством этого интерфейса можно получить подробную информацию о произошедшем событии, например, элемент, послуживший источником события, состояние клавиш, местоположение курсора мыши и состояние ее кнопок. Стоит заметить, что объект event доступен только на время обработки конкретного события. При этом не все свойства в контексте определенного события имеют смысл. Например, значения, возвращаемые функциями get_fromElement и get_toElement, доступны только при обработке событий мыши onmouseover и onmouseout.

    В следующем примере в обработчике определяется нажатая клавиша и выводится соответствующее диалоговое окно. Если была нажата клавиша Enter, то дальнейшая обработка отменяется.

    void CMyHtmlView::OnKeyDown( DISPID id, VARIANT* pVarResult)
    {
        HRESULT hr;    
        LPDISPATCH pDispatch = GetHtmlDocument();    
        if(pDispatch != NULL)
        {
            IHTMLDocument2* pHtmlDoc;
            hr = pDispatch->QueryInterface(__uuidof( IHTMLDocument2),
                  (void**)&pHtmlDoc );
            
            IHTMLWindow2*  pWindow;
            IHTMLEventObj* pEvent;
            
            hr = pHtmlDoc->get_parentWindow(&pWindow);
            ASSERT(SUCCEEDED(hr));    
            hr = pWindow->get_event(&pEvent);
            ASSERT(SUCCEEDED( hr ));
            
            // Определяем нажатую клавишу
            long nKey;
            hr = pEvent->get_keyCode( &nKey );
            ASSERT( SUCCEEDED( hr ) );
    
            // Если нажат Enter, завершаем обработку
            if ( nKey == VK_RETURN)
            {            
                V_VT(pVarResult) = VT_BOOL; 
                V_BOOL(pVarResult) = FALSE;             
            }
            
            pDispatch->Release();
            pWindow->Release();
            pEvent->Release();
            pHtmlDoc->Release();
            
            CString sMes;
            sMes.Format("CEventView::OnKeyDown(DISPID = %d)\nKeyCode: %d", id, nKey);
            AfxMessageBox(sMes);
        }           
    }

    CHtmlEventSink - удобное подключение к событиям

    Настало время применить все вышеизложенное на практике. Заметьте, реализация IDispatch сводится к одной функции Invoke, каждый из обработчиков определяется только своим DISPID, информация о наступившем событии не передается в функцию-обработчик, а доступна через объект window.event. Многие из событийных интерфейсов содержат однотипные события. В общем, было бы гораздо удобней не реализовывать отдельный объект-приемник под разные события, а перенаправлять их в одну функцию-обработчик, расположенную в том же классе, где сосредоточена вся основная работа с WebBrowser (например, CHtmlView при использовании MFC).

    Нет, конечно, можно "вручную" написать COM-объекты для каждой функции, однако представьте, что число обработчиков переваливает за десяток, а каждый раз нужно реализовывать по сути одно и тоже. Можно написать универсальный код, который позволит подключать одну функцию-обработчик к нескольким событиям. Понятно, к чему я клоню? Самое время вспомнить о шаблонах С++. Итак, напишем простенький шаблонный класс. Чтобы не зависеть от конкретной библиотеки, реализуем пару IUnknown, IDispatch вручную. Реализация IUnknown вполне стандартна, а из IDispatch необходимо реализовать только функцию Invoke.

    Листинг 2. Реализация CHtmlEventSink
    template <class T> class CHtmlEventSink : public IDispatch  
    {
        // Прототип-функции обработчика
        typedef void (T::*EVENTFUNCTIONCALLBACK)(DWORD dwSource, 
                                                 DISPID idEvent,  
                                                 VARIANT* pVarResult);
    public:
        CHtmlEventSink()  { m_cRef = 1; }
       ~CHtmlEventSink()  { ;}    
        ...
        // Реализация IUnknown и большиства методов IDispatch пропущены
        ...    
        STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid,
            WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult,
            EXCEPINFO * pExcepInfo, UINT * puArgErr)
        {
            TRACE(_T("Invoke dispid = %d\n"), dispIdMember);     
            // Передаем вызов в CALLBACK функцию
            if (m_pT)
                (m_pT->*m_pFunc)(m_dwSource, dispIdMember, pVarResult);
            
            return S_OK;
        }
    public:    
        static LPDISPATCH CreateHandler(T* pT, 
                                 EVENTFUNCTIONCALLBACK pFunc, DWORD dwSource)
        {
            CHtmlEventSink<T>* pFO = new CHtmlEventSink<T>;
            pFO->m_pT = pT;
            pFO->m_pFunc = pFunc;      
            pFO->m_dwSource = dwSource;
            return reinterpret_cast<LPDISPATCH>(pFO);
        }
    protected:    
        EVENTFUNCTIONCALLBACK m_pFunc;
        DWORD m_dwSource;
        T* m_pT;
        long m_cRef;
    };

    Как это работает и как это использовать

    Шаблонный класс CHtmlEventSink содержит урезанную реализацию IDispatch, перенаправляющую все приходящие события в указанную при его создании CALLBACK-функцию. С его помощью удобно подключаться к событиям двумя описанными выше способами: и через точки соединения, и к индивидуальным событиям IHTMLElement::onXXX.

    Шаг 1. Создание функции-обработчика

    В принципе ее можно разместить где угодно. При использовании MFC я предпочитаю создавать ее в классе представления, наследнике CHtmlView. При этом все обработчики сосредоточены в одном месте и не нужно беспокоиться о взаимодействии с классом документа.

    Прототип функции-обработчика имеет вид:
    void OnEvent(DWORD dwSource, DISPID idEvent, VARIANT* pVarResult);

    В параметре dwSource передается значение, указываемое при регистрации обработчика. Это может пригодиться, если захочется реализовать обработку нескольких событий в одной функции. Для этого нужно зарегистрировать одну и ту же функцию с разными значениями параметра dwSource, тогда по приходу событий их легко будет различить.

    idEvent содержит DISPID произошедшего события.

    Параметр pVarResult используется для отмены обработки по умолчанию. При этом достаточно в pVarResult вернуть VARIANT_FALSE. Действие этого параметра аналогично значению свойства returnValue объекта event.

    Шаг 2. Создание экземпляра объекта обработчика

    Вызовом CHtmlEventSink::CreateHandler создаем экземпляр нашего COM-объекта. Передаем в него адрес функции обработчика (шаг 1) и собственный идентификатор события dwSource.

    // Создаем объект-обработчик
    LPDISPATCH dispFO = CHtmlEventSink<CEventView>::CreateHandler(this, OnKeyDown, 1);

    Шаг 3. Регистрация в качестве приемника событий

    Регистрируем созданный объект в качестве приемника событий либо через точку соединения, либо передав ссылку на него интересующему вас элементу.

    Листинг 3. Индивидуальный обработчик document.onkeydown
    VARIANT vIn;                            
    V_VT(&vIn) = VT_DISPATCH; 
    V_DISPATCH(&vIn) = dispFO; 
    
    // устанавливаем обработчик document.onkeydown
    hr = pHtmlDoc->put_onkeydown( vIn );
    Листинг 4. Обработчик всех событий документа (MFC)
    IHTMLDocument2* pHtmlDoc;
    ...
    // устанавливаем обработчик всех событий документа
    // pUnk - указатель на IUnknown документа
    if (!AfxConnectionAdvise( pUnk, __uuidof (HTMLDocumentEvents2), 
                                    dispFO, FALSE, &dwCookie))
        AfxMessageBox( _T("Не удается подключиться к событиям объекта document"));
    Листинг 5. Обработчик всех событий документа (ATL)
    // устанавливаем обработчик всех событий документа
    CComPtr<IHTMLDocument2> spHtmlDoc;
    ...
    if (FAILED(AtlAdvise(pHtmlDoc, m_pEventHandler, __uuidof(HTMLDocumentEvents2), &dwCookie)))            
        MessageBox( _T("Не удается подключиться к событиям объекта document"));

    Заключение

    Чтобы проиллюстрировать изложенное в данной статье, было написано небольшое приложение DHTMLSpy. В этом примере реализовано подключение к событиям объекта document. При возникновении событий на html-странице в таблицу выводится краткая информация о них (рис. 1).

    Как видите, ничего сложного в подключении к событиям "вручную" нет. Так как механизм точек соединения является стандартной технологией COM для подключения к событиям, и каждый раз, по сути, требуется реализовывать одно и то же, совершенно естественно, что каждая библиотека имеет несколько "заготовок" для автоматизации этого процесса. Однако применение подобных оберток, на мой взгляд, только отвлекает программиста от самой сути происходящего, заставляя его бездумно использовать наборы соответствующих макросов. Именно поэтому подключение к событиям многим кажется неким шаманством.

    В этой статье я намеренно показал технологию подключения к событиям DHTML на достаточно низком уровне абстракции. Надеюсь, что прочитав ее до конца, вы сможете не только бездумно использовать предлагаемые Microsoft заготовки, но и понимать, каким образом работает используемая вами функциональность.


    Эта статья опубликована в журнале RSDN Magazine #0. Информацию о журнале можно найти здесь