Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make elements arranged in a horizontal StackPanel share a common baseline for their text content?

Tags:

Here's a trivial example of the problem I'm having:

<StackPanel Orientation="Horizontal">     <Label>Foo</Label>     <TextBox>Bar</TextBox>     <ComboBox>         <TextBlock>Baz</TextBlock>         <TextBlock>Bat</TextBlock>     </ComboBox>     <TextBlock>Plugh</TextBlock>     <TextBlock VerticalAlignment="Bottom">XYZZY</TextBlock> </StackPanel> 

Every one of those elements except the TextBox and ComboBox vertically position the text they contain differently, and it looks plain ugly.

I can line the text in these elements up by specifying a Margin for each. That works, except that the margin is in pixels, and not relative to the resolution of the display or the font size or any of the other things that are going to be variable.

I'm not even sure how I'd calculate the correct bottom margin for a control at runtime.

What's the best way to do this?

like image 279
Robert Rossney Avatar asked Dec 30 '09 23:12

Robert Rossney


People also ask

Which panel would be most appropriate to display child elements in a single line either horizontally or vertically?

WrapPanel. WrapPanel is used to position child elements in sequential position from left to right, breaking content to the next line when it reaches the edge of its parent container. Content can be oriented horizontally or vertically. WrapPanel is useful for simple flowing user interface (UI) scenarios.

What is StackPanel?

StackPanel is a layout panel that arranges child elements into a single line that can be oriented horizontally or vertically. By default, StackPanel stacks items vertically from top to bottom in the order they are declared. You can set the Orientation property to Horizontal to stack items from left to right.

How do you add a space between two controls in WPF?

If you do not use (for some reason) Button's Margin property, you can put transparent Separator (Transparent background color) with desired Width (or/and Height) between your controls (Buttons in your case).


1 Answers

The problem

So as I understand it the problem is that you want to lay out controls horizontally in a StackPanel and align to the top, but have the text in each control line up. Additionally, you don't want to have to set something for every control: either a Style or a Margin.

The basic approach

The root of the problem is that different controls have different amounts of "overhead" between the boundary of the control and the text within. When these controls are aligned at the top, the text within appears in different locations.

So what we want to do is apply an vertical offset that's customized to each control. This should work for all font sizes and all DPIs: WPF works in device-independent measures of length.

Automating the process

Now we can apply a Margin to get our offset, but that means we need to maintain this on every control in the StackPanel.

How do we automate this? Unfortunately it would be very difficult to get a bulletproof solution; it's possible to override a control's template, which would change the amount of layout overhead in the control. But it's possible to cook up a control that can save a lot of manual alignment work, as long as we can associate a control type (TextBox, Label, etc) with a given offset.

The solution

There are several different approaches you could take, but I think that this is a layout problem and needs some custom Measure and Arrange logic:

public class AlignStackPanel : StackPanel {     public bool AlignTop { get; set; }      protected override Size MeasureOverride(Size constraint)     {         Size stackDesiredSize = new Size();          UIElementCollection children = InternalChildren;         Size layoutSlotSize = constraint;         bool fHorizontal = (Orientation == Orientation.Horizontal);          if (fHorizontal)         {             layoutSlotSize.Width = Double.PositiveInfinity;         }         else         {             layoutSlotSize.Height = Double.PositiveInfinity;         }          for (int i = 0, count = children.Count; i < count; ++i)         {             // Get next child.             UIElement child = children[i];              if (child == null) { continue; }              // Accumulate child size.             if (fHorizontal)             {                 // Find the offset needed to line up the text and give the child a little less room.                 double offset = GetStackElementOffset(child);                 child.Measure(new Size(Double.PositiveInfinity, constraint.Height - offset));                 Size childDesiredSize = child.DesiredSize;                  stackDesiredSize.Width += childDesiredSize.Width;                 stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height + GetStackElementOffset(child));             }             else             {                 child.Measure(layoutSlotSize);                 Size childDesiredSize = child.DesiredSize;                  stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);                 stackDesiredSize.Height += childDesiredSize.Height;             }         }          return stackDesiredSize;      }      protected override Size ArrangeOverride(Size arrangeSize)     {         UIElementCollection children = this.Children;         bool fHorizontal = (Orientation == Orientation.Horizontal);         Rect rcChild = new Rect(arrangeSize);         double previousChildSize = 0.0;          for (int i = 0, count = children.Count; i < count; ++i)         {             UIElement child = children[i];              if (child == null) { continue; }              if (fHorizontal)             {                 double offset = GetStackElementOffset(child);                  if (this.AlignTop)                 {                     rcChild.Y = offset;                 }                  rcChild.X += previousChildSize;                 previousChildSize = child.DesiredSize.Width;                 rcChild.Width = previousChildSize;                 rcChild.Height = Math.Max(arrangeSize.Height - offset, child.DesiredSize.Height);             }             else             {                 rcChild.Y += previousChildSize;                 previousChildSize = child.DesiredSize.Height;                 rcChild.Height = previousChildSize;                 rcChild.Width = Math.Max(arrangeSize.Width, child.DesiredSize.Width);             }              child.Arrange(rcChild);         }          return arrangeSize;     }      private static double GetStackElementOffset(UIElement stackElement)     {         if (stackElement is TextBlock)         {             return 5;         }          if (stackElement is Label)         {             return 0;         }          if (stackElement is TextBox)         {             return 2;         }          if (stackElement is ComboBox)         {             return 2;         }          return 0;     } } 

I started from the StackPanel's Measure and Arrange methods, then stripped out references to scrolling and ETW events and added the spacing buffer needed based on the type of element present. The logic only affects horizontal stack panels.

The AlignTop property controls whether the spacing will make text align to the top or bottom.

The numbers needed to align the text may change if the controls get a custom template, but you don't need to put a different Margin or Style on each element in the collection. Another advantage is that you can now specify Margin on the child controls without interfering with the alignment.

Results:

<local:AlignStackPanel Orientation="Horizontal" AlignTop="True" >     <Label>Foo</Label>     <TextBox>Bar</TextBox>     <ComboBox SelectedIndex="0">         <TextBlock>Baz</TextBlock>         <TextBlock>Bat</TextBlock>     </ComboBox>     <TextBlock>Plugh</TextBlock> </local:AlignStackPanel> 

align top example

AlignTop="False":

align bottom example

like image 51
RandomEngy Avatar answered Sep 23 '22 07:09

RandomEngy