使用 Viewbox 缩放包含标签和文本框的网格
Using Viewbox to scale Grid containing Labels and Textboxes
所以我正在尝试构建一个表单,它会根据父容器的可用宽度和相同的列百分比自动按比例上下缩放,如下所示:
周围的其他内容也需要缩放,例如图像和按钮(它们不会在同一个网格中),根据我目前所读的内容,使用 Viewbox 是一种方法去吧。
但是,当我使用 Stretch="Uniform" 将我的网格包裹在一个 Viewbox 中时,Textbox 控制每个折叠到它们的最小宽度,看起来像这样:
如果我增加容器宽度,一切都按预期缩放(好),但文本框仍然折叠到它们的最小可能宽度(坏):
如果我在文本框中键入任何内容,它们会增加宽度以包含文本:
...但我不想要这种行为 - 我希望文本框元素宽度与网格列宽相关联,而不是依赖于内容。
现在,我查看了各种 SO 问题,这个问题最接近我所追求的问题:
How to automatically scale font size for a group of controls?
...但它仍然没有真正专门处理文本框宽度行为(当它与 Viewbox 行为交互时),这似乎是主要问题。
我尝试了多种方法 - 不同的 HorizontalAlignment="Stretch" 设置等等,但到目前为止都没有任何效果。这是我的 XAML:
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Silver" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</StackPanel.Background>
<Viewbox Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="Field A" Grid.Column="0" Grid.Row="0" />
<TextBox Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
<Label Content="Field B" Grid.Column="2" Grid.Row="0" />
<TextBox Grid.Column="3" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
</Grid>
</Viewbox>
<Label Content="Other Stuff"/>
</StackPanel>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Height="100" Width="5"/>
<StackPanel Grid.Column="2">
<Label Content="Body"/>
</StackPanel>
</Grid>
</Window>
此行为的原因是 Viewbox
child 被赋予无限 space 以测量其所需的大小。将 TextBox
es 拉伸到无限 Width
没有多大意义,因为无论如何都无法渲染,因此返回它们的默认大小。
您可以使用转换器来达到所需的效果。
public class ToWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double gridWidth = (double)value;
return gridWidth * 2/6;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
您可以将所有内容都添加到这些资源中。
<Viewbox.Resources>
<local:ToWidthConverter x:Key="ToWidthConverter"/>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Width"
Value="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource ToWidthConverter}}"/>
</Style>
</Viewbox.Resources>
更新
I'm having trouble understanding the original problem of the infinite
grid width.
无限space 方法通常用于确定UIElement
的DesiredSize。简而言之,您为控件提供它可能需要的所有 space(无约束),然后对其进行测量以检索其所需的大小。
Viewbox
使用这种方法来测量它的 child,但是我们的 Grid
的大小是动态的(代码中没有设置 Height
或 Width
),所以 Viewbox
在网格 children 下一个级别,看看它是否可以通过取组件的总和来确定大小。
但是,当组件总和超过可用总大小时,您可能 运行 会遇到问题,如下所示。
我用标签 Foo
和 Bar
替换了文本框,并将它们的背景颜色设置为灰色。现在我们可以看到 Bar
正在入侵 Body
领土,这显然不是我们想要发生的事情。
同样,问题的根源来自 Viewbox
不知道如何将无穷大分成 6 等份(映射到列宽 1*
、2*
、1*
,2*
), 所以我们需要做的就是用网格宽度恢复 link。在 ToWidthConverter
中,目的是将 TextBox
' Width
映射到 2*
的 Grid
s ColumnWidth
,所以我使用了 gridWidth * 2/6
.现在 Viewbox
又可以解方程了:每个 TextBox
得到 gridwidth
的三分之一,每个 Label
得到一半(1*
vs 2*
).
当然,当您通过引入新列来打乱事物时,您必须注意使组件的总和与总可用宽度保持同步。换句话说,方程需要是可解的。 计算所需大小(您未约束的控件的总和,在我们的示例中为标签)和转换后的大小(作为 [=45= 的一部分],在我们的示例中为文本框)需要小于或等于可用大小(在我们的示例中为 gridWidth
)。
我发现如果您对 TextBox
es 使用转换后的大小,并且让星形大小的 ColumnWidth
s 处理大多数其他大小,则缩放效果很好。请记住保持在总可用大小之内。
增加灵活性的一种方法是在组合中添加 ConverterParameter
。
public class PercentageToWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double gridWidth = (double)value;
double percentage = ParsePercentage(parameter);
return gridWidth * percentage;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private double ParsePercentage(object parameter)
{
// I chose to let it fail if parameter isn't in right format
string[] s = ((string)parameter).Split('/');
double percentage = Double.Parse(s[0]) / Double.Parse(s[1]);
return percentage;
}
}
将 gridWidth
平均分配 10 份,并将这些份额相应地分配给组件的示例。
<Viewbox Stretch="Uniform">
<Viewbox.Resources>
<local:PercentageToWidthConverter x:Key="PercentageToWidthConverter"/>
</Viewbox.Resources>
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Label Content="Field A" Grid.Column="0" />
<TextBox Grid.Column="1"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=2/10}" />
<Label Content="Field B" Grid.Column="2" />
<TextBox Grid.Column="3"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=3/10}" />
<Button Content="Ok" Grid.Column="4"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=1/10}" />
</Grid>
</Viewbox>
注意每个控件的份额,分组为 2 - 2 - 2 - 3 - 1(按钮宽度为 1)。
最后,根据您所追求的可重用性,还有其他一些处理方法:
- 在根
Grid
上设置固定大小。缺点:
- 每次更改组件时都需要微调(以实现
所需的水平/垂直/字体大小比例)
- 这个比例可能会因不同的主题、Windows 个版本、...
而中断
- 加一个
Behavior
。正如您 linked FontSize
post 中的一个答案所做的那样,而是实现了将列宽映射到 gridWidth
. 的部分
- 按照@Grx70 的建议创建自定义面板。
您的方法存在的问题是 Grid
与我们直觉上认为的不完全一样。即,star 大小按预期工作 仅 如果满足这些条件:
Grid
的水平对齐设置为 Stretch
Grid
包含在一个有限大小的容器中,即它的 Measure
方法接收到有限 Width
(不是 double.PositiveInfinity
) 的约束
这与列大小有关;行大小是对称的。在您的情况下,不满足第二个条件。我不知道有什么简单的技巧可以使 Grid
像您期望的那样工作,所以我的解决方案是创建自定义的 Panel
来完成这项工作。这样您就可以完全控制控件的布局方式。实现起来并不难,但需要对 WPF 布局系统的工作原理有一定程度的了解。
这是一个执行您的命令的实施示例。为了简洁起见,它只能水平工作,但不难扩展它也能垂直工作。
public class ProportionalPanel : Panel
{
protected override Size MeasureOverride(Size constraint)
{
/* Here we measure all children and return minimal size this panel
* needs to be to show all children without clipping while maintaining
* the desired proportions between them. We should try, but are not
* obliged to, fit into the given constraint (available size) */
var desiredSize = new Size();
if (Children.Count > 0)
{
var children = Children.Cast<UIElement>().ToList();
var weights = children.Select(GetWeight).ToList();
var totalWeight = weights.Sum();
var unitWidth = 0d;
if (totalWeight == 0)
{
//We should handle the situation when all children have weights set
//to 0. One option is to measure them with 0 available space. To do
//so we simply set totalWeight to something other than 0 to avoid
//division by 0 later on.
totalWeight = children.Count;
//We could also assume they are to be arranged uniformly, so we
//simply coerce their weights to 1
for (var i = 0; i < weights.Count; i++)
weights[i] = 1;
}
for (var i = 0; i < children.Count; i++)
{
var child = children[i];
child.Measure(new Size
{
Width = constraint.Width * weights[i] / totalWeight,
Height = constraint.Height
});
desiredSize.Width += child.DesiredSize.Width;
desiredSize.Height =
Math.Max(desiredSize.Height, child.DesiredSize.Height);
if (weights[i] != 0)
unitWidth =
Math.Max(unitWidth, child.DesiredSize.Width / weights[i]);
}
if (double.IsPositiveInfinity(constraint.Width))
{
//If there's unlimited space (e.g. when the panel is nested in a Viewbox
//or a StackPanel) we need to adjust the desired width so that no child
//is given less than desired space while maintaining the desired
//proportions between them
desiredSize.Width = totalWeight * unitWidth;
}
}
return desiredSize;
}
protected override Size ArrangeOverride(Size constraint)
{
/* Here we arrange all children into their places and return the
* actual size this panel is. The constraint will never be smaller
* than the value of DesiredSize property, which is determined in
* the MeasureOverride method. If the desired size is larger than
* the size of parent element, the panel will simply be clipped
* or appear "outside" of the parent element */
var size = new Size();
if (Children.Count > 0)
{
var children = Children.Cast<UIElement>().ToList();
var weights = children.Select(GetWeight).ToList();
var totalWeight = weights.Sum();
if (totalWeight == 0)
{
//We perform same routine as in MeasureOverride
totalWeight = children.Count;
for (var i = 0; i < weights.Count; i++)
weights[i] = 1;
}
var offset = 0d;
for (var i = 0; i < children.Count; i++)
{
var width = constraint.Width * weights[i] / totalWeight;
children[i].Arrange(new Rect
{
X = offset,
Width = width,
Height = constraint.Height,
});
offset += width;
size.Width += children[i].RenderSize.Width;
size.Height = Math.Max(size.Height, children[i].RenderSize.Height);
}
}
return size;
}
public static readonly DependencyProperty WeightProperty =
DependencyProperty.RegisterAttached(
name: "Weight",
propertyType: typeof(double),
ownerType: typeof(ProportionalPanel),
defaultMetadata: new FrameworkPropertyMetadata
{
AffectsParentArrange = true, //because it's set on children and is used
//in parent panel's ArrageOverride method
AffectsParentMeasure = true, //because it's set on children and is used
//in parent panel's MeasuerOverride method
DefaultValue = 1d,
},
validateValueCallback: ValidateWeight);
private static bool ValidateWeight(object value)
{
//We want the value to be not less than 0 and finite
return value is double d
&& d >= 0 //this excludes double.NaN and double.NegativeInfinity
&& !double.IsPositiveInfinity(d);
}
public static double GetWeight(UIElement d)
=> (double)d.GetValue(WeightProperty);
public static void SetWeight(UIElement d, double value)
=> d.SetValue(WeightProperty, value);
}
用法如下:
<local:ProportionalPanel>
<Label Content="Field A" local:ProportionalPanel.Weight="1" />
<TextBox local:ProportionalPanel.Weight="2" />
<Label Content="Field B" local:ProportionalPanel.Weight="1" />
<TextBox local:ProportionalPanel.Weight="2" />
</local:ProportionalPanel>
所以我正在尝试构建一个表单,它会根据父容器的可用宽度和相同的列百分比自动按比例上下缩放,如下所示:
周围的其他内容也需要缩放,例如图像和按钮(它们不会在同一个网格中),根据我目前所读的内容,使用 Viewbox 是一种方法去吧。
但是,当我使用 Stretch="Uniform" 将我的网格包裹在一个 Viewbox 中时,Textbox 控制每个折叠到它们的最小宽度,看起来像这样:
如果我增加容器宽度,一切都按预期缩放(好),但文本框仍然折叠到它们的最小可能宽度(坏):
如果我在文本框中键入任何内容,它们会增加宽度以包含文本:
...但我不想要这种行为 - 我希望文本框元素宽度与网格列宽相关联,而不是依赖于内容。
现在,我查看了各种 SO 问题,这个问题最接近我所追求的问题: How to automatically scale font size for a group of controls?
...但它仍然没有真正专门处理文本框宽度行为(当它与 Viewbox 行为交互时),这似乎是主要问题。
我尝试了多种方法 - 不同的 HorizontalAlignment="Stretch" 设置等等,但到目前为止都没有任何效果。这是我的 XAML:
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Silver" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</StackPanel.Background>
<Viewbox Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="Field A" Grid.Column="0" Grid.Row="0" />
<TextBox Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
<Label Content="Field B" Grid.Column="2" Grid.Row="0" />
<TextBox Grid.Column="3" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
</Grid>
</Viewbox>
<Label Content="Other Stuff"/>
</StackPanel>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Height="100" Width="5"/>
<StackPanel Grid.Column="2">
<Label Content="Body"/>
</StackPanel>
</Grid>
</Window>
此行为的原因是 Viewbox
child 被赋予无限 space 以测量其所需的大小。将 TextBox
es 拉伸到无限 Width
没有多大意义,因为无论如何都无法渲染,因此返回它们的默认大小。
您可以使用转换器来达到所需的效果。
public class ToWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double gridWidth = (double)value;
return gridWidth * 2/6;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
您可以将所有内容都添加到这些资源中。
<Viewbox.Resources>
<local:ToWidthConverter x:Key="ToWidthConverter"/>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Width"
Value="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource ToWidthConverter}}"/>
</Style>
</Viewbox.Resources>
更新
I'm having trouble understanding the original problem of the infinite grid width.
无限space 方法通常用于确定UIElement
的DesiredSize。简而言之,您为控件提供它可能需要的所有 space(无约束),然后对其进行测量以检索其所需的大小。
Viewbox
使用这种方法来测量它的 child,但是我们的 Grid
的大小是动态的(代码中没有设置 Height
或 Width
),所以 Viewbox
在网格 children 下一个级别,看看它是否可以通过取组件的总和来确定大小。
但是,当组件总和超过可用总大小时,您可能 运行 会遇到问题,如下所示。
我用标签 Foo
和 Bar
替换了文本框,并将它们的背景颜色设置为灰色。现在我们可以看到 Bar
正在入侵 Body
领土,这显然不是我们想要发生的事情。
同样,问题的根源来自 Viewbox
不知道如何将无穷大分成 6 等份(映射到列宽 1*
、2*
、1*
,2*
), 所以我们需要做的就是用网格宽度恢复 link。在 ToWidthConverter
中,目的是将 TextBox
' Width
映射到 2*
的 Grid
s ColumnWidth
,所以我使用了 gridWidth * 2/6
.现在 Viewbox
又可以解方程了:每个 TextBox
得到 gridwidth
的三分之一,每个 Label
得到一半(1*
vs 2*
).
当然,当您通过引入新列来打乱事物时,您必须注意使组件的总和与总可用宽度保持同步。换句话说,方程需要是可解的。 计算所需大小(您未约束的控件的总和,在我们的示例中为标签)和转换后的大小(作为 [=45= 的一部分],在我们的示例中为文本框)需要小于或等于可用大小(在我们的示例中为 gridWidth
)。
我发现如果您对 TextBox
es 使用转换后的大小,并且让星形大小的 ColumnWidth
s 处理大多数其他大小,则缩放效果很好。请记住保持在总可用大小之内。
增加灵活性的一种方法是在组合中添加 ConverterParameter
。
public class PercentageToWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double gridWidth = (double)value;
double percentage = ParsePercentage(parameter);
return gridWidth * percentage;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private double ParsePercentage(object parameter)
{
// I chose to let it fail if parameter isn't in right format
string[] s = ((string)parameter).Split('/');
double percentage = Double.Parse(s[0]) / Double.Parse(s[1]);
return percentage;
}
}
将 gridWidth
平均分配 10 份,并将这些份额相应地分配给组件的示例。
<Viewbox Stretch="Uniform">
<Viewbox.Resources>
<local:PercentageToWidthConverter x:Key="PercentageToWidthConverter"/>
</Viewbox.Resources>
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Label Content="Field A" Grid.Column="0" />
<TextBox Grid.Column="1"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=2/10}" />
<Label Content="Field B" Grid.Column="2" />
<TextBox Grid.Column="3"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=3/10}" />
<Button Content="Ok" Grid.Column="4"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=1/10}" />
</Grid>
</Viewbox>
注意每个控件的份额,分组为 2 - 2 - 2 - 3 - 1(按钮宽度为 1)。
最后,根据您所追求的可重用性,还有其他一些处理方法:
- 在根
Grid
上设置固定大小。缺点:- 每次更改组件时都需要微调(以实现 所需的水平/垂直/字体大小比例)
- 这个比例可能会因不同的主题、Windows 个版本、... 而中断
- 加一个
Behavior
。正如您 linkedFontSize
post 中的一个答案所做的那样,而是实现了将列宽映射到gridWidth
. 的部分
- 按照@Grx70 的建议创建自定义面板。
您的方法存在的问题是 Grid
与我们直觉上认为的不完全一样。即,star 大小按预期工作 仅 如果满足这些条件:
Grid
的水平对齐设置为Stretch
Grid
包含在一个有限大小的容器中,即它的Measure
方法接收到有限Width
(不是double.PositiveInfinity
) 的约束
这与列大小有关;行大小是对称的。在您的情况下,不满足第二个条件。我不知道有什么简单的技巧可以使 Grid
像您期望的那样工作,所以我的解决方案是创建自定义的 Panel
来完成这项工作。这样您就可以完全控制控件的布局方式。实现起来并不难,但需要对 WPF 布局系统的工作原理有一定程度的了解。
这是一个执行您的命令的实施示例。为了简洁起见,它只能水平工作,但不难扩展它也能垂直工作。
public class ProportionalPanel : Panel
{
protected override Size MeasureOverride(Size constraint)
{
/* Here we measure all children and return minimal size this panel
* needs to be to show all children without clipping while maintaining
* the desired proportions between them. We should try, but are not
* obliged to, fit into the given constraint (available size) */
var desiredSize = new Size();
if (Children.Count > 0)
{
var children = Children.Cast<UIElement>().ToList();
var weights = children.Select(GetWeight).ToList();
var totalWeight = weights.Sum();
var unitWidth = 0d;
if (totalWeight == 0)
{
//We should handle the situation when all children have weights set
//to 0. One option is to measure them with 0 available space. To do
//so we simply set totalWeight to something other than 0 to avoid
//division by 0 later on.
totalWeight = children.Count;
//We could also assume they are to be arranged uniformly, so we
//simply coerce their weights to 1
for (var i = 0; i < weights.Count; i++)
weights[i] = 1;
}
for (var i = 0; i < children.Count; i++)
{
var child = children[i];
child.Measure(new Size
{
Width = constraint.Width * weights[i] / totalWeight,
Height = constraint.Height
});
desiredSize.Width += child.DesiredSize.Width;
desiredSize.Height =
Math.Max(desiredSize.Height, child.DesiredSize.Height);
if (weights[i] != 0)
unitWidth =
Math.Max(unitWidth, child.DesiredSize.Width / weights[i]);
}
if (double.IsPositiveInfinity(constraint.Width))
{
//If there's unlimited space (e.g. when the panel is nested in a Viewbox
//or a StackPanel) we need to adjust the desired width so that no child
//is given less than desired space while maintaining the desired
//proportions between them
desiredSize.Width = totalWeight * unitWidth;
}
}
return desiredSize;
}
protected override Size ArrangeOverride(Size constraint)
{
/* Here we arrange all children into their places and return the
* actual size this panel is. The constraint will never be smaller
* than the value of DesiredSize property, which is determined in
* the MeasureOverride method. If the desired size is larger than
* the size of parent element, the panel will simply be clipped
* or appear "outside" of the parent element */
var size = new Size();
if (Children.Count > 0)
{
var children = Children.Cast<UIElement>().ToList();
var weights = children.Select(GetWeight).ToList();
var totalWeight = weights.Sum();
if (totalWeight == 0)
{
//We perform same routine as in MeasureOverride
totalWeight = children.Count;
for (var i = 0; i < weights.Count; i++)
weights[i] = 1;
}
var offset = 0d;
for (var i = 0; i < children.Count; i++)
{
var width = constraint.Width * weights[i] / totalWeight;
children[i].Arrange(new Rect
{
X = offset,
Width = width,
Height = constraint.Height,
});
offset += width;
size.Width += children[i].RenderSize.Width;
size.Height = Math.Max(size.Height, children[i].RenderSize.Height);
}
}
return size;
}
public static readonly DependencyProperty WeightProperty =
DependencyProperty.RegisterAttached(
name: "Weight",
propertyType: typeof(double),
ownerType: typeof(ProportionalPanel),
defaultMetadata: new FrameworkPropertyMetadata
{
AffectsParentArrange = true, //because it's set on children and is used
//in parent panel's ArrageOverride method
AffectsParentMeasure = true, //because it's set on children and is used
//in parent panel's MeasuerOverride method
DefaultValue = 1d,
},
validateValueCallback: ValidateWeight);
private static bool ValidateWeight(object value)
{
//We want the value to be not less than 0 and finite
return value is double d
&& d >= 0 //this excludes double.NaN and double.NegativeInfinity
&& !double.IsPositiveInfinity(d);
}
public static double GetWeight(UIElement d)
=> (double)d.GetValue(WeightProperty);
public static void SetWeight(UIElement d, double value)
=> d.SetValue(WeightProperty, value);
}
用法如下:
<local:ProportionalPanel>
<Label Content="Field A" local:ProportionalPanel.Weight="1" />
<TextBox local:ProportionalPanel.Weight="2" />
<Label Content="Field B" local:ProportionalPanel.Weight="1" />
<TextBox local:ProportionalPanel.Weight="2" />
</local:ProportionalPanel>