'Why child control ignores the size set with Arrange in parent container?
I wrote an EditorPanel container, which orders controls in a label/editor order. The complete source of the control follows:
public class EditorPanel : Panel
{
private enum GeneralAlignment
{
Begin,
Center,
End,
Stretch
}
private static GeneralAlignment ToGeneralAlignment(VerticalAlignment verticalAlignment)
{
switch (verticalAlignment)
{
case VerticalAlignment.Top:
return GeneralAlignment.Begin;
case VerticalAlignment.Center:
return GeneralAlignment.Center;
case VerticalAlignment.Bottom:
return GeneralAlignment.End;
case VerticalAlignment.Stretch:
return GeneralAlignment.Stretch;
default:
throw new InvalidEnumArgumentException("Unsupported vertical alignment!");
}
}
private static GeneralAlignment ToGeneralAlignment(HorizontalAlignment horizontalAlignment)
{
switch (horizontalAlignment)
{
case HorizontalAlignment.Left:
return GeneralAlignment.Begin;
case HorizontalAlignment.Center:
return GeneralAlignment.Center;
case HorizontalAlignment.Right:
return GeneralAlignment.End;
case HorizontalAlignment.Stretch:
return GeneralAlignment.Stretch;
default:
throw new InvalidEnumArgumentException("Unsupported horizontal alignment!");
}
}
private Size DesiredSizeWithMargin(UIElement element)
{
if (element == null)
return Size.Empty;
if (element is FrameworkElement frameworkElement)
return new Size(frameworkElement.DesiredSize.Width + frameworkElement.Margin.Left + frameworkElement.Margin.Right,
frameworkElement.DesiredSize.Height + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom);
else
return element.DesiredSize;
}
private static (double elementStart, double elementSize) EvalPlacement(UIElement element,
double placementRectStart,
double placementRectSize,
double elementMarginBegin,
double elementMarginEnd,
double elementDesiredSize,
GeneralAlignment elementAlignment)
{
double resultSize;
double resultStart;
switch (elementAlignment)
{
case GeneralAlignment.Begin:
resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
resultStart = placementRectStart + elementMarginBegin;
break;
case GeneralAlignment.Center:
resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
resultStart = placementRectStart + (placementRectSize - (resultSize + elementMarginBegin + elementMarginEnd)) / 2 + elementMarginBegin;
break;
case GeneralAlignment.End:
resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
resultStart = placementRectStart + placementRectSize - elementMarginEnd - resultSize;
break;
case GeneralAlignment.Stretch:
resultSize = Math.Max(0, placementRectSize - (elementMarginBegin + elementMarginEnd));
resultStart = placementRectStart + elementMarginBegin;
break;
default:
throw new InvalidEnumArgumentException("Unsupported alignment!");
}
return (resultStart, resultSize);
}
private void ArrangeWithAlignment(UIElement element, Rect placementRect, Size cachedDesiredSize)
{
if (cachedDesiredSize == Size.Empty)
cachedDesiredSize = DesiredSizeWithMargin(element);
Thickness elementMargin = new Thickness();
HorizontalAlignment elementHorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment elementVerticalAlignment = VerticalAlignment.Top;
if (element is FrameworkElement frameworkElement)
{
elementMargin = frameworkElement.Margin;
elementHorizontalAlignment = frameworkElement.HorizontalAlignment;
elementVerticalAlignment = frameworkElement.VerticalAlignment;
}
(double elementTop, double elementHeight) = EvalPlacement(element,
placementRect.Top,
placementRect.Height,
elementMargin.Top,
elementMargin.Bottom,
cachedDesiredSize.Height,
ToGeneralAlignment(elementVerticalAlignment));
(double elementLeft, double elementWidth) = EvalPlacement(element,
placementRect.Left,
placementRect.Width,
elementMargin.Left,
elementMargin.Right,
cachedDesiredSize.Width,
ToGeneralAlignment(elementHorizontalAlignment));
element.Arrange(new Rect(elementLeft, elementTop, elementWidth, elementHeight));
}
protected override Size MeasureOverride(Size availableSize)
{
double maxLabelWidth = 0.0;
double maxEditorWidth = 0.0;
double totalLabelEditorPairHeight = 0.0;
for (int i = 0; i < InternalChildren.Count; i += 2)
{
// Measure label
InternalChildren[i].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Size labelDesiredSize = DesiredSizeWithMargin(InternalChildren[i]);
// Measure editor (if any)
Size editorDesiredSize = Size.Empty;
if (i + 1 < InternalChildren.Count)
{
InternalChildren[i + 1].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
editorDesiredSize = DesiredSizeWithMargin(InternalChildren[i + 1]);
}
maxLabelWidth = Math.Max(maxLabelWidth, labelDesiredSize.Width);
maxEditorWidth = Math.Max(maxEditorWidth, editorDesiredSize.Width);
totalLabelEditorPairHeight += Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
}
// This is required height, regardless of how much space is available
double resultHeight = totalLabelEditorPairHeight;
// If space is not constrained, pick as much as labels & editors want. Else, use
// as much, as is given.
double resultWidth = double.IsInfinity(availableSize.Width) ? maxLabelWidth + maxEditorWidth : availableSize.Width;
return new Size(resultWidth, resultHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
// Label area width
double labelAreaWidth = 0;
for (int i = 0; i < InternalChildren.Count; i += 2)
labelAreaWidth = Math.Max(labelAreaWidth, DesiredSizeWithMargin(InternalChildren[i]).Width);
labelAreaWidth = Math.Min(labelAreaWidth, finalSize.Width);
// Editor area width
double editorAreaWidth = Math.Max(0, finalSize.Width - labelAreaWidth);
// Arranging controls
double y = 0;
int controlIndex = 0;
while (controlIndex < InternalChildren.Count)
{
// Retrieve label and editor
UIElement label = InternalChildren[controlIndex++];
Size labelDesiredSize = DesiredSizeWithMargin(label);
UIElement editor = (controlIndex < InternalChildren.Count) ? InternalChildren[controlIndex++] : null;
Size editorDesiredSize = DesiredSizeWithMargin(editor);
double rowHeight = Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
var labelArea = new Rect(0, y, labelAreaWidth, rowHeight);
ArrangeWithAlignment(label, labelArea, label.DesiredSize);
// Arrange editor
if (editor != null)
{
var editorArea = new Rect(labelAreaWidth, y, editorAreaWidth, rowHeight);
ArrangeWithAlignment(editor, editorArea, editor.DesiredSize);
}
y += Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
}
return finalSize;
}
}
Example of usage may look like following:
<Border BorderBrush="Black" BorderThickness="1"
Width="400" Height="100">
<controls:EditorPanel>
<Label>First label</Label>
<TextBox />
<Label>Second label</Label>
<TextBox />
</controls:EditorPanel>
</Border>
When I run the application, it looks like following:
However, if I write more text in the textbox, it starts to escape the boundaries set by the EditorPanel itself.
What is interesting is that the container seems to work properly and allocates proper amount of space for the control:
But the control seems to ignore it completely and ends up bigger than it actually should be:
That leads to my question: why the child control ignores the space it was given through the Arrange call?
Solution 1:[1]
It turns out, that I was too generous during the Measure stage. I put more effort into checking, how much space labels and editors can occupy and now everything works.
protected override Size MeasureOverride(Size availableSize)
{
// Measure labels
List<Size> labelSizes = new List<Size>();
for (int i = 0; i < InternalChildren.Count; i += 2)
{
// Measure label
Thickness labelMargin = new Thickness(0);
if (InternalChildren[i] is FrameworkElement frameworkElement)
labelMargin = frameworkElement.Margin;
InternalChildren[i].Measure(new Size(availableSize.Width - (labelMargin.Left + labelMargin.Right), availableSize.Height - (labelMargin.Top + labelMargin.Bottom)));
labelSizes.Add(DesiredSizeWithMargin(InternalChildren[i]));
}
double maxLabelWidth = labelSizes.Max(ls => ls.Width);
// Measure editors
List<Size> editorSizes = new List<Size>();
for (int i = 1; i < InternalChildren.Count; i += 2)
{
Thickness editorMargin = new Thickness(0);
if (InternalChildren[i] is FrameworkElement frameworkElement)
editorMargin = frameworkElement.Margin;
InternalChildren[i].Measure(new Size(availableSize.Width - maxLabelWidth - (editorMargin.Left + editorMargin.Right), availableSize.Height - (editorMargin.Top + editorMargin.Bottom)));
editorSizes.Add(DesiredSizeWithMargin(InternalChildren[i]));
}
double maxEditorWidth = editorSizes.Max(es => es.Width);
// Equalize count
while (editorSizes.Count < labelSizes.Count)
editorSizes.Add(Size.Empty);
// Evaluate total height
double totalLabelEditorPairHeight = labelSizes.Zip(editorSizes)
.Select(sizes => Math.Max(sizes.First.Height, sizes.Second.Height))
.Sum();
// This is required height, regardless of how much space is available
double resultHeight = totalLabelEditorPairHeight;
// If space is not constrained, pick as much as labels & editors want. Else, use
// as much, as is given.
double resultWidth = double.IsInfinity(availableSize.Width) ? maxLabelWidth + maxEditorWidth : availableSize.Width;
return new Size(resultWidth, resultHeight);
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Spook |




