Making a Viewbox scale vertically but stretch hori

2019-01-11 04:24发布

问题:

I want to make a Viewbox (or something similar) that scales only its height, and then stretches its content horizontally.

If I do this:

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

then I get this:

http://www.excastle.com/misc/viewbox-center.png

It acts as if both of the buttons had HorizontalAlignment="Center", and then scales the result. But I don't want HorizontalAlignment="Center"; I want HorizontalAlignment="Stretch", like this:

http://www.excastle.com/misc/viewbox-stretch.png

So I want it to read its contents' desired height, calculate a scaling factor based only on the height, and then allow the scaled content to stretch horizontally.

Is there any way to accomplish this using the Viewbox, and/or some third-party panel?

回答1:

There is no such control include with WPF but you can write one yourself without too much trouble. Here is a custom ViewboxPanel that has your specifications:

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;
    }
}

and you use it like this:

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

It definitely needs some work but this might get you started.



回答2:

To keep width to work correctly:

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;
    }


回答3:

I'm pretty sure the answer is "not easily".

Here's an idea that seems to work but it's kind of clunky:

  1. Throw your StackPanel into a UserControl (UserControl1 in the example below).

  2. Place two copies of this UserControl into a container (a Grid in the example below).

    a. Align the first copy top/left so that it remains at its default size. This copy is strictly for measurement purposes and should have its Visibility set to Hidden.

    b. Place the second copy inside a Viewbox. This copy is the one the user will actually see.

  3. Use a MultiBinding with a MultiValueConverter to adjust the width of the second UserControl so that it has the correct aspect ratio before it gets expanded by the Viewbox.

Here's the markup:

<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>

Here's the 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();
    }
}

Result for 525 x 350 container



回答4:

I had an almost similar problem. My panel had to fit all its children, placing tham in a row and stretching them, filling the panel uniformly. Algorithm above uses render transform to scale elements. The problem is that render transform stretches children themselves, but ignores margin. If margin is high and scale coefficient is below 1, elements fall out from panel. You have to correct scale for RenderTransform according to margin on that axis and use same scale coefficient for arranging. MeasureOverride I used was

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;
}

Once again: in my project all children are similar in size and margin, algorithm BREAKS otherwise.



标签: wpf viewbox