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

Rob*_*ney 20 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>
Run Code Online (Sandbox Code Playgroud)

这些元素中的每一个除了TextBoxComboBox垂直定位它们包含的文本不同,它看起来很丑陋.

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

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

最好的方法是什么?

Ran*_*ngy 19

问题

因此,据我所知,问题是您想要在a中水平布局控件StackPanel并与顶部对齐,但每个控件行中都有文本.此外,您不希望为每个控件设置一些内容:a Style或a Margin.

基本方法

问题的根源是不同的控件在控件的边界和文本内的文本之间具有不同的"开销".当这些控件在顶部对齐时,其中的文本显示在不同的位置.

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

自动化流程

现在我们可以应用a 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我从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>
Run Code Online (Sandbox Code Playgroud)

对齐顶部示例

AlignTop="False":

对齐底部示例


Joe*_*ite 7

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

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

实际上不是像素

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

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

我认为文档的早期版本倾向于将其称为"像素",或者稍后称为"与设备无关的像素".随着时间的推移,他们逐渐意识到这个术语是一个巨大的错误,因为WPF 在物理像素方面实际上并没有做任何事情 - 他们使用这个术语来表示新的东西,但他们的观众认为它意味着什么它一直都有.因此,现在的文档倾向于通过回避对"像素"的任何引用来避免混淆; 他们现在使用"设备无关单元"代替.

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

所以你假设边距"不是[相对于显示器的分辨率"]是不正确的.您可以通过编写WPF应用程序,然后切换到120dpi或144dpi并运行应用程序来自行验证,并观察所有内容仍然排列.您对保证金"与显示器分辨率无关"的担忧结果证明不是问题.

(在Windows Vista中,通过右键单击桌面>个性化,然后单击侧栏中的"调整字体大小(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>
Run Code Online (Sandbox Code Playgroud)

Combobox字体大小不会影响边距

注意ComboBox chrome的厚度不受字体大小的影响.无论您使用的是默认字体大小还是完全极端的字体大小,从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>
Run Code Online (Sandbox Code Playgroud)
  • 100%: 标签+组合框+保证金,100%
  • 在125%: 标签+组合框+保证金,125%
  • 150%: 标签+组合框+保证金,150%

他们总是排队.利润率是要走的路.


Aka*_*ava 0

这很棘手,因为 ComboBox 和 TextBlock 具有不同的内部边距。在这种情况下,我总是把一切都留给了 VerticalAlignment 作为中心,看起来不太好,但确实可以接受。

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