WPF y enfoque inicial

190

Parece que cuando se inicia una aplicación WPF, nada tiene foco.

Esto es muy extraño. Todos los demás marcos que he usado hacen exactamente lo que esperarías: pone el foco inicial en el primer control en el orden de tabulación. Pero he confirmado que es WPF, no solo mi aplicación: si creo una nueva ventana, y solo pongo un TextBox y ejecuto la aplicación, el TextBox no tendrá foco hasta que haga clic en él o presione Tab . Yuck

Mi aplicación actual es más complicada que solo un TextBox. Tengo varias capas de UserControls dentro de UserControls. Uno de esos UserControls tiene manejadores Focusable = "True" y KeyDown / KeyUp, y quiero que tenga el foco tan pronto como se abra mi ventana. Sin embargo, sigo siendo un novato de WPF y no tengo mucha suerte para descubrir cómo hacerlo.

Si inicio mi aplicación y presiono la tecla Tab, el foco se dirige a mi control enfocable y comienza a funcionar de la manera que quiero. Pero no quiero que mis usuarios tengan que presionar Tab antes de que puedan comenzar a usar la ventana.

He jugado con FocusManager.FocusedElement, pero no estoy seguro de en qué control configurarlo (la ventana de nivel superior? El padre que contiene el control enfocable? El control enfocable en sí mismo) o en qué configurarlo.

¿Qué debo hacer para que mi control profundamente anidado tenga el foco inicial tan pronto como se abra la ventana? O mejor aún, ¿enfocar el primer control enfocable en el orden de tabulación?

Joe White
fuente

Respuestas:

164

Esto también funciona:

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

   <DataGrid x:Name="SomeElement">
     ...
   </DataGrid>
</Window>
Sean
fuente
44
Estoy sorprendido de ser la primera persona que comentó sobre esto. Estaba confundido en cuanto a dónde iba esto porque podría tener casi cualquier control. En respuesta a esta pregunta específica, creo que iría en la ventana, pero puede leer los comentarios en msdn.microsoft.com/en-us/library/… para comprender cómo importa el control que le asigna.
Joel McBeth
He utilizado este enfoque en un panel de pila con éxito. Si uno está interesado, hay un ejemplo en stackoverflow.com/a/2872306/378115
Julio Nobre
Esto funcionó para mí mucho mejor que la respuesta aceptada porque necesito enfocarme en el elemento que está primero.
Puterdo Borato
163

Tuve la brillante idea de buscar en Reflector para ver dónde se usa la propiedad Focusable, y encontré el camino hacia esta solución. Solo necesito agregar el siguiente código al constructor de mi ventana:

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

Esto seleccionará automáticamente el primer control en el orden de tabulación, por lo que es una solución general que debería poder colocarse en cualquier ventana y Just Work.

Joe White
fuente
21
Agregue convertir eso en un comportamiento. <Window FocusBehavior.FocusFirst = "true"> ... </Window>
wekempf
66
@wekempf, no estaba familiarizado con la idea de los comportamientos, pero lo investigué y no es una mala idea en absoluto. Si alguien más (como yo) ya no está familiarizado con los comportamientos adjuntos, aquí hay una explicación: codeproject.com/KB/WPF/AttachedBehaviors.aspx
Joe White
1
Además, esto funciona si el elemento deseado es un UserControl que contiene el elemento enfocable real (incluso en jerarquías profundas). ¡Excelente!
Daniel Albuschat
1
Gran idea, pero a veces no funciona si el control que aceptaría el foco es a Button. Para solucionar esto, volteo la MoveFocusllamada al despachador con ContextIdleprioridad ( Backgroundo superior no funciona). Además, hay una FocusNavigationDirection.Firstque coincide mejor con la intención y hace lo mismo en este caso.
Anton Tykhyy
¡Ese debería ser el comportamiento predeterminado! ¡Yuck (en la publicación original) tiene razón!
NH.
61

Basado en la respuesta aceptada implementada como un comportamiento adjunto:

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.Next));
            }
        }
    }
}

Úselo así:

<Window xmlns:Behaviors="clr-namespace:UI.Behaviors"
        Behaviors:FocusBehavior.FocusFirst="true">
Mizipzor
fuente
66
En mi opinión, esta es, con mucho, la mejor solución que he encontrado. ¡Gracias!
Shion
1
Hay un error en el código en esta respuesta en la llamada a DependencyProperty.RegisterAttached. El tercer parámetro debería ser typeof(FocusBehavior), no typeof(Control). Hacer este cambio evitará que el diseñador informe la propiedad 'FocusFirst' ya registrada por errores de 'Control'.
Tony Vitabile
@TonyVitabile fijo. Siempre puedes editar y mejorar las respuestas si puedes. :)
Mizipzor
No debería controlar. ¿El controlador de eventos cargado se anulará durante la descarga?
andreapier
@andreapier Podrías si te importara, pero omitir el desregistro no causaría una pérdida de memoria ni nada. Solo debe preocuparse por los eventos que causan pérdidas de memoria si un objeto de corta duración tiene un método adjunto a un evento en un objeto de larga duración. En este caso, la vida útil es la de la ventana, así que estás bien.
Joe White
14

Encontré otra posible solución. Mark Smith publicó una extensión de marcado FirstFocusedElement para usar con FocusManager.FocusedElement.

<UserControl x:Class="FocusTest.Page2"
    xmlns:FocusTest="clr-namespace:FocusTest"
    FocusManager.FocusedElement="{FocusTest:FirstFocusedElement}">
Joe White
fuente
Totalmente hábil! ¡Gracias!
Andy
8

Después de tener una 'pesadilla de enfoque inicial de WPF' y basarme en algunas respuestas en la pila, lo siguiente demostró que era la mejor solución.

Primero, agregue su App.xaml OnStartup () lo siguiente:

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

Luego agregue el evento 'WindowLoaded' también en App.xaml:

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

        }));
    }

El problema de subprocesos debe usarse ya que el enfoque inicial de WPF falla principalmente debido a algunas condiciones de carrera marco.

Encontré la siguiente solución mejor, ya que se usa globalmente para toda la aplicación.

Espero eso ayude...

Orán

OrPaz
fuente
55
Use en BeginInvokelugar de esa Sleep(100)declaración de miedo .
l33t el
8

Tenía el mismo problema resuelto con una solución simple: en la ventana principal:

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

En el control del usuario:

private void UserControl_GotFocus_1(object sender, RoutedEventArgs e)
        {
            targetcontrol.Focus();
            this.GotFocus -= UserControl_GotFocus_1;  // to set focus only once
        }
Vladik Y
fuente
3
Solo funciona si el control está directamente dentro de la Ventana, no si está anidado dentro de un UserControl.
Joe White
8

Puede tener fácilmente el control configurado como el elemento enfocado en XAML.

<Window>
   <DataGrid FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}">
     ...
   </DataGrid>
</Window>

Nunca he intentado configurar esto en un control de usuario y ver si funciona, pero puede funcionar.

Simon Gillbee
fuente
Suena interesante, porque no tiene que nombrar el control solo por un problema de enfoque. Por otro lado, mi prueba con control de usuario no funcionó.
heringer
Eso no me sorprende @heringer ... eso sería como tratar de enfocar un <border> o un control no interactivo similar. Podría intentar aplicar este atributo FocusedElement en un control interactivo dentro del control de usuario. Pero eso puede no ser una opción.
Simon Gillbee
He utilizado este enfoque en un panel de pila para establecer qué botón secundario quería enfocar una vez que se carga el formulario. Muchas gracias
Julio Nobre
Tenga cuidado, puede hacer que las fijaciones se rompan por completo. stackoverflow.com/questions/30676863/…
Der_Meister
2

Una versión mínima de la respuesta de Mizipzor para C # 6+.

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

Use en su XAML:

<Window local:FocusBehavior.GiveInitialFocus="True" />
Drew Noakes
fuente
1

Si eres como yo y estás usando algunos marcos que, de alguna manera, alteran los comportamientos básicos de enfoque y hacen que todas las soluciones anteriores sean irrelevantes, aún puedes hacer esto:

1 - Tenga en cuenta el elemento que obtiene el foco (¡lo que sea!)

2 - Agregue esto en su código detrás de xxx.xaml.cs

private bool _firstLoad;

3 - Agregue esto en el elemento que obtiene el primer foco:

GotFocus="Element_GotFocus"

4 - Agregue el método Element_GotFocus en el código detrás y especifique el elemento con nombre WPF que necesita el primer foco:

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

5 - Administrar el evento cargado

en XAML

Loaded="MyWindow_Loaded"   

en xaml.cs

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

Espero que esto ayude como solución de último recurso

G.Dealmeida
fuente
0

También enfrenté el mismo problema. Tenía tres cuadros de texto dentro del contenedor de lienzo y quería que el primer cuadro de texto se enfocara cuando se abre el control de usuario. El código WPF siguió el patrón MVVM. Creé una clase de comportamiento separada para enfocar el elemento y lo vinculé a mi punto de vista de esta manera.

Código de comportamiento del lienzo

public  class CanvasLoadedBehavior : Behavior<Canvas>
{
    private Canvas _canvas;
    protected override void OnAttached()
    {
        base.OnAttached();
        _canvas = AssociatedObject as Canvas;
        if (_canvas.Name == "ReturnRefundCanvas")
        {

            _canvas.Loaded += _canvas_Loaded;
        }


    }

    void _canvas_Loaded(object sender, RoutedEventArgs e)
    {
        FocusNavigationDirection focusDirection = FocusNavigationDirection.Next;

        // MoveFocus takes a TraveralReqest as its argument.
        TraversalRequest request = new TraversalRequest(focusDirection);
        UIElement elementWithFocus = Keyboard.FocusedElement as UIElement;
        if (elementWithFocus != null)
        {
            elementWithFocus.MoveFocus(request);
        }

    }

}

Código para ver

<Canvas  Name="ReturnRefundCanvas" Height="200" Width="1466" DataContext="{Binding RefundSearchViewModel}">
                <i:Interaction.Behaviors>
                    <b:CanvasLoadedBehavior />
                </i:Interaction.Behaviors>
                <uc:Keyboard Canvas.Left="973" Canvas.Top="111" ToolTip="Keyboard" RenderTransformOrigin="-2.795,9.787"></uc:Keyboard>
                <Label  Style="{StaticResource Devlbl}" Canvas.Left="28" Content="Return and Refund Search" Canvas.Top="10" />
                <Image Height="30" Width="28" Canvas.Top="6" Canvas.Left="5" Source="pack://application:,,,/HomaKiosk;component/images/searchF.png">
                    <Image.OpacityMask>
                        <ImageBrush ImageSource="pack://application:,,,/HomaKiosk;component/images/searchF.png"/>
                    </Image.OpacityMask>
                </Image>

                <Separator Height="4" Canvas.Left="6" Margin="0" Canvas.Top="35" Width="1007"/>

                <ContentControl Canvas.Top="45" Canvas.Left="21"
                    ContentTemplate="{StaticResource ErrorMsg}"
                    Visibility="{Binding Error, Converter={c:StringNullOrEmptyToVisibilityConverter}}" 
                    Content="{Binding Error}" Width="992"></ContentControl>

                <Label  Style="{StaticResource Devlbl}" Canvas.Left="29" Name="FirstName" Content="First Name" Canvas.Top="90" />
                <wpf:AutoCompleteTextBox  Style="{StaticResource AutoComp}" Height="32" Canvas.Left="33" ToolTip="First Name"  Canvas.Top="120" Width="205"                     Padding="10,5" TabIndex="1001"
                    VerticalAlignment="Top"

                    Watermark=""
                    IconPlacement="Left"
                    IconVisibility="Visible"
                    Delay="100"

                    Text="{Binding FirstName, Mode=TwoWay, TargetNullValue=''}" 
                    Provider="{Binding FirstNameSuggestions}">
                    <wpf:AutoCompleteTextBox.ItemTemplate>
                        <DataTemplate>
                            <Border Padding="5">
                                <StackPanel Orientation="Vertical">
                                    <TextBlock Text="{Binding}"
                   FontWeight="Bold" />
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </wpf:AutoCompleteTextBox.ItemTemplate>
                </wpf:AutoCompleteTextBox>

                <Label Style="{StaticResource Devlbl}" Canvas.Left="250" Content="Last Name" Canvas.Top="90" />
                <wpf:AutoCompleteTextBox  Style="{StaticResource AutoComp}" Height="32" ToolTip="Last Name" Canvas.Left="250"  Canvas.Top="120" Width="205" Padding="10,5"  TabIndex="1002"
                    VerticalAlignment="Top"
                    Watermark=""
                    IconPlacement="Left"
                    IconVisibility="Visible"
                    Delay="100"
                   Text="{Binding LastName, Mode=TwoWay, TargetNullValue=''}" 
                    Provider="{Binding LastNameSuggestions}">
                    <wpf:AutoCompleteTextBox.ItemTemplate>
                        <DataTemplate>
                            <Border Padding="5">
                                <StackPanel Orientation="Vertical">
                                    <TextBlock Text="{Binding}"
                   FontWeight="Bold" />
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </wpf:AutoCompleteTextBox.ItemTemplate>
                </wpf:AutoCompleteTextBox>

                <Label Style="{StaticResource Devlbl}" Canvas.Left="480" Content="Receipt No" Canvas.Top="90" />
                             <wpf:AutoCompleteTextBox  Style="{StaticResource AutoComp}" Height="32" ToolTip="Receipt No" Canvas.Left="480"  Canvas.Top="120" Width="205" Padding="10,5"  TabIndex="1002"
                    VerticalAlignment="Top"
                    Watermark=""
                    IconPlacement="Left"
                    IconVisibility="Visible"
                    Delay="100"
                    Text="{Binding ReceiptNo, Mode=TwoWay, TargetNullValue=''}" 
                    Provider="{Binding ReceiptIdSuggestions}">
                    <wpf:AutoCompleteTextBox.ItemTemplate>
                        <DataTemplate>
                            <Border Padding="5">
                                <StackPanel Orientation="Vertical" >
                                    <TextBlock Text="{Binding}"
                   FontWeight="Bold">

                                    </TextBlock>
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </wpf:AutoCompleteTextBox.ItemTemplate>
                    <i:Interaction.Behaviors>
                        <b:AllowableCharactersTextBoxBehavior RegularExpression="^[0-9]+$" MaxLength="15" />
                    </i:Interaction.Behaviors>
                </wpf:AutoCompleteTextBox>
                <!--<Label Style="{StaticResource Devlbl}" Canvas.Left="710" Content="Duration" Canvas.Top="79" />-->
                <!--<ComboBox AllowDrop="True" Canvas.Left="710" ToolTip="Duration" Canvas.Top="107" Width="205" TabIndex="1004"
                    Style="{StaticResource CommonComboBox}"      
                    ItemsSource="{Binding Durations}" DisplayMemberPath="Description" SelectedValuePath="Id" SelectedValue="{Binding SelectedDate, Mode=TwoWay}">

                </ComboBox>-->

                <Button Content="Search" Style="{StaticResource MyButton}" ToolTip="Search" 
                    Canvas.Top="116" Canvas.Left="710" Cursor="Hand" 
                    Command="{Binding SearchCommand}" TabIndex="2001">
                </Button>
                <Button Content="Clear" Style="{StaticResource MyButton}"  ToolTip="Clear"
                    Canvas.Top="116" Canvas.Left="840" Cursor="Hand" 
                    Command="{Binding ClearCommand}" TabIndex="2002">
                </Button>
                <Image Height="25" Width="25" Canvas.Top="175" Canvas.Left="25" Source="pack://application:,,,/HomaKiosk;component/images/chkpending.png"/>
                <Label  Style="{StaticResource LegendLbl}" Canvas.Left="50" Content="Check Returned and Payment Pending" Canvas.Top="178" />
                <Image Height="25" Width="25" Canvas.Top="175" Canvas.Left="300" Source="pack://application:,,,/HomaKiosk;component/images/chkrepaid.png"/>
                <Label  Style="{StaticResource LegendLbl}" Canvas.Left="325" Content="Repaid" Canvas.Top="178" />
                <Image Height="25" Width="25" Canvas.Top="175" Canvas.Left="395" Source="pack://application:,,,/HomaKiosk;component/images/refund.png"/>
                <Label  Style="{StaticResource LegendLbl}" Canvas.Left="415" Content="Refunded" Canvas.Top="178" />
                 </Canvas>
BSG
fuente
0

La solución anterior no funcionó como se esperaba para mí, he cambiado ligeramente el comportamiento propuesto por Mizipzor de la siguiente manera:

De esta parte

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

A esto

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

Y no estoy adjuntando este comportamiento a Window o UserControl, pero para controlar quiero enfocarme inicialmente, por ejemplo:

<TextBox ui:FocusBehavior.InitialFocus="True" />

Oh, perdón por diferentes nombres, estoy usando el nombre InitialFocus para la propiedad adjunta.

Y esto está funcionando para mí, tal vez podría ayudar a alguien más.

BrightShadow
fuente