Загрузка...



  • СТАТЬЯ  Написание Plugin'ов для Internet Explorer 
  • ЭКЗАМЕН 
  • Программирование на Visual C++

    Выпуск №62 от 3 февраля 2002 г.

    Здравствуйте, уважаемые подписчики!

    СТАТЬЯ 

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

    Автор: Борис Гулай aka BoresExpress

    Источник: журнал "Программист" №1 за 2002

    Исходные тексты примера – 11 KB

    Всем памятны обвинения в адрес Microsoft в том, что включение браузера Microsoft Internet Explorer в состав операционной системы Windows недопустимо. Ответом корпорации было то, что браузер является неотъемлемой частью системы. Теперь мы можем сказать даже больше – Internet Explorer как единое приложение не существует. Это набор компонентов, которые собираются в единое целое только при запуске приложения. Сейчас мы попробуем включить в этот стройный ряд компонентов свой, чтобы он тоже стал неотъемлемой частью, ну если не операционной системы, то конкретной копии браузера точно.

    Архитектура Internet Explorer


    Что мы будем делать?

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

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

    Как это работает?

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

    Прежде всего, браузер загружает нашу библиотеку, это происходит вместе с загрузкой самого IE. Затем, после первого нажатия на кнопку (!), он вызывает экспортируемую функцию DllGetClassObject и запрашивает у неё указатель на интерфейс IClassFactory. Далее, из полученного интерфейса он вызвает метод CreateInstace и запрашивает у него интерфейс 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) returnE_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, т.к. мы отвечаем только за одну кнопку и пункт меню.

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

    Как это подключить?

    Теперь остался последний штрих – регистрация нашего компонента в реестре. В первую очередь, необходимо корректно зарегистрировать наш плагин как 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. Подробнее о стиле, в котором должны быть решены эти кнопки можно прочитать здесь).

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

    Что в итоге?

    Теперь, если вы следовали приведённым выше действиям, на панели инструментов Internet Explorer должна появиться кнопка, а в меню 'Сервис' строка меню, запускающая наш плагин.

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

    ЭКЗАМЕН 

    How do you create an edit control that echoes '#' for any character the user types? 

    1. Super-class the edit control, override the WM_CHAR message, and also override the WM_PAINT message to echo the '#' char.

    2. Send the edit control a WM_PASSWORD message and then send a WM_SETPASSWORDHAR message to specify the '#' char.

    3. Send the edit control an EM_PASSWORD message and then send an EM_SETPASSWORDHAR message to specify the '#' char.

    4. Use the ES_PASSWORD style and then send an EM_SETPASSWORDCHAR message to specify the '#' char.

    5. Sub-class the edit control, override the WM_CHAR message, and also override the WM_PAINT message to echo the '#' char. 

    Вариант 1 неверен, т.к. действие "super-class the edit control" не имеет смысла. Варианты 2 и 3 также неверны, потому что сообщений WM_PASSWORD, WM_SETPASSWORDCHAR и EM_PASSWORD не существует. Вариант 5 неверен, т.к. существует стандартный и более простой способ. Правильный ответ — вариант 4, существует и стиль ES_PASSWORD, и сообщение EM_SETPASSWORDCHAR, которые предназначены как раз для решения этой проблемы.


    Это все на сегодня. Пока! 

    (Алекс Jenter jenter@rsdn.ru) (Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN. )







    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх