WPF 入门教程 ListView控件(二)

具有左对齐列名的 ListView

在普通的 ListView 中,列名是左对齐的,但出于某种原因,Microsoft 决定在 WPF ListView 中默认将列名居中。在许多情况下,与其他 Windows 应用程序相比,这会使您的应用程序看起来不合时宜。这是 默认情况下 ListView 在 WPF 中的外观:

让我们尝试将其更改为左对齐的列名称。不幸的是,GridViewColumn 上没有直接属性来控制它,但幸运的是,这并不意味着它不能更改。

推荐一款WPF MVVM框架开源项目:Newbeecoder.UI

https://www.zhihu.com/video/1504809412245700608

Demo下载:

使用针对 GridViewColumHeader(用于显示 GridViewColumn 的标题的元素)的样式,我们可以更改 Horizo​​ntalAlignment 属性。在这种情况下,它默认为 Center,但我们可以将其更改为 Left,以完成我们想要的:

<Window x:Class="WpfTutorialSamples.ListView_control.ListViewGridViewSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewGridViewSample" Height="200" Width="400">
		<ListView Margin="10" Name="lvUsers">
			<ListView.Resources>
				<Style TargetType="{x:Type GridViewColumnHeader}">
					<Setter Property="HorizontalContentAlignment" Value="Left" />
				</Style>
			</ListView.Resources>
			<ListView.View>
				<GridView>
					<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
					<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
					<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
				</GridView>
			</ListView.View>
		</ListView>
	</Grid>
</Window>

为我们完成所有工作的部分是 ListView 的资源中定义的样式:

<Style TargetType="{x:Type GridViewColumnHeader}">
					<Setter Property="HorizontalContentAlignment" Value="Left" />
</Style>

本地或全球风格

通过在控件本身内定义样式,它仅适用于这个特定的 ListView。在许多情况下,您可能希望将其应用于同一窗口/页面中的所有 ListView,甚至可能应用于整个应用程序中的全局。您可以通过将样式复制到 Window 资源或应用程序资源来完成此操作。这是相同的示例,我们将样式应用于整个 Window 而不是特定的 ListView:

<Window x:Class="WpfTutorialSamples.ListView_control.ListViewGridViewSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewGridViewSample" Height="200" Width="400">
	<Window.Resources>
		<Style TargetType="{x:Type GridViewColumnHeader}">
			<Setter Property="HorizontalContentAlignment" Value="Left" />
		</Style>
	</Window.Resources>
		<ListView Margin="10" Name="lvUsers">
			<ListView.View>
				<GridView>
					<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
					<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
					<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
				</GridView>
			</ListView.View>
		</ListView>
	</Grid>
</Window>

如果您想要另一个对齐方式,例如右对齐,您只需更改样式的值,如下所示:

<Setter Property="HorizontalContentAlignment" Value="Right" />

列表视图分组

WPF ListView 非常灵活。分组是它开箱即用支持的另一件事,它既易于使用又高度可定制。让我们直接进入第一个示例,然后我将对其进行解释,之后我们可以使用标准的 WPF 技巧来进一步自定义外观。

对于本文,我借用了上一篇文章中的示例代码,然后对其进行了扩展以支持分组。它看起来像这样:

<Window x:Class="WpfTutorialSamples.ListView_control.ListViewGroupSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewGroupSample" Height="300" Width="300">
    <Grid Margin="10">
        <ListView Name="lvUsers">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                </GridView>
            </ListView.View>
            <ListView.GroupStyle>
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <TextBlock FontWeight="Bold" FontSize="14" Text="{Binding Name}"/>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>
    </Grid>
</Window>


using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
namespace WpfTutorialSamples.ListView_control
	public partial class ListViewGroupSample : Window
		public ListViewGroupSample()
			InitializeComponent();
			List<User> items = new List<User>();
			items.Add(new User() { Name = "John Doe", Age = 42, Sex = SexType.Male });
			items.Add(new User() { Name = "Jane Doe", Age = 39, Sex = SexType.Female });
			items.Add(new User() { Name = "Sammy Doe", Age = 13, Sex = SexType.Male });
			lvUsers.ItemsSource = items;
			CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(lvUsers.ItemsSource);
			PropertyGroupDescription groupDescription = new PropertyGroupDescription("Sex");
			view.GroupDescriptions.Add(groupDescription);
	public enum SexType { Male, Female };
	public class User
		public string Name { get; set; }
		public int Age { get; set; }
		public string Mail { get; set; }
		public SexType Sex { get; set; }

在 XAML 中,我向 ListView 添加了一个 GroupStyle,我在其中为每个组的标题定义了一个模板。它由一个 TextBlock 控件组成,我在其中使用了一个稍大且粗体的文本来表明它是一个组 - 正如我们稍后将看到的,这当然可以进行更多自定义。TextBlock Text 属性绑定到 Name 属性, 但请注意,这不是数据对象(在本例中为 User 类)的 Name 属性 。相反,它是组的名称,由 WPF 分配,基于我们用于将对象划分为组的属性。

在 Code-behind 中,我们做的和之前一样:我们创建一个列表并向其中添加一些 User 对象,然后我们将列表绑定到 ListView - 除了我添加的新 Sex 属性之外,没有什么新东西,它告诉用户是男性还是女性。

分配 ItemsSource 后,我们使用它来获取 ListView 为我们创建的 CollectionView。这个专门的 View 实例包含很多可能性,包括对项目进行分组的能力。我们通过向视图的 GroupDescriptions 添加一个所谓的 PropertyGroupDescription 来使用它。这基本上告诉 WPF 按数据对象上的特定属性进行分组,在本例中为 Sex 属性。

自定义组标题

上面的示例非常适合展示 ListView 分组的基础知识,但看起来有点乏味,所以让我们利用 WPF 允许我们定义自己的模板并增加趣味这一事实。一个常见的要求是能够折叠和展开组,虽然默认情况下 WPF 不提供此行为,但自己实现它有点容易。我们将通过完全重新模板化组容器来实现。

它可能看起来有点麻烦,但使用的原理有些简单,在自定义 WPF 控件时您会在其他情况下看到它们。这是代码:

<Window x:Class="WpfTutorialSamples.ListView_control.ListViewCollapseExpandGroupSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewCollapseExpandGroupSample" Height="300" Width="300">
    <Grid Margin="10">
        <ListView Name="lvUsers">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                </GridView>
            </ListView.View>
            <ListView.GroupStyle>
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style TargetType="{x:Type GroupItem}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate>
                                        <Expander IsExpanded="True">
                                            <Expander.Header>
                                                <StackPanel Orientation="Horizontal">
                                                    <TextBlock Text="{Binding Name}" FontWeight="Bold" Foreground="Gray" FontSize="22" VerticalAlignment="Bottom" />
                                                    <TextBlock Text="{Binding ItemCount}" FontSize="22" Foreground="Green" FontWeight="Bold" FontStyle="Italic" Margin="10,0,0,0" VerticalAlignment="Bottom" />
                                                    <TextBlock Text=" item(s)" FontSize="22" Foreground="Silver" FontStyle="Italic" VerticalAlignment="Bottom" />
                                                </StackPanel>
                                            </Expander.Header>
                                            <ItemsPresenter />
                                        </Expander>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>
    </Grid>
</Window>

代码隐藏与第一个示例中使用的完全相同 - 随意向上滚动并抓住它。

现在我们的组看起来更令人兴奋了,它们甚至包括一个扩展按钮,当你点击它时,它会切换组项目的可见性(这就是为什么单身女性用户在屏幕截图上不可见 - 我折叠了那个特定的组) . 通过使用组公开的 ItemCount 属性,我们甚至可以显示每个组当前包含的项目数。

正如您所看到的,它需要的标记比我们习惯的要多一些,但是这个例子也超出了我们通常所做的一点,所以这看起来很公平。当您通读代码时,您会很快意识到许多行只是样式和模板等常见元素。

列表视图排序

我们看到了如何通过访问 ListView 的 View 实例然后添加组描述来对 WPF ListView 中的项目进行分组。对 ListView 应用排序同样简单,大部分过程完全相同。让我们尝试一个简单的例子,我们按年龄对用户对象进行排序:

<Window x:Class="WpfTutorialSamples.ListView_control.ListViewSortingSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewSortingSample" Height="200" Width="300">
    <Grid Margin="10">
        <ListView Name="lvUsers">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace WpfTutorialSamples.ListView_control
	public partial class ListViewSortingSample : Window
		public ListViewSortingSample()
			InitializeComponent();
			List<User> items = new List<User>();
			items.Add(new User() { Name = "John Doe", Age = 42 });
			items.Add(new User() { Name = "Jane Doe", Age = 39 });
			items.Add(new User() { Name = "Sammy Doe", Age = 13 });
			items.Add(new User() { Name = "Donna Doe", Age = 13 });
			lvUsers.ItemsSource = items;
			CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(lvUsers.ItemsSource);
			view.SortDescriptions.Add(new SortDescription("Age", ListSortDirection.Ascending));
	public class User
		public string Name { get; set; }
		public int Age { get; set; }

XAML 看起来就像前面的示例,其中我们只有几列用于显示有关用户的信息——这里没有什么新鲜事。

在代码隐藏中,我们再次创建一个 User 对象列表,然后将其分配为 ListView 的 ItemsSource。完成后,我们使用 ItemsSource 属性来获取 ListView 自动为我们创建的 CollectionView 实例,我们可以使用它来操纵 ListView 如何显示我们的对象。

使用我们手中的视图对象,我们向它添加一个新的 SortDescription,指定我们希望我们的列表按 Age 属性按升序排序。正如您从屏幕截图中看到的,这非常有效 - 列表按年龄排序,而不是与添加项目的顺序相同。

多个排序条件

如第一个示例所示,排序非常简单,但在屏幕截图中,您会看到 Sammy 排在 Donna 之前。它们具有相同的年龄,因此在这种情况下,WPF 将仅使用它们的添加顺序。幸运的是,WPF 允许我们指定任意数量的排序条件。在上面的示例中,尝试将与视图相关的代码更改为如下所示:

CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(lvUsers.ItemsSource);
view.SortDescriptions.Add(new SortDescription("Age", ListSortDirection.Ascending));
view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));

现在视图将首先使用年龄进行排序,当找到两个相同的值时,名称将用作辅助排序参数。

使用列排序的 ListView

我们看到了如何从代码隐藏中轻松地对 ListView 进行排序,虽然这对于某些情况已经足够了,但它不允许最终用户决定排序。除此之外,没有指示 ListView 是按哪一列排序的。在 Windows 和一般的许多用户界面中,通常通过在当前用于排序的列名称旁边绘制一个三角形来说明列表中的排序方向。

在这篇指南文章中,我将为您提供一个实用的解决方案,它为我们提供了上述所有内容,但请记住,此处的某些代码超出了我们迄今为止所学的范围 - 这就是为什么它具有“操作方法”标签。

这篇文章建立在前一篇的基础上,但我仍然会在我们进行的过程中解释每个部分。这是我们的目标 - 具有列排序的 ListView,包括排序字段和方向的视觉指示。用户只需单击要排序的列,如果再次单击同一列,则排序方向相反。这是它的外观:

XAML

我们需要的第一件事是一些 XAML 来定义我们的用户界面。它目前看起来像这样:

<Window x:Class="WpfTutorialSamples.ListView_control.ListViewColumnSortingSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewColumnSortingSample" Height="200" Width="350">
    <Grid Margin="10">
        <ListView Name="lvUsers">
            <ListView.View>
                <GridView>
                    <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Name" Click="lvUsersColumnHeader_Click">Name</GridViewColumnHeader>
                        </GridViewColumn.Header>
                    </GridViewColumn>
                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Age}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Age" Click="lvUsersColumnHeader_Click">Age</GridViewColumnHeader>
                        </GridViewColumn.Header>
                    </GridViewColumn>
                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Sex}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Sex" Click="lvUsersColumnHeader_Click">Sex</GridViewColumnHeader>
                        </GridViewColumn.Header>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

请注意我是如何使用实际的 GridViewColumnHeader 元素而不是仅指定字符串为每一列指定标题的。这样做是为了我可以设置其他属性,在本例中是 Tag 属性以及 Click 事件。

标签 属性用于认为将用于进行排序,如果点击这个特定的列字段名称。这是在每个列订阅的 lvUsersColumnHeader_Click 事件中完成的 。

这是 XAML 的关键概念。除此之外,我们绑定到我们现在将讨论的代码隐藏属性 Name、Age 和 Sex。

代码隐藏

在代码隐藏中,发生了很多事情。我总共使用了三个类,您通常会将它们分成单独的文件,但为了方便起见,我将它们保存在同一个文件中,总共约 100 行。首先是代码,然后我将解释它是如何工作的:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfTutorialSamples.ListView_control
	public partial class ListViewColumnSortingSample : Window
		private GridViewColumnHeader listViewSortCol = null;
		private SortAdorner listViewSortAdorner = null;
		public ListViewColumnSortingSample()
			InitializeComponent();
			List<User> items = new List<User>();
			items.Add(new User() { Name = "John Doe", Age = 42, Sex = SexType.Male });
			items.Add(new User() { Name = "Jane Doe", Age = 39, Sex = SexType.Female });
			items.Add(new User() { Name = "Sammy Doe", Age = 13, Sex = SexType.Male });
			items.Add(new User() { Name = "Donna Doe", Age = 13, Sex = SexType.Female });
			lvUsers.ItemsSource = items;
		private void lvUsersColumnHeader_Click(object sender, RoutedEventArgs e)
			GridViewColumnHeader column = (sender as GridViewColumnHeader);
			string sortBy = column.Tag.ToString();
			if(listViewSortCol != null)
				AdornerLayer.GetAdornerLayer(listViewSortCol).Remove(listViewSortAdorner);
				lvUsers.Items.SortDescriptions.Clear();
			ListSortDirection newDir = ListSortDirection.Ascending;
			if(listViewSortCol == column && listViewSortAdorner.Direction == newDir)
				newDir = ListSortDirection.Descending;
			listViewSortCol = column;
			listViewSortAdorner = new SortAdorner(listViewSortCol, newDir);
			AdornerLayer.GetAdornerLayer(listViewSortCol).Add(listViewSortAdorner);
			lvUsers.Items.SortDescriptions.Add(new SortDescription(sortBy, newDir));
	public enum SexType { Male, Female };
	public class User
		public string Name { get; set; }
		public int Age { get; set; }
		public string Mail { get; set; }
		public SexType Sex { get; set; }
	public class SortAdorner : Adorner
		private static Geometry ascGeometry =
			Geometry.Parse("M 0 4 L 3.5 0 L 7 4 Z");
		private static Geometry descGeometry =
			Geometry.Parse("M 0 0 L 3.5 4 L 7 0 Z");
		public ListSortDirection Direction { get; private set; }
		public SortAdorner(UIElement element, ListSortDirection dir)
			: base(element)
			this.Direction = dir;
		protected override void OnRender(DrawingContext drawingContext)
			base.OnRender(drawingContext);
			if(AdornedElement.RenderSize.Width < 20)
				return;
			TranslateTransform transform = new TranslateTransform
					AdornedElement.RenderSize.Width - 15,
					(AdornedElement.RenderSize.Height - 5) / 2
			drawingContext.PushTransform(transform);
			Geometry geometry = ascGeometry;
			if(this.Direction == ListSortDirection.Descending)
				geometry = descGeometry;
			drawingContext.DrawGeometry(Brushes.Black, null, geometry);
			drawingContext.Pop();

请允许我从底部开始,然后在解释发生的情况的同时逐步向上。文件中的最后一个类是名为 SortAdorner 的 Adorner 类 。这个小类所做的就是根据排序方向绘制一个向上或向下的三角形。WPF 使用装饰器的概念来允许您在其他控件上绘制内容,这正是我们在这里想要的:能够在我们的 ListView 列标题顶部绘制排序三角形。

所述 SortAdorner 的工作原理是限定两个 几何 对象,其基本上用于描述二维形状-在这种情况下与尖端朝上和一个与尖端朝下的三角形。Geometry.Parse() 方法使用点列表来绘制三角形,这将在后面的文章中更彻底地解释。

SortAdorner 知道排序方向的,因为它需要画出正确的三角形,但不知道外地的,我们订购-这是在UI层处理。

用户 类只是一个基本的信息类,用来包含有关用户的信息。其中一些信息用于 UI 层,在这里我们绑定到 Name、Age 和 Sex 属性。

在 Window 类中,我们有两个方法:构建用户列表并将其分配给 ListView 的 ItemsSource 的构造函数,然后是更有趣的单击事件处理程序,当用户单击列时将被命中。在类的顶部,我们定义了两个私有变量: listViewSortCol listViewSortAdorner 。这些将帮助我们跟踪我们当前正在排序的列以及我们放置以指示它的装饰器。

在 lvUsersColumnHeader_Click 事件处理程序中,我们首先获取对用户单击的列的引用。有了这个,我们可以决定对 User 类的哪个属性进行排序,只需查看我们在 XAML 中定义的 Tag 属性。然后我们检查我们是否已经按列排序 - 如果是这种情况,我们删除装饰器并清除当前的排序描述。

之后,我们准备决定方向。默认值是升序,但我们会检查是否已经按用户单击的列进行排序 - 如果是这种情况,我们将方向更改为降序。

最后,我们创建一个新的 SortAdorner,传入它应该呈现的列以及方向。我们将它添加到列标题的 AdornerLayer,最后,我们向 ListView 添加一个 SortDescription,让它知道要根据哪个属性以及在哪个方向进行排序。

列表视图过滤

我们已经对 ListView 做了几件不同的事情,比如分组和排序,但另一个非常有用的功能是过滤。显然,您可以首先限制添加到 ListView 的项目,但通常您需要在运行时动态过滤 ListView,通常基于用户输入的过滤器字符串。对我们来说幸运的是,ListView 的视图机制也可以很容易地做到这一点,就像我们在排序和分组中看到的那样。

过滤实际上很容易做到,所以让我们直接进入一个例子,然后我们会在后面讨论它:

<Window x:Class="WpfTutorialSamples.ListView_control.FilteringSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="FilteringSample" Height="200" Width="300">
    <DockPanel Margin="10">
        <TextBox DockPanel.Dock="Top" Margin="0,0,0,10" Name="txtFilter" TextChanged="txtFilter_TextChanged" />
        <ListView Name="lvUsers">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                </GridView>
            </ListView.View>
        </ListView>
    </DockPanel>
</Window>


using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
namespace WpfTutorialSamples.ListView_control
	public partial class FilteringSample : Window
		public FilteringSample()
			InitializeComponent();
			List<User> items = new List<User>();
			items.Add(new User() { Name = "John Doe", Age = 42 });
			items.Add(new User() { Name = "Jane Doe", Age = 39 });
			items.Add(new User() { Name = "Sammy Doe", Age = 13 });
			items.Add(new User() { Name = "Donna Doe", Age = 13 });
			lvUsers.ItemsSource = items;
			CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(lvUsers.ItemsSource);
			view.Filter = UserFilter;
		private bool UserFilter(object item)
			if(String.IsNullOrEmpty(txtFilter.Text))
				return true;
				return ((item as User).Name.IndexOf(txtFilter.Text, StringComparison.OrdinalIgnoreCase) >= 0);
		private void txtFilter_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
			CollectionViewSource.GetDefaultView(lvUsers.ItemsSource).Refresh();
	public enum SexType { Male, Female };
	public class User
		public string Name { get; set; }
		public int Age { get; set; }