29

显然, WPF WebBrowser 控件存在一些严重的键盘和焦点问题。我已经组合了一个简单的 WPF 应用程序,只有一个 WebBrowser 和两个按钮。该应用程序加载了一个非常基本的可编辑 HTML 标记 ( <body contentEditable='true'>some text</body>) 并演示了以下内容:

  • 制表符行为不端。用户需要按两次 Tab 才能看到 WebBrowser 中的插入符号(文本光标)并能够键入。

  • 当用户离开应用程序(例如,使用 Alt-Tab)然后返回时,插入符号消失了,她根本无法打字。需要物理鼠标单击 WebBrowser 的窗口客户区才能取回插入符号和击键。

  • 不一致的是,在 WebBrowser 周围出现了一个虚线焦点矩形(在选项卡时,但在单击时不显示)。我找不到摆脱它的方法(FocusVisualStyle="{x:Null}"没有帮助)。

  • 在内部,WebBrowser 从不接收焦点。这对于逻辑焦点 ( FocusManager ) 和输入焦点 ( Keyboard ) 都是如此。和事件永远不会为 WebBrowser 触发Keyboard.GotKeyboardFocusEventFocusManager.GotFocusEvent尽管它们都为同一焦点范围内的按钮触发)。即使插入符号位于 WebBrowser 内,它也FocusManager.GetFocusedElement(mainWindow)指向先前聚焦的元素(按钮)并且Keyboard.FocusedElementnull. 同时,((IKeyboardInputSink)this.webBrowser).HasFocusWithin()返回true

我想说,这样的行为几乎是功能失调的,不可能是真的,但这就是它的工作原理。我可能会想出一些技巧来修复它,并将它与原生 WPF 控件(如TextBox. 我仍然希望,也许我在这里遗漏了一些晦涩但简单的东西。有没有人处理过类似的问题?任何有关如何解决此问题的建议将不胜感激。

在这一点上,我倾向于基于HwndHost为 WebBrowser ActiveX 控件开发一个内部 WPF 包装器。我们还在考虑 WebBrowser 的其他替代方案,例如 Chromium Embedded Framework (CEF)。

可以从这里下载 VS2012 项目,以防有人想玩它。

这是 XAML:

<Window x:Class="WpfWebBrowserTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Width="640" Height="480" Background="LightGray">

    <StackPanel Margin="20,20,20,20">
        <ToggleButton Name="btnLoad" Focusable="True" IsTabStop="True" Content="Load" Click="btnLoad_Click" Width="100"/>

        <WebBrowser Name="webBrowser" Focusable="True" KeyboardNavigation.IsTabStop="True" FocusVisualStyle="{x:Null}" Height="300"/>

        <Button Name="btnClose" Focusable="True" IsTabStop="True" Content="Close" Click="btnClose_Click" Width="100"/>
    </StackPanel>

</Window>

这是 C# 代码,它有一堆诊断跟踪来显示焦点/键盘事件是如何路由的以及焦点在哪里:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;

namespace WpfWebBrowserTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // watch these events for diagnostics
            EventManager.RegisterClassHandler(typeof(UIElement), Keyboard.PreviewKeyDownEvent, new KeyEventHandler(MainWindow_PreviewKeyDown));
            EventManager.RegisterClassHandler(typeof(UIElement), Keyboard.GotKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(MainWindow_GotKeyboardFocus));
            EventManager.RegisterClassHandler(typeof(UIElement), FocusManager.GotFocusEvent, new RoutedEventHandler(MainWindow_GotFocus));
        }

        private void btnLoad_Click(object sender, RoutedEventArgs e)
        {
            // load the browser
            this.webBrowser.NavigateToString("<body contentEditable='true' onload='focus()'>Line 1<br>Line 3<br>Line 3<br></body>");
            this.btnLoad.IsChecked = true;
        }

        private void btnClose_Click(object sender, RoutedEventArgs e)
        {
            // close the form
            if (MessageBox.Show("Close it?", this.Title, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
                this.Close();
        }

        // Diagnostic events

        void MainWindow_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            Debug.Print("{0}, source: {1}, {2}", FormatMethodName(), FormatType(e.Source), FormatFocused());
        }

        void MainWindow_GotFocus(object sender, RoutedEventArgs e)
        {
            Debug.Print("{0}, source: {1}, {2}", FormatMethodName(), FormatType(e.Source), FormatFocused());
        }

        void MainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            Debug.Print("{0}, key: {1}, source: {2}, {3}", FormatMethodName(), e.Key.ToString(), FormatType(e.Source), FormatFocused());
        }

        // Debug output formatting helpers

        string FormatFocused()
        {
            // show current focus and keyboard focus
            return String.Format("Focus: {0}, Keyboard focus: {1}, webBrowser.HasFocusWithin: {2}",
                FormatType(FocusManager.GetFocusedElement(this)),
                FormatType(Keyboard.FocusedElement),
                ((System.Windows.Interop.IKeyboardInputSink)this.webBrowser).HasFocusWithin());
        }

        string FormatType(object p)
        {
            string result = p != null ? String.Concat('*', p.GetType().Name, '*') : "null";
            if (p == this.webBrowser )
                result += "!!";
            return result;
        }

        static string FormatMethodName()
        {
            return new StackTrace(true).GetFrame(1).GetMethod().Name;
        }

    }
}

[更新]如果我托管WinForms WebBrowser(代替 WPF WebBrowser 或与 WPF WebBrowser 并排),情况不会好转:

<StackPanel Margin="20,20,20,20">
    <ToggleButton Name="btnLoad" Focusable="True" IsTabStop="True" Content="Load" Click="btnLoad_Click" Width="100"/>

    <WebBrowser Name="webBrowser" Focusable="True" KeyboardNavigation.IsTabStop="True" FocusVisualStyle="{x:Null}" Height="150" Margin="10,10,10,10"/>

    <WindowsFormsHost Name="wfHost" Focusable="True" Height="150" Margin="10,10,10,10">
        <wf:WebBrowser x:Name="wfWebBrowser" />
    </WindowsFormsHost>

    <Button Name="btnClose" Focusable="True" IsTabStop="True" Content="Close" Click="btnClose_Click" Width="100"/>
</StackPanel>

唯一的改进是我确实在WindowsFormsHost.

[更新]一个极端情况:两个 WebBrowser 控件同时显示两个插入符号:

<StackPanel Margin="20,20,20,20">
    <ToggleButton Name="btnLoad" Focusable="True" IsTabStop="True" Content="Load" Click="btnLoad_Click" Width="100"/>

    <WebBrowser Name="webBrowser" Focusable="True" KeyboardNavigation.IsTabStop="True" FocusVisualStyle="{x:Null}" Height="150" Margin="10,10,10,10"/>
    <WebBrowser Name="webBrowser2" Focusable="True" KeyboardNavigation.IsTabStop="True" FocusVisualStyle="{x:Null}" Height="150" Margin="10,10,10,10"/>

    <Button Name="btnClose" Focusable="True" IsTabStop="True" Content="Close" Click="btnClose_Click" Width="100"/>
</StackPanel>

this.webBrowser.NavigateToString("<body onload='text.focus()'><textarea id='text' style='width: 100%; height: 100%'>text</textarea></body>");
this.webBrowser2.NavigateToString("<body onload='text.focus()'><textarea id='text' style='width: 100%; height: 100%'>text2</textarea></body>");

这也说明焦点处理问题并非特定于contentEditable=true内容。

4

2 回答 2

7

对于其他偶然发现这篇文章并需要将键盘焦点设置到浏览器控件(不一定是控件中的特定元素)的任何人,这段代码对我有用。

首先,为Microsoft.mshtml.

接下来,每当您想关注浏览器控件时(例如,当窗口加载时),只需“关注” HTML 文档:

// Constructor
public MyWindow()
{
    Loaded += (_, __) =>
    {
        ((HTMLDocument) Browser.Document).focus();
    };
}

这会将键盘焦点放在 Web 浏览器控件内,以及“不可见”的 ActiveX 窗口内,从而允许 PgUp / PgDown 等键在 HTML 页面上工作。

如果您愿意,您也许可以使用 DOM 选择来查找页面上的特定元素,并尝试找到focus()该特定元素。我自己没有尝试过。

于 2015-02-19T20:06:30.177 回答
5

它以这种方式运行的原因与它是一个 ActiveX 控件这一事实有关,它本身就是一个完全的 Windows 类(它处理鼠标和键盘交互)。事实上,很多时候你看到使用的组件你会发现它是占据整个窗口的主要组件。不必这样做,但它会带来问题。

这是一个讨论完全相同问题的论坛,可以通过阅读最后的评论员文章链接来澄清其原因:

http://social.msdn.microsoft.com/Forums/vstudio/en-US/1b50fec6-6596-4c0a-9191-32cd059f18f7/focus-issues-with-systemwindowscontrolswebbrowser

概述您遇到的问题

  • 制表符行为不端。用户需要按两次 Tab 才能看到 WebBrowser 中的插入符号(文本光标)并能够键入。

    那是因为浏览器控件本身是一个可以通过标签进入的窗口。它不会立即将选项卡“转发”到它的子元素。

    改变这一点的一种方法是为组件本身处理 WM 消息,但请记住,当您希望其中的“子”文档能够处理消息时,这样做会变得很棘手。

    请参阅:防止 WebBrowser 控件窃取焦点?特别是“答案”。尽管他们的回答没有说​​明您可以通过设置 Silent 属性来控制组件是否通过对话框与用户交互(WPF控件中可能存在也可能不存在......不确定)

  • 当用户离开应用程序(例如,使用 Alt-Tab)然后返回时,插入符号消失了,她根本无法打字。需要物理鼠标单击 WebBrowser 的窗口客户区才能取回插入符号和击键。这是因为控件本身已经获得了焦点。另一个考虑是添加代码来处理 GotFocus 事件,然后“改变”焦点所在的位置。棘手的部分是弄清楚这是“来自”文档 -> 浏览器控件还是您的应用程序 -> 浏览器控件。我可以想到一些 hacky 方法来做到这一点(例如,基于在 gotfocus 上检查的失去焦点事件的可变参考),但没有什么可以尖叫优雅。

  • 不一致的是,在 WebBrowser 周围出现了一个虚线焦点矩形(在选项卡时,但在单击时不显示)。我找不到摆脱它的方法(FocusVisualStyle="{x:Null}" 没有帮助)。我想知道改变 Focusable 是否会有所帮助或阻碍。从未尝试过,但我会冒险猜测,如果它确实有效,它将完全阻止它在键盘上导航。

  • 在内部,WebBrowser 从不接收焦点。逻辑焦点(FocusManager)和输入焦点(键盘)都是如此。Keyboard.GotKeyboardFocusEvent 和 FocusManager.GotFocusEvent 事件永远不会为 WebBrowser 触发(尽管它们都为相同焦点范围内的按钮触发)。即使插入符号位于 WebBrowser 内,FocusManager.GetFocusedElement(mainWindow) 也会指向先前聚焦的元素(按钮)并且 Keyboard.FocusedElement 为空。同时,((IKeyboardInputSink)this.webBrowser).HasFocusWithin() 返回 true。人们遇到了两个浏览器控件都显示焦点(嗯......插入符号)甚至隐藏控件获得焦点的问题。

总而言之,您可以使用该组件做的事情非常棒,但它只是让您控制/更改行为以及令人抓狂的预定义行为集的正确组合。

我的建议是尝试对消息进行子类化,以便您可以直接通过代码直接控制焦点并绕过它的窗口尝试这样做。

于 2013-08-28T18:25:11.923 回答