65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中自定义 ListBox 布局

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (57投票s)

2007年4月25日

CPOL

5分钟阅读

viewsIcon

421949

downloadIcon

11932

逐步回顾如何自定义 ListBox 中项目的排列方式。

引言

本文探讨了如何自定义 ListBox(或任何 ItemsControl 的子类)中项目的排列方式。它利用 ItemsPanel 属性来进行自定义。

背景

有一天,我发现 ItemsControlItemsPanel 属性,当时我恍然大悟。此属性允许您选择用于排列 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>

如果您此时编译并运行项目,您将看到一个似乎是空的 WindowListBox 显示在那里;但是,它还没有任何项目。

第二步 – 用图片填充 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 的大小,它看起来就像这样:

Screenshot - CustomItemsPanel_NoTemplate.png

上图中的屏幕截图显然不是我们想要的。如果我们可以看到存储在 BitmapImage 中的图像,而不是图像的 URI,那就更好了。之所以显示 URI,是因为 BitmapImage 对象本身没有显示它的内在支持。当 ListBox 渲染每个 BitmapImage 对象时,它会调用该对象的 ToString 方法,因为 BitmapImage 没有继承自 UIElement 类。然后它会显示从 BitmapImage 对象 ToString 重写返回的字符串。

第三步 – 创建用于显示图片的模板

下一步是向 ListBox 解释它应该如何渲染 BitmapImage。为了实现这一点,我们将应用一个 StyleListBox。此 StyleListBoxItemTemplate 属性设置为 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>

如果您现在运行该应用程序,它看起来就像这样:

Screenshot - CustomItemsPanel_WithTemplate.png

嗯,这样好多了。但请注意,如果用户想看更多机器人,他们将不得不向下滚动。也许此应用程序的逻辑要求用户能够尽可能多地在 Window 中看到机器人。这时 ItemsPanel 属性就能派上用场了。

第四步 – 替换默认的项目面板

默认情况下,ListBox 使用一种称为 VirtualizingStackPanel 的面板来显示其项目。基本上,VirtualizingStackPanel 是一种 StackPanel,它只为控件中当前可见的项目创建视觉对象。对于滚出视图的项目,面板会丢弃用于渲染它们的视觉对象。当控件包含大量项目时,此技术可以极大地提高性能和内存消耗。

对于 VirtualizingStackPanel 不是 ListBox 中项目的理想布局机制的情况,我们可以指定任何我们想要的面板来显示项目。在这里,一个不错的选择是使用 WrapPanel 来承载 ListBox 的项目。WrapPanel 默认情况下会从左到右排列其子项,当没有水平空间时,它会在前一行下方创建另一行项目。它会一直遵循这个模式,直到所有项目都显示出来。当 WrapPanel 调整大小时,它会更新布局以确保尽可能多的项目完全可见。

最后一步是将 ListBoxItemsPanel 属性设置为 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"
  />

当您现在运行该应用程序时,它看起来就像这样:

Screenshot - CustomItemsPanel_WrapPanel.png

如果您调整 Window 的大小,WrapPanel 将调整布局以适应新的尺寸。例如:

Screenshot - CustomItemsPanel_WrapPanel_Wide.png

上面看到的 XAML 中有一件重要的事情需要注意。必须指定 ListBox 内的 ScrollViewer 禁用其水平滚动条。这样做可以确保 WrapPanel 的宽度限制在 ScrollViewer 的可见宽度内。它还可以防止水平滚动条出现,这在此场景中是可取的。这是设置该属性的 <Style> 中的 XAML:

<Setter
  Property="ScrollViewer.HorizontalScrollBarVisibility"
  Value="Disabled"
  />

此演示项目的源代码可以在本文顶部下载。

历史

  • 2007 年 4 月 25 日 – 文章创建
© . All rights reserved.