views:

27

answers:

2

I'm trying to do something which seems like it should be extremely simple and yet I can't see how. I have a very simple layout, a TextBox with an image next to it, similar to the way it might look adorned with an ErrorProvider in a WinForms application. The problem is, I want the image to be no higher than the TextBox it's next to. If I lay it out like this, say:

<Grid>  
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="Auto"/>
  </Grid.ColumnDefinitions>
  <TextBox Grid.Row="0" Grid.Column="0" MinWidth="100"/>
  <Image Grid.Row="0" Grid.Column="1" Source="error.png" />
</Grid>

the row will size to the height of the image if the image is taller than the TextBox. This also happens if I use a DockPanel or StackPanel.

The naive solution would be to bind the Height to the TextBox's ActualHeight. I'm sure this is wrong. But what's right?

Edit

Here's an example of what looks wrong to me: In both of these layouts (which are both horizontal StackPanels), the FontSize is the only variable:

Two examples showing unscaled icons

You can see that the first TextBox is constrained to the height of the icon, and as a result has an unnecessary bottom padding under the text. And the icon next to the second is out of scale to the TextBox it's next to.

As it happens, I found a completely different (and much better) way to approach the problem - originally I was scaling my layout by changing the FontSize on the Window, but using a ScaleTransform is a whole lot easier and seems to work perfectly. But even so, it still seems odd to me that it's so hard to do this.

+2  A: 

Name your TextBox, reference the TextBox from the Image as follows.

<TextBox Name="myTextBox" 
         Grid.Row="0" Grid.Column="0" 
         MinWidth="100"/> 
<Image Grid.Row="0" 
       Grid.Column="1" 
       Source="error.png" 
       Height="{Binding ActualHeight, ElementName=myTextBox}"/> 
Zamboni
Well, like I said, this is the naive solution, and I suspect it's wrong - not because it won't work, but because binding to ActualHeight and ActualWidth can cause really noticeable performance problems.
Robert Rossney
I use this idea to keep an image's height the same as the height text in a combobox with good success.
Zamboni
A: 

You want a layout algorithm that measures the other elements with a height constraint equal to the desired height of a specific one. While several of the existing Panel implementations will reduce the available space for remaining elements based on the size used by previous ones, none of them will set the constraint the way you want. If you want the behavior you describe in a single layout pass, you will need to write your own layout algorithm.

For example, you can get close by overriding the behavior of StackPanel like this:

public class SizeToFirstChildStackPanel
    : StackPanel
{
    protected override Size MeasureOverride(Size constraint)
    {
        if (Children.Count > 0)
        {
            var firstChild = Children[0];
            firstChild.Measure(constraint);
            if (Orientation == Orientation.Horizontal)
            {
                constraint = new Size(
                    constraint.Width,
                    Math.Min(firstChild.DesiredSize.Height, constraint.Height));
            }
            else
            {
                constraint = new Size(
                    Math.Min(firstChild.DesiredSize.Width, constraint.Width),
                    constraint.Height);
            }
        }
        return base.MeasureOverride(constraint);
    }
}

This will constrain the height of all children of the StackPanel to the desired height of the first one (or width if the panel is oriented vertically).

It's still not ideal because the first child will get measured a second time by the base class MeasureOverride, and the second measure will have a constraint. This is extra work, and will cause odd behavior if the first child wants to get larger.

To do it right you would need to implement the entire MeasureOverride method method yourself. I'm not going to do that here because it would be a lot of code that isn't really relevant to the problem, and it depends on how exactly you want the layout to work. The important point is to measure the specific element first and use its DesiredSize to determine the availableSize when you call Measure on the others.

Quartermeister