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

时间:2018-05-21 22:34:27

标签: c# wpf textbox grid viewbox

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

Desired result

还有其他周边内容需要扩展,例如图像和按钮(不会在同一个网格中),从我到目前为止所读到的内容,使用Viewbox是可行的方法。

然而,当我将网格包装在一个Stretch =“Uniform”的Viewbox中时,Textbox控制每个网格折叠到它们的最小宽度,如下所示: Viewbox with grid and collapsed textboxes

如果我增加容器宽度,所有内容都会按预期(好)进行缩放,但文本框仍然会折叠到最小可能宽度(错误): Increased width

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

...但我不希望这种行为 - 我希望Textbox元素的宽度与网格列的宽度相关联,而不是依赖于内容。

现在,我已经看过各种各样的SO问题了,这个问题最接近我所追求的问题: How to automatically scale font size for a group of controls?

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

我尝试过各种各样的东西 - 不同的Horizo​​ntalAlignment =“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 个答案:

答案 0 :(得分:4)

此行为的原因是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>

<强>更新

  

我无法理解无限的原始问题   网格宽度。

无限空间方法通常用于确定UIElement的{​​{3}}。简而言之,您可以为控件提供它可能需要的所有空间(无约束),然后测量它以检索其所需的大小。 Viewbox使用此方法衡量其子级,但我们的Grid大小是动态的(代码中未设置HeightWidth),因此Viewbox在网格儿童的另一个级别,看看它是否可以通过获取组件的总和来确定大小。

但是,如果此组件总数超出总可用大小,则可能会遇到问题,如下所示。

DesiredSize

我将文本框替换为标签FooBar,并将其背景颜色设置为灰色。现在我们可以看到Bar正在入侵Body领域,这显然不是我们想要发生的事情。

同样,问题的根源来自Viewbox,不知道如何将无穷大划分为6个相等的份额(映射到列宽1*2*1*2*),所以我们需要做的就是恢复网格宽度的链接。在ToWidthConverter中,目标是将TextBox'Width映射到Grid的{​​{1}} ColumnWidth,因此我使用了2* }。现在gridWidth * 2/6能够再次解决方程:每个Viewbox得到TextBox的三分之一,而gridwidth的一半得到Label vs {{1* 1}})。

当然,当您通过引入新列来加扰时,您必须注意保持组件的总和与总可用宽度同步。换句话说,该等式需要是可解的。 输入数学,所需大小(您没有约束的控件,我们示例中的标签)和转换后的大小(作为2*的部分,我们示例中的文本框)的总和需要是小于或等于可用大小(在我们的示例中为gridWidth)。

如果您使用gridWidth es的转换尺寸,我会发现缩放效果很好,并让星号TextBox处理大多数其他尺寸。请记住保持在总可用尺寸范围内。

添加一些灵活性的一种方法是在混合中添加ColumnWidth

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个相等份额的示例,并相应地将这些份额分配给组件。

gridWidth

请注意每个控件的份额,分组为2 - 2 - 2 - 3 - 1(按钮宽度为1)。

Invade

最后,根据您所使用的可重用性,还有其他一些方法可以解决这个问题:

  • 在根<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> 上设置固定大小。缺点:
    • 每次更换组件时都需要进行微调(以实现 所需的水平/垂直/字体大小比例)
    • 这个比例可能会因不同的主题,Windows版本,...
    • 而中断
  • 添加Grid。正如您在链接的Behavior帖子中的一个答案中所做的那样,而是实现了将列宽映射到FontSize的部分。
  • 按照@ Grx70的建议创建自定义面板。

答案 1 :(得分:1)

您的方法存在的问题是,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>