Версия для печати


Использование функциональности IE или заметки о WebBrowser
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=366

Ihor Osov'yak
дата публикации 26-02-2001 00:00

Использование функциональности IE или заметки о WebBrowser

Содержание:

Введение.
Где в Delphi живет WebBrowser?
Начало
Где искать информацию?
Несколько слов о реализации простого веб-броузера и не только ..
Итак, маленькое лирическое отступление в сторону COM
И снова об WebBrowser
Простой парсинг

 

Введение.

  Довольно часто современному программисту приходится решать вопросы, которые связанные с отображением или обработкой информации, представленной в виде html-ресурсов. Например, на некотором сайте приводятся ежедневные котировки акций и Вам нужно собирать и обрабатывать статистку за определенный период времени. Или нужно создать сторожа, который "наблюдает" за он-лайн прайс-листом конкурента и который должен "предупреждать" об изменениях цены на определенные позиции. Или нужно написать "паука", который должен пробегать по некоторому сайту в поисках определенного текста, причем в процессе пробежки ему нужно заполнить несколько регистрационных форм, а фрагменты текста, которые он отыскал - выделить определенным цветом. Можно назвать бесконечно много подобных примеров, но суть их сводится к одному - получение веб-страницы, извлечение из нее определенной части HTML-кода программными средствами (парсинг), и, возможно, программное влияние на эту часть кода.
  Можно, конечно, используя  WinInet.dll, получить доступ к требуемому веб-ресурсу, а затем с помощью многочисленных строковых функций получить интересующею нас ее часть. Эта технология вполне работает, но довольно трудоемкая и, в большинстве случаев далека от элегантности. Другой путь - использование функциональности Microsoft Internet Explorer.

  Internet Explorer (далее - IE)- это не одна отдельная программа, а целая коллекция компонент, которые можно использовать при разработке своих приложений. Наиболее интересными с точки зрения прикладного программиста есть компоненты из shdocvw.dll и mshtml.dll. Первая DLL содержит WebBrowser - Microsoft ActiveX control, используемый для визуального просмотра веб-страниц (рабочая область окна Internet Explorer и есть тот самый компонент WebBrowser). Вторая DLL содержит синтаксический анализатор HTML кода, а также средства взаимодействия с отдельными элементами этого кода. По скромному мнению автора этой статьи - Microsoft Internet Explorer в том виде, в котором он существует в настоящее время, есть очень удачный пример использования COM-технологии.

 Целью написания этого цикла статей есть демонстрация некоторых приемов использования функциональности ActiveX-контрола WebBrowser в прикладных дельфийских программах. Автор не претендует на какое-то новшество в этом вопросе. Все, что Вы прочитаете далее, уже более-менее подробно описано в многочисленных веб-ресурсах. То небольшое, что отличает этот материал (по мнению автора) от аналогичных - это во-первых, ориентация на Delphi, а во вторых - обобщение личного опыта автора, а не  пересказ выдержек  с MSDN.

  Также эти статьи есть своего рода благодарность тем людям, которые довольно сильно помогли мне в то время, когда я делал первые шаги в направлении, к которому имеет отношение эта статья. Я особенно благодарен Борису Ладугину за тот "ликбез" в части COM, который он провел со мной в личной переписке, а также Лене Филиповой и всем местным жителям, советы которых на "круглом столе" не раз давали толчок для движения в верном направлении.

 Автор рассчитывает на то, что читатель уже имеет некоторый опыт программирования на Delphi и хотя бы в общих чертах знаком с COM-технологиями. Хотя автор "разборку" с WebBrowser и COM делал одновременно. Для тех читателей, которые не знакомы с COM, я постараюсь по ходу дела делать маленькие отступления, которые конечно, не заменят специализированного материала, но, надеюсь, дадут хотя бы направление поиска в случае затруднений.

Где в Delphi живет WebBrowser?

 Для любого зарегистрированного в палитре ActivX-контрола Delphi при импорте создает класс-оболочку, которая наследуется от TOleControl . Для начала не станем углубляться в особенности TOleControl и производных от нее оболочек - ибо сие дело поначалу может не так прояснить, как запутать ситуацию. Отметим только, что оболочка и сам ActiveX есть несколько разные вещи. Собственно TOleControl и производные от него оболочки есть не более, чем средство, которое обеспечивают возможность работы с ActiveX, как с "родными" VCL-компонентами. Для WebBrowser от IE такой оболочкой есть TWebBrowser. Если Вы используете Delphi5, то соответствующий компонент можно отыскать на закладке "Internet " палитры компонентов. Если Вы работаете с Delphi4 , то Вам нужно провести импорт соответствующего ActiveX-контрола. Для этого следует воспользоваться меню "Import ActiveX Control" и в списке ActiveX выбрать "Microsoft Internet Controls" (разумеется, у Вас должен быть установлен IE). Компонент-оболочка по умолчанию устанавливается на закладку "ActiveX" палитры компонентов. Если Вам нужен не только компонент для отображения Web-страниц, а Вы еще собираетесь проводить парсинг загруженных страниц, то Вам также следует провести импорт mshtml.dll. Это можно сделать при помощи меню Import Type Library, выбрав в списке строчку Microsoft HTML Object Library.
  Даже если Вы используете Delphi5, в определенных случаях есть смысл исключить предустановленный компонент TWebBrowser и провести импорт соответствующих компонентов самостоятельно. Это может быть необходимым в случае, если Вы желаете написать приложение, совместимое с IE4, а в Delphi5 модули mshtml.pas и shdocvw.pas рассчитаны на использование IE5. И как следствие, довольно много интерфейсов, которые декларируются в соответствующих модулях, не будут поддерживаться IE4 (к примеру,  тот же IHTMLDocument3). Или наоборот, Вас, возможно, заинтересовала какая-то особенность новой версии IE, декларация которой отсутствует в mshtml.pas (или shdocvw.pas) c поставки Delphi.
  Если Вы решитесь для Delphi5 проводить самостоятельный импорт mshtml.dll через Import Type Library - уберите галочку с "Generate Component Wrapper" - в противном случае Delphi создаст никому не нужные класы-оболочки для интерфейсов и раздует результирующий файл (mshtml_tbl.pas)до несусветных размеров.
Понятно, что если Вы провели импорт, то в Delphi5 Вам вместо

 uses mshtml,shdocvw;
придется использовать
 uses mshtml_tbl,shdocvw_tbl;

Если Вы проведете импорт, то Вы наверняка обратите внимание на то, что помимо упоминаемого TWebBrowser рядышком будет TWebBrowser_V1. Что это за зверь? Ответ довольно прост - это совместимый с IE3 контрол. В IE4 он введен для совместимости с теми прикладными программами, которые разрабатывались в расчете на IE3.

  И заканчивая тему экспорта - в библиотеке типов от IE довольно часто используются имена, которые есть зарезервироваными для Delphi. В большинстве случаев Delphi справляется с этой задачей (к примеру переименовывая метод type некоего интерфейса в type_). Но для mshtml.dll от IE5 есть один неприятный момент - там декларируется константа
const
  True = $00000001;
И если Вы делаете импорт в Delphi4 - то  никакого переименования не происходит. B как следствие в каком-то безобидном месте наподобие нижеследующего
implemantation
 uses mshtml_tbl;
 
 function IsOk:boolean;
 begin
  result:=true;
  // .....
 end;
 
получаете сообщение компилятора о несовместимости типов. Что делать?
Или смирится и писать:
  result:=system.true;<  BR>
или "научить" Delphi4 обходному маневру: перед импортом mshtml.dll добавить в DELPHI\BIN\tlibimp.sym две строчки:
 True
 False
 

Начало


Где искать информацию?

 Итак, мы уже разобрались, где наш контрол живет. И как его импортировать в случае необходимости.
Теперь несколько слов об дополнительных источниках информации. Если Вы работаете с Delphi5, то для начала можно посмотреть встроенную контекстную справку по TWebBrowser. Но к сожаленью, она довольно скудна, и описывает (и то поверхностно) только основные свойства компонента TWebBrowser. Вы ни слова не найдете об возможностях mshtml.dll (а там запрятаны основные вкусности). Наиболее радикальное решение - приобрести свежее издание MSDN (или работать с ее онлайновой версией http://msdn.microsoft.com). Но и при таком решении не все будет гладко - так как MSDN в первую очередь не учебник, а справочник. И к тому же некоторые аспекты в нем освещены не так полно, как хотелось бы. Но увы, это пожалуй наиболее полный источник информации, пробелы в котором можно компенсировать только многочисленными экспериментами и анализом происходящего. Ответ на конкретный вопрос можно попытаться отыскать в конференциях. Можно и в "общих", таких как старый, добрый fido7.delphi.ru, или здесь, на Круглом столе. А также в "специализированных" - http://www.talk.ru/forum/talk.ru.delphi.webbrowser  и  http://www.egroups.com/group/delphi-webbrowser   (последняя - англоязычная). Также рекомендую сайт IE & Delphi .

  Я также надеюсь, что время, затраченное мной на написание, а Вами на прочтение этой статьи, потрачено не зря.
Ну, а если Вы находитесь в самом начале пути - то можно просмотреть статью Александра Лозовюка  Как сделать WebBrowser средствами Delphi 5  (рубрика Hello, world )  ...

Несколько слов о реализации простого веб-броузера и не только ..

 В принципе, создать простенький веб-броузер c использованием TWebBrowser - дело мало чем более одной минуты. Открываем новый проект, центрируем форму, в нижней части размещаем панель, на которую бросаем ComboBox для вввода URL, слева от нее соответствующий Label, справа кнопочку "Go". Разместим также главное действующие лицо TWebBrowser над панелью. Дадим более-менее вразумительные имена нашим компонентам (например, ComboBox можно назвать "selUrl"), проставим соответствующие опции выравнивания. Ну, и самая "трудная" часть задачи - создадим обработчик нажатия на кнопочку "Go":
procedure TFormSimpleWB.btGoClick(Sender: TObject);
var
  _URL, Flags, TargetFrameName, PostData, Headers: Olevariant;
begin
  _URL := selUrl.Text;
  Flags := 0; TargetFrameName := 0; Postdata := 0; Headers := 0;
  WebBrowser1.Navigate2(_URL, Flags, TargetFrameName, PostData, Headers);
end;

 Исходники этого "шедевра" (Delphi5) c теми дополнениями, о которых речь идет ниже, можно взять здесь (3k)

  Ну а теперь несколько более подробно. Для начала посмотрим список методов и свойств TWebBrowser. Здесь следует отличать "обычные" методы и свойства компонентов VCL, и те, которые "отражают" методы соответствующего ActiveX элемента. Первые нас не очень интересуют (если читатель имеет хотя бы небольшой опыт работы с Delphi, то по отношению к чисто VCL-свойствам типа Align , TabOrder ему должно быть все понятно.) Остановимся на второй группе свойств и методов.Их  можно разделить на две группы - те, которые "отражают" default-интерфейс (в нашем случае это IWebBrowser2 и те, которые "отражают" нотификационный интерфейс DWebBrowserEvents2 ).
 
"Cвязку" между методами интерфейсов и методами класса-оболочки делает експерт импорта ActiveX, основываясь на особенностях реализации TOleControl. Проанализируйте исходники TOleControl и TWebBrowser - и Вы увидите эти связки. Это довольно интересное и довольно утомительное занятие ... Лично мне до конца пройти этот путь не хватило терпения - я остановился на той стадии, когда начал более-менее понимать основные принципы интеграции ActiveX в VCL.  Для заинтересовавшихся подсказка - обратите в первую очередь внимание на методы TWebBrowser.InitControlData и на  TOleControl.GetEventMethod(DispID: TDispID; var Method: TMethod); 

  Да, вполне возможно, что читатель не знаком с основами СOM, и понятия "интерфейс", "нотификационный интерфейс" ему ни о чем не говорят. Я не знаю, смогу ли я в двух-трех предложениях компенсировать этот пробел. Но все - же попытаюсь.  Если читатель ориентируется в COM-технологиях, но следующих несколько абзацев можно пропустить.

Итак, маленькое лирическое отступление в сторону COM

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

  Внимательный читатель  задаст вопрос - а как же с разделением адресного пространства, ведь  довольно часто клиент и объект COM живут в разных процессах, и следовательно их адресные пространства изолированы друг от друга? Как же тогда клиент и объект COM работают с одной таблицей?
  Эту проблему решает библиотека поддержки СOM, которая внедряет в адресное пространство клиента и сервера специальные служебные объекты, называемые заместителями (для клиента) и заглушками (для сервера) (здесь под сервером понимается тот процесс, который создал один или несколько экземпляров объектов COM). Таким образом клиент будет взаимодействовать с заглушкой, а сервер с заместителем. Организация взаимодействия между заглушкой и заместителем - проблема библиотеки поддержки COM.
  К счастью, вся эта алхимия в большинстве случаев не требует от программиста какого-то либо вмешательства. Я во всяком случае, встречал довольно много программистов, которые активно используют COM-технологии, но понятия не имеют о тех вещах, которых мы вскользь коснулись выше.
 

 Подытоживая, можно сказать,что интерфейс есть спецификация, которая на на уровне бинарного кода "отражается" в таблицу вызовов в памяти.
  В СОМ интерфейсы - это все. Для клиента сервер представляет собой набор интерфейсов. Клиент с сервером может взаимодействовать только посредством интерфейсов. Мало того, клиент даже может не знать о всех интерфейсах, поддерживаемых сервером.
  Все интерфейсы наследуются от базового интерфейса IUnnknown . Причем, если говорят о наследовании интерфейсов, то понимают не наследование реализации (с ней мы имеем дело, когда работаем в пределах объектной модели хотя бы того же Delphi), а наследование деклараций. Под наследованием
деклараций понимается то, что если некий интерфейс IB наследуется от интерфейса IA, то в соответствующей таблице вызовов для интерфейса IB сначала будут идти адреса методов, которые декларируются в IA, а затем адреса методов от IB. Причем списки формальных параметров наследуемых методов не должны быть изменены.   Если вспомнить, что интерфейсы есть спецификации, то становится понятным, почему по отношению к ним может идти речь только о наследовании деклараций. Конечно, при реализации конкретного COM-обьекта можно использовать технологию наследования реализации, но это будет внутреннее дело объекта, которое никак не затрагивает клиента.

В завершение разговора об COM, я хотел бы упомянуть о некоторых методах базового интерфейса IUnnknown, так как во первых, эти методы присутствуют в любом интерфейсе (вспомним о наследовании деклараций и о том, что любой интерфейс наследуется от IUnnknown) и во вторых, на использовании этих методов строится вся идеология работы с COM.

Итак, разрешите представить - QueryInterface. С помощью этого интерфейса клиент может определить, поддерживает ли COM-обьектом какой либо другой интерфейс, который известен клиенту, и получить указатель на тот интерфейс, если он поддерживается объектом. При работе с СOM, это пожалуй самый популярный вызов. В Dеlphi он иногда вызывается явно, иногда неявно. Неявный вызов происходит при применении оператора as для интерфейсных ссылок.
   Интерфейс IUnnknown также декларирует два метода интерфейса AddRef и Release, которые ответственны за подсчет использования COM-обьекта (одно из требований к COM-обьектам - они должны уметь сами себя уничтожить, если в их услугах более никто не нуждается). Вам вряд ли придется вызывать эти методы напрямую, так как Delphi генерирует их вызовы автоматически.

Сейчас, пожалуй, самое время время взглянуть на mshtml.pas - как видим он почти на 100% состоит из одних деклараций интерфейсов - ведь нам как клиенту важно знать спецификацию. И совсем не обязательно быть в курсе особенностей реализации.

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

   И снова об WebBrowser

 В нашем случае ActiveX нам предоставляет интерфейс IWebBrowser2 и ожидает от нас, что мы предоставим ему реализацию нотификационного интерфейса DWebBrowserEvents2 . К счастью, всю необходимую работу за нас сделал эксперт импорта ActiveX,  "наследовав" TWebBrowser от TOleControl, инкапсулировав при этом IWebBrowser2  посредством соответствующих методов и свойств и реализовав обработчики для каждого метода нотификационного интерфейса.

 Перед тем, как продолжить рассказ, хотелось бы обратить Ваше внимание на такой момент. Как Вы знаете, веб-документ может состоять из одного фрейма (более корректно - не иметь фреймов) или состоять из нескольких фреймов. Каждый фрейм - это тот же WebBrowser, который входит в WebBrowse более высокого уровня. WebBrowser самого верхнего уровня и есть тот AxtivX, который инкапсулируется VCL-компонентом TWebBrowser. Он как бы живет все время, пока живет TWebBrowser, тогда как WebBrowser более нижнего уровня могут динамически создаваться и уничтожатся в зависимости от того, делаем мы навигацию к много-фреймовым или к одно-фреймовым документам. Так вот, к методам WebBrowser верхнего уровня мы можем получить доступ как через методы и свойства соответствующего TWebBrowser, так и через соответствующие интерфейсные ссылки. К методам "вложенных" WebBrowser - только через интерфейсные ссылки. Интерфейсную ссылку на WebBrowser верхнего уровня можно получить через свойство TWebBrowser.ControlInterface или через TWebBrowser.DefaultInterface Получить интерфейсные ссылки на WebBrowser нижнего уровня можно посредством простого парсинга или при помощи некоторых обработчиков событий, которые сопровождают процесс навигации (но об этом ниже).

 Рассмотрим вкратце сначала основные методы и свойства "от" IWebBrowser2, а затем обработчики "от" DWebBrowserEvents2 .

 В первую очередь следует упомянуть метод Navigate. Этот метод дает команду WebBrowser начать навигацию к указанному ресурсу. Синтаксис этого метода следующий:

    procedure Navigate(const URL: WideString; var Flags: OleVariant; 
                       var TargetFrameName: OleVariant; var PostData: OleVariant; 
                       var Headers: OleVariant); overload;
Здесь

URL - адресс навигации
для значения Flags определены такие константы:
 
navOpenInNewWindow   1 - открывает URL в новом окне браузера по умолчанию. То есть, в IE. Как заставить открыть URL в новом екземпляре нашего броузера я раскажу ниже при описании обработчика OnNewWindow2
navNoHistory   2 - не заносит адрес в список History.
navNoReadFromCache   4 - не использует сохраненную в кеше страницу, а загружает с сервера.
navNoWriteToCache   8 - не записывает страницу в дисковый кеш.
navAllowAutosearch   16 - если броузер не может найти указанный домен, он передает его в поисковый механизм.
TargetFrameName - определяет целевой фрейм по имени. Если присвоить system.NULL (или имя несуществующего фрейма ) страница просто загрузиться в текущий броузер
PostData - определяет данные для передачи на сервер.
Headers - определяет HTTP-хидер для передачи на сервер.
Пример вызова этого метода для обычной навигации можно посмотреть в примере, ссылка на который была
выше.
Для передачи данных можно воспользоваться следующим фрагментом кода, который предложен Hans Gulo:
procedure TForm1.SubmitPostForm;
  var strPostData: string; Data: Pointer; URL,
  Flags,
    TargetFrameName,
  PostData, Headers: OleVariant;
   begin 
   { 
    <form method="post" action= "http://127.0.0.1/cgi-bin/register.pl">
    <input name=   "FIRSTNAME" value="Hans"> 
    <input name= "LASTNAME"  value="Gulo">
    <inputname="NOTE"value="thatsit">
    <inputtype="submit"value="thatsit"></form>}
    strPostData:='FIRSTNAME=Hans&LASTNAME=Gulo&NOTE=thats+it';
    PostData :=  VarArrayCreate([0, Length(strPostData) - 1], varByte);
    Data := VarArrayLock(PostData);
    try
      Move(strPostData[1], Data^, Length(strPostData));
    finally
      VarArrayUnlock(PostData);
    end;
    URL := 'http://127.0.0.1/cgi-bin/register.pl';
    Flags := EmptyParam;
    TargetFrameName := EmptyParam;
    Headers := EmptyParam; // TWebBrowser will see that we are providing
			   // post data and then should automatically fill
			   // this Headers with appropriate value
    WebBrowser1.Navigate2(URL, Flags, TargetFrameName, PostData, Headers);
  end;

Важным есть property Busy . Если это свойство активно (равно True), то это свидетельствует о том, что наш АктивИкс  еще не закончил загрузки страницы или выполняет некоторую команду. И может быть, что он проигнорирует новую команду. Так что в этом случае лучше подождать, когда это свойство станет равным false (или когда идет загрузка, то остановить ее можно с помощью метода Stop).

Теперь несколько слов о событиях, которые сопровождают процесс загрузки. Они, как отмечалось выше, есть своего "продолжение" соответствующих методов DWebBrowserEvents2 . Наиболее существенными из них есть (они возникают для каждого фрейма):
OnBeforeNavigate2
Возникает при попытке начать навигацию. Из параметров наиболее существенным есть pDisp: IDispatch;. Этот параметр определяет броузер, который начинает навигацию. Для многофреймового документа этот броузер может не соответствовать броузеру верхнего уровня. К сожалению, этот обработчик не вызывается при вызове метода Refresh.
OnNavigateComplete2
Возникает, когда попытка навигации была успешной. Наблюдение за WebBrowser позволяют сделать предположение, что это событие возникает после того, как с сервера придет первая порция данных. Документ еще продолжает загружаться.
OnDocumentComplete
Возникает при окончании загрузки в независимости от того, был ли документ загружен полностью или нет. К сожалению нет простого критерия для определения того, была ли страница загружена полностью или нет. Как решить єту проблему я попытаюсь рассказать в следующих статьях этого цикла.
OnNewWindow2
Возникает при попытке открыть документ в новом окне. Если Вы хотите, чтобы документ был открыт в Вашем экземпляре броузера, то Вам нужно создать свой экземпляр броузера и параметру ppDisp присвоить интерфейсную ссылку на этот экземпляр:
procedure TFormSimpleWB.WebBrowser1NewWindow2(Sender: TObject;
  var ppDisp: IDispatch; var Cancel: WordBool);
 var newForm:TFormSimpleWB;
begin 
 newForm:=TFormSimpleWB.Create(Application);
 newForm.Show;
 ppDisp:=newForm.WebBrowser1.ControlInterface;
end;



С остальными методами должно быть более-менее понятно из их названия. Если это не так - можно посмотреть уже упоминаемую статью Александра Лозовюка.

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

 Первым делом это   TWebBrowser.Document:IDispatch . Через это свойство можно получить доступ к интерфейсу IHtmlDocument2.. Далее через этот интерфейс можно получить доступ к большинству средств по взаимодействию с загруженным документом. То есть это очень интересное и "нужное" свойство. Но немного забегая наперед, скажу, что если Вы попытаетесь использовать TWebBrowser.Document:IDispatch, то Вы рано или поздно заметите довольно странную "утечку" памяти в процессе навигации. В чем же дело? После анализа ситуации, удалось определить, что для любой интерфейсной ссылки на документ, которая получена через этот свойство, счетчик использования "необоснованно" увеличивается на 1 и соответствующий COM-обьект никогда не будет освобожден. При более детальном изучении нашлась и создательница этой проблемы -   function TOleControl.GetIDispatchProp(Index: Integer): IDispatch;, через которую и работает TWebBrowser.Document:IDispatch (я речь веду о Delphi5, возможно в Delphi4 все нормально, не проверял).  Детальный рассказ об этой ситуации  выходит за рамки этой статьи..   
   К счастью эту проблему легко обойти, использовав для получения IHtmlDocument2  альтернативные возможности, хотя бы WebBrowser1.ControlInterface.Document .

 Также хочется упомянуть о  property LocationURL: WideString;  Как утверждается в вышеупомянутой статье Александра Лозовюка , оно содержит URL ресурса, загруженного в браузер. Того же мнения придерживается и контекстная справка от Delphi5. Мало того - об этом также говорится в  MSDN . - во всяком случае так было на момент написания статьи ...

  Но это не совсем так. Дополним наш "шедевр" обработчиком события окончания загрузки документа:

implementation

{$R *.DFM}

uses mshtml;

procedure TFormSimpleWB.WebBrowser1DocumentComplete(Sender: TObject;
  const pDisp: IDispatch; var URL: OleVariant);
begin //
  Caption:=WebBrowser1.LocationURL+' || '+
    ((pDisp as IWebBrowser).Document as IHtmlDocument2).URL;
end;

  Обратите внимание на включение модуля mshtml, который позволяет использовать функциональность mshtml.dll.

 Перед тем как продолжить рассказ я снова вынужден сделать маленькое отступление в сторону COM.

   Обратите внимание, как имея в руках интерфейс типа IDispatch от броузера (параметр pDisp), который закончил загрузку документа (это можно подсмотреть в описании события DocumentComplete в MSDN ), мы посредством "as" получаем интерфейс типаbIWebBrowser на тот же объект (здесь имеем неявный вызов QueryInterface). Этот интерфейс при помощи свойства Documentпbозволяет нам получить интерфейс IHtmlDocument2 к загруженому документу. И вот в конце-концов через этот интерфейс мы можем обратится к интересующему нас свойству URL , которое и возвращает адрес того ресурса, который в действительности загружен в браузер (а property LocationURL говорит только о том, что мы броузеру сказали загружать).  Да,  нам еще не раз придется продиратся через такие дебри интерфейсов, свойств и запросов. И что наиболее печально - MSDN не всегда внятно говорит где и от кого можно запросить интересующий нас интерфейс ... Также на первых порах вызывает недоумение тот факт, почему тот же IWebBrowser.Document есть типа IDispatch, а не хотя бы тот же IHtmlDocument2. Но это довольно легко понять, если вспомнить во первых, что WebBrowser позволяет работать с ним разного рода скриптовым языкам, а во вторых, что интерфейс IDispatch позволяет вызывать свойства и методы по имени (что собственно и делают скриптовые языки). В принципе, мы бы также работать в Delphi с  WebBrowser  в стиле скриптовых языков, но я сознательно не привожу примеров такого подхода, так как он чреват возникновением разного рода ошибок, которые можно будет обнаружить только во время выполнения (и которые отсеиваются на этапе компиляции при использовании "нормальных" интерфейсов).

  Но довольно теории - сделаем маленький эксперимент: запустим "шедевр" на выполнение и дадим команду навигации на заведомо отсутствующий ресурс. И что же мы видим:



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

Простой парсинг

  А теперь пожалуй пришло время очень сильно подружится с интерфейсами, ибо вся работа с основными вкусностями WebBrowser возможна только через них.  Декларации основных интерфейсов Вы найдете в модулях mshtml и SHDocVw.
  Перед тем, как организовать взаимодействие с составляющими документа, естественно что нужно этот документ разобрать на составляющие, то есть провести парсинг. Это довольно просто можно сделать при помощи интерфейса IHtmlDocument2, который предоставляет средства по доступу к документу, который загружен в соответствующий броузер. Сам же IHtmlDocument2 можно получить, имея "в руках" интерфейс IWebBrowser2 на броузер, в котором этот документ содержится.

Как уже отмечалось, для документа самого верхнего уровня это сделать довольно просто:
 var doc:IHtmlDocument2;
 .....
 if assigned(WebBrowser1.ControlInterface.Document) 
  then   WebBrowser1.ControlInterface.Document.QueryInterface(IHtmlDocument2,doc);
 
 Хотел бы обратить Ваше внимание на условие "if" - это связано с тем, что если броузер еще не делал навигации, то свойство Document не будет проинициализировано. Также я надеюсь, Вы помните, почему используется конструкция WebBrowser1.ControlInterface.Document а не WebBrowser1.Document

  А как же получить доступ к вложенным фреймам?

Это можно сделать как минимум двумя способами
Первый:
Использовать OnDocumentComplete для получения интерфейса к броузеру каждого фрейма, примерно так, как приводилось выше в примере procedure TFormSimpleWB.WebBrowser1DocumentComplete
Второй:
Использовать свойства самого IHtmlDocument2 для получения доступа к фреймам. Понятно, что нужно иметь доступ к IHtmlDocument2 самого верхнего уровня. Пример реализации этого подхода:
type TDoerOneDoc = procedure (iDoc:IHtmlDocument2);

procedure DoWithFrames(iDoc:IHtmlDocument2; aDoer:TDoerOneDoc);
 { процедура aDoer будет вызвана для каждого  IHtmlDocument2, начиная с главного 
   и для каждого IHtmlDocument2 с любого уровня вложенности фреймов}
  var frames:IHTMLFramesCollection2;
      i:integer;
      ov1:OleVariant;
      iDisp:IDispatch;
      IWindow2:IHTMLWindow2;
begin
 if not assigned(aDoer) then Exit;
 aDoer(iDoc);
 frames:=iDoc.frames;
 if not assigned(frames) then exit;
 if frames.length=0 then exit;
 for i:=1 to frames.length do begin
   ov1:=i-1;
   try
    iDisp:=frames.item(ov1);
    iDisp.QueryInterface(IHTMLWindow2,IWindow2);
    if assigned(IWindow2) then  DoWithFrames(IWindow2.document,aDoer);
   except
     { ShowMessage('Find error !!!');}
   end;
  end;
end;

  Итак, имея в руках IHtmlDocument2 можно приступить и к парсингу ...
Наиболее простой способ для этого - использование метода All интерфейса IHtmlDocument2, который позволяет получить список или всех тегов или только тегов определенного вида. Посмотрим пример для получения списка всех тегов:
 
procedure TFormSimpleWB.WebBrowser1DocumentComplete(Sender: TObject; 
        const pDisp: IDispatch; var URL:  OleVariant); 
var
      i : integer;
      iDoc : IHtmlDocument2; 
      iDisp : IDispatch;
      iElement : IHTMLElement;
      iInputElement : IHTMLInputElement;
      S : string; 
begin
  Memo1.Clear;
  iDoc:=(pDisp as IWebBrowser).Document as IHtmlDocument2;
  for i:=1 to iDoc.All.Get_length do begin
    iDisp:=iDoc.Get_all.item(pred(i),0);
    iDisp.QueryInterface(IHTMLElement, iElement);
    Str(pred(i),S);
    S:= S+'';
     if assigned(iElement) then
     begin
         S:=S+'tag='+iElement.Get_tagName+' ';
         iElement.QueryInterface(IHtmlInputElement,iInputElement);
      if assigned(iInputElement) then
      begin
         S:=S+'name='+iInputElement.Get_name;
      end;
      Memo1.Lines.Add(S);
    end;
  end;

end;

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

Далее посмотрим пример получения списка тегов определенного типа:

procedure TFormSimpleWB.btPutDataClick(Sender: TObject);
 var iDoc:IHtmlDocument2;
     i:integer;
     ov:OleVariant;
     iDisp: IDispatch;
     iColl:IHTMLElementCollection;
     iInputElement:IHTMLInputElement;
begin //

  WebBrowser1.ControlInterface.Document.QueryInterface(IHtmlDocument2,iDoc);
  if not assigned(iDoc) then begin
    ShowMessage(' !!!??? Nothing dowloaded ... ');
    Exit;
  end;
  ov:='INPUT';
  IDisp:=iDoc.all.tags(ov);
  if assigned(IDisp) then begin
     IDisp.QueryInterface(IHTMLElementCollection,iColl);
     if assigned(iColl) then begin
       for i:=1 to iColl.Get_length do begin
         iDisp:=iColl.item(pred(i),0);
         iDisp.QueryInterface(IHTMLInputElement,iInputElement);
         if assigned(iInputElement) then begin
           if iInputElement.Get_name='mn'
           then iInputElement.Set_value('Ihor');
           if iInputElement.Get_name='pw'
           then iInputElement.Set_value('PASSWORD');
         end;
       end;
     end;
   end;


end;

 В этом примере получаем список тегов типа "INPUT", а потом для некоторых тегов (которые отбираем по имени) делается попытка сделать "ввод данных". Полностью этих два примера (как проект) можно взять здесь (4k).

Ну, для начала пожалуй и хватит. Если у Вас есть вопросы к автору, их можно задать на http://www.talk.ru/forum/talk.ru.delphi.webbrowser, ведущим которого является автор этой статьи. Вполне возможно, что Ваши вопросы подскажут направление продолжения этой статьи.