dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
6.9k stars 1.14k forks source link

Focus is not recovered from control hosted in WindowsFormsHost inside a Popup #9257

Open vgriph opened 2 weeks ago

vgriph commented 2 weeks ago

Description

When using a WindowsFormsHost to host a Windows Forms control in a WPF Popup, focus gets trapped in the Windows forms control.

Reproduction Steps

Create a WPF windows with the following XAML definition

<Window x:Class="WinFormsIteropTest.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:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"  
        xmlns:local="clr-namespace:WinFormsIteropTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel x:Name="Root">
        <Popup IsOpen="True" Width="400" Placement="Center" PlacementTarget="{Binding ElementName=Root}">
            <StackPanel>
                <TextBox x:Name="wpfTextBox"/>
                <WindowsFormsHost Name="winForms">
                    <wf:TextBox Name="wfTextBox"/>
                </WindowsFormsHost>
            </StackPanel>
        </Popup>
    </StackPanel>
</Window>

When opened, focus the first (WPF TextBox), then focus the second (Windows Forms) textbox and then focus the WPF textbox again.

Expected behavior

The focus should be moved to the WPF textbox

Actual behavior

The input caret is moved to the WPF textbox, so it looks as if it has focus, but any input goes into the Windows Forms textbox

Regression?

No. The same problem exists in .NET 4.8 and .NET 8

Known Workarounds

Listening to the Keyboard.GotKeyboardFocusEvent and, after each such event verify that the Win32 focus is on a WPF owned HWND, and if not force the focus back to the WPF window that WPF though had got the keyboard focus.

using System;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;

namespace WinFormsIteropTest
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private bool ignoreFocusChange;

        public App()
        {
            InitializeComponent();
            EventManager.RegisterClassHandler(typeof(Popup), Keyboard.GotKeyboardFocusEvent, new RoutedEventHandler(OnWindowGotFocus));

        }

        private void OnWindowGotFocus(object sender, RoutedEventArgs e)
        {
            var popup = (UIElement)sender;
            var windowHandle = ((HwndSource)PresentationSource.FromVisual(popup))?.Handle;
            var focusedNativeWindow = NativeMethods.GetFocus();
            if (ignoreFocusChange)
            {
                return;
            }

            if (focusedNativeWindow == windowHandle)
            {
                return;
            }

            popup.Dispatcher.InvokeAsync(() => VerifyPopupFocus(popup, e), System.Windows.Threading.DispatcherPriority.Input);
        }

        private void VerifyPopupFocus(UIElement popup, RoutedEventArgs e)
        {
            var focusedElement = Keyboard.FocusedElement;
            var focusedNativeWindow = NativeMethods.GetFocus();

            if (focusedElement is not Visual focusedVisual
                || PresentationSource.FromVisual(focusedVisual) is not HwndSource nativeWindow)
            {
                // The element with keyboard focus is not a WPF element
                return;
            }

            var focusScope = FocusManager.GetFocusScope((DependencyObject)e.Source);
            var focusScopeWindowHandle = ((HwndSource)PresentationSource.FromVisual((Visual)focusScope))?.Handle;

            if (focusedNativeWindow == focusScopeWindowHandle)
            {
                // Focused native window is the focus scope window no change needed
                return;
            }

            var nativeWindowHandle = nativeWindow.Handle;

            if (nativeWindowHandle == focusedNativeWindow)
            {
                // The focus is within WPF. Nothing needs to be done.
                return;
            }

            var activeWindow = System.Windows.Application.Current.Windows.Cast<Window>()
                .FirstOrDefault(x => x.IsActive);

            var windowToFocus = nativeWindowHandle;
            var focusScopeToFocus = focusScope;
            if (activeWindow != null)
            {
                var activeWindowHandle = new WindowInteropHelper(activeWindow).Handle;
                if (activeWindowHandle == focusedNativeWindow)
                {
                    // The focus is within WPF. Nothing needs to be done.
                    return;
                }

                windowToFocus = activeWindowHandle;
                focusScopeToFocus = activeWindow;
            }

            ForceFocusToWpfElement(windowToFocus, focusScopeToFocus, focusedElement);
        }

        private void ForceFocusToWpfElement(IntPtr windowHandle, DependencyObject focusScope, IInputElement focusedElement)
        {
            // The focused HWND is not a WPF window, but WPF think it has Keyboard Focus.
            // This means that the focus is out of sync. (The HWND that has focused will receive keyboard input,
            // but WPF will render the UI as if the focused element has focus.

            // To fox this state, we need to focus the WPF window so that it receives the input.
            // We then have to restore the focus to the previously focused element in WPF.
            ignoreFocusChange = true;
            try
            {
                NativeMethods.SetFocus(windowHandle);
                var elementFocusedByMovingFocusToWpfWindow = FocusManager.GetFocusedElement(focusScope);
                if (elementFocusedByMovingFocusToWpfWindow != null
                    && elementFocusedByMovingFocusToWpfWindow != focusedElement)
                {
                    // Focus them element from WPF as well, since WPF seems to not fully update its internal state
                    // when just restoring focus when the main window receives focus by a native call.
                    elementFocusedByMovingFocusToWpfWindow.Focus();
                }

                focusedElement.Focus();
            }
            finally
            {
                ignoreFocusChange = false;
            }
        }

        private static class NativeMethods
        {
            [DllImport("user32.dll")]
            internal static extern IntPtr SetFocus(IntPtr hWnd);

            [DllImport("user32.dll")]
            internal static extern IntPtr GetFocus();
        }
    }
}

Impact

No response

Configuration

.NET 8 and .NET 4.8 on Windows 10

Other information

No response

lindexi commented 1 week ago

@vgriph Could you try use SetFocus to WPF window?

SetFocus(wpfWindowHandle);

[DllImport("User32.dll")]
public static extern IntPtr SetFocus(IntPtr hWnd);

Or use the SetForegroundWindow

[DllImport("USER32.DLL")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetForegroundWindow(IntPtr hWnd);

public static void ActivatePopup(Popup popup)
{
    HwndSource source = (HwndSource)PresentationSource.FromVisual(popup.Child);
    IntPtr handle = source.Handle;

    SetForegroundWindow(handle);
}

You can find my demo code in : https://github.com/lindexi/lindexi_gd/tree/1666e742fbd5ebda36e840a8e5f4b866251b3004/GakelfojeNairwogewerwhiheecem

vgriph commented 1 week ago

@lindexi Just setting the focus did not work correctly in all cases. (The focus was returned to the Windows Forms control)

But using it in combination with UIElement.Focus() I managed to correctly transfer the focus back to the WPF control. (Except for that the main window below the popup has the title bar in "inactive state" when I focus the WPF text box in the popup after having the Windows Forms text box in the popup focused. (Otherwise it is styled as inactive only when focus is in the windows forms textbox in the popup.

I managed to put together the following work around at application level, which seems to work for multi window applications.

using System;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;

namespace WinFormsIteropTest
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private bool ignoreFocusChange;

        public App()
        {
            InitializeComponent();
            EventManager.RegisterClassHandler(typeof(Popup), Keyboard.GotKeyboardFocusEvent, new RoutedEventHandler(OnWindowGotFocus));

        }

        private void OnWindowGotFocus(object sender, RoutedEventArgs e)
        {
            var popup = (UIElement)sender;
            var windowHandle = ((HwndSource)PresentationSource.FromVisual(popup))?.Handle;
            var focusedNativeWindow = NativeMethods.GetFocus();
            if (ignoreFocusChange)
            {
                return;
            }

            if (focusedNativeWindow == windowHandle)
            {
                return;
            }

            popup.Dispatcher.InvokeAsync(() => VerifyPopupFocus(popup, e), System.Windows.Threading.DispatcherPriority.Input);
        }

        private void VerifyPopupFocus(UIElement popup, RoutedEventArgs e)
        {
            var focusedElement = Keyboard.FocusedElement;
            var focusedNativeWindow = NativeMethods.GetFocus();

            if (focusedElement is not Visual focusedVisual
                || PresentationSource.FromVisual(focusedVisual) is not HwndSource nativeWindow)
            {
                // The element with keyboard focus is not a WPF element
                return;
            }

            var focusScope = FocusManager.GetFocusScope((DependencyObject)e.Source);
            var focusScopeWindowHandle = ((HwndSource)PresentationSource.FromVisual((Visual)focusScope))?.Handle;

            if (focusedNativeWindow == focusScopeWindowHandle)
            {
                // Focused native window is the focus scope window no change needed
                return;
            }

            var nativeWindowHandle = nativeWindow.Handle;

            if (nativeWindowHandle == focusedNativeWindow)
            {
                // The focus is within WPF. Nothing needs to be done.
                return;
            }

            var activeWindow = System.Windows.Application.Current.Windows.Cast<Window>()
                .FirstOrDefault(x => x.IsActive);

            var windowToFocus = nativeWindowHandle;
            var focusScopeToFocus = focusScope;
            if (activeWindow != null)
            {
                var activeWindowHandle = new WindowInteropHelper(activeWindow).Handle;
                if (activeWindowHandle == focusedNativeWindow)
                {
                    // The focus is within WPF. Nothing needs to be done.
                    return;
                }

                windowToFocus = activeWindowHandle;
                focusScopeToFocus = activeWindow;
            }

            ForceFocusToWpfElement(windowToFocus, focusScopeToFocus, focusedElement);
        }

        private void ForceFocusToWpfElement(IntPtr windowHandle, DependencyObject focusScope, IInputElement focusedElement)
        {
            // The focused HWND is not a WPF window, but WPF think it has Keyboard Focus.
            // This means that the focus is out of sync. (The HWND that has focused will receive keyboard input,
            // but WPF will render the UI as if the focused element has focus.

            // To fox this state, we need to focus the WPF window so that it receives the input.
            // We then have to restore the focus to the previously focused element in WPF.
            ignoreFocusChange = true;
            try
            {
                NativeMethods.SetFocus(windowHandle);
                var elementFocusedByMovingFocusToWpfWindow = FocusManager.GetFocusedElement(focusScope);
                if (elementFocusedByMovingFocusToWpfWindow != null
                    && elementFocusedByMovingFocusToWpfWindow != focusedElement)
                {
                    // Focus them element from WPF as well, since WPF seems to not fully update its internal state
                    // when just restoring focus when the main window receives focus by a native call.
                    elementFocusedByMovingFocusToWpfWindow.Focus();
                }

                focusedElement.Focus();
            }
            finally
            {
                ignoreFocusChange = false;
            }
        }

        private static class NativeMethods
        {
            [DllImport("user32.dll")]
            internal static extern IntPtr SetFocus(IntPtr hWnd);

            [DllImport("user32.dll")]
            internal static extern IntPtr GetFocus();
        }
    }
}
lindexi commented 1 week ago

@vgriph Thank you. The wpf set popup with WS_EX_NOACTIVATE, and that is the reason of NativeMethods.SetFocus. But as Microsoft says in https://connect.microsoft.com/VisualStudio/feedback/details/389998/wpf-popup-messes-with-ime-switching, the broken link, it be fixed on .NET Framework 4.6.2

Without more research, I don't know the root cause of the problem.