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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1983134/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 21:22:51  来源:igfitidea点击:

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

wpflayoutstackpanel

提问by Robert Rossney

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 TextBoxand ComboBoxvertically position the text they contain differently, and it looks plain ugly.

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

I can line the text in these elements up by specifying a Marginfor 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.

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

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?

做到这一点的最佳方法是什么?

回答by RandomEngy

The problem

问题

So as I understand it the problem is that you want to lay out controls horizontally in a StackPaneland 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 Styleor a Margin.

因此,据我所知,问题是您想在 a 中水平布置控件StackPanel并与顶部对齐,但将每个控件中的文本排成一行。此外,您不想为每个控件都设置一些东西:aStyle或 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.

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

Automating the process

自动化流程

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

现在我们可以应用 aMargin来获取我们的偏移量,但这意味着我们需要在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.

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

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:

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

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.

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

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

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

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 Marginor Styleon each element in the collection. Another advantage is that you can now specify Marginon the child controls without interfering with the alignment.

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

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

AlignTop="False"

align bottom example

对齐底部示例

回答by Joe White

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:

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

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

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

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

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

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.

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

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.

因此,您认为边距“与显示器的分辨率无关”的假设是不正确的。您可以通过编写 WPF 应用程序自己验证这一点,然后切换到 120dpi 或 144dpi 并运行您的应用程序,并观察一切是否仍然一致。您担心边距“与显示器的分辨率无关”,结果证明不是问题。

(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.)

(在 Windows Vista 中,您可以通过右键单击桌面 > 个性化,然后单击侧栏中的“调整字体大小 (DPI)”链接来切换到 120dpi。我相信这与 Windows 7 中的类似。请注意,这需要每次重新启动到时候改了。)

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 Kaxamlor any other WPF editor:

至于字体大小,这也不是问题。这是你可以证明的方法。将以下 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

组合框字体大小不影响边距

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.

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

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.

使用不同的字体也没有关系,只要标签和ComboBox内容使用相同的字体,相同的字体大小,字体样式等。 标签的顶部会对齐,并且如果顶部对齐,基线也会对齐。

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.

我知道,这听起来很草率。但是 WPF 没有内置的基线对齐,而边距是他们为我们提供的处理此类问题的机制。他们做到了,所以利润率会起作用。

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?

这是一个提示。当我第一次测试这个时,我不相信组合框的 chrome 会与 3 像素的顶部边距完全对应——毕竟,WPF 中的许多东西,包括特别是字体大小,都是以精确的、非整数的方式测量的尺寸,然后捕捉到设备像素——我怎么知道在 120dpi 或 144dpi 屏幕设置下不会由于四舍五入而出现错位?

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.

答案很简单:将代码模型粘贴到 Kaxaml 中,然后放大(窗口左下方有一个缩放滑块)。如果即使放大后一切仍然排列整齐,那么你没问题。

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.

将以下代码粘贴到 Kaxaml 中,然后开始放大,向自己证明边距确实是可行的方法。如果红色叠加在 100% 缩放、125% 缩放 (120dpi) 和 150% 缩放 (144dpi) 时与蓝色标签的顶部对齐,那么您可以非常确定它可以处理任何事情。我已经试过了,在 ComboBox 的情况下,我可以告诉你,他们确实使用了整体尺寸的 chrome。上边距 3 将使您的标签每次都与 ComboBox 文本对齐。

(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.)

(如果您不想使用 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>
  • At 100%: Label + combobox + margin, 100 percent
  • At 125%: Label + combobox + margin, 125 percent
  • At 150%: Label + combobox + margin, 150 percent
  • 100% 时: 标签 + 组合框 + 边距,100%
  • 125% 时: 标签 + 组合框 + 边距,125%
  • 150% 时: 标签 + 组合框 + 边距,150%

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

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

回答by Rohit Vats

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

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

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.

边距以像素为单位指定相对于其他 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>

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.

在这里,我为每个控件提供了一个大小为 10 的内部统一填充,您可以随时使用它来更改它的左、上、右、下填充大小。

Without PaddingWith Padding

无填充带填充

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

请参阅上面附加的屏幕截图以供参考(1)无填充和(2)有填充我希望这可能对您有所帮助...

回答by tcables

VerticalContentAlignment & HorizontalContentAlignment, then specify padding and margin of 0 for each child control.

VerticalContentAlignment & Horizo​​ntalContentAlignment,然后为每个子控件指定 padding 和 margin 为 0。

回答by Robert Rossney

How I ended up solving this was to use fixed-size margins and padding.

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

The real problem that I was having was that I was letting users change the font size within the application. This seemed like a good idea to someone who was coming to this problem from the perspective of Windows Forms. But it screwed up all of the layout; margins and padding that looked just fine with 12pt text looked terrible with 36pt text.

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

From a WPF perspective, though, a much easier (and better) way to accomplish what I was really trying for - an UI whose size the user could adjust to suit his/her taste - was to just put a ScaleTransformover the view, and bind its ScaleXand ScaleYto the value of a slider.

但是,从 WPF 的角度来看,完成我真正想要的东西的更简单(更好)的方法 - 用户可以调整其大小以适合他/她的口味的 UI - 只是ScaleTransform在视图上放置一个,然后绑定它ScaleXScaleY滑块的值。

This not only gives users much more fine-grained control over the size of their UI, it also means that all of the alignment and tweaking done to get things lined up correctly still works irrespective of the size of the UI.

这不仅使用户能够更精细地控制其 UI 的大小,还意味着无论 UI 大小如何,为使内容正确排列所做的所有对齐和调整仍然有效。

回答by Kirill Shur

May be this will help:

可能这会有所帮助:

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

回答by Akash Kava

This is tricky as ComboBox and TextBlock have different internal margins. In such circumstances, I always left everything to have VerticalAlignment as Center that does not look very great but yes quite acceptable.

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

Alternative is you create your own CustomControl derived from ComboBox and initialize its margin in constructor and reuse it everywhere.

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