Информационные технологииStfw.Ru 🔍

Написание Plugin'ов для Internet Explorer

Борис Гулай
🕛 18.07.2006, 12:43
Всем памятны обвинения в адрес Microsoft в том, что включение браузера Internet Explorer в состав операционной системы Windows недопустимо. Ответом корпорации было то, что браузер является неотъемлемой частью системы. Теперь мы можем сказать даже больше - Internet Explorer как единое приложение не существует. Это набор компонентов, которые собираются в единое целое только при запуске приложения. Сейчас мы попробуем включить в этот стройный ряд компонентов свой, чтобы он тоже стал неотъемлемой частью, ну если не операционной системы, то конкретной копии браузера точно.
Что мы будем делать?

Что представляет собой плагин для Internet Explorer? Это обычный внутрипроцессный (In Process) COM-сервер (т.е. DLL-файл), который содержит объект, реализующий как минимум 2 интерфейса: IOleCommandTarget и IObjectWithSite. Кроме того, наш DLL-файл должен экспортировать не менее 2 функций: DllGetClassObject и DllCanUnloadNow. Думаю, их назначение всем известно.

Наш плагин будет очень простым. Он будет сохранять в результирующий файл все ссылки со страницы, которые указывают на файлы с заданными в .ini-файле расширениями. Такой плагин может быть полезен, например, при создании списков скачиваемых файлов для download-менеджеров. Искать и сохранять ссылки он будет при нажатии на кнопку, которую мы добавим на панель инструментов браузера, или при выборе соответствующего пункта в меню Сервис. Кнопку и пункт меню мы будем делать доступными (enabled) только в том случае, если в браузере открыт файл с расширением .htm или .html (это мы сделаем просто для демонстрации такой возможности).
Как это работает?

Теперь, когда мы определились, что будем писать, самое время узнать, как это будет работать. А работать это будет следующим образом: прежде всего, браузер загружает нашу библиотеку, это происходит вместе с загрузкой самого IE. Затем, после первого нажатия на кнопку, он вызывает экспортируемую функцию DllGetClassObject и запрашивает у неё указатель на интерфейс IClassFactory. Из полученного интерфейса он вызывает метод CreateInstance и запрашивает у него интерфейс IUnknown. Это должен быть IUnknown компонента, который реализует и IOleCommandTarget и IObjectWithSite.

Два вышеназванных интерфейса должны быть реализованы именно в одном компоненте. Internet Explorer будет запрашивать один через QueryInterface другого. Поэтому реализовать их отдельно нет никакой возможности.

Такое поведение контейнера выглядит логичным, если принять во внимание то, зачем компоненту интерфейс IObjectWithSite. Через его метод SetSite браузер передаёт указатель на интерфейс, через который можно добраться до IWebBrowser - основного интерфейса WebBrowser Control. Это может потребоваться компоненту при обработке нажатия на кнопку или выбора пункта меню, если он захочет узнать, в каком контексте произошло это событие. Поэтому совершенно логично, что IObjectWithSite должен реализовывать тот же компонент, который обрабатывает нажатие на кнопку.

После того, как произошло первое нажатие на кнопку, Internet Explorer вызывает метод SetSite интерфейса IObjectWithSite и передаёт в него IUnknown объекта, реализующего интерфейс IShellBrowser. Хочу обратить ваше внимание, что вызов вышеназванного метода происходит только один раз.

Затем, в ответ на нажатие кнопки, вызывается метод IOleCommandTarget::Exec, в котором и происходит обработка события.

После вызова IObjectWithSite::SetSite IE периодически вызывает метод IOleCommandTarget::QueryStatus, где плагин может при необходимости изменить статус своей кнопки и пункта меню (enabled/disabled).

При завершении своей работы браузер вызывает IObjectWithSite::SetSite со значением NULL в качестве единственного аргумента, что говорит плагину о необходимости освободить (release) сохранённый после первого вызова SetSite интерфейс браузера (если он его сохранял, конечно). Затем IE освобождает все интерфейсы плагина и при положительном ответе функции DllCanUnloadNow выгружает плагин.
Так выглядят, в общих чертах, то, что нам придётся запрограммировать.
Как это написать?

После знакомства с механизмом интеграции плагинов в Internet Explorer мы можем приступать к написанию кода. Я предполагаю, что читатель знаком с основами COM, поэтому не буду описывать создание COM-сервера и добавление в него компонентов. Сразу перейдем к самому интересному: реализации методов интерфейсов, которые необходимы плагину для полноценной работы.
Следует сразу сказать, что метод IObjectWithSite::GetSite в реализации не нуждается (хотя в примере он и реализован), т.к. браузер его никогда не вызывает (он ведь всегда знает, какая страница в нём открыта).

Начнём мы с самого простого, а именно с метода IObjectWithSite::SetSite. Для начала добавим в объявление объекта переменную типа IWebBrowser2Ptr (я предпочитаю использовать то, что в MSDN называется «compiler COM support classes»; это значительно ускоряет работу). Через эту переменную мы всегда будем иметь доступ ко всем предоставляемым браузером интерфейсам. Код этого метода выгладит следующим образом:
STDMETHODIMP IMyIEExtention::SetSite(IUnknown *pUnkSite)
{
if (!pUnkSite)
{
if (m_pWebBrowser2.GetInterfacePtr())
m_pWebBrowser2.Release();
return S_OK;
}

IServiceProviderPtr pServProv(pUnkSite);
return pServProv->QueryService(SID_SWebBrowserApp,
IID_IWebBrowser2, (void**)&m_pWebBrowser2);
} 

В начале я проверяю, не хочет ли IE сказать мне этим вызовом, что происходит завершение его работы, и я должен освободить его интерфейсы. Дальше - интересней. Я запрашиваю интерфейс IWebBrowser2, но не как обычно, через вызов QueryInterface, а посредством вызова метода QueryService предварительно полученного интерфейса IServiceProvider. Зачем нужны такие странные манипуляции для решения, казалось бы, стандартной задачи?

Интерфейс IServiceProvider предназначен для использования в следующих ситуациях. Предположим, существует некое приложение-контейнер, которое использует несколько COM-серверов. У каждого из них, естественно, есть доступ к интерфейсам контейнера (посредством IObjectWithSite::SetSite, например). Но вот какому-то из COM-серверов потребовалось получить доступ к интерфейсам другого COM-сервера, также содержащегося в контейнере.

Как же ему решить эту задачу? Ведь стандартными средствами он до другого сервера никак не доберётся, поскольку контейнер, в соответствии с идеологией COM, не предоставляет доступ к интерфейсам содержащихся в нём объектов непосредственно через вызовы QueryInterface своих интерфейсов.

Для решения таких задач как раз и предназначен интерфейс IServiceProvider. Его единственный метод - QueryService - отличается от QueryInterface одним параметром - идентификатором сервиса. Фактически это идентификатор одного из COM-компонентов, используемых приложением-контейнером. И когда COM-сервер хочет получить интерфейс другого сервера, используемого тем же клиентом, он просто вызывает вышеназванный метод с соответствующим идентификатором сервиса.

Клиент же, в свою очередь, просто определяет, какой из содержащихся в нём компонентов соответствует переданному идентификатору и вызывает его QueryInterface.
Возвращаясь к нашей задаче, легко заметить, что здесь аналогичная ситуация. Internet Explorer представляет собой зоопарк компонентов, где наш COM-сервер (т.е. плагин) - один из питомцев. Поэтому нам и приходится использовать вышеописанную технику для получения доступа к интерфейсам другого компонента (которым, в нашем примере, является WebBrowser Control).
Следующим в очереди на реализацию у нас стоит метод QueryStatus интерфейса IOleCommandTarget. Его текст выглядит следующим образом:
STDMETHODIMP IMyIEExtention::QueryStatus(const GUID *pCmdGroup,
ULONG cCmds, OLECMD prgCmds[], OLECMDTEXT *pCmdText)
{
if (!prgCmds)
return E_POINTER;
ASSERT(cCmds == 1);
if (!cCmds)
return E_UNEXPECTED;

BSTR url;
HRESULT hRes=S_OK;
hRes=m_pWebBrowser2->get_LocationURL(&url);
CHECK_COM_RESULT(hRes)
bstr_t pszUrl(url, false);

LPCTSTR pExt=(LPCTSTR)pszUrl+pszUrl.length()-5;
if (!_tcsicmp(pExt, _T(".html")) || !_tcsicmp(pExt+1, _T(".htm")))
prgCmds[0].cmdf=OLECMDF_ENABLED;
else
prgCmds[0].cmdf=OLECMDF_SUPPORTED;

return S_OK;
}
В начале необходимо удостовериться в корректности переданных данных. Затем мы просто запрашиваем текущий URL и, если его последние символы .htm или .html, делаем кнопку и пункт меню доступными, и недоступными в противном случае. Следует заметить, что в этот метод всегда должен передаваться только один элемент в массиве prgCmds, т.к. мы отвечаем только за одну кнопку и пункт меню.

Теперь мы вплотную подошли к реализации основной функции нашего плагина. В ней мы будем искать содержащиеся на странице ссылки и сохранять их в файле. Код этой функции я не буду приводить здесь, т.к. он прост и не имеет прямого отношения к написанию плагинов. С ним можно ознакомиться в исходном тексте демонстрационного приложения, который можно скачать с сайта журнала www.programme.ru.
Как это подключить?

Теперь остался последний штрих - регистрация нашего компонента в реестре. В первую очередь необходимо корректно зарегистрировать плагин как COM-сервер. Я не буду описывать эту процедуру здесь, поскольку это лежит за рамками моей статьи, да и информации на эту тему немало. Остановимся подробнее на регистрации нашей DLL в качестве плагина для Internet Explorer. Для этого необходимо создать следующий ключ в реестре:
<key root>\Software\Microsoft\Internet Explorer\Extensions \<ваш GUID>

В качестве <key root> может выступать либо HKEY_CURRENT_USER (в этом случае плагин будет доступен только текущему пользователю), либо HKEY_LOCAL_MACHINE (плагин будет доступен всем пользователям). Теперь в нём необходимо создать следующие параметры:
ButtonText - Текст всплывающей подсказки для кнопки. Значение может быть как текстом, так и строкой формата @dll_path,-ID, где dll_path путь к DLL плагина, ID - идентификатор строки в string table.
CLSID - Всегда {1FBA04EE-3024-11d2-8F1F-0000F87ABD16}
Default Visible - Будет ли кнопка сразу после регистрации плагина находиться на панели ('yes'), или пользователь должен будет добавить её на панель самостоятельно ('no' или если параметр отсутствует).
ClsidExtension - GUID плагина, как COM-сервера (из раздела HKCR\CLSID).
HotIcon - Путь к иконке, соответствующей активному состоянию кнопки (когда на неё наведена мышь). Если путь указывает на файл .dll или .exe, то после него через запятую указывается идентификатор ресурса.
Icon - Путь к иконке, соответствующей обычному состоянию кнопки.
MenuText - Текст пункта в меню <Сервис>.
MenuStatusBar - Текст подсказки, появляющейся в строке состояния, когда пункт меню активен (формат аналогичен параметру ButtonText).

Файл, на который указывает параметр HotIcon, должен содержать следующие цветные значки:
16х16 16 цветов
20х20 16 цветов (не обязательно)
20х20 256 цветов

Второй файл (соответствующий параметру Icon) должен содержать значки в оттенках серого. Параметры этих значков следующие:
16х16 16 оттенков серого
20х20 16 оттенков серого (не обязательно)
20х20 256 оттенков серого

Вообще-то и эти значки могут быть цветными, но в таком случае они не будут соответствовать общему стилю оформления панелей инструментов Internet Explorer. Подробнее о стиле, в котором должны быть решены эти кнопки можно прочитать здесь (http://msdn.microsoft.com/library/default.asp?url=/workshop/browser/ext/tutorials/button.asp).

Выполнение операций по добавлению информации в реестр логично возложить на функцию DllRegisterServer, экспортируемую нашей DLL. Именно так сделано в примере к этой статье. Также в демонстрационном приложении реализована функция DllUnregisterServer, удаляющая всю информацию о плагине из реестра.
Что в итоге?

Теперь, если вы следовали приведённым выше действиям, на панели инструментов Internet Explorer должна появиться кнопка, а в меню «Сервис» строка, запускающая наш плагин.
Надеюсь, что написание плагинов для самого популярного в мире браузера не показалось вам трудным. Если так, то у вас наверняка появилось желание поэкспериментировать с компонентами, расширяющими функциональность браузера. В этом случае я могу считать, что цель, к которой я стремился при написании этой статьи, достигнута.

Си: C/C++   Теги:

Читать IT-новости в Telegram
Информационные технологии
Мы в соцсетях ✉