如何使在水平StackPanel中排列的元素共享其文本内容的公共基线?

时间:2009-12-30 23:45:12

标签: wpf layout stackpanel

这是我遇到的问题的一个简单例子:

<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>

TextBoxComboBox之外的所有元素中的每一个都垂直放置它们包含的文本,并且看起来很丑陋。

我可以通过为每个元素指定Margin来排列这些元素中的文本。这是有效的,除了边距是以像素为单位,而不是相对于显示器的分辨率或字体大小或任何其他可变的东西。

我甚至不确定如何在运行时为控件计算正确的底部边距。

最好的方法是什么?

7 个答案:

答案 0 :(得分:19)

问题

据我所知,问题在于您希望在StackPanel中水平布局控件并与顶部对齐,但每个控件行中都有文本。此外,您不希望为每个控件设置一些内容:StyleMargin

基本方法

问题的根源在于,不同的控件在控件的边界与其中的文本之间具有不同的“开销”。当这些控件在顶部对齐时,其中的文本显示在不同的位置。

所以我们要做的是应用为每个控件定制的垂直偏移。这适用于所有字体大小和所有DPI:WPF适用于与设备无关的长度测量。

自动化流程

现在我们可以应用Margin来获取偏移量,但这意味着我们需要在StackPanel的每个控件上保持这一点。

我们如何自动化?不幸的是,要获得防弹解决方案是非常困难的。可以覆盖控件的模板,这将改变控件中的布局开销量。但是,只要我们可以将控件类型(TextBox,Label等)与给定的偏移量相关联,就可以制作一个可以节省大量手动对齐工作的控件。

解决方案

您可以采用几种不同的方法,但我认为这是一个布局问题,需要一些自定义的测量和排列逻辑:

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;
    }
}

我从StackPanel的Measure and Arrange方法开始,然后剥离了对滚动和ETW事件的引用,并根据存在的元素类型添加了所需的间距缓冲区。该逻辑仅影响水平堆栈面板。

AlignTop属性控制间距是否使文本与顶部或底部对齐。

如果控件获得自定义模板,则对齐文本所需的数字可能会更改,但您不需要在集合中的每个元素上放置不同的MarginStyle。另一个优点是,您现在可以在子控件上指定Margin,而不会干扰对齐。

结果:

<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

答案 1 :(得分:7)

  

这是有效的,除了边距是以像素为单位,而不是相对于显示器的分辨率或字体大小或任何其他可变的东西。

您的假设不正确。 (我知道,因为我曾经有过相同的假设和相同的担忧。)

实际上不是像素

首先,边距不是像素。(你已经认为我疯了,对吗?)来自FrameworkElement.Margin的文档:

  

厚度测量的默认单位是与设备无关的单位(1/96英寸)。

我认为以前版本的文档倾向于将其称为“像素”,或者稍后称为“与设备无关的像素”。随着时间的推移,他们逐渐意识到这个术语是一个巨大的错误,因为WPF在物理像素方面实际上并没有任何 - 他们使用这个术语来表示新的东西,但是他们的观众认为这意味着它始终拥有的东西。因此,现在的文档倾向于通过回避对“像素”的任何引用来避免混淆;他们现在使用“与设备无关的单位”。

如果计算机的显示设置设置为96dpi(默认Windows设置),则这些与设备无关的设备将与像素一一对应。但是,如果您将显示设置设置为120dpi(在以前版本的Windows中称为“大字体”),则高度=“96”的WPF元素实际上将是120个物理像素高。

因此,您认为保证金“不[相对于显示器的分辨率”]是不正确的。您可以通过编写WPF应用程序,然后切换到120dpi或144dpi并运行您的应用程序来验证这一点,并观察所有内容仍然排队。您担心保证金“与分辨率无关”显示“原来是不成问题。

(在Windows Vista中,通过右键单击桌面&gt;个性化,然后单击侧栏中的“调整字体大小(DPI)”链接切换到120dpi。我相信它在Windows 7中类似。请注意这一点每次更改时都需要重新启动。)

字体大小无关紧要

至于字体大小,这也是一个非问题。这是你如何证明它。将以下XAML粘贴到Kaxaml或任何其他WPF编辑器中:

<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

观察ComboBox镶边的粗细不受字体大小的影响。无论您使用的是默认字体大小还是完全极端的字体大小,从ComboBox顶部到TextBlock顶部的距离都完全相同。 组合框的内置边距是不变的。

如果您使用不同的字体,只要您对标签和ComboBox内容使用相同的字体,以及相同的字体大小,字体样式等,它甚至无关紧要。标签的顶部将排成行如果顶部对齐,那么基线也会如此。

所以是的,使用边距

我知道,这听起来很草率。但是WPF没有内置的基线对齐,而边距是他们为处理这类问题而给我们提供的机制。而且他们这样做是有利可图的。

这是一个提示。当我第一次测试时,我并不相信组合框的镀铬会完全对应于3像素的上边距 - 毕竟,WPF中的许多东西,包括特别是字体大小,都是精确的,非整体的。大小然后捕捉到设备像素 - 我怎么知道由于四舍五入,在120dpi或144dpi屏幕设置下事物不会错位?

答案很简单:将代码的模型粘贴到Kaxaml中,然后放大(窗口左下方有一个缩放滑块)。如果即使你放大了所有内容仍然排队,那么你没事。

将以下代码粘贴到Kaxaml中,然后开始放大,以向自己证明边距确实是要走的路。如果红色覆盖与100%变焦的蓝色标签顶部对齐,并且还有125%变焦(120dpi)和150%变焦(144dpi),那么您可以非常肯定它可以用于任何事情。我试过了,在ComboBox的情况下,我可以告诉你,他们确实使用了整体尺寸的铬。上边距为3将使您的标签每次都与ComboBox文本对齐。

(如果你不想使用Kaxaml,你可以在你的XAML中添加一个临时的ScaleTransform,将它扩展到1.25或1.5,并确保仍然排队。即使你喜欢的XAML编辑器也不行没有缩放功能。)

<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>
  • 100%:Label + combobox + margin, 100 percent
  • 125%:Label + combobox + margin, 125 percent
  • 150%:Label + combobox + margin, 150 percent

他们总是排队。利润率是可行的方式。

答案 2 :(得分:2)

VerticalContentAlignment&amp; Horizo​​ntalContentAlignment,然后为每个子控件指定填充和0的边距。

答案 3 :(得分:1)

每个UIElement都附加了一些内部填充,这对于标签,文本块和任何其他控件都是不同的。我认为为每个控件设置填充都适合你。 **

  

保证金指定相对于的空间   其他可能的UIElement像素   调整大小或任何不一致   其他操作,而填充是   内部的每个UIElement将   在调整大小时不受影响   窗口。

**

 <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>

在这里,我为每个控件提供了一个大小为10的内部均匀填充,您可以随时使用它来改变左,上,右,底部填充尺寸。

Without Padding With Padding

请参阅上面附带的屏幕截图以供参考(1)不使用填充和(2)使用填充 我希望这可能有任何帮助......

答案 4 :(得分:1)

我最终解决这个问题的方法是使用固定大小的边距和填充。

我遇到的真正问题是我让用户更改应用程序中的字体大小。对于从Windows窗体角度解决此问题的人来说,这似乎是一个好主意。但它搞砸了所有的布局;使用12pt文本看起来很好的边距和填充在36pt文本中看起来很糟糕。

从WPF的角度来看,一个更容易(也更好)的方法来实现我真正想要的东西 - 一个用户可根据自己的口味调整大小的用户界面 - 就是只放一个{{1在视图上方,将其ScaleTransformScaleX绑定到滑块的值。

这不仅可以让用户对UI的大小进行更细粒度的控制,而且还意味着无论UI的大小如何,所有对齐和调整以使事情正确排列仍然有效。

答案 5 :(得分:1)

可能会有所帮助:

<Window x:Class="Wpfcrm.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Wpfcrm"
        mc:Ignorable="d"
        Title="Business" Height="600" Width="1000" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="434*"/>
            <ColumnDefinition Width="51*"/>
            <ColumnDefinition Width="510*"/>
        </Grid.ColumnDefinitions>
        <StackPanel x:Name="mainPanel" Orientation="Vertical" Grid.ColumnSpan="3">
            <StackPanel.Background>
                <RadialGradientBrush>
                    <GradientStop Color="Black" Offset="0"/>
                    <GradientStop Color="White"/>
                    <GradientStop Color="White"/>
                </RadialGradientBrush>
            </StackPanel.Background>

            <DataGrid Name="grdUsers" ColumnWidth="*" Margin="0,-20,0,273" Height="272">

            </DataGrid>

        </StackPanel>

        <StackPanel Orientation="Horizontal" Grid.ColumnSpan="3">

            <TextBox Name="txtName" Text="Name" Width="203" Margin="70,262,0,277"/>
            <TextBox x:Name="txtPass" Text="Pass" Width="205" Margin="70,262,0,277"/>
            <TextBox x:Name="txtPosition" Text="Position" Width="205" Margin="70,262,0,277"/>
        </StackPanel>

        <StackPanel Orientation="Vertical" VerticalAlignment="Bottom" Height="217" Grid.ColumnSpan="3" Margin="263,0,297,0">
            <Button Name="btnUpdate" Content="Update" Height="46" FontSize="24" FontWeight="Bold" FontFamily="Comic Sans MS" Margin="82,0,140,0" BorderThickness="1">
                <Button.Background>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="Black"/>
                        <GradientStop Color="#FF19A0AE" Offset="0.551"/>
                    </LinearGradientBrush>
                </Button.Background>


            </Button>
        </StackPanel>

    </Grid>


</Window>

答案 6 :(得分:0)

这很棘手,因为ComboBox和TextBlock具有不同的内部边距。在这种情况下,我总是把所有东西都设置为VerticalAlignment为中心,看起来不是很好但是可以接受。

替代方法是您创建自己的CustomControl派生自ComboBox并在构造函数中初始化其边距并在任何地方重用它。