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.