|
::Главная страница :: С++/Си :: Статьи |
Подключение к событиям объектной модели DHTML при использовании WebBrowser-control
РАССЫЛКА САЙТА RSDN.RU
Демонстрационное приложение - event.zip (181 Kb)
В последнее время путешествуя по форумам и группам новостей все чаще можно встретить вопросы, касающиеся ActiveX-элемента WebBrowser. К сожалению зачастую эти вопросы теряются в общем ворохе сообщений, а материалов касающихся использования функциональности Internet Exploler катастрофически мало. В то же время у этих технологий находиться все больше поклонников. Уж слишком заманчиво выглядит возможность написать собственный броузер, приложив минимум усилий.
Например, текущая версия продукта, в разработке которой я принимаю участие, использует эти технологии для отображения информации из документно-ориентированной базы данных. При этом, как показывает практика, интерфейс web-представлений в данном случае, гораздо дружественнее пользователю, чем использование множества диалоговых форм, а чтобы его сменить достаточно подправить пару шаблонов в ресурсах.
Предлагаемая вашему вниманию статья посвящена одному из аспектов написания подобных программ. В ней будет рассмотрено как можно подключиться с событиям объектной модели броузера, а значит позволить вашему приложению использовать те преимущества, которые дает DHTML. Я попытаюсь рассказать вам как непосредственно обрабатывать события, возбуждаемые объектной моделью броузера в процессе работы пользователя со страницей.
Как и большинство ActiveX элементов WebBrowser является источником событий подключаемых через стандартный механизм Connection Point. К числу таких событий относятся OnBeforeNavigate, OnDocumentComplete и т.п. Несомненно они важны для управления приложением в целом, однако их возможностей явно недостаточно, если мы захотим более тесно познакомиться с DOM DHTML, например, узнать о перемещении мыши над элементами страницы или о нажатии клавиши на клавиатуре или вообще быть в курсе всех событий, которые можно использовать в сценариях DHTML.
При использовании объектной модели DHTML из сценариев возможно создавать собственные обработчики событий простым присваиванием наблюдаемого элемента соответствующим свойствам, например:
<SCRIPT LANGUAGE="jscript"> function mousedownhandler() { // функция обработчик } function afterPageLoads() { someElement.onmousedown = mousedownhandler; } </SCRIPT>
Возникает вопрос. А можно ли получить нечто подобное из клиента на С++? Конечно, причем похожим образом. Достаточно создать свою функцию обработки и зарегистрировать ее.
Теперь поподробнее. Для этого нужно проделать не так уж и много. Необходимо реализовать простой Com-объект для каждой функции обработчика. Этот объект должен реализовывать всего два стандартных интерфейса IUnknown и IDispatch. Далее ссылка на этот объект присваивается соответствующему свойству элемента, событие которого мы хотим наблюдать. При возникновении события броузер просто вызовет IDispatch::Invoke нашего объекта со значением DISPID = DISPID_VALUE (=0).
Замечу, что это не единственный способ заставить приложения реагировать на события. Например, можно заставить работать механизм window.external. Тогда соответствующий скрипт будет выглядеть например, так:
function mousedownhandler() { // функция обработки window.external.onmousedown; }
Думаю идея понятна. Однако этот способ удобен если мы сами формируем страницу. Сегодня мы пойдем первым путем.
Итак, настало время применить все вышеизложенное на практике. Конечно, можно руками написать COM-объекты для каждой функции, однако представьте, что число обработчиков переваливает за десяток, а каждый раз нужно реализовывать по сути одно и тоже. Думаю, понятно, к чему я клоню :) Самое время вспомнить о шаблонах С++. Итак, напишем простенький шаблонный класс. Чтобы не зависеть от конкретной библиотеки реализуем пару IUnknown, IDispatch вручную. Реализация IUnknown вполне стандартна, а из IDispatch необходимо реализовать только функцию Invoke.
template <class T> class CHtmlEventObject : public IDispatch { typedef void (T::*EVENTFUNCTIONCALLBACK)(DISPID id, VARIANT* pVarResult); public: CHtmlEventObject() { m_cRef = 0; } ~CHtmlEventObject() {} HRESULT __stdcall QueryInterface(REFIID riid, void** ppvObject) { *ppvObject = NULL; if (IsEqualGUID(riid, IID_IUnknown)) *ppvObject = reinterpret_cast<void**>(this); if (IsEqualGUID(riid, IID_IDispatch)) *ppvObject = reinterpret_cast<void**>(this); if (*ppvObject) { ((IUnknown*)*ppvObject)->AddRef(); return S_OK; } else return E_NOINTERFACE; } DWORD __stdcall AddRef() { return InterlockedIncrement(&m_cRef); } DWORD __stdcall Release() { if (InterlockedDecrement(&m_cRef) == 0) { delete this; return 0; } return m_cRef; } STDMETHOD(GetTypeInfoCount)(unsigned int FAR* pctinfo) { return E_NOTIMPL; } STDMETHOD(GetTypeInfo)(unsigned int iTInfo, LCID lcid, ITypeInfo FAR* FAR* ppTInfo) { return E_NOTIMPL; } STDMETHOD(GetIDsOfNames)(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId) { return S_OK; } STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO * pExcepInfo, UINT * puArgErr) { if (DISPID_VALUE == dispIdMember) (m_pT->*m_pFunc)(m_id, pVarResult); else TRACE(_T("Invoke dispid = %d\n"), dispIdMember); return S_OK; } public: static LPDISPATCH CreateHandler(T* pT, EVENTFUNCTIONCALLBACK pFunc, DISPID id) { CHtmlEventObject<T>* pFO = new CHtmlEventObject<T>; pFO->m_pT = pT; pFO->m_pFunc = pFunc; pFO->m_id = id; return reinterpret_cast<LPDISPATCH>(pFO); } protected: T* m_pT; EVENTFUNCTIONCALLBACK m_pFunc; DISPID m_id; long m_cRef; };
Как применять этот класс? Проще простого.
Шаг 1. Создаем свою функцию обработчик по прототипу OnEvent(DISPID id, VARIANT* pVarResult). В принципе ее можно разместить где угодно. Я предпочитаю создавать ее в классе представления, наследнике CHtmlView. При этом все обработчики сосредоточены в одном месте и не нужно беспокоится о взаимодействии с классом документа.
Шаг 2. Регистрируем ее в качестве обработчика интересующего нас события. Для этого через вызов CHtmlEventObject::CreateObject создаем экземпляр нашего COM-объекта. Передаем в него адрес функции обработчика и собственный идентификатор события. После этого передаем ссылку на него интересующему нас элементу.
// Создаем объект-обработчик LPDISPATCH dispFO = CHtmlEventObject<CEventView>::CreateHandler(this, OnKeyDown, 1); VARIANT vIn; V_VT(&vIn) = VT_DISPATCH; V_DISPATCH(&vIn) = dispFO; // устанавливаем обработчик document.onkeydown hr = pHtmlDoc->put_onkeydown( vIn );
Здесь есть одна тонкость. Зарегистрировать обработчик можно только тогда, когда документ уже загружен, иначе GetHtmlDocument() вернет NULL. Для этого можно отслеживать событие OnDocumentComplete. Ну вот собственно и все.
Рассмотрим еще раз прототип функции обработчика
OnEvent(DISPID id, VARIANT* pVarResult);
В качестве id передается значение которое мы указали при регистрации обработчика. Это может пригодиться если мы захотим реализовать обработку нескольких событий в одной функции. Для этого нужно зарегистрировать одну и ту же функцию с разными DISPID, тогда по приходу событий мы сможет их различать.
pVarResult нужно, если не требуется обработки по умолчанию.При этом достаточно в pVarResult вернуть VARIANT_FALSE.
Итак, когда вызывается наш обработчик никакой дополнительной информации о событии в функцию не передается. А как же тогда поподробнее узнать, что произошло? Для этого необходимо воспользоваться интерфейсом IHTMLEventObj, доступным через объект window текущего документа. Посредством этого интерфейса можно получить подробную информацию о произошедшем событии, например, элемент, послуживший источником событий, состояние клавиш, местоположение курсора мыши и состояние ее кнопок.
Вот его краткое описание из MSDN:
Методы IHTMLEventObj
get_altKey | Состояние клавиши Alt |
get_button | Возвращает информацию о нажатых кнопках мыши |
get_cancelBubble | Возвращает будет ли продолжена обработка события вверх по иерархии обработчиков |
get_clientX | Возвращает горизонтальную позицию курсора мыши относительно клиентской области окна |
get_clientY | Возвращает вертикальную позицию курсора мыши относительно клиентской области окна |
get_ctrlKey | Состояние клавиши Ctrl |
get_fromElement | Возвращает указатель на интерфейс IHTMLElement позволяющий получить доступ к элементу с которого "ушел" курсор мыши при событиях onmouseover или onmouseout. |
get_keyCode | Возвращает код нажатой клавиши |
get_offsetX | Возвращает горизонтальную позицию курсора относительно контейнера элемента |
get_offsetY | Возвращает позицию курсора относительно контейнера элемента |
get_qualifier | Возвращает идентификатор события |
get_reason | Возвращает состояние передачи данных для объекта источника данных |
get_returnValue | Возвращаемое значение события или диалога |
get_screenX | Горизонтальная координата относительно координат экрана |
get_screenY | Вертикальная координата относительно координат экрана |
get_shiftKey | Состояние клавиши Shift |
get_srcElement | Возвращает указатель на интерфейс IHTMLElement послуживший источником событий |
get_srcFilter | Возвращает объект фильтр возбудивший событие onfilterchange |
get_toElement | Возвращает указатель на интерфейс IHTMLElement позволяющий получить доступ к элементу с на который "пришел" курсор мыши при событиях onmouseover или onmouseout |
get_type | Возвращает строковое название события |
get_x | Возвращает горизонтальную позицию мыши относительно родительского объекта в иерархии, позиционированного с помощью атрибутов CSS |
get_y | Возвращает вертикальную позицию мыши относительно родительского объекта в иерархии, позиционированного с помощью атрибутов CSS |
put_cancelBubble | Задать будет ли продолжена обработка события вверх по иерархии обработчиков |
put_keyCode | Задать код нажатой клавиши |
put_returnValue | Задать возвращаемое событием значение |
Стоит заметить, что интерфейс IHTMLEventObj доступен только на время обработки конкретного события. При этом не все свойства в контексте определенного события имеют смысл. Например, значения возвращаемые функциями 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); } }
Чтобы собрать воедино все фрагменты приведу небольшой пример (event.zip). Запустите его и выберите команду меню Event\OnKeydown. Теперь понажимайте клавиши внутри страницы. И посмотрите, что из этого получится. В этом примере регистрируется только один обработчик, но я думаю дочитав эту статью вы без труда сможете реализовать любой другой.
В заключение хочется заметить, что в этой статье я затронул только один небольшой аспект использования элемента WebBrowser. Если Вас заинтересует данная тема, пишите, продолжим.
Тематические
ссылки
|
Ваша ссылка | Ваша ссылка |
Обмен кнопками, ведение статистики, реклама. |
|||