具有水平方向的 ListView ItemsPanelTemplate,如何识别第一行的 ListView 项目?

ListView ItemsPanelTemplate with horizontal orientation, how to Identify ListViewItems on first row?

首先,我正在使用 MVVM / WPF / .Net Framework 4.6.1

我有一个 ListView 配置为水平方向 ItemsPanelTemplate,显示来自 DataTemplate 的项目。此设置允许我在 ListViewWidth 内放置尽可能多的项目(witdth 大小与 Window),并在我调整 window 大小时做出响应。

到目前为止一切都很好,现在我只想确定第一行的位置,包括 window 调整大小和第一行内的项目增加或减少的时间。

我只是想完成此行为,因为我想为这些项目应用不同的模板样式(比如更大的图像或不同的文本颜色)。

下面 ListView 的 XAML 定义:

<ListView x:Name="lv"  
          ItemsSource="{Binding Path = ItemsSource}"
          SelectedItem="{Binding Path = SelectedItem}">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal"></WrapPanel>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid Width="180" Height="35">
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32" 
                         VerticalAlignment="Top" HorizontalAlignment="Left">
                    <Ellipse.Fill>
                        <ImageBrush ImageSource="{Binding IconPathName}" />
                    </Ellipse.Fill>
                </Ellipse>
                <TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
                           HorizontalAlignment="Left" VerticalAlignment="Top"
                           Text="{Binding Name}" />

            </Grid> 
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

顺便说一句:我已经解决了从每个 ListViewItem 获取 Index 并针对 Width[=51= 进行计算的问题DataTemplateGrid 的 ] 是固定值 180,但不幸的是它没有像我预期的那样工作,因为我不得不使用 DependencyProperty 绑定 [=40= ListView 的 ]ActualWidth 到我的 ViewModel 并且在我调整 window.

大小时没有很好地响应

我知道我正在寻找一种非常特殊的行为,但如果有人对如何处理这个问题有任何建议,我将不胜感激。即使您认为我应该使用不同的控件,也欢迎任何想法,请详细说明。

提前致谢!

您不应在任何视图模型中处理布局。如果您没有扩展 ListView,请考虑使用附加行为(原始示例):

ListBox.cs

public class ListBox : DependencyObject
{
  #region IsAlternateFirstRowTemplateEnabled attached property

  public static readonly DependencyProperty IsAlternateFirstRowTemplateEnabledProperty = DependencyProperty.RegisterAttached(
    "IsAlternateFirstRowTemplateEnabled", 
    typeof(bool), typeof(ListView), 
    new PropertyMetadata(default(bool), ListBox.OnIsEnabledChanged));

  public static void SetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty, value);

  public static bool GetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement) => (bool)attachingElement.GetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty);

  #endregion

  private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is System.Windows.Controls.ListBox listBox))
    {
      return;
    }

    if ((bool)e.NewValue)
    {
      listBox.Loaded += ListBox.Initialize;
    }
    else
    {
      listBox.SizeChanged -= ListBox.OnListBoxSizeChanged;
    }
  }

  private static void Initialize(object sender, RoutedEventArgs e)
  {
    var listBox = sender as System.Windows.Controls.ListBox;
    listBox.Loaded -= ListBox.Initialize;

    // Check if items panel is WrapPanel
    if (!listBox.TryFindVisualChildElement(out WrapPanel panel))
    {
      return;
    }

    listBox.SizeChanged += ListBox.OnListBoxSizeChanged;
    ListBox.ApplyFirstRowDataTemplate(listBox);
  }

  private static void OnListBoxSizeChanged(object sender, SizeChangedEventArgs e)
  {
    if (!e.WidthChanged)
    {
      return;
    }
    var listBox = sender as System.Windows.Controls.ListBox;
    ListBox.ApplyFirstRowDataTemplate(listBox);
  }

  private static void ApplyFirstRowDataTemplate(System.Windows.Controls.ListBox listBox)
  {
    double calculatedFirstRowWidth = 0;
    var firstRowDataTemplate = listBox.Resources["FirstRowDataTemplate"] as DataTemplate;
    foreach (FrameworkElement itemContainer in listBox.ItemContainerGenerator.Items
      .Select(listBox.ItemContainerGenerator.ContainerFromItem).Cast<FrameworkElement>())
    {
      calculatedFirstRowWidth += itemContainer.ActualWidth;
      if (itemContainer.TryFindVisualChildElement(out ContentPresenter contentPresenter))
      {
        if (calculatedFirstRowWidth > listBox.ActualWidth - listBox.Padding.Right - listBox.Padding.Left)
        {
          if (contentPresenter.ContentTemplate == firstRowDataTemplate)
          {
            // Restore the default template of previous first row items
            contentPresenter.ContentTemplate = listBox.ItemTemplate;
            continue;
          }

          break;
        }

        contentPresenter.ContentTemplate = firstRowDataTemplate;
      }
    }
  }
}

助手扩展方法

/// <summary>
/// Traverses the visual tree towards the leafs until an element with a matching element type is found.
/// </summary>
/// <typeparam name="TChild">The type the visual child must match.</typeparam>
/// <param name="parent"></param>
/// <param name="resultElement"></param>
/// <returns></returns>
public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
  where TChild : DependencyObject
{
  resultElement = null;

  if (parent is Popup popup)
  {
    parent = popup.Child;
    if (parent == null)
    {
      return false;
    }
  }

  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

    if (childElement is TChild child)
    {
      resultElement = child;
      return true;
    }

    if (childElement.TryFindVisualChildElement(out resultElement))
    {
      return true;
    }
  }

  return false;
}

用法

<ListView x:Name="lv"  
          ListBox.IsAlternateFirstRowTemplateEnabled="True"
          ItemsSource="{Binding Path = ItemsSource}"
          SelectedItem="{Binding Path = SelectedItem}">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.Resources>
        <DataTemplate x:Key="FirstRowDataTemplate">

            <!-- Draw a red border around first row items -->
            <Border BorderThickness="2" BorderBrush="Red">
                <Grid Width="180" Height="35">
                    <Grid.RowDefinitions>
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32" 
                         VerticalAlignment="Top" HorizontalAlignment="Left">
                        <Ellipse.Fill>
                            <ImageBrush ImageSource="{Binding IconPathName}" />
                        </Ellipse.Fill>
                    </Ellipse>
                    <TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
                           HorizontalAlignment="Left" VerticalAlignment="Top"
                           Text="{Binding Name}" />

                </Grid> 
            </Border>
        </DataTemplate>
    </ListView.Resources>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid Width="180" Height="35">
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32" 
                         VerticalAlignment="Top" HorizontalAlignment="Left">
                    <Ellipse.Fill>
                        <ImageBrush ImageSource="{Binding IconPathName}" />
                    </Ellipse.Fill>
                </Ellipse>
                <TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
                           HorizontalAlignment="Left" VerticalAlignment="Top"
                           Text="{Binding Name}" />

            </Grid> 
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

备注
如果第一行的视觉树本身不会改变,请考虑将第二个附加的 属性 添加到 ListBox class(例如,IsFirstRowItem),您将在其上设置ListBoxItems。然后您可以使用 DataTrigger 修改控件属性以更改外观。这很可能也会提高性能。