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

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


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


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.

Your assumptions are incorrect. (I know, because I used to have the same assumptions and the same concerns.)

Not actually pixels

First of all, the margin isn't in pixels. (You already think I'm crazy, right?) From the docs for FrameworkElement.Margin:

The default unit for a Thickness measure is device-independent unit (1/96th inch).

I think previous versions of the documentation tended to call this a "pixel" or, later, a "device-independent pixel". Over time, they've come to realize that this terminology was a huge mistake, because WPF doesn't actually do anything in terms of physical pixels -- they were using the term to mean something new, but their audience was assuming it meant what it always had. So now the docs tend to avoid the confusion by shying away from any reference to "pixels"; they now use "device-independent unit" instead.

If your computer's display settings are set to 96dpi (the default Windows setting), then these device-independent units will correspond one-to-one with pixels. But if you've set your display settings to 120dpi (called "large fonts" in previous versions of Windows), your WPF element with Height="96" will actually be 120 physical pixels high.

So your assumption that the margin will "not [be] relative to the resolution of the display" is incorrect. You can verify this yourself by writing your WPF app, then switching to 120dpi or 144dpi and running your app, and observing that everything still lines up. Your concern that the margin is "not relative to the resolution of the display" turns out to be a non-issue.

(In Windows Vista, you switch to 120dpi by right-clicking the desktop > Personalize, and clicking the "Adjust font size (DPI)" link in the sidebar. I believe it's something similar in Windows 7. Beware that this requires a reboot every time you change it.)

Font size doesn't matter

As for the font size, that's also a non-issue. Here's how you can prove it. Paste the following XAML into Kaxaml or any other WPF editor:

<StackPanel Orientation="Horizontal" VerticalAlignment="Top">    <ComboBox SelectedIndex="0">    <TextBlock Background="Blue">Foo</TextBlock>  </ComboBox>  <ComboBox SelectedIndex="0" FontSize="100pt">    <TextBlock Background="Blue">Foo</TextBlock>  </ComboBox></StackPanel>

Combobox font size does not affect margins

Observe that the thickness of the ComboBox chrome is not affected by the font size. The distance from the top of the ComboBox to the top of the TextBlock is exactly the same, whether you're using the default font size or a totally extreme font size. The combobox's built-in margin is constant.

It doesn't even matter if you use different fonts, as long as you use the same font for both the label and the ComboBox content, and the same font size, font style, etc. The tops of the labels will line up, and if the tops line up, the baselines will too.

So yes, use margins

I know, it sounds sloppy. But WPF doesn't have built-in baseline alignment, and margins are the mechanism they gave us to deal with this sort of problem. And they made it so margins would work.

Here's a tip. When I was first testing this, I wasn't convinced that the combobox's chrome would correspond exactly to a 3-pixel top margin -- after all, many things in WPF, including and especially font sizes, are measured in exact, non-integral sizes and then snapped to device pixels -- how could I know that things wouldn't be misaligned at 120dpi or 144dpi screen settings due to rounding?

The answer turns out to be easy: you paste a mockup of your code into Kaxaml, and then you zoom in (there's a zoom slider bar in the lower left of the window). If everything still lines up even when you're zoomed in, then you're okay.

Paste the following code into Kaxaml, and then start zooming in, to prove to yourself that margins really are the way to go. If the red overlay lines up with the top of the blue labels at 100% zoom, and also at 125% zoom (120dpi) and 150% zoom (144dpi), then you can be pretty sure it'll work with anything. I've tried it, and in the case of ComboBox, I can tell you that they did use an integral size for the chrome. A top margin of 3 will get your label to line up with the ComboBox text every time.

(If you don't want to use Kaxaml, you can just add a temporary ScaleTransform to your XAML to scale it to 1.25 or 1.5, and make sure things still line up. That will work even if your preferred XAML editor doesn't have a zoom feature.)

<Grid>  <StackPanel Orientation="Horizontal" VerticalAlignment="Top">      <TextBlock Background="Blue" VerticalAlignment="Top" Margin="0 3 0 0">Label:</TextBlock>    <ComboBox SelectedIndex="0">      <TextBlock Background="Blue">Combobox</TextBlock>    </ComboBox>  </StackPanel>  <Rectangle Fill="#6F00" Height="3" VerticalAlignment="Top"/></Grid>
  • At 100%: Label + combobox + margin, 100 percent
  • At 125%: Label + combobox + margin, 125 percent
  • At 150%: Label + combobox + margin, 150 percent

They always line up. Margins are the way to go.


Every UIElement have some internal padding attached to it which is different for label,textblock and any other control. I think setting padding for each control will do for you.**

Margin specifies space relative to other UIElement in pixels which may not be consistent on resizing or any other operation whereas padding is internal for each UIElement which will remain unaffected on resizing of window.

**

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

Here, i provide an internal uniform padding of size 10 to every control, you can always play with it to change it with respect to left,top,right,bottom padding sizes.

Without Padding With Padding

See the above attached screenshots for reference (1) Without Padding and (2) With PaddingI hope this might be of any help...