I'm trying to write a custom Panel
class for WPF, by overriding MeasureOverride
and ArrangeOverride
but, while it's mostly working I'm experiencing one strange problem I can't explain.
In particular, after I call Arrange
on my child items in ArrangeOverride
after figuring out what their sizes should be, they aren't sizing to the size I give to them, and appear to be sizing to the size passed to their Measure
method inside MeasureOverride
.
Am I missing something in how this system is supposed to work? My understanding is that calling Measure
simply causes the child to evaluate its DesiredSize
based on the supplied availableSize, and shouldn't affect its actual final size.
Here is my full code (the Panel, btw, is intended to arrange children in the most space-efficient manner, giving less space to rows that don't need it and splitting remaining space up evenly among the rest--it currently only supports vertical orientation but I plan on adding horizontal once I get it working properly):
Edit: Thanks for the responses. I will examine them more closely in a moment. However let me clarify how my intended algorithm works since I didn't explain that.
First of all, the best way to think of what I'm doing is to imagine a Grid with each row set to *. This divides the space up evenly. However, in some cases the element in a row may not need all that space; if this is the case, I want to take any leftover space and give it to those rows that could use the space. If no rows need any extra space, I just try to space things evenly (that's what extraSpace
is doing, it's only for that case).
I do this in two passes. The ultimate point of the first pass is to determine the final "normal size" of a row--i.e. the size of the rows that will be shrunk (given a size smaller than its desired size). I do this by stepping through smallest item to biggest and adjusting the calculated normal size at each step, by adding the leftover space from each small item to each subsequent larger item until no more items "fit" and then break.
In the next pass I use this normal value to determine if an item can fit or not, simply by taking the Min
of the normal size with the item's desired size.
(I also changed the anonymous method to a lambda function for simplicity.)
Edit 2: My algorithm seems to work great at determining the proper size of the children. However, the children just aren't accepting their given size. I tried Goblin's suggested MeasureOverride
by passing PositiveInfinity and returning Size(0,0), but this causes the children to draw themselves as though there are no space constraints at all. The part that's not obvious about this is that it's happening because of a call to Measure
. Microsoft's documentation on this subject is not at all clear, as I've read over each class and property description several times. However, it's now clear that calling Measure
does in fact affect the rendering of the child, so I will attempt to split the logic up among the two functions as BladeWise suggested.
Solved!! I got it working. As I suspected, I needed to call Measure() twice on each child (once to evaluate DesiredSize and a second to give each child its proper height). It seems odd to me that layout in WPF would be designed in such an odd way, where it's split up into two passes, but the Measure pass actually does two things: measures and sizes children and the Arrange pass does next to nothing besides actually physically position the children. Very bizarre.
I'll post the working code at the bottom.
First, the original (broken) code:
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;
}
And here's the working code:
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;
}
It may not be super-efficient what with having to call Measure
twice (and iterating Children
three times), but it works. Any optimizations to the algorithm would be appreciated.
Let's see if I got right how the Panel
should work:
UIElement
childUIElement
size should be adjusted so that the entire space is filled (i.e. every element size will be incremented by a portion of the remaining space)If I get it right, your current implementation cannot accomplish this task, since you need to change the desired size of the children themselves, not only their the render size (which is done by the Measure and Arrange passes).
Keep in mind that the Measure pass is used to determine how much space an UIElement
would require, given a size constraint (the availableSize
passed to the method). In case of a Panel
, it invokes a Measure pass on its children too, but does not set the desired size of its children (in other words, the size of the children is an input for the measure pass of the panel).
As for the Arrange pass, it is used to determine the rectangle where the UI element will be finally rendered, whatever the measured size. In case of a Panel
, it invokes an Arrange pass on its children too, but just like the measure pass it will not change the desired size of the children (it will just define their render space).
To achieve the required behaviour:
AttachedProperty
(i.e. RequiredHeight) in place of the desired size of the children (you have no control on the child size unless it is set to Auto
, so there is no need to take DesiredSize
)Since I'm not sure I have understood the purpose of the panel, I wrote an example:
a. Create a new Wpf solution (WpfApplication1) and add a new class file (CustomPanel.cs*)
b. Open the CustomPanel.cs file and paste this code
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. Open the project MainWindow.xaml file and paste this code
<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>