WindowsFormsHost 的存在导致 IsKeyboardFocusWithinChanged 最多被触发两次而不是更多
The presence of WindowsFormsHost causes the IsKeyboardFocusWithinChanged to be fired at most twice and not more
我发现 WindowsFormsHost
在 WPF 中有一个非常奇怪的行为。我发现如果 WPF 控件没有 WindowsFormsHost
作为子控件,那么 IsKeyboardFocusWithinChanged
fires properly-- 只要 WPF 控件获得或失去焦点,变量 IsKeyboardFocusWithin
按预期切换( true
当控件获得焦点时, false
当失去焦点时)。
但是,如果我在 WPF 中承载一个 WindowsFormHost
,那么不久之后,WPF 母控件和 WindowsFormHost
子控件将不再触发 IsKeyboardFocusWithinChanged
事件控制。
我在 MSDN 文档中找不到或者为什么会这样,有什么原因吗?
这是我的代码:
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
事件都不会再被触发。
更新答案 - 05/11
优化以前的解决方案以支持 Window
中的多个 WindowsFormsHost
元素。此外,如果 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
正如 Hans Passant 在评论中提到的,此行为是由于 WindowsFormsHost
和 MaskedTextBox
具有不同的 Hwnd(s)。
第一次点击宿主控件时,子控件会获得焦点,IsKeyboardFocusedWithin设置正确。但是一旦子控件获得焦点,OS 就会注意到 Hwnd 中的差异,并将 kill-focus 消息发送到 WPF window - 这反过来将 IsKeyboardFocusedWithin 设置为 false。
你可以做的是添加一个 WndProc
挂钩到你的 WPF 主 window,并抑制 kill-focus 消息 - 仅当主机控件的 IsKeyboardFocusedWithin
值为真时.
但是,有一个副作用 - 当您从 WPF window 切换开时,主机控件的 IsKeyboardFocusedWithin
值可能保持为真。为了解决这个问题,您可以使用一个简单的遍历技巧在发送 window-diactivated 消息时转移焦点,从而根据当前状态更新 属性 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;
}
}
而且,结果将如下所示:
有焦点
无焦点
我发现 WindowsFormsHost
在 WPF 中有一个非常奇怪的行为。我发现如果 WPF 控件没有 WindowsFormsHost
作为子控件,那么 IsKeyboardFocusWithinChanged
fires properly-- 只要 WPF 控件获得或失去焦点,变量 IsKeyboardFocusWithin
按预期切换( true
当控件获得焦点时, false
当失去焦点时)。
但是,如果我在 WPF 中承载一个 WindowsFormHost
,那么不久之后,WPF 母控件和 WindowsFormHost
子控件将不再触发 IsKeyboardFocusWithinChanged
事件控制。
我在 MSDN 文档中找不到或者为什么会这样,有什么原因吗?
这是我的代码:
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
事件都不会再被触发。
更新答案 - 05/11
优化以前的解决方案以支持 Window
中的多个 WindowsFormsHost
元素。此外,如果 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
正如 Hans Passant 在评论中提到的,此行为是由于 WindowsFormsHost
和 MaskedTextBox
具有不同的 Hwnd(s)。
第一次点击宿主控件时,子控件会获得焦点,IsKeyboardFocusedWithin设置正确。但是一旦子控件获得焦点,OS 就会注意到 Hwnd 中的差异,并将 kill-focus 消息发送到 WPF window - 这反过来将 IsKeyboardFocusedWithin 设置为 false。
你可以做的是添加一个 WndProc
挂钩到你的 WPF 主 window,并抑制 kill-focus 消息 - 仅当主机控件的 IsKeyboardFocusedWithin
值为真时.
但是,有一个副作用 - 当您从 WPF window 切换开时,主机控件的 IsKeyboardFocusedWithin
值可能保持为真。为了解决这个问题,您可以使用一个简单的遍历技巧在发送 window-diactivated 消息时转移焦点,从而根据当前状态更新 属性 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;
}
}
而且,结果将如下所示:
有焦点
无焦点