【问题标题】:How to implement a WrapPanel with a header and footer如何实现带有页眉和页脚的 WrapPanel
【发布时间】:2023-04-09 23:55:01
【问题描述】:

我正在尝试实现一个与标准 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>

【问题讨论】:

  • 我在您的代码中看不到任何内容来覆盖OnRender() 方法以实际绘制页眉和页脚项目。请在您上面发布的代码中解释您希望导致这种情况发生的原因是什么?
  • 我怀疑问题是由于这样一个事实,虽然,是的,您正在为使用 Measure 和 Arrange 覆盖的项目腾出空间,但该列表仍然数据绑定到 {Binding SomeItems}。因此,当 WPF 进入放下项目时,它仍然只处理绑定列表中的项目。我可能错了,但是如果您的初始方法不起作用,您可能会考虑另一种方法。如果您使用包含页眉和页脚的可扩展元素包装您的列表怎么办?换句话说,不要将页眉和页脚作为列表的一部分,而是将其作为控件的一部分。
  • @PeterDuniho 提供给 OnRender() 方法的 DrawingContext 似乎只支持非常基本的渲染命令。当然,您不必为标准 WPF 控件重新编写呈现代码,但我没有看到自己绘制它们的方法。正如我在原来的帖子中所说,这是我第一次尝试创建自定义控件,但在找到有关如何创建这样的自定义控件的有用文档或示例方面并不是很成功。
  • @ryancdotnet 我同意问题可能是 WPF 不知道它需要放置页眉和页脚控件,(也许像 PeterDuniho 建议的那样在 OnRender() 方法中不知何故?)。我不确定您不将页眉和页脚作为列表的一部分但仍将其作为控件的一部分是什么意思。这是我的初衷!

标签: c# wpf xaml custom-controls panel


【解决方案1】:

您在 cmets 中写道:

提供给 OnRender() 方法的 DrawingContext 似乎只支持非常基本的渲染命令。当然,您不必为标准 WPF 控件重新编写呈现代码,但我没有看到自己绘制它们的方法

如果“基本”是指您仅限于 DrawingContext 操作,那么可以。这正是它的用途。这实际上是 WPF 的绘图 API。在更高的层次上,您正在处理隐藏实际绘图活动的视觉和框架元素。但是要覆盖此类对象的绘制方式,需要深入到该绘制级别,根据需要替换或补充它。

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

我很少发现真正需要自定义Control 子类。唯一出现这种情况的情况是您需要完全控制整个渲染过程,绘制任何其他方式根本不可能的东西,或者提供所需的性能(以方便为代价)。 更多时候,甚至几乎所有时间,您想要做的是利用现有控件并让它们为您完成所有繁重的工作。

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

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

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;
    }
}

当然,请注意,这样做,您需要“转发”所有您希望能够设置的各种控件属性,从UserControl 对象到ItemsPanel。有些,比如Background,你可能只需要在UserControl上设置就可以达到预期的效果,但其他一些特别适用于ItemsControl,比如ItemTemplateItemTemplateSelector等。你必须找出那些是,并绑定属性,源是UserControl,目标是ItemsControl,在UserControl类中声明任何不属于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;
        }
    }
}

【讨论】:

  • 哇,我做的比它需要的复杂得多。感谢您提供如此完整和翔实的答案!我确实注意到您为 _OnHeaderFooterPropertyChanged() 方法提供的代码中有一个小错误 - 您总是将对象设置为标题!我对该方法的修订代码将 DependencyObjectPropertyChangedEventArgs 传递给该方法,并将内容设置为该参数的 NewValue 属性:panel._composedCollection[index] = e.NewValue;
  • @aswanson:是的,很好。不正确的代码是不完全重构的牺牲品。 :) 你的修复很好;我已经编辑了代码示例以匹配。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 2011-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多