自定义自动调整大小的WPF Panel类

时间:2010-06-09 01:53:31

标签: c# wpf custom-controls panel

我正在尝试通过覆盖PanelMeasureOverride为WPF编写自定义ArrangeOverride类,但是,当它主要正在工作时,我正在体验一个奇怪的问题,我无法解释。

特别是,在确定他们的尺寸应该是什么后,我在Arrange的儿童用品上打ArrangeOverride之后,他们的尺寸不会达到我给他们的尺寸,而且似乎是调整大小到Measure内传递给MeasureOverride方法的大小。

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

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

修改:感谢您的回复。我马上就会仔细研究它们。但是,让我澄清一下我的预期算法是如何工作的,因为我没有解释它。

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

我这两次通过。第一遍的最终要点是确定一行的最终“正常大小” - 即。将缩小的行的大小(给定的大小小于其所需的大小)。我这样做是通过将最小项目逐步调整到最大值并在每一步调整计算出的正常大小,方法是将每个小项目的剩余空间添加到每个后续较大项目中,直到没有更多项目“适合”然后中断。

在下一个传递中,我使用此正常值来确定某个项目是否适合,只需将正常大小的Min与项目所需的大小相匹配。

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

编辑2:我的算法似乎非常适合确定孩子的正确大小。然而,孩子们只是不接受他们给定的尺寸。我通过传递PositiveInfinity并返回Size(0,0)来尝试Goblin的建议MeasureOverride,但是这会让孩子们自己绘制,好像根本没有空间限制。对此不明显的部分是由于调用Measure而发生的事情。微软关于这个主题的文档并不清楚,因为我已经多次阅读过每个类和属性描述。但是,现在很清楚,调用Measure确实影响了孩子的渲染,因此我将尝试将这些逻辑分解为BladeWise建议的两个函数。

解决了!! 我开始工作了。我怀疑,我需要对每个孩子调用两次Measure()(一次评估DesiredSize,一次给每个孩子一个适当的高度)。我似乎很奇怪,WPF中的布局将以如此奇怪的方式设计,其中它被分成两个通道,但是测量通道实际上做了两件事:测量大小的子项和编配通道除了实际上对孩子们进行身体定位外,几乎没有什非常离奇。

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

首先,原始(破碎)代码:

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)可能效率不高,但它有效。任何对算法的优化都将受到赞赏。

2 个答案:

答案 0 :(得分:8)

让我们看看我是否正确Panel应该如何运作:

  • 应确定每个UIElement孩子的理想大小
  • 根据此类尺寸,应确定是否有可用空间
  • 如果存在这样的空间,应调整每个UIElement大小,以便填充整个空间(即每个元素大小将增加一部分剩余空间)

如果我做对了,你当前的实现就无法完成这个任务,因为你需要改变孩子们自己想要的大小,而不仅仅是他们的渲染大小(由测量和编排传递完成)。

请注意,在给定大小约束(传递给方法的UIElement)时,Measure pass用于确定availableSize需要多少空间。如果是Panel,它也会对其子项调用一个Measure传递,但不会设置其子项的所需大小(换句话说,子项的大小是输入对于小组的测量通过)。 对于编配传递,它用于确定最终渲染UI元素的矩形,无论测量的大小如何。如果是Panel,它也会对其子项调用一个编配传递,但就像传递它一样,不会改变所需的子项大小(它只会定义它们的渲染空间)。

要达到所需的行为:

  • 正确分割测量和排列传递之间的逻辑(在您的代码中,所有逻辑都在编配传递中,而用于确定每个子项需要多少空间的代码应放在测量传递中)< / LI>
  • 使用正确的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>

答案 1 :(得分:2)

我试图简化你的代码:

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中返回可用的大小 - 它可能是正无穷大,这将导致异常。因为你基本上不在乎它的大小,只需返回新的大小(0,0)。
  • 关于你孩子身高的问题 - 我认为这与实际的孩子有关 - 他们是否因为StyleAlignment而在某种程度上通过Style或属性限制?

编辑:版本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;
    }

}