Сообщений 3    Оценка 40        Оценить  
Система Orphus

Пример расширения возможностей TWebBrowser

Автор: Киселев Роман
ООО "Салон 2116"

Источник: RSDN Magazine #5-2005
Опубликовано: 27.04.2006
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
«Что не устраивает?»
Стратегия захвата
Расширяем DOM (Document Object Model)
Вызов скрипта из кода на Object Pascal
Разбор HTML
Протокол res:// или Как сделать 2 в 1
Заключение
Литература

Исходные тексты к статье

Введение

Компоненты с названиями TWebBrowser из библиотеки VCL или WebBrowser из NET Framework 2.0 вызывают неподдельный интерес, так как предоставляют возможность не только отображения документов HTML, но и автоматизации браузера. Но если компонент System.Windows.Forms.WebBrowser предлагает богатый инструментарий для управления своим поведением, то минимум, реализованный в TWebBrowser, причем реализованный автоматическим импортом библиотеки shdocvw.dll (Microsoft Internet Controls), заставляет задуматься о том, как доработать TWebBrowser.

ПРИМЕЧАНИЕ

Реализовать компонент TWebBrowser из VCL можно за считанные секунды следующим образом. Запустив среду Delphi, выберите в меню «Component» | «Import Component…», импортируйте библиотеку типов из shdocvw.dll – и компонент готов! Созданный средой в процессе импорта модуль SHDocVw_TLB.pas представляет собой модуль SHDocVw.pas из стандартной поставки Delphi.

Исходные тесты примеров к статье снабжены файлами проектов для BDS 2006. Однако никаких возможностей BDS 2006, несовместимых с Delphi 2005 и, возможно, Delphi 7, в исходных текстах и ресурсах не используется. При упоминании VCL я имею в виду исключительно Win32 VCL, а не NET VCL. Кроме того, под браузером понимается Internet Explorer.

«Что не устраивает?»

Создадим форму Form1: TForm, разместив на ней компоненты WebBrowser1: TWebBrowser, Edit1: TEdit для ввода URL и кнопку Button1: TButton, при нажатии на которую WebBrowser1: TWebBrowser (главный герой нашего повествования) будет выполнять переход по URL. В обработчике нажатия на кнопку воспользуемся одним из методов TWebBrowser.Navigate для отображения браузером страницы с заданным URL. Должно получиться примерно так:

      unit Unit1;

interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, OleCtrls, SHDocVw, StdCtrls;

type
  TForm1 = class(TForm)
    WebBrowser1: TWebBrowser; { главный герой нашего повествования }
    Edit1: TEdit;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
{ Private declarations }
public
{ Public declarations }
end;

var
  Form1: TForm1;

implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
  WebBrowser1.Navigate(Edit1.Text);
end;

end.

Результат впечатляет. Действительно заметно, что Delphi — среда быстрой разработки программ: несколько движений мышью, строка кода в обработчике, и перед нами готовый браузер! С другой стороны, не вызывает сомнения, что за всем этим скрывается мощь технологии СОМ, без которой нам также не обойтись.

Фактически, созданное приложение предлагает своему пользователю многие возможности Internet Explorer, который и скрывается за TWebBrowser’ом. Можно пользоваться «горячими» клавишами Ctrl+O (диалог открытия новой страницы), Ctrl+N (новое окно), Ctrl+P (вывод на печать), Сtrl+S (сохранить), Ctrl+A (выделить все), Ctrl+C (копировать), Backspace (переход назад), F5 (обновить) и т.д., а также вызывать контекстное меню для просмотра HTML-кода страницы. Однако если не планируется предоставлять пользователю эти возможности, то от TWebBrowser’а остается неприятный осадок, поскольку простого пути для управления этой функциональностью разработчики VCL не предусмотрели.

Стратегия захвата

Для запрета «горячих» клавиш и контекстного меню можно использовать SetWindowsHookEx. Однако такой подход дает не слишком много.

Интуиция разработчика под Windows заставляет подозревать, что без COM ничего не происходит. Именно, заглянув в MSDN / Web Development / Internet Explorer Development / Programming and Reusing the Browser, становится очевидным, что, реализовав интерфейсы IDocHostUIHandler и IDocHostShowUI, помимо желаемого запрета на «горячие» клавиши и контекстное меню, удается получить возможность расширения DHTML, контроля над диалоговыми окнами и справкой, а также управления многими другими функциями браузера.

Естественно, хотелось бы автоматизировать процесс создания модуля с соответствующими объявлениями интерфейсов. Для этого возьмем файл MsHtmHst.Idl из Platform SDK. Отметив, что в нем отсутствует секция library, добавим ее, уберем cpp_quote и pragma, и получим:

      import
      "ocidl.idl";
import"objidl.idl";
import"oleidl.idl";
import"oaidl.idl";
import"docobj.idl";

[
  uuid(051AF297-E6C8-4d15-A3C7-239CCC2CE906),
  version(1.0),
  helpstring("MSHTMLCustomUI Library")
]
library MSHTMLCustomUI
{
  importlib("stdole32.tlb");
  importlib("stdole2.tlb");

  [
    object,
    uuid(53DEC138-A51E-11d2-861E-00C04FA35C89),
    pointer_default(unique),
    local
  ]
  interface IHostDialogHelper : IUnknown
  {
    HRESULT ShowHTMLDialog(
            HWND hwndParent,
            IMoniker *pMk,
            VARIANT *pvarArgIn,
            WCHAR *pchOptions,
            VARIANT *pvarArgOut,
            IUnknown *punkHost
            );
  };

  [
    uuid(429AF92C-A51F-11d2-861E-00C04FA35C89)
  ]
  coclass HostDialogHelper
  {
    [default] interface IHostDialogHelper;
  };

  typedef enum tagDOCHOSTUITYPE 
  {
    DOCHOSTUITYPE_BROWSE  = 0,
    DOCHOSTUITYPE_AUTHOR  = 1,
  } DOCHOSTUITYPE;

  typedefenum tagDOCHOSTUIDBLCLK 
  {
    DOCHOSTUIDBLCLK_DEFAULT     = 0,
    DOCHOSTUIDBLCLK_SHOWPROPERTIES  = 1,
    DOCHOSTUIDBLCLK_SHOWCODE    = 2,
  } DOCHOSTUIDBLCLK ;

  typedefenum tagDOCHOSTUIFLAG 
  {
    DOCHOSTUIFLAG_DIALOG          = 0x00000001,
    DOCHOSTUIFLAG_DISABLE_HELP_MENU     = 0x00000002,
    DOCHOSTUIFLAG_NO3DBORDER        = 0x00000004,
    DOCHOSTUIFLAG_SCROLL_NO         = 0x00000008,
    DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE   = 0x00000010,
    DOCHOSTUIFLAG_OPENNEWWIN        = 0x00000020,
    DOCHOSTUIFLAG_DISABLE_OFFSCREEN     = 0x00000040,
    DOCHOSTUIFLAG_FLAT_SCROLLBAR      = 0x00000080,
    DOCHOSTUIFLAG_DIV_BLOCKDEFAULT      = 0x00000100,
    DOCHOSTUIFLAG_ACTIVATE_CLIENTHIT_ONLY   = 0x00000200,
    DOCHOSTUIFLAG_OVERRIDEBEHAVIORFACTORY   = 0x00000400,
    DOCHOSTUIFLAG_CODEPAGELINKEDFONTS     = 0x00000800,
    DOCHOSTUIFLAG_URL_ENCODING_DISABLE_UTF8 = 0x00001000,
    DOCHOSTUIFLAG_URL_ENCODING_ENABLE_UTF8  = 0x00002000,
    DOCHOSTUIFLAG_ENABLE_FORMS_AUTOCOMPLETE = 0x00004000,
    DOCHOSTUIFLAG_ENABLE_INPLACE_NAVIGATION = 0x00010000,
    DOCHOSTUIFLAG_IME_ENABLE_RECONVERSION   = 0x00020000,
    DOCHOSTUIFLAG_THEME           = 0x00040000,
    DOCHOSTUIFLAG_NOTHEME           = 0x00080000,
    DOCHOSTUIFLAG_NOPICS          = 0x00100000,
    DOCHOSTUIFLAG_NO3DOUTERBORDER       = 0x00200000,
    DOCHOSTUIFLAG_DISABLE_EDIT_NS_FIXUP   = 0x00400000,
    DOCHOSTUIFLAG_LOCAL_MACHINE_ACCESS_CHECK= 0x00800000,
    DOCHOSTUIFLAG_DISABLE_UNTRUSTEDPROTOCOL = 0x01000000,
  } DOCHOSTUIFLAG ;

  [
    object,
    uuid(bd3f23c0-d43e-11cf-893b-00aa00bdce1a),
    pointer_default(unique),
    local
  ]
  interface IDocHostUIHandler : IUnknown
  {
    typedefstruct _DOCHOSTUIINFO
    {
      ULONG     cbSize;
      DWORD     dwFlags;
      DWORD     dwDoubleClick;
      OLECHAR *   pchHostCss;
      OLECHAR *   pchHostNS;
    } DOCHOSTUIINFO;

    HRESULT ShowContextMenu(
      [in] DWORD dwID,
      [in] POINT* ppt,
      [in] IUnknown* pcmdtReserved,
      [in] IDispatch* pdispReserved);
    HRESULT GetHostInfo([in, out] DOCHOSTUIINFO * pInfo);
    HRESULT ShowUI(
      [in] DWORD dwID,
      [in] IOleInPlaceActiveObject * pActiveObject,
      [in] IOleCommandTarget * pCommandTarget,
      [in] IOleInPlaceFrame * pFrame,
      [in] IOleInPlaceUIWindow * pDoc);
    HRESULT HideUI();
    HRESULT UpdateUI();
    HRESULT EnableModeless([in] BOOL fEnable);
    HRESULT OnDocWindowActivate([in] BOOL fActivate);
    HRESULT OnFrameWindowActivate([in] BOOL fActivate);
    HRESULT ResizeBorder(
      [in] LPCRECT prcBorder,
      [in] IOleInPlaceUIWindow * pUIWindow,
      [in] BOOL fRameWindow);
    HRESULT TranslateAccelerator(
      [in] LPMSG lpMsg,
      [in] const GUID * pguidCmdGroup,
      [in] DWORD nCmdID);
    HRESULT GetOptionKeyPath([out] LPOLESTR * pchKey, [in] DWORD dw);
    HRESULT GetDropTarget(
      [in] IDropTarget * pDropTarget,
      [out] IDropTarget ** ppDropTarget);
    HRESULT GetExternal([out] IDispatch **ppDispatch);
    HRESULT TranslateUrl([in] DWORD dwTranslate, [in] OLECHAR *pchURLIn,
      [out] OLECHAR **ppchURLOut);
    HRESULT FilterDataObject([in]IDataObject *pDO, 
      [out] IDataObject **ppDORet);
  }

  [
    object,
    uuid(3050f6d0-98b5-11cf-bb82-00aa00bdce0b),
    pointer_default(unique),
    local
  ]
  interface IDocHostUIHandler2 : IDocHostUIHandler
  {
    HRESULT GetOverrideKeyPath([out] LPOLESTR * pchKey, [in] DWORD dw);
  }

  [
    object,
    uuid(3050f3f0-98b5-11cf-bb82-00aa00bdce0b),
    pointer_default(unique),
    local
  ]
  interface ICustomDoc : IUnknown
  {
    HRESULT SetUIHandler(
      [in] IDocHostUIHandler * pUIHandler);
  }

  [
    object,
    uuid(c4d244b0-d43e-11cf-893b-00aa00bdce1a),
    pointer_default(unique),
    local
  ]
  interface IDocHostShowUI : IUnknown
  {
    HRESULT ShowMessage(
      [in] HWND hwnd,
      [in] LPOLESTR lpstrText,
      [in] LPOLESTR lpstrCaption,
      [in] DWORD dwType,
      [in] LPOLESTR lpstrHelpFile,
      [in] DWORD dwHelpContext,
      [out] LRESULT * plResult);
    HRESULT ShowHelp(
      [in] HWND hwnd,
      [in] LPOLESTR pszHelpFile,
      [in] UINT uCommand,
      [in] DWORD dwData,
      [in] POINT ptMouse,
      [out] IDispatch * pDispatchObjectHit);
  }

  [
      object,
        uuid(342D1EA0-AE25-11D1-89C5-006008C3FBFC),
      pointer_default(unique),
      local
  ]
  interface IClassFactoryEx : IClassFactory
  {
      HRESULT CreateInstanceWithContext(IUnknown *punkContext, 
        IUnknown *punkOuter, REFIID riid, [out] void **ppv);
  };

  [
    object,
    pointer_default(unique),
    uuid(3050f5fc-98b5-11cf-bb82-00aa00bdce0b)
  ]
  interface IHTMLOMWindowServices : IUnknown
  {
    HRESULT moveTo([in] LONG x,[in] LONG y);
    HRESULT moveBy([in] LONG x,[in] LONG y);
    HRESULT resizeTo([in] LONG x,[in] LONG y);
    HRESULT resizeBy([in] LONG x,[in] LONG y);
  };

}

Полученный IDL-текст с помощью транслятора MIDL из Platform SDK (его можно найти также в каталоге Bin среды Delphi) превращаем в библиотеку типов TLB, которая очень просто импортируется с помощью команд «Component» | «Import Component…»

ПРИМЕЧАНИЕ

Чтобы не тратить время, пользуйтесь готовыми файлами из архива к статье.

Для завершения создания необходимой инфраструктуры наследуем новый компонент от TWebBrowser следующим образом:

TMyWebBrowser = class(TWebBrowser, IDocHostUIHandler, IDocHostShowUI, IDispatch)

или, подробнее,

TMyWebBrowser.pas

Нажав Ctrl+Shift+C, когда курсор находится внутри объявления класса, получаем пустую реализацию методов. Функции на первых порах могут содержать всего одну строчку:

Result := E_NOTIMPL;

дающую понять вызывающей стороне, что функция не реализована. Если функция возвращает S_OK, то считается, что необходимые действия (например, вывод контекстного меню) ею произведены; возврат S_FALSE, напротив, обозначает, что после вызова функции браузер задействует свою стандартную реакцию. За деталями реализации можно обратиться к архиву с исходными текстами.

Расширяем DOM (Document Object Model)

Внимательный читатель заметит, что помимо IDocHostUIHandler и IDocHostShowUI, помогающих контролировать меню, «горячие» клавиши, вывод диалоговых окон и справки, класс TMyWebBrowser реализует еще и IDispatch. Зачем же? Короткий ответ: для расширения объектной модели документа HTML (DOM).

Однако если браузер встречает в скрипте конструкцию вида:

window.external.что_то

где что_то — некоторый идентификатор, то, при условии, что реализован интерфейс IDocHostUIHandler, вызывается метод IDocHostUIHandler.GetExternal(out ppDispatch: IDispatch), возвращающий ссылку на объект, поддерживающий IDispatch, в надежде, что объект знает, что такое что_то и сумеет произвести необходимые действия: получение значения что_то (приведенная строчка кода), присвоение значения что_то, вызов метода что_то(). Далее как обычно в Automation, следует вызов IDispatch.GetIDsOfNames и, в случае успеха, вызов IDispatch.Invoke, который и получает значения что_то (приведенная строчка кода), присвоение значения что_то, вызов метода что_то().

Ясно, что это путь к вызову Object Pascal-кода из скриптов. Остается только научиться принимать в вызываемой функции и передавать ей в вызове Invoke произвольное количество параметров, в противном случае решение не будет общим и, как следствие, полезным.

Для передачи произвольного количества параметров используется один параметр типа array of OleVariant, элементы которого создаются при вызове Invoke. Вторым важным условием успешного вызова процедуры Object Pascal из скрипта является вызов TMyBrowser.Bind для установления связи между идентификатором внутри скрипта и любой функцией типа:

      function(Params: arrayof OleVariant): OleVariant ofobject

Естественно, подобный подход не является обязательным; можно представлять его себе как первую ступеньку на пути к реализации более удобного механизма взаимодействия скриптов и Object Pascal.

Вызов скрипта из кода на Object Pascal

Для удобства работы потребуется небольшая подготовка. Во-первых, нужно импортировать библиотеку типов mshtml.tlb (Microsoft HTML Object Library), а во-вторых, вырезать из сгенерированного средой Delphi модуля размером 12 Мб самое интересное для нас:

MiniMSHTML.pas
СОВЕТ

Можно, конечно, пользоваться и сгенерированным модулем в 12 Мб. Однако, кроме того, что бОльшая его часть не поможет пониманию обсуждаемой темы, придется мириться с тем, что работает среда медленнее (даже на моей рабочей машине с двуядерным Athlon-64 3800 и 1Гб оперативной памяти).

После изучения объявления интерфейса IHTMLDocument2Disp, остается только узнать, что именно его свойство Script: IDispatch позволяет осуществлять вызов скриптов с помощью IDispatch.GetIDsOfNames и IDispatch.Invoke.

В классе TMyWebBrowser вызов скрипта производит функция TMyWebBrowser.InvokeScript(ScriptName: WideString; ScriptParams: array of OleVariant): OleVariant, принимая в качестве параметров имя скрипта и его параметры и возвращая результат, если это предусмотрено в скрипте. Детали данного варианта реализации, как обычно, можно изучить по исходным текстам из архива к статье.

Разбор HTML

Думаю, ни у кого не вызовет сомнения то, что браузер обязан производить синтаксический разбор HTML. Разбор осложняется тем, что есть несколько стандартов, и немалое количество разработчиков (в том числе, разработчиков браузеров) и Web-дизайнеров, понимающих эти стандарты по-своему.

Уверен, если вам придется производить синтаксический разбор HTML, то захочется воспользоваться услугами уже готовой библиотеки. Для Windows-программиста может оказаться интересным использование возможностей упоминавшейся ранее mshtml.tlb (Microsoft HTML Object Library), которая всегда под рукой, с помощью, например, Automation.

Вот простой пример, рисующий дерево разбора с помощью TTreeView:

      procedure TMainForm.MyWebBrowserDocumentComplete(ASender: TObject;
  const pDisp: IDispatch; var URL: OleVariant);

    procedure Build(collection: OleVariant; Root: TTreeNode);
    var
      elem: OleVariant;
      i: integer;
      tag: WideString;
    beginfor i := 0 to collection.length - 1 dobegin
        elem := collection.item(i, tag);
        tag := elem.tagName;
        DocTreeView.Items.AddChild(Root, tag);
        Build(elem.children, 
          DocTreeView.Items[DocTreeView.Items.Count-1]);
        end;
    end;

var
  doc: OleVariant;
begin
  doc := MyWebBrowser.Document;
  DocTreeView.Items.Clear;

  Build(doc.all.item(0, '').children, DocTreeView.TopItem);
  Build(doc.all.item(1, '').children, DocTreeView.TopItem);
end;

В заключение раздела замечу, что у интерфейса элемента HTML (см. файл MiniMSHTML.pas) имеется немало свойств, позволяющих менять оформление и содержание уже загруженного документа так же, как это обычно делается с помощью скриптов.

Протокол res:// или Как сделать 2 в 1

Если в программе приходится отображать HTML, то можно хранить HTML в ресурсах вместо загрузки с помощью TWebBrowser.Navigate и/или вместо TWebBrowser.Document.Write(длинная_строка_с_HTML). При таком вызове, конечно же, необходимо приводить тип свойства Document к IHTMLDocument2 или (что и делает компилятор в первом случае) запрашивать ссылку на интерфейс IHTMLDocument2 или приводить тип Document к OleVariant и пользоваться Automation. Преимущество хранения HTML в ресурсах состоит в том, что потеря необходимых приложению данных, находящихся в ресурсах, может произойти только вместе с потерей файла с исполнимым кодом. Сохраненные в ресурсах данные извлекаются при необходимости с помощью FindResource и LoadResource.

Однако такой подход затрудняет использование ссылок в HTML.

Для удобного использования возможностей HTML в сочетании с хранением HTML в ресурсах, в браузер, начиная с Internet Explorer 4.0, была встроена поддержка протокола res. Адрес в этом протоколе задается как res://sFile[/sType]/sID, где sFile, sType, sID представляют собой, соответственно, путь к образу с ресурсами, тип ресурса и идентификатор ресурса. Если тип не указан, то предполагается, что тип представляет собой RT_HTML.

Для использования на практике этих сведений создадим файл, который откомпилируем с помощью brcc32 — компилятора ресурсов от Borland, поставляющегося вместе с Delphi:

html_res.rc
INDEX       RT_HTML     "page.html"
ABOUT       RT_HTML     "about.html"
ABOUTLOGO   RT_RCDATA   "logo.gif"

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

      {$R 'html_res.res' 'html_res.rc'}
    

С помощью следующего кода сохраненная в ресурсах страница page.html отображается TWebBrowser’ом при создании формы:

      procedure TForm1.FormCreate(Sender: TObject);
begin
    MyWebBrowser1.Navigate('res://' + ParamStr(0) + '/RT_HTML/INDEX');
end;

Вот так можно задать ссылку на рисунок в HTML-документе:

<img src="/RT_RCDATA/ABOUTLOGO" alt="logo" />

Аналогичным образом задаются ссылки на другие документы. Пример можно найти в архиве с исходными текстами к статье.

Заключение

В настоящей статье были представлены методы управления интерфейсом пользователя компонента TWebBrowser, работы с DOM, вызова скриптов из кода на Object Pascal, вызова кода Object Pascal из скриптов. Практическое значение имеют также примеры работы с элементами документа HTML с помощью Automation и хранения HTML в ресурсах приложения.

Литература

  1. MSDN Library / Web Development
  2. Roberts, Scott. Programming Microsoft Internet Explorer 5, ISBN 0-7356-0781-8, 1999

Авторские права © 2005 Киселев Роман unicorn@kurskline.ru. Публикуется с разрешения автора. Материал не может быть воспроизведен в любой форме и любыми средствами без разрешения автора.
    Сообщений 3    Оценка 40        Оценить