自定义自动resize的WPF Panel类

我正在尝试通过覆盖MeasureOverrideArrangeOverride为WPF编写自定义Panel类,但是,虽然它主要工作,但我遇到了一个我无法解释的奇怪问题。

特别是,在确定了他们的尺寸应该是什么之后,我在ArrangeOverride调用我的子项目时,它们的大小不是我给它们的大小,并且看起来是在尺寸上调整到尺寸传递到MeasureOverrideMeasure方法。

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

这是我的完整代码(面板,顺便说一句,旨在以最节省空间的方式安排孩子,为不需要它的行提供更少的空间,并在其余部分之间平均分配剩余空间 – 它目前仅支持垂直方向,但我打算一旦我正常工作就加水平):

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

首先,想到我正在做的最好的方法是想象一个Grid,每行设置为*。 这样可以均匀地划分空间。 但是,在某些情况下,行中的元素可能不需要所有空间; 如果是这种情况,我想占用任何剩余空间并将其分配给可以使用该空间的那些行。 如果没有行需要任何额外的空间,我只是尝试均匀地extraSpace事物(这就是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().OrderBy( 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 (ie 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().OrderBy( 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三次),它可能不是超级高效的,但它有效。 可以理解对算法的任何优化。

让我们看看Panel应该如何工作:

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

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

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

要实现所需的行为:

  • 正确分割测量和排列传递之间的逻辑(在您的代码中,所有逻辑都在编配传递中,而用于确定每个子项需要多少空间的代码应放在测量传递中)
  • 使用适当的AttachedProperty (即RequiredHeight )代替所需的子项大小(除非将其设置为Auto ,否则无法控制子项大小,因此无需使用DesiredSize

由于我不确定我是否了解该小组的目的,我写了一个例子:

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

打开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 { ///  /// RequiredHeight Attached Dependency Property ///  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().OrderBy(new Func(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文件并粘贴此代码

             

我尝试简化你的代码:

 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().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中返回可用的大小 – 它可能是正无穷大,这将导致exception。 因为你基本上不在乎它的大小,只需返回新的大小(0,0)。
  • 至于你的孩子身高的问题 – 我认为它与实际的孩子有关 – 他们的尺寸有限,不论是通过Style还是关于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().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; } }