【问题标题】:How to pump COM messages?如何泵送 COM 消息?
【发布时间】:2016-03-29 00:25:00
【问题描述】:

我想等待 WebBrowser 控件完成导航。所以我创建了一个Event,然后我想等待它被设置:

procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
   FEvent.ResetEvent;
   WebBrowser.Navigate2('about:blank'); //Event is signalled in the DocumentComplete event

   Self.WaitFor;
end;

然后我在DocumentComplete 事件中设置事件:

procedure TContoso.DocumentComplete(ASender: TObject; const pDisp: IDispatch; const URL: OleVariant);
var
    doc: IHTMLDocument2;
begin
    if (pDisp <> FWebBrowser.DefaultInterface) then
    begin
       //This DocumentComplete event is for another frame
       Exit;
    end;

    //Set the event that it's complete
    FEvent.SetEvent;
end;

问题在于如何等待这个事件发生。

等一下

第一反应是等待事件被触发:

procedure TContoso.WaitFor;
begin
   FEvent.WaitFor;
end;

问题在于DocumentComplete 事件永远无法触发,因为应用程序永远不会足够空闲以允许 COM 事件通过。

忙睡眠等待

我的第一反应是忙着睡觉,等待一个flag:

procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
   FIsDocumentComplete := False;
   WebBrowser.Navigate2('about:blank'); //Flag is set in the DocumentComplete event
   Self.WaitFor;
end;

procedure TContoso.WaitFor;
var
   n: Iterations;
const
   MaxIterations = 25; //100ms each * 10 * 5 = 5 seconds
begin
   while n < MaxIterations do
   begin
      if FIsDocumentComplete then
         Exit;
      Inc(n);
      Sleep(100); //100ms
   end;
end;

Sleep 的问题在于它不允许应用程序足够空闲以允许 COM 事件消息通过。

使用 CoWaitForMultipleHandles

经过研究,似乎 COM 人创建了一个专门针对这种情况创建的函数:

当单线程单元 (STA) 中的线程阻塞时,我们会为您发送某些消息。阻塞期间的消息泵送是 Microsoft 的黑魔法之一。抽水过多会导致重入,从而使您的应用程序所做的假设无效。泵送太少会导致死锁。从 Windows 2000 开始,OLE32 公开了 CoWaitForMultipleHandles,以便您可以“恰到好处”地泵送。

所以我尝试了:

procedure TContoso.WaitFor;
var
   hr: HRESULT;
   dwIndex: DWORD;
begin
   hr := CoWaitForMultipleHandles(0, 5000, 1, @FEvent.Handle, {out}dwIndex);
   OleCheck(hr);
end;

问题是这行不通;它不允许 COM 事件出现。

使用 UseCOMWait 等待

我也可以试试 Delphi 自己的 TEvent 的秘密功能:UseCOMWait

UseCOMWait设置为True以确保当线程被阻塞并等待对象时,任何STA COM调用都可以返回到该线程中。

太棒了!让我们使用它:

FEvent := TEvent.Create(True);

function TContoso.WaitFor: Boolean;
begin
   FEvent.WaitFor;
end;

除非那行不通;因为回调事件永远不会被触发。

MsgWaitForMultipleBugs

所以现在我开始深入研究可怕的,awfulawfulawful,buggy,error -容易、重入诱导、草率、需要轻推鼠标,有时会导致MsgWaitForMultipleObjects 的世界崩溃:

function TContoso.WaitFor: Boolean;
var
//  hr: HRESULT;
//  dwIndex: DWORD;
//  msg: TMsg;
    dwRes: DWORD;
begin
//  hr := CoWaitForMultipleHandles(0, 5000, 1, @FEvent.Handle, {out}dwIndex);
//  OleCheck(hr);
//  Result := (hr = S_OK);

    Result := False;
    while (True) do
    begin
        dwRes := MsgWaitForMultipleObjects(1, @FEvent.Handle, False, 5000, QS_SENDMESSAGE);
        if (dwRes = WAIT_OBJECT_0) then
        begin
            //Our event signalled
            Result := True;
            Exit;
        end
        else if (dwRes = WAIT_TIMEOUT) then
        begin
            //We waited our five seconds; give up
            Exit;
        end
        else if (dwRes = WAIT_ABANDONED_0) then
        begin
            //Our event object was destroyed; something's wrong
            Exit;
        end
        else if (dwRes = (WAIT_OBJECT_0+1)) then
        begin
            GetMessage(msg, 0, 0, 0);
        if msg.message = WM_QUIT then
        begin
            {
                http://blogs.msdn.com/oldnewthing/archive/2005/02/22/378018.aspx

                PeekMessage will always return WM_QUIT. If we get it, we need to
                cancel what we're doing and "re-throw" the quit message.

                    The other important thing about modality is that a WM_QUIT message
                    always breaks the modal loop. Remember this in your own modal loops!
                    If ever you call the PeekMessage function or The GetMessage
                    function and get a WM_QUIT message, you must not only exit your
                    modal loop, but you must also re-generate the WM_QUIT message
                    (via the PostQuitMessage message) so the next outer layer will
                    see the WM_QUIT message and do its cleanup as well. If you fail
                    to propagate the message, the next outer layer will not know that
                    it needs to quit, and the program will seem to "get stuck" in its
                    shutdown code, forcing the user to terminate the process the hard way.
            }
            PostQuitMessage(msg.wParam);
            Exit;
        end;
        TranslateMessage(msg);
        DispatchMessage(msg);
    end;
end;

上面的代码是错误的,因为:

  • 我不知道要唤醒什么样的消息(是否发送了 com 事件?)
  • 我不知道我不想调用 GetMessage,因为它会获取消息;我只想得到 COM 消息(见第一点)
  • 我可能应该使用 PeekMessage(见第 2 点)
  • 我不知道是否必须循环调用 GetMessage,直到它返回 false(请参阅 Old New Thing

如果我要发送自己的消息,我已经编程了足够长的时间来逃跑,很远。

问题

所以我有四个问题。都有关系。这篇文章是四个之一:

  • 如何使 WebBrower.Navigate2 同步?
  • 如何抽取 COM 消息?
  • 泵送 COM 消息会导致 COM 事件回调吗?
  • 如何使用 CoWaitForMultipleHandles

我正在编写并使用 Delphi。但显然任何本机代码都可以工作(C、C++、汇编、机器代码)。

另见

【问题讨论】:

  • 我想我在您的描述中遗漏了一些东西 - 为什么您的应用程序没有足够空闲来处理加载单个(非常小的)文档?当我使用WebBrowser 时,加载文档和处理事件是一个简单的设置。 (我为此使用了 VB6,但 COM 端在 Delphi 中应该是相同的。)
  • 你为什么要让Navigate2同步?你想通过这个来达到什么目的?对我来说,这几乎听起来像是一个 X/Y 问题(不过我可能遗漏了一些细节)。
  • @xxbbcc:您必须等待浏览器的Document 完全加载,即使导航到about:blank,才能安全地操作它。导航到about:blank,等待就绪,然后将自定义的 HTML 动态流式传输到浏览器中并不少见。
  • 我想说“因为我说过”,但雷米知道我在做什么。对于其他所有人:MSDN: Loading HTML content from a Stream。像往常一样:不要将示例与问题混淆。问题不在于如何将 HTML 加载到网络浏览器中,或者如何使 Navigate2 同步——而是如何泵送 COM 消息。如果我使用 ADO 或任何其他 COM 技术,我同样的问题也适用。
  • @IanBoyd 已经有了答案,但您只需要一个消息泵。我仍然不明白你为什么需要它 - 大多数WebBrowser 都设计为异步运行,所以你似乎在尝试一些非标准的东西。

标签: windows delphi winapi events com


【解决方案1】:

总而言之,您必须正常泵送所有消息,您不能自己单独挑出 COM 消息(此外,没有记录的消息可以自己查看/泵送,它们是只有 COM 的内部知道)。

如何使 WebBrower.Navigate2 同步?

你不能。但是您也不必等待OnDocumentComplete 事件。您可以在 NavigateToEmpty() 自身内部进行忙循环,直到 WebBrowser 的 ReadyState 属性为 READYSTATE_COMPLETE,当消息等待处理时抽出消息队列:

procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
  WebBrowser.Navigate2('about:blank');
  while (WebBrowser.ReadyState <> READYSTATE_COMPLETE) and (not Application.Terminated) do
  begin
    // if MsgWaitForMultipleObjects(0, Pointer(nil)^, False, 5000, QS_ALLINPUT) = WAIT_OBJECT_0 then
    // if GetQueueStatus(QS_ALLINPUT) <> 0 then
      Application.ProcessMessages;
  end;
end;

如何抽取 COM 消息?

你不能,反正自己也不行。抽空一切,并准备好处理由此导致的任何再入问题。

泵送 COM 消息会导致 COM 事件回调吗?

是的。

如何使用 CoWaitForMultipleHandles

试试这样的:

procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
var
  hEvent: THandle;
  dwIndex: DWORD;
  hr: HRESULT;
begin
  // when UseCOMWait() is true, TEvent.WaitFor() does not wait for, or
  // notify, when messages are pending in the queue, so use
  // CoWaitForMultipleHandles() directly instead.  But you have to still
  // use a waitable object, just don't signal it...
  hEvent := CreateEvent(nil, True, False, nil);
  if hEvent = 0 then RaiseLastOSError;
  try
    WebBrowser.Navigate2('about:blank');
    while (WebBrowser.ReadyState <> READYSTATE_COMPLETE) and (not Application.Terminated) do
    begin
      hr := CoWaitForMultipleHandles(COWAIT_INPUTAVAILABLE, 5000, 1, hEvent, dwIndex);
      case hr of
        S_OK: Application.ProcessMessages;
        RPC_S_CALLPENDING, RPC_E_TIMEOUT: begin end;
      else
        RaiseLastOSError(hr);
      end;
    end;
  finally
    CloseHandle(hEvent);
  end;
end;

【讨论】:

  • ProcessMessages 的问题当然是它会导致重入问题——以前不存在的问题。当前的实现是 a) 调用 Navigate2 b) 忙睡眠 5 秒 c) 看到 WebBrowser.Document 是否存在,如果存在的话 - 提前开始使用它。它有效吗?是的。它工作了8年吗?是的。这样对吗?不,我不能以引入错误的名义破坏现有代码。
  • 哦,事实证明,如今,无论出于何种原因,ReadyState 永远不会完成
  • 我知道再入问题(我在回答中提到过)。您必须处理消息以使ReadyState 进入状态,并触发OnDocumentComplete 事件。在Document 不为零之前,您可以忙着睡觉而不处理消息,但这并不能保证Document 已准备好使用,因为它在Loaded/Interactive 状态期间被分配@987654336 之前达到@状态。
  • 正确 的解决方案是不要一开始就阻止您的代码。 Navigate/2() 是异步的,所以这样对待它。调用它并将控制权返回给主消息循环而不阻塞您的代码,然后等待OnDocumentComplete 事件触发,然后再继续您的逻辑。
  • 我希望我知道 ADO 如何将异步数据库调用和回调转换为同步调用。这是以前在 Windows COM 世界中已经解决的问题。我只是不知道怎么做。
猜你喜欢
  • 2018-03-29
  • 2010-12-11
  • 2014-03-01
  • 2014-02-08
  • 2013-07-18
  • 2013-04-11
  • 1970-01-01
  • 1970-01-01
  • 2010-12-07
相关资源
最近更新 更多