您如何以编程方式将焦点设置到已具有焦点的 WPF ListBox 中的 SelectedItem?

Posted

技术标签:

【中文标题】您如何以编程方式将焦点设置到已具有焦点的 WPF ListBox 中的 SelectedItem?【英文标题】:How do you programmatically set focus to the SelectedItem in a WPF ListBox that already has focus? 【发布时间】:2012-05-13 17:50:50 【问题描述】:

我们希望以编程方式设置ListBoxSelectedItem,并希望该项目具有焦点,以便箭头键相对于该选定项目起作用。看起来很简单。

但问题是,如果 ListBox 在以编程方式设置 SelectedItem 时已经拥有键盘焦点,虽然它确实正确更新了 ListBoxItem 上的 IsSelected 属性,但它没有为其设置键盘焦点,因此,箭头键相对于列表中先前聚焦的项目移动,而不是预期的新选择的项目。

这会让用户感到非常困惑,因为它使选择在使用键盘时看起来会跳来跳去,因为它会快速回到编程选择发生之前的位置。

注意:正如我所说,这只有在您以编程方式将SelectedItem 属性设置在本身已经具有键盘焦点的ListBox 上时才会发生。如果没有(或者如果有但你离开了,那么马上回来),当键盘焦点返回到ListBox 时,正确的项目现在将按预期获得键盘焦点。

这里有一些示例代码显示了这个问题。为了演示这个,运行代码,使用鼠标在列表中选择“Seven”(从而将焦点放在ListBox),然后单击“Test”按钮以编程方式选择第四项。最后,点击键盘上的“Alt”键以显示焦点矩形。您会看到它仍在“七”上,而不是您所期望的“四”,因此,如果您使用向上和向下箭头,它们是相对的“七”行,而不是“四”,更加混乱用户,因为他们在视觉上看到的内容和实际关注的内容不同步。

重要提示:请注意,我将按钮上的 Focusable 设置为 false。如果我没有,当您单击它时,它会获得焦点,而ListBox 会失去它,从而掩盖问题,因为当ListBox 重新获得焦点时,它会正确聚焦选定的ListBoxItem。问题是当ListBox 已经 具有焦点并且您以编程方式选择ListBoxItem

XAML 文件:

<Window x:Class="Test.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="525" Height="350" WindowStartupLocation="CenterScreen"
    Title="MainWindow" x:Name="Root">

    <DockPanel>

        <Button Content="Test"
            DockPanel.Dock="Bottom"
            HorizontalAlignment="Left"
            Focusable="False"
            Click="Button_Click" />

        <ListBox x:Name="MainListBox" />

    </DockPanel>

</Window>

代码隐藏:

using System.Collections.ObjectModel;
using System.Windows;

namespace Test

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

            MainListBox.ItemsSource = new string[]
                "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight"
            ;

        

        private void Button_Click(object sender, RoutedEventArgs e)
        
            MainListBox.SelectedItem = MainListBox.Items[3];
        

    


注意:有些人建议使用IsSynchronizedWithCurrentItem,但该属性将ListBoxSelectedItem 与关联视图的Current 属性同步。与焦点无关,因为这个问题仍然存在。

我们的解决方法是暂时将焦点设置在其他位置,然后设置所选项目,然后将焦点设置回ListBox,但这会产生不希望的效果,我们必须让ViewModel 意识到ListBox 本身,然后根据它是否具有焦点等执行逻辑(即,如果'这里'没有焦点,你不想简单地说'焦点在别处然后回到这里')您会从其他地方窃取它。)另外,您不能简单地通过声明性绑定来处理它。不用说这很丑。

再说一次,“丑陋”的船,就是这样。

【问题讨论】:

【参考方案1】:

这是几行代码。如果您不想在代码隐藏中使用它,我相信它可以打包在附加的行为中。

private void Button_Click(object sender, RoutedEventArgs e)

    MainListBox.SelectedItem = MainListBox.Items[3];
    MainListBox.UpdateLayout(); // Pre-generates item containers 

    var listBoxItem = (ListBoxItem) MainListBox
        .ItemContainerGenerator
        .ContainerFromItem(MainListBox.SelectedItem);

    listBoxItem.Focus();

【讨论】:

ContainerFromItem 如果尚未为其生成容器,则将返回 Null,这是虚拟化列表的情况,并且项目不在屏幕上。另外,如果您尝试从绑定中设置值,它会崩溃,因为您无权访问 ListBox,也不应该。 (下面继续...) 即使是附加的行为也会带来问题,因为您不希望控件盲目地将键盘焦点设置为 ListBoxItem,除非 a) 控件已经具有焦点(易于测试),并且 b) 发生了变化从代码隐藏(没有子类不容易测试。)否则,在多选模式和取消选择时,您可能会无意中从另一个控件中窃取焦点(案例 a)或弄乱当前控件中的焦点(案例 b)一行,通常应该保留键盘焦点,但会设置为新的 SelectedItem,如果有的话,会导致奇怪的行为。 实际上,再想一想,我认为我有解决上述案例“B”的方法。您确实创建了您所说的附加行为,但您没有直接在 XAML 中设置它。相反,您在 ViewModel 上创建一个属性并将行为绑定到该属性。这样你就不需要知道(或关心)谁在听。然后,就在您从后面的代码设置所选项目之前,启用该行为,选择该项目,然后再次禁用该行为。这解决了上面的“B”。 (当然,你仍然需要'A'。)投票给你作为答案,因为虽然不完整,但确实让我走上了这条路。 在调用 ContainerFromItem() 之前,我已经编辑到 UpdateLayout(),这应该会预先生成项目容器并避免意外的 null 结果。 如果使用 listBox.ScrollIntoView(listBox.SelectedItem) 比 ListboxItem.Focus() 会更好,不需要使用 ContainerFromItem();【参考方案2】:

也许有附加行为?类似的东西

public static DependencyProperty FocusWhenSelectedProperty = DependencyProperty.RegisterAttached(
            "FocusWhenSelected", 
            typeof(bool), 
            typeof(FocusWhenSelectedBehavior), 
            new PropertyMetadata(false, new PropertyChangedCallback(OnFocusWhenSelectedChanged)));

private static void OnFocusWhenSelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    
        var i = (ListBoxItem)obj;
        if ((bool)args.NewValue)
           i.Selected += i_Selected;
        else
           i.Selected -= i_Selected;
    

static void i_Selected(object sender, RoutedEventArgs e)

    ((ListBoxItem)sender).Focus();

在 xaml 中

       <Style TargetType="ListBoxItem">
            <Setter Property="local:FocusWhenSelectedBehavior.FocusWhenSelected" Value="True"/>
        </Style>

【讨论】:

这是我最初的目标,但问题是如果你有一个虚拟列表,这些项目还不存在,所以附加的行为还没有被连接起来响应。您仍然必须以某种方式首先执行 ScrollIntoView 方法。其次,如果你严格响应 IsSelected,在多选模式下事情可能会变得有点疯狂,但话又说回来,这可能被认为是期望的行为。虚拟化(默认开启)才是真正的杀手。【参考方案3】:

你只需要使用 ListBox.SelectedItem 然后使用 ListBox.ScrollIntoView(listBox.SelectedItem)

示例代码:

        private void textBox2_TextChanged(object sender, TextChangedEventArgs e)
    

        var comparision = StringComparison.InvariantCultureIgnoreCase;
        string myString = textBox2.Text;
        List<dynamic> index = listBox.Items.SourceCollection.OfType<dynamic>().Where(x=>x.Nombre.StartsWith(myString,comparision)).ToList();
        if (index.Count > 0)  
        listBox.SelectedItem= index.First();


            listBox.ScrollIntoView(listBox.SelectedItem);


        

    

【讨论】:

【参考方案4】:

在您的 XAML 中,您尝试过这个但没有用?

<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="Binding Path=YourCollectionView" SelectedItem="Binding SelectedItem"></ListBox>

还有SelectedItem 属性:

    private YourObject _SelectedItem;
    public YourObject SelectedItem
    
        get
        
            return _SelectedItem;
        
        set
        
            if (_SelectedItem == value)
                return;

            _SelectedItem = value;

            OnPropertyChanged("SelectedItem");
        
    

现在您可以在代码中执行以下操作:

SelectedItem = theItemYouWant;

对我来说,这种方法总是有效的。

【讨论】:

我会打电话给你的。你试过了吗?加载你的列表框,比如 100 个项目。用鼠标单击第 50 个项目。然后在表单上添加一个按钮,以编程方式将SelectedItem 设置为第 10 项。选择确实发生了变化,但是 a) 所选项目不会滚动到视图中,因为 b) 它没有聚焦。您可以通过点击“Alt”键看到这一点,您仍然会看到您点击的项目仍然具有键盘焦点。 顺便说一句...为了确定,您必须先使用鼠标或键盘物理选择另一行。 @MarquelV 我可能对你想要什么感到困惑。我以为您想以编程方式设置选定的行。选定的行是焦点行。您必须使用 ScrollIntoView 才能移动到那里。是的,我的方法不适用于多项选择。 您的语句“选定的行是焦点行”实际上是不正确的,这是我要解决的问题。即使在您上面的代码中,如果您按照我的 cmets 中的步骤操作,您也会看到这一点。另请注意:当前项目是第一个选择的项目,而不是焦点项目。这就是我认为你感到困惑的地方。我添加了一个代码 sn-p 来显示这一点。【参考方案5】:

首先) 您必须使用 ListBox.Items.IndexOf() 在列表框中找到选定的项目。 Second) 现在使用 ListBox.SelectedItems.Add() 添加项目。

这是我的代码:

DataRow[] drWidgetItem = dtItemPrice.Select(widgetItemsFilter);
lbxWidgetItem.SelectedItems.Clear(); foreach(DataRow drvItem in
drWidgetItem)
lbxWidgetItem.SelectedItems.Add(lbxWidgetItem.Items[dtItemPrice.Rows.IndexOf(drvItem)]);

如果你想在 ListBox 中选择一个项目,你可以这样使用: ListBox.SelectedItem = (你的 ListBoxItem); 如果您想在 ListBox 中选择某些项目,您必须使用这种方式: ListBox.SelectedItems.Add(你的 ListBoxItem);

【讨论】:

我不是 100% 确定,但我认为您可能误解了我的问题。选择不是问题。重点是。您的代码确实选择了项目,但它没有设置焦点,这是我要解决的问题。 对不起!所以,我认为如果你集中你的控制之后你的项目必须集中。 (对不起我的英语不好) 但是如果控件已经有了焦点,就不能重新设置了。您必须先关注其他事情。

以上是关于您如何以编程方式将焦点设置到已具有焦点的 WPF ListBox 中的 SelectedItem?的主要内容,如果未能解决你的问题,请参考以下文章

将焦点设置在 xaml wpf 中的文本框上

Xamarin iOS - 我们如何以编程方式将可访问性焦点设置为按钮

以编程方式移除焦点?

Android TextField:以编程方式设置焦点+软输入

WPF 打开新窗口 如何设置 焦点还在原来的窗口 谢谢

AppKit 上的 SwiftUI - 在弹出窗口中将焦点设置为 TextField