这是我遇到的问题的一个简单例子:
<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
来排列这些元素中的文本。这是有效的,除了边距是以像素为单位,而不是相对于显示器的分辨率或字体大小或任何其他可变的东西。
我甚至不确定如何在运行时为控件计算正确的底部边距。
最好的方法是什么?
答案 0 :(得分:19)
据我所知,问题在于您希望在StackPanel
中水平布局控件并与顶部对齐,但每个控件行中都有文本。此外,您不希望为每个控件设置一些内容:Style
或Margin
。
问题的根源在于,不同的控件在控件的边界与其中的文本之间具有不同的“开销”。当这些控件在顶部对齐时,其中的文本显示在不同的位置。
所以我们要做的是应用为每个控件定制的垂直偏移。这适用于所有字体大小和所有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
属性控制间距是否使文本与顶部或底部对齐。
如果控件获得自定义模板,则对齐文本所需的数字可能会更改,但您不需要在集合中的每个元素上放置不同的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"
:
答案 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镶边的粗细不受字体大小的影响。无论您使用的是默认字体大小还是完全极端的字体大小,从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>
他们总是排队。利润率是可行的方式。
答案 2 :(得分:2)
VerticalContentAlignment&amp; HorizontalContentAlignment,然后为每个子控件指定填充和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的内部均匀填充,您可以随时使用它来改变左,上,右,底部填充尺寸。
请参阅上面附带的屏幕截图以供参考(1)不使用填充和(2)使用填充 我希望这可能有任何帮助......
答案 4 :(得分:1)
我最终解决这个问题的方法是使用固定大小的边距和填充。
我遇到的真正问题是我让用户更改应用程序中的字体大小。对于从Windows窗体角度解决此问题的人来说,这似乎是一个好主意。但它搞砸了所有的布局;使用12pt文本看起来很好的边距和填充在36pt文本中看起来很糟糕。
从WPF的角度来看,一个更容易(也更好)的方法来实现我真正想要的东西 - 一个用户可根据自己的口味调整大小的用户界面 - 就是只放一个{{1在视图上方,将其ScaleTransform
和ScaleX
绑定到滑块的值。
这不仅可以让用户对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并在构造函数中初始化其边距并在任何地方重用它。