如何实现带有页眉和页脚的 WrapPanel

How to implement a WrapPanel with a header and footer

我正在尝试实现一个自定义控件,它的行为类似于标准 WrapPanel,但允许您指定页眉和页脚。从视觉上看,这就是我要实现的目标:

我创建了一个自定义控件,似乎为页眉和页脚项目留出了空间,但我无法让它们直观地显示出来。这是我第一次尝试任何类型的自定义控件,因此非常感谢任何帮助或输入!

C#

using System;
using System.Windows;
using System.Windows.Controls;

namespace MyProject.Extras
{
    public class HeaderedFooteredPanel : Panel
    {
        public FrameworkElement Header
        {
            get { return (FrameworkElement) GetValue(HeaderProperty); }
            set { SetValue(HeaderProperty, value); }
        }
        public FrameworkElement Footer
        {
            get { return (FrameworkElement)GetValue(FooterProperty); }
            set { SetValue(FooterProperty, value); }
        }

        public static DependencyProperty HeaderProperty = DependencyProperty.Register(
            nameof(Header),
            typeof(FrameworkElement),
            typeof(HeaderedFooteredPanel),
            new PropertyMetadata((object)null));
        public static DependencyProperty FooterProperty = DependencyProperty.Register(
            nameof(Footer),
            typeof(FrameworkElement),
            typeof(HeaderedFooteredPanel),
            new PropertyMetadata((object)null));

        protected override Size MeasureOverride(Size constraint)
        {
            double x = 0.0;
            double y = 0.0;
            double largestY = 0.0;
            double largestX = 0.0;

            var measure = new Action<FrameworkElement>(element =>
            {
                element.Measure(constraint);
                if (x > 0 &&                                                // Not the first item on this row
                    (x + element.DesiredSize.Width > constraint.Width) &&   // We are too wide to fit on this row
                    ((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
                {
                    y = largestY;
                    x = element.DesiredSize.Width;
                }
                else
                {
                    /* 1) Always place the first item on a row even if width doesn't allow it
                     *      otherwise:
                     * 2) Keep placing on this row until we reach our width constraint
                     *      otherwise:
                     * 3) Keep placing on this row if the max height is reached */

                    x += element.DesiredSize.Width;
                }

                largestY = Math.Max(largestY, y + element.DesiredSize.Height);
                largestX = Math.Max(largestX, x);
            });

            measure(Header);

            foreach (FrameworkElement child in InternalChildren)
            {
                measure(child);
            }

            measure(Footer);

            return new Size(largestX, largestY);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            double x = 0.0;
            double y = 0.0;
            double largestY = 0.0;
            double largestX = 0.0;

            var arrange = new Action<FrameworkElement>(element =>
            {
                if (x > 0 &&                                                // Not the first item on this row
                    (x + element.DesiredSize.Width > finalSize.Width) &&    // We are too wide to fit on this row
                    ((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
                {
                    y = largestY;
                    element.Arrange(new Rect(new Point(0.0, y), element.DesiredSize));
                    x = element.DesiredSize.Width;
                }
                else
                {
                    /* 1) Always place the first item on a row even if width doesn't allow it
                     *      otherwise:
                     * 2) Keep placing on this row until we reach our width constraint
                     *      otherwise:
                     * 3) Keep placing on this row if the max height is reached */

                    element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
                    x += element.DesiredSize.Width;
                }

                largestY = Math.Max(largestY, y + element.DesiredSize.Height);
                largestX = Math.Max(largestX, x);
            });

            arrange(Header);

            foreach (FrameworkElement child in InternalChildren)
            {
                arrange(child);
            }

            arrange(Footer);

            return new Size(largestX, largestY);
        }
    }
}

在XAML中的用法:

<ItemsControl ItemsSource="{Binding SomeItems}" ItemTemplate="{StaticResource SomeTemplate}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <extras:HeaderedFooteredPanel>
                <extras:HeaderedFooteredPanel.Header>
                    <TextBlock Text="Header" />
                </extras:HeaderedFooteredPanel.Header>
                <extras:HeaderedFooteredPanel.Footer>
                    <TextBlock Text="Footer" />
                </extras:HeaderedFooteredPanel.Footer>
            </extras:HeaderedFooteredPanel>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

你在评论里写:

The DrawingContext supplied to the OnRender() method only seems to support very basic rendering commands. Surely you don't have to re-write the rendering code for a standard WPF control, but I am not seeing a way to draw them on my own

如果 "basic" 你的意思是你只能进行 DrawingContext 操作,那么是的。这正是它的用途。这实际上是 WPF 的绘图 API。在更高层次上,您正在处理视觉效果和框架元素,它们隐藏了实际绘图 activity。但是要覆盖此类对象的绘制方式,将需要深入到该级别的绘图,根据需要进行替换或补充。

可能会出现的一个重大困难(除了在该级别处理绘图的更基本困难之外)是在该级别,没有数据模板之类的东西,也无法访问其他元素。您必须从头开始绘制所有内容。这最终会否定使 WPF 如此有用的很大一部分:通过使用内置控件和使您可以控制其外观的属性,对数据在屏幕上的精确表示进行方便而强大的控制。

我很少发现确实需要自定义 Control sub-class。只有当您需要完全控制整个渲染过程、绘制其他任何方式都不可能绘制的东西或提供所需的性能(以牺牲便利性为代价)时,才会出现这种情况。 更经常,甚至几乎所有时间,您想要做的是利用现有控件并让它们为您完成所有繁重的工作。

在这种特殊情况下,我认为解决问题的关键是一种叫做 CompositeCollection 的类型。就像听起来一样,它允许您将集合构建为其他对象的组合,包括其他集合。有了这个,您可以将页眉和页脚数据合并到一个集合中,可以通过 ItemsControl.

显示

在某些情况下,只需创建该集合并将其直接与 ItemsControl 对象一起使用可能就足以满足您的需要。但是,如果您想要一个理解页眉和页脚概念的完整、可重用的用户定义控件,您可以将 ItemsControl 包装在一个 UserControl 对象中,该对象公开您需要的属性,包括 HeaderFooter 属性。这是一个可能看起来像的示例:

XAML:

<UserControl x:Class="TestSO43008469HeaderFooterWrapPanel.HeaderFooterWrapPanel"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:TestSO43008469HeaderFooterWrapPanel"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
  <ItemsControl x:Name="wrapPanel1">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <WrapPanel IsItemsHost="True"/>
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
  </ItemsControl>
</UserControl>

C#:

public partial class HeaderFooterWrapPanel : UserControl
{
    private const int _kheaderIndex = 0;
    private const int _kfooterIndex = 2;

    private readonly CompositeCollection _composedCollection = new CompositeCollection();
    private readonly CollectionContainer _container = new CollectionContainer();

    public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
        "Header", typeof(string), typeof(HeaderFooterWrapPanel),
        new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kheaderIndex)));
    public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(
        "Footer", typeof(string), typeof(HeaderFooterWrapPanel),
         new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kfooterIndex)));
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
        "ItemsSource", typeof(IEnumerable), typeof(HeaderFooterWrapPanel),
        new PropertyMetadata(_OnItemsSourceChanged));

    private static void _OnHeaderFooterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, int index)
    {
        HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;

        panel._composedCollection[index] = e.NewValue;
    }

    private static void _OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;

        panel._container.Collection = panel.ItemsSource;
    }

    public string Header
    {
        get { return (string)GetValue(HeaderProperty); }
        set { SetValue(HeaderProperty, value); }
    }

    public string Footer
    {
        get { return (string)GetValue(FooterProperty); }
        set { SetValue(FooterProperty, value); }
    }

    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public HeaderFooterWrapPanel()
    {
        InitializeComponent();

        _container.Collection = ItemsSource;
        _composedCollection.Add(Header);
        _composedCollection.Add(_container);
        _composedCollection.Add(Footer);
        wrapPanel1.ItemsSource = _composedCollection;
    }
}

当然要注意,如果这样做,您需要 "forward" 您希望能够设置的所有各种控件属性,从 UserControl 对象到ItemsPanel。有些像Background,你可能只设置在UserControl上就可以达到预期的效果,但其他的则专门适用于ItemsControl,如ItemTemplate,[=29] =],等等。你必须弄清楚那些是哪些,并绑定属性,源是 UserControl ,目标是 ItemsControl 里面,在你的 [= 中声明为依赖属性20=] class 任何尚未属于 UserControl 类型的部分。

这里有一个小示例程序,展示了如何使用上面的内容:

XAML:

<Window x:Class="TestSO43008469HeaderFooterWrapPanel.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO43008469HeaderFooterWrapPanel"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        DataContext="{Binding RelativeSource={x:Static RelativeSource.Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Grid.Row="0">
      <TextBlock Text="Header: "/>
      <TextBox Text="{Binding Header, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal" Grid.Row="1">
      <TextBlock Text="Footer: "/>
      <TextBox Text="{Binding Footer, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
    <Button Content="Random List Change" Click="Button_Click" HorizontalAlignment="Left" Grid.Row="2"/>
    <l:HeaderFooterWrapPanel x:Name="headerFooterWrapPanel1" ItemsSource="{Binding Items}"
                             Header="Header Item" Footer="Footer Item" Grid.Row="3">
      <l:HeaderFooterWrapPanel.Resources>
        <DataTemplate DataType="{x:Type s:String}">
          <Border BorderBrush="Black" BorderThickness="1">
            <TextBlock Text="{Binding}" FontSize="16"/>
          </Border>
        </DataTemplate>
      </l:HeaderFooterWrapPanel.Resources>
    </l:HeaderFooterWrapPanel>
  </Grid>
</Window>

为了便于说明,我将 Window.DataContext 属性 设置为 Window 对象本身。这通常不是一个好主意——最好有一个合适的视图模型用作数据上下文——但对于像这样的简单程序来说,这很好。同样,HeaderFooter 属性通常会绑定到某个视图模型 属性,而不是仅仅将一个框架元素的 属性 绑定到另一个

C#:

public partial class MainWindow : Window
{
    public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();

    public MainWindow()
    {
        InitializeComponent();

        Items.Add("Item #1");
        Items.Add("Item #2");
        Items.Add("Item #3");
    }

    private static readonly Random _random = new Random();

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        switch (Items.Count > 0 ? _random.Next(2) : 0)
        {
            case 0: // add
                Items.Insert(_random.Next(Items.Count + 1), $"Item #{_random.Next()}");
                break;
            case 1: // remove
                Items.RemoveAt(_random.Next(Items.Count));
                break;
        }
    }
}