WPF 中自定义 ListBox 布局






4.98/5 (57投票s)
逐步回顾如何自定义 ListBox 中项目的排列方式。
引言
本文探讨了如何自定义 ListBox
(或任何 ItemsControl
的子类)中项目的排列方式。它利用 ItemsPanel 属性来进行自定义。
背景
有一天,我发现 ItemsControl
的 ItemsPanel
属性,当时我恍然大悟。此属性允许您选择用于排列 ItemsControl
或其任何派生控件(如 ListBox
)中显示的项目的布局面板。这项功能证明了 WPF 的强大灵活性,因为它允许您完全重新定义列表中项目的相对排列方式。
此处所示的演示应用程序用玩具机器人的图像填充了 ListBox
。最初,图像从 ListBox
的顶部到底部进行排列,这是正常行为。自定义完成后,图像将以从左到右、从上到下的布局显示,就像页面上的文本一样(对于我们这些从左到右阅读的人来说)。此自定义布局是通过使用 WrapPanel 来为我们排列图像来实现的。
第一步 – 将 ListBox 放入窗口
此任务的实现可以分为四个逻辑步骤。第一步只是将 ListBox
放入 Window
中。
<Window x:Class="CustomItemsPanel.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomItemsPanel"
Title="Custom ItemsPanel" Height="600" Width="260"
>
<!-- This ListBox is the Content of the Window.
Normally you would have a panel of some type
as the Window's Content, but let's keep it simple. -->
<ListBox ItemsSource="{Binding}" />
</Window>
如果您此时编译并运行项目,您将看到一个似乎是空的 Window
。ListBox
显示在那里;但是,它还没有任何项目。
第二步 – 用图片填充 ListBox
接下来,让我们看看用于用机器人图像填充 ListBox
的类。
public static class RobotImageLoader
{
public static List<BitmapImage> LoadImages()
{
List<BitmapImage> robotImages = new List<BitmapImage>();
DirectoryInfo robotImageDir = new DirectoryInfo( @"..\..\Robots" );
foreach( FileInfo robotImageFile in robotImageDir.GetFiles( "*.jpg" ) )
{
Uri uri = new Uri( robotImageFile.FullName );
robotImages.Add( new BitmapImage( uri ) );
}
return robotImages;
}
}
上面的简单代码假定您的项目有一个名为“Robots”的文件夹,其中包含一些 JPG 图像。在更实际的应用程序中,这些硬编码的依赖关系应外包到配置系统中。我们可以使用 RobotImageLoader
类,并在上面声明的 Window
类中使用以下标记:
<Window.DataContext>
<ObjectDataProvider
ObjectType="{x:Type local:RobotImageLoader}"
MethodName="LoadImages"
/>
</Window.DataContext>
上面的 XAML 表明,Window
中所有视觉元素(visual elements)的隐式数据源(implicit data source)将默认为调用静态 RobotImageLoader.LoadImages
方法时返回的对象。
如果您现在运行该应用程序并稍微调整 Window
的大小,它看起来就像这样:
上图中的屏幕截图显然不是我们想要的。如果我们可以看到存储在 BitmapImage
中的图像,而不是图像的 URI,那就更好了。之所以显示 URI,是因为 BitmapImage
对象本身没有显示它的内在支持。当 ListBox
渲染每个 BitmapImage
对象时,它会调用该对象的 ToString
方法,因为 BitmapImage
没有继承自 UIElement
类。然后它会显示从 BitmapImage
对象 ToString
重写返回的字符串。
第三步 – 创建用于显示图片的模板
下一步是向 ListBox
解释它应该如何渲染 BitmapImage
。为了实现这一点,我们将应用一个 Style
到 ListBox
。此 Style
将 ListBox
的 ItemTemplate
属性设置为 DataTemplate
,该属性指定在尝试渲染 BitmapImage
对象时应显示一个包装在 Border
中的 Image
元素。
这是修改后的 XAML:
<Window x:Class="CustomItemsPanel.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomItemsPanel"
Title="The images are shown" Height="600" Width="260"
>
<Window.Resources>
<Style TargetType="{x:Type ListBox}">
<!-- Set the ItemTemplate of the ListBox to a DataTemplate which
explains how to display an object of type BitmapImage. -->
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="4"
CornerRadius="5" Margin="6"
>
<Image
Source="{Binding Path=UriSource}"
Stretch="Fill"
Width="100" Height="120"
/>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Window.DataContext>
<ObjectDataProvider
ObjectType="{x:Type local:RobotImageLoader}"
MethodName="LoadImages" />
</Window.DataContext>
<!-- This ListBox is the Content of the Window.
Normally you would have a panel of some type
as the Window's Content, but let's keep it simple. -->
<ListBox ItemsSource="{Binding}" />
</Window>
如果您现在运行该应用程序,它看起来就像这样:
嗯,这样好多了。但请注意,如果用户想看更多机器人,他们将不得不向下滚动。也许此应用程序的逻辑要求用户能够尽可能多地在 Window
中看到机器人。这时 ItemsPanel
属性就能派上用场了。
第四步 – 替换默认的项目面板
默认情况下,ListBox
使用一种称为 VirtualizingStackPanel
的面板来显示其项目。基本上,VirtualizingStackPanel
是一种 StackPanel
,它只为控件中当前可见的项目创建视觉对象。对于滚出视图的项目,面板会丢弃用于渲染它们的视觉对象。当控件包含大量项目时,此技术可以极大地提高性能和内存消耗。
对于 VirtualizingStackPanel
不是 ListBox
中项目的理想布局机制的情况,我们可以指定任何我们想要的面板来显示项目。在这里,一个不错的选择是使用 WrapPanel
来承载 ListBox
的项目。WrapPanel
默认情况下会从左到右排列其子项,当没有水平空间时,它会在前一行下方创建另一行项目。它会一直遵循这个模式,直到所有项目都显示出来。当 WrapPanel
调整大小时,它会更新布局以确保尽可能多的项目完全可见。
最后一步是将 ListBox
的 ItemsPanel
属性设置为 WrapPanel
。以下 XAML 也将放置在前面代码片段中看到的 Style
中:
<!-- Swap out the default items panel with a WrapPanel so that
the images will be arranged with a different layout. -->
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<!-- Set this attached property to 'Disabled' so that the
ScrollViewer in the ListBox will never show a horizontal
scrollbar, and the WrapPanel it contains will be constrained
to the width of the ScrollViewer's viewable surface. -->
<Setter
Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled"
/>
当您现在运行该应用程序时,它看起来就像这样:
如果您调整 Window
的大小,WrapPanel
将调整布局以适应新的尺寸。例如:
上面看到的 XAML 中有一件重要的事情需要注意。必须指定 ListBox
内的 ScrollViewer
禁用其水平滚动条。这样做可以确保 WrapPanel
的宽度限制在 ScrollViewer
的可见宽度内。它还可以防止水平滚动条出现,这在此场景中是可取的。这是设置该属性的 <Style>
中的 XAML:
<Setter
Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled"
/>
此演示项目的源代码可以在本文顶部下载。
历史
- 2007 年 4 月 25 日 – 文章创建