让Viewbox在垂直方向上缩放,但在水平方向上拉伸

19

我想创建一个Viewbox(或类似的东西),该Viewbox仅按比例缩放其高度,然后在水平方向上拉伸其内容。

如果我这样做:

<Viewbox>
  <StackPanel>
    <Button>Foo</Button>
    <Button>Bar</Button>
  </StackPanel>
</Viewbox>

然后我得到了这个:


(来源:excastle.com)

它的表现就像两个按钮都设置了HorizontalAlignment="Center",然后缩放结果。但我不想要HorizontalAlignment="Center";我想要HorizontalAlignment="Stretch",就像这样:


(来源:excastle.com)

所以我希望它读取其内容的期望高度,仅基于高度计算一个缩放因子,然后允许缩放后的内容横向拉伸。

是否有办法使用Viewbox或某些第三方面板来实现这一点?

5个回答

19

在WPF中没有这样的控件,但是您可以自己编写一个而不会遇到太多麻烦。下面是一个自定义的ViewboxPanel,它符合您的规格要求:

public class ViewboxPanel : Panel
{
    private double scale;

    protected override Size MeasureOverride(Size availableSize)
    {
        double height = 0;
        Size unlimitedSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
        foreach (UIElement child in Children)
        {
            child.Measure(unlimitedSize);
            height += child.DesiredSize.Height;
        }
        scale = availableSize.Height / height;

        return availableSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        Transform scaleTransform = new ScaleTransform(scale, scale);
        double height = 0;
        foreach (UIElement child in Children)
        {
            child.RenderTransform = scaleTransform;
            child.Arrange(new Rect(new Point(0, scale * height), new Size(finalSize.Width / scale, child.DesiredSize.Height)));
            height += child.DesiredSize.Height;
        }

        return finalSize;
    }
}

你可以像这样使用它:

<local:ViewboxPanel>
    <Button>Foo</Button>
    <Button>Bar</Button>
</local:ViewboxPanel>

这绝对需要一些工作,但这可能会让你有一个起点。


感谢您提供的出色解决方案。我已经在方法 protected override Size MeasureOverride(Size availableSize) 中添加了以下代码:if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) { availableSize = new Size(200, 200); },这样设计师就可以正常工作而不会抛出异常。 - Mike

4
为了让宽度正常工作,请按以下步骤操作:
protected override Size MeasureOverride(Size availableSize)
    {
        double height = 0;
        Size unlimitedSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
        foreach (UIElement child in Children)
        {
            child.Measure(unlimitedSize);
            height += child.DesiredSize.Height;
        }
        scale = availableSize.Height / height;

        foreach (UIElement child in Children)
        {
            unlimitedSize.Width = availableSize.Width / scale;
            child.Measure(unlimitedSize);
        }           

        return availableSize;
    }

0
除了Rick Sladkey的回答外,在所有元素的摘要高度变得太小且水平方向上发生过度调整以使右侧剪切元素时,这里有一个测量方法的小修复:
protected override Size MeasureOverride(Size availableSize)
    {
        double height = 0;
        Size unlimitedSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
        foreach (UIElement child in Children)
        {
            child.Measure(unlimitedSize);
            height += child.DesiredSize.Height;
        }
        scale = availableSize.Height / height;
        
        foreach (UIElement child in Children)
        {
            var childDesireWidth = child.DesiredSize.Width;
            if (childDesireWidth * scale > availableSize.Width)
                scale = availableSize.Width / childDesireWidth;
        }

        return availableSize;
    }

0

我相信答案是“不容易”。

这里有一个看起来可行但有点笨重的想法:

  1. 将你的 StackPanel 放入一个 UserControl 中(在下面的示例中为 UserControl1)。

  2. 将两个此 UserControl 放入一个容器中(在下面的示例中为 Grid)。

    a. 将第一个副本顶部/左侧对齐,以保持其默认大小。该副本仅用于测量目的,应将其 Visibility 设置为 Hidden。

    b. 将第二个副本放置在 Viewbox 内。这个副本是用户实际上会看到的。

  3. 使用 MultiBinding 和 MultiValueConverter 调整第二个 UserControl 的宽度,以便在被 Viewbox 扩展之前具有正确的纵横比。

以下是标记:

<Grid>
    <local:UserControl1 x:Name="RawControl" HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Hidden" />
    <Viewbox>
        <local:UserControl1>
            <local:UserControl1.Width>
                <MultiBinding Converter="{StaticResource WidthAdjuster}">
                    <Binding ElementName="RawControl" Path="ActualHeight" />
                    <Binding RelativeSource="{RelativeSource AncestorType=Grid}" Path="ActualWidth" />
                    <Binding RelativeSource="{RelativeSource AncestorType=Grid}" Path="ActualHeight" />
                </MultiBinding>
            </local:UserControl1.Width>
        </local:UserControl1>
    </Viewbox>
</Grid>

这是 MultiValueConverter

public class WidthAdjuster : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var rawHeight = (double)values[0];
        var containerWidth = (double)values[1];
        var containerHeight = (double)values[2];
        var ratio = containerWidth / containerHeight;
        return rawHeight * ratio;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

525 x 350 容器的结果

alt text


0

我曾经遇到过类似的问题。我的面板需要适应所有子元素,将它们放在一行中并拉伸它们,使面板均匀填充。上面的算法使用渲染变换来缩放元素。问题在于,渲染变换会拉伸子元素本身,但忽略了边距。如果边距很大且比例系数小于1,则元素会从面板中掉出来。您必须根据该轴上的边距来纠正RenderTransform的比例,并使用相同的比例系数进行排列。我使用的MeasureOverride是

protected override Size MeasureOverride(Size availableSize)
{
    double width = 0; double maxHeight = 0; double mY=0; double mX=0;
    Size unlimitedSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
    foreach (UIElement child in Children)
    {
        child.Measure(unlimitedSize);
        width += child.DesiredSize.Width;
        maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);

        FrameworkElement cld = child as FrameworkElement;
        mY = cld.Margin.Top + cld.Margin.Bottom;
        mX = cld.Margin.Left + cld.Margin.Right;
    }

    double scaleX = availableSize.Width / width;
    double scaleY = availableSize.Height / maxHeight;
    //That is scale for arranging
    positionScaling = Math.Min(scaleX, scaleY); 

    try
    {
        // Let FrameworkElement hight be Xn. mY = Element.Margin.Top + Element.Margin.bottom.
        // DesiredSize includes margin therefore:
        // (Yn + mY) * scaleY = availableSize.Height
        // But render transform doesn't scales margin. Actual render height with margin will be
        // Yn * RenderScaleY + mY = availableSize.Height;
        // We must find render transform scale coeff like this:

        double yn = availableSize.Height / scaleY - mY;
        scaleY = (availableSize.Height - mY) / yn;

        double xn = availableSize.Width / scaleX - mX;
        scaleX = (availableSize.Width - mX) / xn;


        scale = Math.Min(scaleX, scaleY); //scale to use in RenderTransform   
        // In my project all children are similar in size and margin, algorithm BREAKS otherwise!!!
    }
    catch { scale = 1; }

    return availableSize;
}

再强调一遍:在我的项目中,所有子元素的大小和边距都相似,否则算法会出错。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接