防止WebBrowser控件窃取焦点?

12

有没有办法阻止WebBrowser控件导致其父窗体将自己带到前台?

如果您使用InvokeScript方法调用调用JavaScript函数,该函数在主父文档内部的iframe上调用focus(),它将导致窗口直接移到前面(或至少会导致任务栏图标开始闪烁)。 有没有办法防止这种情况发生?

更新:

我找到了一个临时的解决方案。

当WebBrowser的父表单的Deactive事件被触发时,我从其容器中删除WebBrowser,并在旧的父表单再次被激活时重新添加它。

这有点粗暴,但很有效。不过,我也愿意听取更好的建议。


可能只需将其隐藏而不是完全删除,这肯定是一种解决方法。如果之前我没有理解清楚,以为这是一个JavaScript/HTML问题而不是.NET中的表单问题,那么很抱歉。 - Abel
@Abel:这主要是由于JavaScript/浏览器问题导致WinForms出现问题。我尝试过先将浏览器控件设置为不可见,但直到我彻底从表单中删除该控件才解决了问题。 - rossisdead
我完全重写了答案。这不是一项容易的任务,但是采用我的答案,根据您的需求进行调整,您就可以准备好了。 - Abel
2个回答

8

编辑:完整的问题已经重新编写,我误解了原始问题。

让我们将问题归纳为一个控件或组件,你无法对其进行控制,但可以调用FlashWindow(Win32 API函数)以引起用户的注意。你不想这样。

通常有两种解决方案:使用API挂钩或消息挂钩。由于API挂钩很复杂,所以我将介绍一种消息挂钩的解决方案。

FlashWindow

微软没有明确说明FlashWindow的作用。不幸的是,它没有发送特定的消息(例如WM_FLASH或类似消息),否则就会更容易捕获和取消此行为。相反,FlashWindow执行以下三个操作:

  1. 设置闪烁间隔的系统计时器
  2. 在第一次闪烁时发送WM_NCACTIVATE消息
  3. 在计时器到期时(接收到WM_SYSTIMER)发送WM_NCACTIVATE消息

取决于组件如何调用FlashWindow,这可能是无限期的,直到发生另一个超时,直到它获得焦点或仅一次。每个WM_NCACTIVATE消息都会激活或取消激活NC区域(标题栏,任务栏上的按钮)。它不会更改输入焦点。

挑战

任何防止闪烁的解决方案都有些复杂。主要挑战包括:

  1. WM_SYSTIMER事件是异步发送的PostMessage,并且不会被窗体的WndProc方法接收到(它只处理同步消息)
  2. WM_NCACTIVATE消息也用于当用户单击标题栏或任务栏按钮以设置输入焦点时,简单地取消这些消息将产生不良影响
  3. 无论WM_SYSTIMER是否触发,FlashWindow都至少会闪烁一次。

WM_SYSTIMER消息是未记录的。它的值为0x0118,在Windows内部用于计时诸如光标闪烁、菜单打开延迟等事项。在此处,它用于闪烁之间的时间。

解决方案

我在此提供的解决方案是进一步开发的基础。它不是完整的解决方案,但可以在许多情况下解决问题。将以下代码放入你的窗体代码中:

protected override void WndProc(ref Message m)
{
    bool messageHandled = false;
    if (m.Msg == WM_NCACTIVATE)
    {
        // add logic here to determine user action, losing focus etc and set 
        // messageHandled and m.Result only when user action is not the cause 
        // of triggering WM_NCACTIVATE
        m.Result = IntPtr.Zero;
        messageHandled = true;
    }

    if(!messageHandled)
        base.WndProc(ref m);
}

上面的代码已经完全防止了闪烁。您需要添加一些逻辑来更改标题栏,因为完全忽略WM_NCACTIVATE意味着标题栏看起来始终处于活动状态,即使它并不是。

以下代码可以给您更多的控制权。您可以使用它来对闪烁本身做出反应。通常,主窗口不会经常收到WM_SYSTIMER事件,但您需要进行实验,以确定是否应该进行例外处理。似乎对于FlashWindowwParam总是设置为0xFFF8,但请进行实验,因为这在任何地方都没有记录。
public class MyMessageFilter : IMessageFilter
{
    // an application can have many windows, only filter for one window at the time
    IntPtr FilteredHwnd = IntPtr.Zero;

    public MyMessageFilter(IntPtr hwnd)
    {
        this.FilteredHwnd = hwnd;
    }

    public bool PreFilterMessage(ref Message m)
    {
        if (this.FilteredHwnd == m.HWnd && m.Msg == WM_SYSTIMER)
            return true;     // stop handling the message further
        else
            return false;    // all other msgs: handle them
    }
}

要激活此消息过滤器,只需在表单加载事件中添加以下行:
Application.AddMessageFilter(new MyMessageFilter(this.Handle));

以下常量应放置在类级别。它们在上述两个代码部分中均被使用:
public const UInt32 WM_SYSTIMER = 0x0118;
public const UInt32 WM_NCACTIVATE = 0x86;

结论

虽然这个问题本身是可以解决的,但并不容易。使用上述方法,你应该能够解决大部分问题。使用过滤器来防止闪烁,但第一次“闪烁”仍会发生。使用WinProc覆盖以防止第一个问题的发生,但需要添加一些逻辑来防止你的应用程序行为过于奇怪(例如:始终处于非活动状态的标题栏或始终处于活动状态)。你已经有了一些代码,可以与此组合使用来设置一些布尔标志。


我喜欢这个答案,尽管它对我所需的有点复杂。也就是说,我有一个特定的设置适用于我的应用程序,因此我的hacky方法不会成为这种情况的问题。但是,我将把它集成到另一个应用程序中。谢谢! - rossisdead
我完全同意,这是一个复杂的解决方法,只是因为没有类似于“if is_flashing then”的东西。我希望有另一种方法,但据我所知,实际上并没有。 - Abel

5

在对象中实现IProtectFocus::AllowFocusChange

在实现IOleClientSite的同一对象上实现IServiceProvider,然后通过IServiceProvider::QueryService响应SID_SProtectFocus,返回一个公开IProtectFocus的对象

这是IE7中的新接口,所以旧版本没有这个功能。


你是否有一个C#的工作示例,可以展示如何实现这个功能? - Stefan Koell
谢谢您的回复!我实际上是想要一个关于如何使用接口的示例代码。C#实现的代码片段很有帮助,但接下来该怎么做呢? - Stefan Koell
一旦您实现了接口,IE将调用您的实现。无需自己调用接口。 - Sheng Jiang 蒋晟
很抱歉,我仍然不能理解。我需要从Web浏览器控件中继承并实现接口吗?我已经这样做了,将pfAllow设置为false,但似乎没有起作用。像谷歌搜索这样的网站仍然会夺取焦点... 我不确定自己是否操作正确... - Stefan Koell
1
这取决于您的WebBrowser类如何实现IOleClientSite。例如,如果您使用Windows Forms,则WebBrowser.WebBrowserSite实现了IOleClientSite,您需要从中派生以实现IServiceProvider,然后从WebBrowser派生以使用您的新站点。如果您使用自己的IOleClientSite实现的ActiveX(例如csexwb示例),则将IServiceProvider添加到自己的IOleClientSite实现中,在查询服务时返回IProtectFocus实现。 - Sheng Jiang 蒋晟

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接