22

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

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

TextBox除了和之外,这些元素中的每一个都以不同的方式ComboBox垂直放置它们包含的文本,而且看起来很丑陋。

我可以通过为每个元素指定一个来排列这些元素中的文本Margin。这可行,除了边距以像素为单位,与显示器的分辨率或字体大小或任何其他可变因素无关。

我什至不确定如何在运行时计算控件的正确下边距。

最好的方法是什么?

4

7 回答 7

19

问题

因此,据我了解,问题是您希望在 a 中水平布置控件StackPanel并与顶部对齐,但要使每个控件行中的文本对齐。此外,您不想为每个控件设置一些东西:aStyle或 a Margin

基本方法

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

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

自动化流程

现在我们可以应用 aMargin来获得我们的偏移量,但这意味着我们需要在StackPanel.

我们如何实现自动化?不幸的是,很难找到一个防弹的解决方案;可以覆盖控件的模板,这会改变控件中的布局开销。但是,只要我们可以将控件类型(文本框、标签等)与给定的偏移量相关联,就可以制作一个可以节省大量手动对齐工作的控件。

解决方案

您可以采用几种不同的方法,但我认为这是一个布局问题,需要一些自定义 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;
    }
}

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

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

如果控件获得自定义模板,对齐文本所需的数字可能会发生变化,但您不需要在集合中的每个元素上放置不同的Margin或。Style另一个优点是您现在可以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>

对齐顶部示例

AlignTop="False"

对齐底部示例

于 2011-04-29T02:34:46.490 回答
7

这有效,除了边距以像素为单位,与显示器的分辨率或字体大小或任何其他可变因素无关。

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

实际上不是像素

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

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

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

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

因此,您认为边距“不会相对于显示器的分辨率”的假设是不正确的。您可以自己验证这一点,方法是编写 WPF 应用程序,然后切换到 120dpi 或 144dpi 并运行您的应用程序,并观察一切仍然排成一行。您对边距“与显示器分辨率无关”的担忧被证明不是问题。

(在 Windows Vista 中,您可以通过右键单击桌面 > 个性化来切换到 120dpi,然后单击侧栏中的“调整字体大小 (DPI)”链接。我相信这在 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 镶边的粗细不受字体大小的影响。从 ComboBox 顶部到 TextBlock 顶部的距离完全相同,无论您使用的是默认字体大小还是完全极端的字体大小。组合框的内置边距是恒定的。

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

所以是的,使用边距

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

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

答案很简单:将代码模型粘贴到 Kaxaml 中,然后放大(窗口左下角有一个缩放滑块)。如果即使放大后一切仍然排成一行,那你就没事了。

将以下代码粘贴到 Kaxaml 中,然后开始放大,向自己证明边距确实是要走的路。如果红色覆盖在 100% 缩放、125% 缩放 (120dpi) 和 150% 缩放 (144dpi) 时与蓝色标签的顶部对齐,那么您可以确定它适用于任何东西。我已经尝试过了,在 ComboBox 的情况下,我可以告诉你,他们确实为 chrome 使用了整数大小。上边距 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% 时:标签 + 组合框 + 边距,100%
  • 在 125% 时:标签 + 组合框 + 边距,125%
  • 在 150% 时:标签 + 组合框 + 边距,150%

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

于 2011-04-30T16:50:55.627 回答
2

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

于 2011-04-26T18:38:29.323 回答
2

每个 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 的内部统一填充,您可以随时使用它来更改它的左、上、右、下填充大小。

无填充 带填充物

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

于 2011-04-26T17:40:52.067 回答
1

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

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

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

这不仅为用户提供了对其 UI 大小的更细粒度的控制,还意味着无论 UI 大小如何,为使事物正确排列而完成的所有对齐和调整仍然有效。

于 2011-04-26T19:32:23.407 回答
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>
于 2017-04-03T15:15:49.340 回答
0

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

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

于 2009-12-31T08:00:29.617 回答