Показаны сообщения с ярлыком IE. Показать все сообщения
Показаны сообщения с ярлыком IE. Показать все сообщения

понедельник, 4 мая 2015 г.

Об альтернативе Application.ProcessMessages для TWebBrowser и разрыве стека выполнения

  При использовании TWebBrowser существуют две неприятности, во-первых, это сам TWebBrowser =), а во-вторых, это Application.ProcessMessages, который необходимо выполнять чуть ли не на каждое действие (загрузить документ, сменить режим редактирования и т.п).
  Использование Application.ProcessMessages может вызывать неожиданные проблемы. Особенно это актуально, когда используются оконные windows сообщения для разрыва стека выполнения. Но нашелся способ, которое позволяет не выбирать все сообщения из очереди. На первый взгляд, решение даже работает, однако буду рекомендовать никогда его не использовать и всегда использовать Application.ProcessMessage и об этом ниже.
  Очевидно, что WebBrowser обрабатывает какие-то сообщения. Но поиск их казался гиблой идеей.   В действительности оказалось, что ни так все плохо. С помощью Window Detective было обнаружено, что в момент загрузки происходит подозрительная посылка сообщений окну с именем класса  'Internet Explorer_Hidden'. Решил проверить и выбрал из очереди оконных сообщений в момент загрузки документа сообщения предназначенные только этому окну. К моему удивлению - все заработало.

Сообщения получаемые окном IE

Вообщем вот заготовка кода:

TMyWebBrowser = class(TWebBrowser)
protected
    procedure WBProcessMessage;
    procedure InternalSetValue(const AValue: string);
public
    procedure SetValue(const AValue: string); // Входная точка в примера
    procedure WaitWB;  
end;  


function EnumWindowsToFindIEHiddenProc(AHandle: HWND; AParam:NativeInt): boolean; stdcall;

var
  IEHiddenHandle: Hwnd;

implementation

procedure TMyWebBrowser.SetValue(const AValue: string);
begin
  InternalSetValue(AValue); // выполняем каким либо способом присваивание разметки
  WaitWB; // Ждем завершение загрузки документа.
  FooFunction; //Какой то функционал для работы которого необходим полностью загруженные html документ.
end;

procedure TMyWebBrowser.WaitWB;
begin
  //Как то так обычно выглядит ожидание пока документ полностью не загрузиться
  while HTMLDocument2.readyState <> 'complete' do
  begin
    WBProcessMessage; // выбираем только нужные сообщения
    //Forms.Application.ProcessMessages; // выбираем все сообщения из очереди
  end;
end;


procedure TbtkHTMLEditor.WBProcessMessage;
var
  msg: Windows.tagMSG;
  processID : THandle;
begin
  IEHiddenHandle := 0;
  processID := GetCurrentProcessId;
  if EnumWindows(@EnumWindowsToFindIEHiddenProc, processID) then // �щем хендл окна IE в нашем процессе перебирая все окна
    if IEHiddenHandle <> 0 then // Проверяем найденный хендл валидный
      if PeekMessage(msg, IEHiddenHandle, 0, 0, PM_REMOVE) then // извлекаем из очереди оконных сообщений все сообщения для окна IEHiddenHandle
      begin
        Windows.DispatchMessage(msg); // Передаем извлеченные сообщения окну IE
      end;
end;

function EnumWindowsToFindIEHiddenProc(AHandle: HWND; AParam:NativeInt): boolean;
var
  processId: NativeInt;
  classbuf: array[0..255] of Char;
const
   IEWndClassName = 'Internet Explorer_Hidden';
begin
     result := true;
    if Windows.GetWindowThreadProcessId(AHandle,@processId) <> 0 then
    begin
      if AParam = processId then
      begin
          GetClassName(AHandle, classbuf, SizeOf(classbuf));
          if lstrcmp(@classbuf[0], @IEWndClassName[1]) = 0 then
          begin
            IEHiddenHandle := AHandle;
            result := false;
          end;
      end;
    end;
 
end;

  Код на скорую руку, проверялся с IE 9-10 в Windows 7-8. Но в данном случае я избегаю слова "решение", тут больше подходит - "грязный хак". На самом деле, если у вас есть проблема того, что внезапный ProcessMessages нарушает строгий порядок ваших вызовов, то проблема не в ProcessMessages. ProcessMessages это данность архитектуры Windows и VCL. Если посылаете оконное сообщение, то будьте готовы, что оно может быть извлечено раньше, чем вы предполагаете. Например, из окна посылаем себе же сообщение WM_Close. Сообщение извлекается вот таким неожиданным ProcessMessages еще до раскрутки стека выполнения, который вызвал посылку этого сообщения. В результате дальнейшая раскрутка стека выполнения пойдет по освобожденным объектам, так как на WM_Close будет убито родительское окно и все дети на нем.
  Подобные проблемы не решить изъятием вызовов ProcessMessages. Любой модальный диалог нарушит подобный не очень хитрый план. Для избегания подобной проблемы нужно использовать другие подходы. Мы, например, подобные проблемы решаем так называемыми контекстами асинхронного выполнения и подсистемой асинхронных команд. Говоря "асинхронные команды", я не подразумеваю много-поточное выполнение, а только разрыв стека выполнения. Что и происходит, когда посылаем в свой же поток оконное сообщение.
Вся суть в том, что асинхронные команды - это надстройка над механизмом оконных сообщений, позволяющая передавать в качестве сообщений объекты, а контекст - это состояние системы, определяющее возможность исполнения этого объекта.
Таким образом, асинхронная команда - это объект, который посылается в очередь сообщений, сообщением  WM_AsyncCommand, где в качестве параметра указатель на объект. При извлечении сообщения WM_AsyncCommand из очереди сообщений, у объекта асинхронной команды вызывается обработчик, который и исполняет полезный код. Асинхронная команда принадлежит контексту. Контекст создается при запуске приложения. Если на момент извлечения асинхронной команды из очереди сообщений контекст заблокирован, тогда асинхронная команда перемещается в особый буфер команд, где дожидается разблокировки контекста.  Все что осталось, это заблокировать контекст на момент начала роста стека вызовов, в котором могут посылаться команды, имеющие возможность порушить стек выполнения:

Context.Lock;
try
    AnyUserAction;
finally
    context.Unlock;
end;

Выше я приводил пример с WM_Close. Для этого мы используем TFormCloseAsyncCommand = class(TAsyncCommand). Ко всему прочему, контексты могут быть вложенными, а команды имеют фьючеры для возможности их отмены. Но это явно не вопрос темы текущей статьи (если кому интересно, то могу накидать шаблон кода и варианты использования в отдельной статье).
Вернемся к "грязному обходному пути" и оставим в стороне вопрос о том, что ProcessMessages - данность и не является безусловным злом. И тем не менее выборочная диспетчеризация сообщений для класса "Internet Explorer_Hidden" -  сомнительный путь:
1. Данный вопрос никак не документирован. В MSDN мне попадался только код вида while (browser.IsBusy){System.Windows.Forms.Application.DoEvents();} .
2. "Решение" не проверялось на всех возможных версиях IE и Windows.
3. Нет гарантии, что мы обрабатываем все нужные сообщения.
4. В будущем архитектура WebBrowser может измениться и данный подход уже может  не работать.
5. Извлекая только определенные сообщения, мы можем нарушить порядок обработки сообщений

пятница, 29 июля 2011 г.

IE9 in edit Mode & TWebBrowser = EZeroDivide

  В начале предисловия предисловие


  Приветствую тебя случайный гость. Данный блог давно задумывался, но все что то мешало, то не было темы с которой можно начать, то природная лень брала верх над намеченным. Основная цель преследуемая блогом это упорядочивание знаний, закрепление опыта, развитие навыков изложения материала, вообщем как и у всех, т.е. для себя. Если такие цели, то "какого черта" простите ждать? Садимся и пишем! Так и появилась первая запись в этом блоге, посмотрим что в дальнейшем из этого может выйти.

  На этой недели вылезла не самая тривиальная ошибка которой и хочу поделиться в первую очередь с собой=) и с возможным читателем. 

  В программной системе на моей текущей работе для отображения HTML разметки используется компонент TWebBrowser, который как известно является оберткой для COM ядра Internet Explorer'а. С недавних пор у некоторых клиентов нашего приложения  фрейм содержащий TWebBrowser стал валиться c исключением EZeroDivide - ошибка деления на ноль. В дальнейшем выяснилось, что ошибка имеет место лишь при установленном IE9 находящемся в режиме редактирования в OS Windows 7 64bit. И как только пользователь касался полосы прокрутки всплывало исключение. 
Место генерации исключения было не в Delphi обертке и находилось где то внутри IE.  Гугление показало, что я не одинок в своей проблеме, и нас как минимум трое=). Удалось установить и причину проблемы.   
  Все дело в том, что по умолчанию в MSVC и библиотеках написанных на нем при выполнение операций в FPU не возбуждаются исключения на ошибки, а возвращает значения NaN, +INF, -INF.  За настройку данного поведения в FPU отвечает так называемый регистр управляющее слово - Control Word (CW).Кроме того, через него можно задать и другие параметры влияющие на вычисление операций над числами с плавающей запятой, например точность. 
Для решения проблемы достаточно установить нужную маску в данный регистр перед созданием TWebBrowser и восстановить значение регистра после закрытия. Это может выглядеть как то так:

var 
 CW: word;

procedure TForm1.FormCreate(Sender: TObject);
begin
   CW := Get8087CW(); // Запоминаем предыдущее состояние регистра
   System.Set8087CW($133f);  // Выключаем исключения
end; 

procedure TForm1.FormDestroy(Sender: TObject);
begin
   System.Set8087CW(CW); // Восстанавливаем предыдущее значение регистра 
end;

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


TEventObject = class(TInterfacedObject,IDispatch)
  private
    FOnEvent: TProc;
  protected
    function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
    function GetIDsOfNames(const IID: TGUID; Names: Pointer;
      NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
      Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
  public
    constructor Create(const OnEvent: TProc);
    property OnEvent: TProc read FOnEvent write FOnEvent;
  end;

var
  webBrowser: IWebBrowser;
  iDocument2: IHTMLDocument2;
  eoMouseLeave, eoMouseEnter: TEventObject;
  iElement2: IHTMLElement2;
  j: Integer;
  CW: Word;

implementation

function TEventObject.Invoke(DispID: Integer; const IID: TGUID;
  LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo,
  ArgErr: Pointer): HResult;
begin
  if (Dispid = DISPID_VALUE) then
  begin
    if Assigned(FOnEvent) then
      FOnEvent; // Вызываем обработчик события 
    Result := S_OK;
  end
  else Result := E_NOTIMPL;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  {Создание объектов обработчиков реализующих IDispatch}
  eoMouseEnter := TEventObject.Create(self.OnMouseEnter);
  eoMouseLeave := TEventObject.Create(self.OnMouseLeave);
end;

procedure TForm1.WebBrowser1DocumentComplete(Sender: TObject;
  const pDisp: IDispatch; var URL: OleVariant);
begin
  {Устанавливаем обработчики на события после загрузки страницы}
  webBrowser := pDisp as IWebBrowser;
  if Assigned(webBrowser.Document) then
  begin
    iDocument2 := webBrowser.Document as IHTMLDocument2;
    iDocument2.DesignMode := 'On';

    for j:=0 to iDocument2.All.Length-1 do
    begin
      iElement2 := iDocument2.All.item(j,EmptyParam) as IHTMLElement2;
      iElement2.AttachEvent('onmouseenter', eomouseEnter);
      iElement2.AttachEvent('onmouseleave', eoMouseLeave);
    end;
  end;
end;

procedure TForm1.WebBrowser1BeforeNavigate2(Sender: TObject;
  const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData,
  Headers: OleVariant; var Cancel: WordBool);
begin
  {отсоединяем обработчики от событий, перед загрузкой новой страницы}  
  if Assigned(webBrowser.Document) then
  begin
    iDocument2 := webBrowser.Document as IHTMLDocument2;
    for j:=0 to iDocument2.All.Length-1 do
    begin
      iElement2:=All.item(i,EmptyParam) as IHTMLElement2;
      iElement2.detachEvent('onmouseenter', eoMouseEnter);
      iElement2.detachEvent('onmouseleave', eoMouseLeave);
    end;
  end;
end;


procedure TForm1.OnMouseEnter;
begin
  CW := Get8087CW();  
  System.Set8087CW($133f); // Отключаем исключения
end;

procedure TForm1.OnMouseLeave;
begin
   System.Set8087CW(CW); // Восстанавливаем значение регистра
end;

  Здесь мы вешаем обработчики на события OnMouseEnter и OnMouseLeave, т.к. MSDN говорит нам, что события не всплывающие(не проходят по всей иерархии DOM до верхнего уровня), то обработчики устанавливаем на все объекты документа.  В качестве обработчика метод IHTMLDocument.AttachEvent просит объект с IDispatch, для этого реализуем TEventObject. И не забываем, что eoMouseEnter, eoMouseleave надо удалить перед закрытием формы.