【问题标题】:A custom auto-sizing WPF Panel class自定义自动调整大小的 WPF 面板类
【发布时间】:2011-03-01 11:12:33
【问题描述】:

我正在尝试通过覆盖 MeasureOverrideArrangeOverride 为 WPF 编写自定义 Panel 类,但是,虽然它大部分在工作,但我遇到了一个奇怪的问题,我可以'不解释。

特别是,在我对 ArrangeOverride 中的子项目调用 Arrange 并确定它们的尺寸后,它们的尺寸不符合我给他们的尺寸,而且似乎是尺寸合适在MeasureOverride 中传递给他们的Measure 方法。

我是否错过了这个系统应该如何工作的东西?我的理解是,调用Measure 只会导致孩子根据提供的availableSize 评估其DesiredSize,并且不应影响其实际的最终大小。

这是我的完整代码(面板,顺便说一句,旨在以最节省空间的方式安排子项,为不需要它的行提供更少的空间,并将剩余空间平均分配给其余的行——它目前只支持垂直方向,但我计划在它正常工作后添加水平方向):

编辑:感谢您的回复。我稍后会更仔细地检查它们。不过,让我澄清一下我的预期算法是如何工作的,因为我没有解释。

首先,思考我在做什么的最好方法是想象一个每行设置为 * 的 Grid。这将空间均匀地划分。但是,在某些情况下,一行中的元素可能不需要所有空间;如果是这种情况,我想占用任何剩余空间并将其分配给那些可以使用该空间的行。如果没有行需要任何额外的空间,我只是尝试均匀地间隔(这就是extraSpace 正在做的事情,仅适用于这种情况)。

我分两次执行此操作。第一遍的最终目的是确定行的最终“正常大小”——即将被缩小的行的大小(给定一个小于其所需大小的大小)。为此,我将最小的项目逐步增加到最大,并在每一步调整计算出的正常大小,将每个小项目的剩余空间添加到每个后续较大的项目,直到没有更多项目“适合”,然后打破。

在下一轮中,我使用这个正常值来确定一个项目是否适合,只需将正常尺寸的Min 与项目所需的尺寸相结合。

(为了简单起见,我还将匿名方法更改为 lambda 函数。)

编辑 2: 我的算法似乎在确定孩子的适当大小方面效果很好。然而,孩子们只是不接受他们给定的尺寸。我通过传递 PositiveInfinity 并返回 Size(0,0) 尝试了 Goblin 建议的 MeasureOverride,但这会导致孩子们绘制自己,就好像根本没有空间限制一样。对此不明显的部分是它的发生是因为调用了Measure。微软关于这个主题的文档一点都不清楚,因为我已经多次阅读了每个类和属性描述。但是,现在很明显,调用 Measure 实际上确实会影响子级的渲染,因此我将尝试按照 BladeWise 的建议将逻辑拆分到两个函数中。

解决了!!我搞定了。正如我所怀疑的,我需要对每个孩子调用 Measure() 两次(一次用于评估 DesiredSize,第二次为每个孩子提供适当的高度)。在我看来,WPF 中的布局会以如此奇怪的方式设计,它被分成两个通道,但 Measure 通道实际上做了两件事:测量 大小的子级以及 Arrange 通道除了实际定位孩子之外,几乎什么都不做。很奇怪。

我将在底部发布工作代码。

一、原(坏掉的)代码:

protected override Size MeasureOverride( Size availableSize ) {
    foreach ( UIElement child in Children )
        child.Measure( availableSize );

    return availableSize;
}

protected override System.Windows.Size ArrangeOverride( System.Windows.Size finalSize ) {
    double extraSpace = 0.0;
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>( child=>child.DesiredSize.Height; );
    double remainingSpace = finalSize.Height;
    double normalSpace = 0.0;
    int remainingChildren = Children.Count;
    foreach ( UIElement child in sortedChildren ) {
        normalSpace = remainingSpace / remainingChildren;
        if ( child.DesiredSize.Height < normalSpace ) // if == there would be no point continuing as there would be no remaining space
            remainingSpace -= child.DesiredSize.Height;
        else {
            remainingSpace = 0;
            break;
        }
        remainingChildren--;
    }

    // this is only for cases where every child item fits (i.e. the above loop terminates normally):
    extraSpace = remainingSpace / Children.Count;
    double offset = 0.0;

    foreach ( UIElement child in Children ) {
        //child.Measure( new Size( finalSize.Width, normalSpace ) );
        double value = Math.Min( child.DesiredSize.Height, normalSpace ) + extraSpace;
            child.Arrange( new Rect( 0, offset, finalSize.Width, value ) );
        offset += value;
    }

    return finalSize;
}

这是工作代码:

double _normalSpace = 0.0;
double _extraSpace = 0.0;

protected override Size MeasureOverride( Size availableSize ) {
    // first pass to evaluate DesiredSize given available size:
    foreach ( UIElement child in Children )
        child.Measure( availableSize );

    // now determine the "normal" size:
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>( child => child.DesiredSize.Height );
    double remainingSpace = availableSize.Height;
    int remainingChildren = Children.Count;
    foreach ( UIElement child in sortedChildren ) {
        _normalSpace = remainingSpace / remainingChildren;
        if ( child.DesiredSize.Height < _normalSpace ) // if == there would be no point continuing as there would be no remaining space
            remainingSpace -= child.DesiredSize.Height;
        else {
            remainingSpace = 0;
            break;
        }
        remainingChildren--;
    }
    // there will be extra space if every child fits and the above loop terminates normally:
    _extraSpace = remainingSpace / Children.Count; // divide the remaining space up evenly among all children

    // second pass to give each child its proper available size:
    foreach ( UIElement child in Children )
        child.Measure( new Size( availableSize.Width, _normalSpace ) );

    return availableSize;
}

protected override System.Windows.Size ArrangeOverride( System.Windows.Size finalSize ) {
    double offset = 0.0;

    foreach ( UIElement child in Children ) {
        double value = Math.Min( child.DesiredSize.Height, _normalSpace ) + _extraSpace;
        child.Arrange( new Rect( 0, offset, finalSize.Width, value ) );
        offset += value;
    }

    return finalSize;
}

调用Measure 两次(并迭代Children 三次)可能不是超级高效,但它可以工作。对算法的任何优化将不胜感激。

【问题讨论】:

  • 还有一件事:我不担心在MeasureOverride 中返回availableSize 会出现异常,因为面板在ScrollViewer 这样的无限空间内毫无意义。只有在空间有限时才有意义。
  • 我也有这个问题。 ArrangeOverride 中的任何大小更改都只是剪辑。不是我对阅读文档或任何 WPF 书籍的期望。也曾两次调用Measure来解决这个问题。谢谢。

标签: c# wpf custom-controls panel


【解决方案1】:

我试图简化你的代码:

public class CustomPanel:Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (UIElement child in Children)
            child.Measure(new Size(double.PositiveInfinity,double.PositiveInfinity));

        return new Size(0,0);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        double remainingSpace = Math.Max(0.0,finalSize.Height - Children.Cast<UIElement>().Sum(c => c.DesiredSize.Height));
        var extraSpace = remainingSpace / Children.Count;
        double offset = 0.0;

        foreach (UIElement child in Children)
        {
            double height = child.DesiredSize.Height + extraSpace;
            child.Arrange(new Rect(0, offset, finalSize.Width, height));
            offset += height;
        }

        return finalSize;
    }

}

几点说明:

  • 您不应该在 MeasureOverride 中返回可用大小 - 它可能是正无穷大,这将导致异常。而且由于您基本上不在乎它的大小,因此只需返回 new Size(0,0)。
  • 至于孩子的身高问题 - 我认为这与实际的孩子有关 - 他们的大小是否通过样式或与 Horizo​​ntalAlignment 相关的属性以某种方式受到限制?

编辑:2.0 版:

    public class CustomPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (UIElement child in Children)
            child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

        return new Size(0, 0);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        double optimumHeight = finalSize.Height / Children.Count;
        var smallElements = Children.Cast<UIElement>().Where(c => c.DesiredSize.Height < optimumHeight);
        double leftOverHeight = smallElements.Sum(c => optimumHeight - c.DesiredSize.Height);
        var extraSpaceForBigElements = leftOverHeight / (Children.Count - smallElements.Count());
        double offset = 0.0;

        foreach (UIElement child in Children)
        {
            double height = child.DesiredSize.Height < optimumHeight ? child.DesiredSize.Height : optimumHeight + extraSpaceForBigElements;
            child.Arrange(new Rect(0, offset, finalSize.Width, height));
            offset += height;
        }

        return finalSize;
    }

}

【讨论】:

  • 哦,等等 - 我想我错过了您对面板的意图...您如何确定哪些元素可以获得剩余空间?现在你的代码让他们都比他们想要的高一点。
  • Goblin,您的代码只考虑了一种情况,即所有项目自然适合给定空间并有剩余空间。这只是面板预期用途中的一种边缘情况,它期望所有元素的总高度大于面板的高度,但也期望一些子项小于总空间的 1/n,并且将这个“空白”空间(将出现在相同大小的行中)分配给更高的孩子。
【解决方案2】:

让我们看看我是否理解正确 Panel 应该如何工作:

  • 它应该确定每个 UIElement 孩子的所需大小
  • 根据这些大小,它应该确定是否有一些可用空间
  • 如果存在这样的空间,则应调整每个 UIElement 的大小以填充整个空间(即每个元素的大小将增加一部分剩余空间)

如果我做对了,您当前的实现将无法完成此任务,因为您需要更改子项本身的所需大小,而不仅仅是它们的渲染大小(由 Measure 和 Arrange 通道完成)。

请记住,在给定大小约束(将availableSize 传递给方法)的情况下,Measure pass 用于确定UIElement 需要多少空间。在Panel 的情况下,它也会对其子级调用 Measure 传递,但没有设置其子级的所需大小(换句话说,子级的大小是测量面板的通过)。 至于 Arrange pass,它用于确定最终呈现 UI 元素的矩形,无论测量的大小如何。在Panel 的情况下,它也会对其子级调用 Arrange 传递,但就像测量传递一样,它不会改变子级的所需大小(它只会定义它们的渲染空间) .

要实现所需的行为:

  • 正确拆分 Measure 和 Arrange pass 之间的逻辑(在您的代码中,所有逻辑都在 Arrange pass 中,而用于确定每个孩子需要多少空间的代码应放在 measure pass 中)
  • 使用适当的AttachedProperty(即RequiredHeight)代替所需的孩子大小(您无法控制孩子的大小,除非它设置为Auto,所以有不用拍DesiredSize)

由于我不确定我是否理解面板的目的,所以我写了一个示例:

a. 创建一个新的 Wpf 解决方案 (WpfApplication1) 并添加一个新的类文件 (CustomPanel.cs*)

b. 打开 CustomPanel.cs 文件并粘贴此代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;

namespace WpfApplication1
{
 public class CustomPanel : Panel
 {

  /// <summary>
  /// RequiredHeight Attached Dependency Property
  /// </summary>
  public static readonly DependencyProperty RequiredHeightProperty = DependencyProperty.RegisterAttached("RequiredHeight", typeof(double), typeof(CustomPanel), new FrameworkPropertyMetadata((double)double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(OnRequiredHeightPropertyChanged)));

  private static void OnRequiredHeightPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
  { 

  }

  public static double GetRequiredHeight(DependencyObject d)
  {
   return (double)d.GetValue(RequiredHeightProperty);
  }

  public static void SetRequiredHeight(DependencyObject d, double value)
  {
   d.SetValue(RequiredHeightProperty, value);
  }

  private double m_ExtraSpace = 0;

  private double m_NormalSpace = 0;

  protected override Size MeasureOverride(Size availableSize)
  {
   //Measure the children...
   foreach (UIElement child in Children)
    child.Measure(availableSize);

   //Sort them depending on their desired size...
   var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(new Func<UIElement, double>(delegate(UIElement child)
   {
    return GetRequiredHeight(child);
   }));

   //Compute remaining space...
   double remainingSpace = availableSize.Height;
   m_NormalSpace = 0.0;
   int remainingChildren = Children.Count;
   foreach (UIElement child in sortedChildren)
   {
    m_NormalSpace = remainingSpace / remainingChildren;
    double height = GetRequiredHeight(child);
    if (height < m_NormalSpace) // if == there would be no point continuing as there would be no remaining space
     remainingSpace -= height;
    else
    {
     remainingSpace = 0;
     break;
    }
    remainingChildren--;
   }

   //Dtermine the extra space to add to every child...
   m_ExtraSpace = remainingSpace / Children.Count;
   return Size.Empty;  //The panel should take all the available space...
  }

  protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
  {
   double offset = 0.0;

   foreach (UIElement child in Children)
   {
    double height = GetRequiredHeight(child);
    double value = (double.IsNaN(height) ? m_NormalSpace : Math.Min(height, m_NormalSpace)) + m_ExtraSpace;
    child.Arrange(new Rect(0, offset, finalSize.Width, value));
    offset += value;
   }

   return finalSize;   //The final size is the available size...
  }
 }
}

c.打开项目MainWindow.xaml文件并粘贴这段代码

<Window x:Class="WpfApplication1.MainWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:local="clr-namespace:WpfApplication1"
 Title="MainWindow" Height="350" Width="525">
 <Grid>
        <local:CustomPanel>
            <Rectangle Fill="Blue" local:CustomPanel.RequiredHeight="22"/>
            <Rectangle Fill="Red" local:CustomPanel.RequiredHeight="70"/>
            <Rectangle Fill="Green" local:CustomPanel.RequiredHeight="10"/>
            <Rectangle Fill="Purple" local:CustomPanel.RequiredHeight="5"/>
            <Rectangle Fill="Yellow" local:CustomPanel.RequiredHeight="42"/>
            <Rectangle Fill="Orange" local:CustomPanel.RequiredHeight="41"/>
        </local:CustomPanel>
    </Grid>
</Window>

【讨论】:

  • 感谢您的回复——我认为这是朝着正确方向迈出的一步。考虑到RequiredHeights,它似乎运作良好。但是,由于面板的目的是自动调整大小,因此必须明确地为每一行指定所需的高度,这违背了目的。也就是说,我认为仍然可以自动调整面板大小,但我可能必须对每个孩子调用 Measure() 两次才能做到这一点(首先评估所需的高度,然后实际“告诉”它的高度应该是,因为显然这是必要的)。让我测试一下这个理论......
  • 我会给你答案,因为这个回复对我找到解决方案的帮助最大!
  • 谢谢,我了解您想要实现的目标,但我担心这很棘手,因为如果将子尺寸设置为“自动”,则所需尺寸(很可能)与您传递给 Measure 方法的约束。这意味着,虽然自动调整大小可以适用于大小固定的对象,但自动调整大小的对象的行为将不一致。您是否考虑过使用示例中的RequiredHeight 并通过转换器将其绑定到某个属性(DesiredSize?)的可能性?这样,您可以保持整洁的面板实现,同时尝试实现自动调整大小。