Управляющая клавиатура WebBrowser и поведение фокуса

По-видимому, есть серьезные проблемы с клавиатурой и фокусом с WPF WebBrowser control. Я собрал тривиальное приложение WPF, просто WebBrowser и две кнопки. Приложение загружает очень базовую редактируемую разметку HTML (<body contentEditable='true'>some text</body>) и демонстрирует следующее:

  • Вкладка не работает. Пользователю нужно дважды нажать клавишу Tab, чтобы увидеть курсор (текстовый курсор) внутри WebBrowser и иметь возможность печатать.

  • Когда пользователь отключается от приложения (например, с помощью Alt-Tab), затем возвращается назад, каретка исчезает, и она не может набрать вообще. Физический щелчок мышью в области клиента окна WebBrowser необходим, чтобы вернуть каретку и нажатия клавиш.

  • Несомненно, прямоугольник с точками фокусировки отображается вокруг WebBrowser (при табуляции, но не при нажатии). Я не мог найти способ избавиться от него (FocusVisualStyle="{x:Null}" не помогает).

  • Внутри WebBrowser никогда не получает фокуса. Это верно для обоих логических фокусов (FocusManager) и фокус ввода (Keyboard). События Keyboard.GotKeyboardFocusEvent и FocusManager.GotFocusEvent никогда не запускаются для WebBrowser (хотя оба они делают для кнопок в той же области фокуса). Даже если каретка находится внутри WebBrowser, FocusManager.GetFocusedElement(mainWindow) указывает на ранее сфокусированный элемент (кнопка), а Keyboard.FocusedElement - null. В то же время ((IKeyboardInputSink)this.webBrowser).HasFocusWithin() возвращает true.

Я бы сказал, что такое поведение почти слишком дисфункционально, чтобы быть правдой, но как это работает. Возможно, я мог бы придумать некоторые хаки, чтобы исправить это и привести его в ряд с встроенными элементами управления WPF, например TextBox. Тем не менее, я надеюсь, может быть, я пропустил что-то неясное, но все же простое здесь. Кто-нибудь имел дело с подобной проблемой? Любые предложения о том, как исправить это, будут очень признательны.

На этом этапе я склонен разрабатывать внутреннюю оболочку WPF для WebBrowser ActiveX Control на основе HwndHost. Мы также рассматриваем другие альтернативы для 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>

Это код С#, он содержит множество диагностических трасс, чтобы показать, как маршрутизируются события фокуса/клавиатуры и где находится фокус:

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;
        }

    }
}

[UPDATE] Ситуация не улучшается, если я размещаю WinForms 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.

Ответ 1

Причина, по которой он ведет себя таким образом, связана с тем, что это элемент управления 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 и затем "сменить", где сосредоточен фокус. Трудная часть выясняет, был ли это "из" элемента управления браузером document → browser или вашего приложения → браузера. Я могу придумать несколько хакерских способов сделать это (переменная ссылка, основанная на потере фокусного события, например, на gotfocus), но ничего, что кричит элегантно.

  • Несомненно, прямоугольник с точками фокусировки отображается вокруг WebBrowser (при табуляции, но не при нажатии). Я не мог найти способ избавиться от него (FocusVisualStyle = "{x: Null}" не помогает). Интересно, поможет ли изменение Focusable или помешает. Никогда не пробовал, но я собираюсь рискнуть предположить, что если бы это сработало, это остановило бы его от навигации на клавиатуре вообще.

  • Внутри WebBrowser никогда не получает фокуса. Это верно как для логической фокусировки (FocusManager), так и для фокуса ввода (клавиатура). События Keyboard.GotKeyboardFocusEvent и FocusManager.GotFocusEvent никогда не запускаются для WebBrowser (хотя оба они делают для кнопок в той же области фокуса). Даже когда каретка находится внутри WebBrowser, FocusManager.GetFocusedElement(mainWindow) указывает на ранее сфокусированный элемент (кнопка) и Keyboard.FocusedElement равен null. В то же время ((IKeyboardInputSink) this.webBrowser).HasFocusWithin() возвращает true. Люди сталкиваются с проблемами, когда два браузера контролируют как отображение фокуса (ну... карет), либо даже скрытый контроль фокусируется.

В целом, это довольно удивительно, что вы можете сделать с компонентом, но это просто правильное сочетание того, чтобы вы могли контролировать/изменять поведение вместе с предопределенными наборами поведения, чтобы сходить с ума.

Мое предложение состояло в том, чтобы попытаться подклассифицировать сообщения, чтобы вы могли напрямую управлять фокусом через код и обойти это окно, пытаясь сделать это.

Ответ 2

Для кого-то, кто наткнулся на это сообщение и ему нужно настроить фокус клавиатуры на элемент управления браузера (не обязательно на элемент внутри элемента управления, обязательно), этот бит кода работал у меня.

Сначала добавьте ссылку на проект (в разделе "Расширения в VS" ) для Microsoft.mshtml.

Далее, всякий раз, когда вы хотите сфокусировать управление браузером (скажем, например, когда загружается окно), просто "сфокусируйте" документ HTML:

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

Это позволит разместить фокус клавиатуры внутри элемента управления веб-браузера и внутри "невидимого" окна ActiveX, позволяя таким функциям, как PgUp/PgDown, работать с HTML-страницей.

Если вы хотите, вы можете использовать выбор DOM, чтобы найти определенный элемент на странице, и попробуйте focus() этот конкретный элемент. Я сам этого не пробовал.