WPF和初始焦点

213

看起来在WPF应用程序启动时,没有任何控件获得了焦点。

这真的很奇怪。我使用过的每个其他框架都会按照你期望的那样:将初始焦点放在选项卡顺序中的第一个控件上。但我已经确认了它是WPF的问题,而不仅仅是我的应用程序——如果我创建一个新窗口,并在其中只放置一个TextBox,然后运行该应用程序,TextBox在我单击它或按Tab之前并没有焦点。糟糕。

我的实际应用程序比一个TextBox更复杂。我有多个层次的UserControl嵌套在UserControl中。其中一个UserControl具有Focusable = "True"和KeyDown/KeyUp处理程序,并且我希望它在我的窗口打开时立即拥有焦点。尽管我还是WPF新手,但我一直试图弄清楚如何做到这一点,但并没有取得太大成功。

如果我启动我的应用程序并按Tab键,则焦点将转移到我的可获取焦点的控件,然后它开始按照我希望的方式工作。但是我不希望用户必须在使用窗口之前按Tab键。

我已经尝试过使用FocusManager.FocusedElement,但我不确定要设置哪个控件(顶层Window?包含可获取焦点控件的父项?可获取焦点控件本身?)或者应该将其设置为什么。

我需要做什么才能使我的深度嵌套控件在窗口打开时立即获得焦点?或更好的是,将焦点放在选项卡顺序中的第一个可获取焦点的控件上?

12个回答

193

这个也可以运行:

<Window FocusManager.FocusedElement="{Binding ElementName=SomeElement}">

   <DataGrid x:Name="SomeElement">
     ...
   </DataGrid>
</Window>

4
我很惊讶我是第一个评论这个的人。我对这个放在哪里感到困惑,因为它可以用于几乎任何控件。回答这个特定问题,我认为它会放在窗口上,但你可以阅读http://msdn.microsoft.com/en-us/library/system.windows.input.focusmanager%28v=vs.110%29.aspx上的说明来了解你附加它的控件如何影响它。 - Joel McBeth
我已经成功地在StackPanel上使用了这种方法。如果有兴趣,可以在https://dev59.com/sHE85IYBdhLWcg3wPBB8#2872306上找到一个示例。 - Julio Nobre
1
这对我来说比被接受的答案更好,因为我需要将焦点放在第一个元素之后的元素上。 - Puterdo Borato

177

我有一个聪明的想法,想要通过反编译工具(Reflector)找到Focusable属性的使用位置,最终我找到了这个解决方案。我只需要在我的窗口构造函数中添加以下代码:

Loaded += (sender, e) =>
    MoveFocus(new TraversalRequest(FocusNavigationDirection.First));

这将自动选择制表符顺序中的第一个控件,因此它是一种通用解决方案,可以轻松应用于任何窗口并且能够正常工作。


21
将其转化为一种行为。 <Window FocusBehavior.FocusFirst="true"> ... </Window> (说明:这是一段代码,其中"FocusBehavior.FocusFirst"是一种行为的名称,将其设置为"true"表示启用该行为,并将焦点放在窗口中的第一个控件上。) - wekempf
6
@wekempf,我以前不熟悉“behaviors”的概念,但是我查了一下,这不是一个坏主意。如果像我一样的其他人还不熟悉附加行为,这里有一个解释:http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx - Joe White
1
此外,如果所需元素是包含实际可聚焦元素的UserControl(即使在深层次结构中),这也可以正常工作。太好了! - Daniel Albuschat
1
很好的想法,但有时如果接受焦点的控件是一个“按钮”,它可能不起作用。为了解决这个问题,我会在ContextIdle优先级上通过调度程序翻转MoveFocus调用(Background或更高的优先级不起作用)。此外,在这种情况下,有一个FocusNavigationDirection.First更符合意图并且可以达到同样的效果。 - Anton Tykhyy
这应该是默认行为!Yuck(原帖中的词)是正确的! - NH.
显示剩余4条评论

68

基于已接受的 答案 实现为附加行为:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace UI.Behaviors
{
    public static class FocusBehavior
    {
        public static readonly DependencyProperty FocusFirstProperty =
            DependencyProperty.RegisterAttached(
                "FocusFirst",
                typeof(bool),
                typeof(FocusBehavior),
                new PropertyMetadata(false, OnFocusFirstPropertyChanged));

        public static bool GetFocusFirst(Control control)
        {
            return (bool)control.GetValue(FocusFirstProperty);
        }

        public static void SetFocusFirst (Control control, bool value)
        {
            control.SetValue(FocusFirstProperty, value);
        }

        static void OnFocusFirstPropertyChanged(
            DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Control control = obj as Control;
            if (control == null || !(args.NewValue is bool))
            {
                return;
            }

            if ((bool)args.NewValue)
            {
                control.Loaded += (sender, e) =>
                    control.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));
            }
        }
    }
}

像这样使用:

<Window xmlns:Behaviors="clr-namespace:UI.Behaviors"
        Behaviors:FocusBehavior.FocusFirst="true">

6
依我看,这是我找到的最佳解决方案,无疑是最好的。谢谢! - Shion
1
在这个答案的代码中存在一个 bug,即在调用 DependencyProperty.RegisterAttached 时。第三个参数应该是 typeof(FocusBehavior),而不是 typeof(Control)。进行此更改将防止设计师报告“已由‘Control’注册了‘FocusFirst’属性”的错误。 - Tony Vitabile
@TonyVitabile 已修复。如果您能够,随时可以编辑和改进答案。 :) - Mizipzor
1
在卸载期间,控件的Loaded事件处理程序不应该被注销吗? - andreapier
@andreapier 如果你在意的话,你可以这样做,但是跳过注销不会导致内存泄漏或其他问题。只有当短寿命对象上的方法被附加到长寿命对象上的事件时,才需要担心事件导致内存泄漏。在这种情况下,生命周期是窗口的生命周期,所以没问题。 - Joe White

14

我找到了另一个可能的解决方案。Mark Smith发布了一个FirstFocusedElement标记扩展,可用于FocusManager.FocusedElement。

<UserControl x:Class="FocusTest.Page2"
    xmlns:FocusTest="clr-namespace:FocusTest"
    FocusManager.FocusedElement="{FocusTest:FirstFocusedElement}">

非常流畅!谢谢! - Andy
损坏的链接,请问 @Joe White 能否解释一下? - paraJdox1
@Hacki,我已经更新了坏掉的链接,感谢你提醒。 - Joe White

9

在经历了“WPF初始焦点噩梦”之后,根据堆栈上的一些答案,以下内容对我来说是最好的解决方案。

首先,在您的App.xaml OnStartup()中添加以下内容:

EventManager.RegisterClassHandler(typeof(Window), Window.LoadedEvent,
          new RoutedEventHandler(WindowLoaded));

然后在App.xaml中也添加“WindowLoaded”事件:

void WindowLoaded(object sender, RoutedEventArgs e)
    {
        var window = e.Source as Window;
        System.Threading.Thread.Sleep(100);
        window.Dispatcher.Invoke(
        new Action(() =>
        {
            window.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));

        }));
    }

线程问题必须被使用,因为WPF的初始焦点通常由于一些框架竞争条件而失败。

我发现以下解决方案最佳,因为它在整个应用程序中都被使用。

希望能对您有所帮助...

Oran


8
иҜ·дҪҝз”ЁBeginInvokeд»ЈжӣҝйӮЈдёӘеҸҜжҖ•зҡ„Sleep(100)иҜӯеҸҘгҖӮ - l33t

9
在主窗口中有同样的问题,我用简单的方法解决了它:
  <Window ....
        FocusManager.FocusedElement="{Binding ElementName=usercontrolelementname}"
         ... />

在用户控件中:
private void UserControl_GotFocus_1(object sender, RoutedEventArgs e)
        {
            targetcontrol.Focus();
            this.GotFocus -= UserControl_GotFocus_1;  // to set focus only once
        }

4
只有在控件直接位于窗口中时才有效,如果它嵌套在用户控件中则无效。 - Joe White

9
你可以在XAML中轻松地将控件设置为焦点元素。
<Window>
   <DataGrid FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}">
     ...
   </DataGrid>
</Window>

我从未尝试将此设置在用户控件中并查看其是否有效,但这可能是可行的。


这听起来很有趣,因为你不必仅仅为了焦点问题而命名控件。另一方面,我使用用户控件的测试没有成功。 - heringer
这并不让我感到惊讶,@heringer...这就像试图在<border>或类似的非交互式控件上设置焦点一样。您可以尝试在用户控件内部的交互式控件上应用此FocusedElement属性。但这可能不是一个选项。 - Simon Gillbee
我在StackPanel上使用了这种方法,以设置在加载表单后我想要聚焦的子按钮。非常感谢。 - Julio Nobre
请注意,它可能会完全破坏绑定。https://dev59.com/Novda4cB1Zd3GeqPca2x - Der_Meister

3

C# 6+的Mizipzor's answer的最小版本。

public static class FocusBehavior
{
    public static readonly DependencyProperty GiveInitialFocusProperty =
        DependencyProperty.RegisterAttached(
            "GiveInitialFocus",
            typeof(bool),
            typeof(FocusBehavior),
            new PropertyMetadata(false, OnFocusFirstPropertyChanged));

    public static bool GetGiveInitialFocus(Control control) => (bool)control.GetValue(GiveInitialFocusProperty);
    public static void SetGiveInitialFocus(Control control, bool value) => control.SetValue(GiveInitialFocusProperty, value);

    private static void OnFocusFirstPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var control = obj as Control;

        if (control == null || !(args.NewValue is bool))
            return;

        if ((bool)args.NewValue)
            control.Loaded += OnControlLoaded;
        else
            control.Loaded -= OnControlLoaded;
    }

    private static void OnControlLoaded(object sender, RoutedEventArgs e) => ((Control)sender).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}

在您的 XAML 中使用:

<Window local:FocusBehavior.GiveInitialFocus="True" />

2
上述解决方案对我来说没有按预期工作,我稍微修改了Mizipzor提出的行为,具体如下:
从这部分开始
if ((bool)args.NewValue)
        {
            control.Loaded += (sender, e) =>
                   control.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
        }

转为这样

if ((bool)args.NewValue)
        {
            control.Loaded += (sender, e) => control.Focus();
        }

我不会将此行为附加到Window或UserControl,而是附加到我想要最初聚焦的控件,例如:
<TextBox ui:FocusBehavior.InitialFocus="True" />

抱歉,我使用InitialFocus名称作为附加属性的名称可能与您不同。
这对我有用,也许可以帮助其他人。

1
如果您和我一样,正在使用某些框架,这些框架会在基本的焦点行为上出现问题,并使所有上述解决方案都无效,那么您仍然可以执行以下操作:

1-注意获取焦点的元素(无论是什么!)

2-将此添加到xxx.xaml.cs的代码后面

private bool _firstLoad;

3-在获取第一个焦点的元素上添加此内容:

GotFocus="Element_GotFocus"

4 - 在代码后台添加Element_GotFocus方法,并指定需要第一个焦点的WPF命名元素:

private void Element_GotFocus(object sender, RoutedEventArgs e)
{
    if(_firstLoad)
    {
        this.MyElementWithFistFocus.Focus();
        _firstLoad = false;
    }
}

5 - 管理 Loaded 事件

在 XAML 中

Loaded="MyWindow_Loaded"   

在 xaml.cs 中。
private void MyWindow_Loaded(object sender, RoutedEventArgs e)
{
        _firstLoad = true;
        this.Element_GotFocus(null, null);
}

希望这个方法作为最后的解决方案能够有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接