Присутствие WindowsFormsHost приводит к срабатыванию IsKeyboardFocusWithinChanged не более двух раз и не более

Я обнаружил очень странное поведение WindowsFormsHost в WPF. Я считаю, что если элемент управления WPF не имеет WindowsFormsHost как дочерний элемент управления, то IsKeyboardFocusWithinChanged срабатывает правильно- он срабатывает всякий раз, когда элемент управления WPF получает или теряет фокус, а переменная IsKeyboardFocusWithin переключается как ожидалось (true когда контроль получает фокус, false когда теряет фокус).

Но, если я принимаю WindowsFormHost в WPF, затем через некоторое время IsKeyboardFocusWithinChanged событие больше не запускается как для основного элемента управления WPF, так и для WindowsFormHost детский контроль.

Я не могу найти в документации MSDN или SO, почему так, по какой-либо причине?

Это мой код:

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" IsKeyboardFocusWithinChanged="Window_IsKeyboardFocusWithinChanged">
    <Grid Name="grid1">
    </Grid>
</Window>

MainWindow.xaml.cs

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

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        var mtbDate = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = mtbDate;
        host.IsKeyboardFocusWithinChanged += Host_IsKeyboardFocusWithinChanged;
        grid1.Children.Add(host);
    }

    private void Host_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(host.IsKeyboardFocusWithin.ToString()+" blah");
    }

    private System.Windows.Forms.Integration.WindowsFormsHost host;

    private void Window_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
            Console.WriteLine(IsKeyboardFocusWithin.ToString());
    }
}

Когда линии с участием WindowsFormHost закомментированы, то IsKeyboardFocusWithin является true всякий раз, когда контроль получает фокус, и false когда контроль теряет фокус.

Когда линии с участием WindowsFormHost тогда IsKeyboardFocusWithin является true, пока я не нажму на элемент управления, а затем host.IsKeyboardFocusWithin становится false, а также IsKeyboardFocusWithin также становится falseи потом, что бы я ни делал, IsKeyboardFocusWithinChanged событие никогда не будет запущено снова.

1 ответ

Решение

Обновленный ответ - 05/11

Оптимизированное предыдущее решение для поддержки нескольких WindowsFormsHost элементы в Window, Кроме того, обновлен стиль для выделения сфокусированного элемента управления зеленой рамкой, если IsKeyboardFocusWithin свойство true,

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="325">
    <Window.Resources>
        <ResourceDictionary>
            <Style TargetType="Border">
                <Setter Property="BorderThickness" Value="2" />
                <Setter Property="BorderBrush" Value="Transparent" />
                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Child.IsKeyboardFocusWithin}" Value="True">
                        <Setter Property="BorderBrush" Value="Green" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Button x:Name="hiddenBtn" Height="1" Width="1" />
        <StackPanel Grid.Column="0" x:Name="leftPanel" Margin="5">
            <Label HorizontalContentAlignment="Right">Start Date</Label>
            <Label HorizontalContentAlignment="Right">End Date</Label>
            <Label HorizontalContentAlignment="Right">Phone Number</Label>
            <Label HorizontalContentAlignment="Right">Zip Code</Label>
        </StackPanel>
        <StackPanel Grid.Column="1" x:Name="rightPanel" Margin="5">

        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

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

        GenerateControls();
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
        source.AddHook(WndProc);
    }

    private const int WM_KILLFOCUS = 0x0008;
    private const int WM_ACTIVATEAPP = 0x001c;
    private const int WM_PARAM_FALSE = 0x00000000;

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // Handle messages...
        if (msg == WM_KILLFOCUS)
        {
            Console.WriteLine(wParam + " " + lParam);
            //suppress kill focus message if host has keyboardfocus, else don't
            var hosts = FindVisualChildren<WindowsFormsHost>(this);
            var focusedControlHwnd = wParam.ToInt32();
            if(focusedControlHwnd != 0)
            {
                handled = hosts.Any(x => x.Child.Handle.ToInt32() == focusedControlHwnd);
            }
        }
        else if (msg == WM_ACTIVATEAPP && wParam.ToInt32() == WM_PARAM_FALSE)
        {
            //now the kill focus could be suppressed event during window switch, which we want to avoid
            //so we make sure that the host control property is updated 
            var hosts = FindVisualChildren<WindowsFormsHost>(this);
            if (hosts.Any(x => x.IsKeyboardFocusWithin))
                hiddenBtn.Focus();
        }

        return IntPtr.Zero;
    }

    private void GenerateControls()
    {
        System.Windows.Forms.MaskedTextBox maskedTextBox;
        System.Windows.Forms.Integration.WindowsFormsHost host;

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("(000)-000-0000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("00000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });
    }

    public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }
}

Скриншот

Предыдущий ответ - 05/05

Как отметил в комментарии Ханс Пассант, такое поведение вызвано тем, что WindowsFormsHost и MaskedTextBox имеют разные Hwnd(ы).

При первом нажатии на элемент управления хостом дочерний элемент управления получает фокус, и IsKeyboardFocusedWithin устанавливается правильно. Но как только дочерний элемент управления получает фокус, ОС замечает разницу в Hwnd и отправляет сообщение kill-focus в окно WPF, которое, в свою очередь, устанавливает значение IsKeyboardFocusedWithin как false.

Что вы можете сделать, это добавить WndProc подключиться к главному окну WPF и подавить сообщение kill-focus - только когда хост-контроль IsKeyboardFocusedWithin значение верно.

Однако, есть побочный эффект - когда вы переключаетесь из окна WPF, хост-контроль IsKeyboardFocusedWithin значение может остаться верным. Чтобы решить эту проблему, вы можете использовать простой трюк обхода для смещения фокуса при отправке сообщения с окном с диагональю окна и, следовательно, для свойства IsKeyboardFocusedWithin, обновленного в соответствии с текущим состоянием.

Пример исходного кода: я использовал StackPanel вместо Grid для отображения TextBox(ов)

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

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        var mtbDate = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = mtbDate;
        host.IsKeyboardFocusWithinChanged += Host_IsKeyboardFocusWithinChanged;
        stackPanel1.Children.Add(host);

        textBox1 = new TextBox();
        stackPanel1.Children.Add(textBox1);

        textBox2 = new TextBox();
        stackPanel1.Children.Add(textBox2);
    }

    private void Host_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(host.IsKeyboardFocusWithin.ToString() + " blah");
        textBox1.Text = $"Host.IsKeyboardFocusedWithin = {host.IsKeyboardFocusWithin}";
    }

    private System.Windows.Forms.Integration.WindowsFormsHost host;
    private TextBox textBox1;
    private TextBox textBox2;

    private void Window_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(IsKeyboardFocusWithin.ToString());
        textBox2.Text = $"Window.IsKeyboardFocusedWithin = {IsKeyboardFocusWithin}";
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
        source.AddHook(WndProc);
    }

    private const int WM_KILLFOCUS = 0x0008;
    private const int WM_ACTIVATEAPP = 0x001c;
    private const int WM_PARAM_FALSE = 0x00000000;

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // Handle messages...
        if (msg == WM_KILLFOCUS) 
        {
            //suppress kill focus message if host has keyboardfocus, else don't
            handled = host.IsKeyboardFocusWithin;
        }
        else if (msg == WM_ACTIVATEAPP && wParam.ToInt32() == WM_PARAM_FALSE)
        {
            //now the kill focus could be suppressed event during window switch, which we want to avoid
            //so we make sure that the host control property is updated by traversal (or any other method)
            if (host.IsKeyboardFocusWithin)
            {
                host.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            }
        }

        return IntPtr.Zero;
    }
}

И результат будет выглядеть так:

С фокусом

Без фокуса

Другие вопросы по тегам