【问题标题】:How do you programmatically set focus to the SelectedItem in a WPF ListBox that already has focus?您如何以编程方式将焦点设置到已具有焦点的 WPF ListBox 中的 SelectedItem?
【发布时间】: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 本身,然后根据它是否具有焦点等执行逻辑(即,如果'这里'没有焦点,你不想简单地说'焦点在别处然后回到这里')您会从其他地方窃取它。)另外,您不能简单地通过声明性绑定来处理它。不用说这很丑。

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

【问题讨论】:

    标签: wpf listbox selecteditem


    【解决方案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% 确定,但我认为您可能误解了我的问题。选择不是问题。重点是。您的代码确实选择了项目,但它没有设置焦点,这是我要解决的问题。
      • 对不起!所以,我认为如果你集中你的控制之后你的项目必须集中。 (对不起我的英语不好)
      • 但是如果控件已经有了焦点,就不能重新设置了。您必须先关注其他事情。
      猜你喜欢
      • 2014-10-04
      • 2011-06-30
      • 2021-10-30
      • 1970-01-01
      • 2020-02-15
      • 2011-03-15
      • 1970-01-01
      • 2011-02-24
      • 2021-01-19
      相关资源
      最近更新 更多