代码之家  ›  专栏  ›  技术社区  ›  jhilgeman

使用ViewBox缩放包含标签和文本框的网格

  •  7
  • jhilgeman  · 技术社区  · 6 年前

    因此,我尝试构建一个表单,它将根据父容器的可用宽度和相同的列百分比比例自动上下缩放,如下所示:

    Desired result

    还有其他需要缩放的周边内容,比如图片和按钮(它们不会在同一个网格中),从我目前所读到的内容来看,使用一个视图框是可行的。

    但是,当我用stretch=“uniform”将网格包装在一个视图框中时,文本框控制每个网格向下折叠到其最小宽度,如下所示: Viewbox with grid and collapsed textboxes

    如果我增加容器宽度,则所有内容都按预期缩放(良好),但文本框仍然折叠到其最小可能宽度(糟糕): Increased width

    如果我在文本框中键入任何内容,它们将增加宽度以包含文本: Textboxes expand to fit content

    …但我不希望这种行为-我希望文本框元素宽度与网格列宽绑定,而不是依赖于内容。

    现在,我已经研究了很多这样的问题,这个问题最接近我想要的: How to automatically scale font size for a group of controls?

    …但它仍然没有真正处理文本框宽度行为(当它与viewbox beahvior交互时),这似乎是主要问题。

    我尝试过各种不同的设置——不同的HorizontalLightment=“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>
    
    2 回复  |  直到 6 年前
        1
  •  4
  •   Funk    6 年前

    这种行为的原因是 Viewbox 子对象被赋予无限的空间来测量其所需的大小。拉伸 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>
    

    更新

    我很难理解无穷大的原始问题 网格宽度。

    无限空间法常被用来确定 DesiredSize A的 UIElement . 简而言之,您为控件提供它可能需要的所有空间(没有约束),然后测量它以检索其所需的大小。 可视框 使用此方法测量其子项,但我们的 Grid 是动态的大小(否 Height 宽度 在代码中设置),因此 可视框 在网格子级下一级,查看它是否可以通过取组件的和来确定大小。

    但是,当组件总数超过总可用大小时,您可能会遇到问题,如下所示。

    Invade

    我用标签替换了文本框 Foo Bar 并将背景色设置为灰色。现在我们可以看到 酒吧 正在入侵 Body 领土,这显然不是我们要发生的事情。

    同样,问题的根源来自 可视框 不知道如何将无穷大分成6等份(以映射到列宽 1* , 2* , 1** , 2** ,所以我们需要做的就是用网格宽度恢复链接。在 ToWidthConverter 目的是绘制 文本框 宽度 网格 S ColumnWidth 属于 2** ,所以我用 gridWidth * 2/6 . 现在 可视框 能够再次求解方程:每个 文本框 得到三分之一 gridwidth ,每个 Label 一半的那个( 1** VS 2** )

    当然,当你把事情搞砸的时候,通过引入新的列,你必须注意保持组件和总的可用宽度同步。换句话说,这个方程需要是可解的。 输入math,所需大小(您尚未约束的控件的大小,我们示例中的标签)和转换后的大小(作为 gridWidth ,在我们的示例中,文本框)需要小于或等于可用大小( 网格宽度 在我们的例子中)。

    我发现如果使用转换后的大小 文本框 让星星大小 列宽 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;
        }
    }
    

    一个划分 网格宽度 超过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(ButtonWidth为1)。

    Parts

    最后,根据您所追求的可重用性,处理这一问题的其他一些方法:

    • 在根上设置固定大小 网格 . 缺点:
      • 每次更改组件时都需要进行微调(以实现 所需的水平/垂直/字体大小比率)
      • 这个比例可能会在不同的主题、Windows版本…
    • 添加一个 Behavior . 在你链接的答案之一中完成 FontSize post,但它的实现是将列宽映射到 网格宽度
    • 根据@grx70的建议创建自定义面板。
        2
  •  1
  •   Grx70    6 年前

    你的方法的问题是 Grid 并不是我们凭直觉认为的那样。也就是说, 明星 按预期调整工程尺寸 只有 如果满足这些条件:

    • 这个 网格 其水平对齐设置为 Stretch
    • 这个 网格 包含在有限尺寸的容器中,即 Measure 方法接收有限约束 Width (不是) double.PositiveInfinity )

    这与列大小有关;行大小是对称的。在您的情况下,第二个条件不满足。我不知道该怎么做 网格 按您的期望工作,所以我的解决方案是创建自定义 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>