Wpf Scroll Image using ScrollViewer with dynamic stretching

I am working on a simple imageviewer app. I am controlling the Stretch property on a binding based on the ViewModel property.

The problem occurs when I change the Stretch attribute based on the "Combobox" bound to the ViewModel and the image "crops" the corners of the wide image when using "UniformToFill". Hence to use the ScrollViewer to scroll through the content of the image.

The problem is that the ScrollViewer is not showing scrollbars so that I can scroll.

WPF markup:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

<!-- Other Grids removed -->

<Grid Name="Container" Grid.Column="2" Grid.Row="0" Grid.RowSpan="2">
    <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">                    
        <Image Source="{Binding SelectedPhoto.Value.Image}" 
                Stretch="{Binding ImageStretch}" Name="PhotoImage" />               
    </ScrollViewer>           
</Grid>

      

I understand that if you set a fixed height and width for the ScrollViewer and Image, it works. But I want to do it dynamically:

  • The ScrollView will be the same height and width as the Grid (Contaioner) control.
  • The image will have a height and width from itself, but let's take Stretch to account for this calculation.

Can it be solved using ActualHeight, ActualWidth? And DependecyProperty?

+3


source to share


3 answers


So eventually I discussed with some of the staff and we agreed that we need to fix the problem before fixing it. In other words, replace the Stretch attribute in combination with the scrollviewer with something more robust that will support the ability of the degree.

The solution I came across will work so far and the best solution to the whole problem will be done in the next scrum sprint.


Solution A custom constraint level that will control the width and height based on the stretch attribute present on the element.



<Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>  

<Grid Grid.Column="2" Grid.Row="0" Grid.RowSpan="2">
    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <Image Name="PhotoImage" 
                Source="{Binding SelectedPhoto.Value.Image}" 
                Stretch="{Binding ImageStretch, NotifyOnTargetUpdated=True}}" 
                extensions:ImageExtensions.ChangeWidthHeightDynamically="True"/>
    </ScrollViewer>           
</Grid>

      

Dependency property

public static bool GetChangeWidthHeightDynamically(DependencyObject obj)
{
    return (bool)obj.GetValue(ChangeWidthHeightDynamicallyProperty);
}

public static void SetChangeWidthHeightDynamically(DependencyObject obj, bool value)
{
    obj.SetValue(ChangeWidthHeightDynamicallyProperty, value);
}

public static readonly DependencyProperty ChangeWidthHeightDynamicallyProperty =
    DependencyProperty.RegisterAttached("ChangeWidthHeightDynamically", typeof(bool), typeof(ImageExtensions), new PropertyMetadata(false, OnChangeWidthHeightDynamically));

private static void OnChangeWidthHeightDynamically(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var image = d as Image;
    if (image == null)
        return;

    image.SizeChanged += Image_SizeChanged;
    image.TargetUpdated += Updated;
}

private static void Updated(object sender, DataTransferEventArgs e)
{
    //Reset Width and Height attribute to Auto when Target updates
    Image image = sender as Image;
    if (image == null)
        return;
    image.Width = double.NaN;
    image.Height = double.NaN;
}

private static void Image_SizeChanged(object sender, SizeChangedEventArgs e)
{
    var image = sender as Image;
    if (image == null)
        return;
    image.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
    if (Math.Abs(image.ActualHeight) <= 0 || Math.Abs(image.ActualWidth) <= 0)
        return;

    switch (image.Stretch)
    {
        case Stretch.Uniform:
            {
                image.Width = Double.NaN;
                image.Height = Double.NaN;
                break;
            }
        case Stretch.None:
            {
                image.Width = image.RenderSize.Width;
                image.Height = image.RenderSize.Height;
                break;
            }
        case Stretch.UniformToFill:
            {
                image.Width = image.ActualWidth;
                image.Height = image.ActualHeight;
                break;
            }
        default:
            {
                image.Width = double.NaN;
                image.Height = double.NaN;
                break;
            }
    }
}

      

+1


source


It's almost impossible. Or should I say that it doesn't make sense to expect to ScrollViewer

know the boundaries of an image with a Stretch = UniformToFill

. According to MSDN :

UniformToFill : The content (your image) is resized to fill the dimensions of the destination (window or grid) while maintaining its native aspect ratio. If the aspect ratio of the destination rectangle is different from the source, the source content is cropped to fit the destination (so the image will be disabled).

So, I think we really need to use Uniform + Proper Scaling

instead UniformToFill

.

The solution, when Stretch

set to UniformToFill

, should be set to Uniform

, and then Image.Width = image actual width * scalingParam

and Image.Height= image actual height * scalingParam

where scalingParam = Grid.Width (or Height) / image actual width (or Height)

. Thus, the borders ScrollViewer

will be the same as the size of the scaled image.

I have provided a working solution to give you an Idea, I am not sure how appropriate this is for your case, but here it is:

I first defined a simple view model for my images:

    public class ImageViewModel: INotifyPropertyChanged
    {

    // implementation of INotifyPropertyChanged ...

    private BitmapFrame _bitmapFrame;

    public ImageViewModel(string path, Stretch stretch)
    {
         // determining the actual size of the image.
        _bitmapFrame = BitmapFrame.Create(new Uri(path), BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);

        Width = _bitmapFrame.PixelWidth;
        Height = _bitmapFrame.PixelHeight;
        Scale = 1;
        Stretch = stretch;
    }

    public int Width { get; set; }
    public int Height { get; set; }

    double _scale;
    public double Scale
    {
        get
        {
            return _scale;
        }
        set
        {
            _scale = value;
            OnPropertyChanged("Scale");
        }
    }
    Stretch _stretch;
    public Stretch Stretch
    {
        get
        {
            return _stretch;
        }
        set
        {
            _stretch = value;
            OnPropertyChanged("Stretch");
        }
    }
}

      



The above code is BitmapFrame

used to determine the actual size of the image. Then I did some initializations in my Mainwindow

(or main view model):

    // currently displaying image
    ImageViewModel _imageVm;
    public ImageViewModel ImageVM
    {
        get
        {
            return _imageVm;
        }
        set
        {
            _imageVm = value;
            OnPropertyChanged("ImageVM");
        }
    }

    // currently selected stretch type
    Stretch _stretch;
    public Stretch CurrentStretch
    {
        get
        {
            return _stretch;
        }
        set
        {
            _stretch = value;
            //ImageVM should be notified to refresh UI bindings
            ImageVM.Stretch = _stretch;
            OnPropertyChanged("ImageVM");
            OnPropertyChanged("CurrentStretch");
        }
    }

    // a list of Stretch types
    public List<Stretch> StretchList { get; set; }
    public string ImagePath { get; set; }
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;

        // sample image path
        ImagePath = @"C:\Users\...\YourFile.png";

        StretchList = new List<Stretch>();
        StretchList.Add( Stretch.None);
        StretchList.Add( Stretch.Fill);
        StretchList.Add( Stretch.Uniform);
        StretchList.Add( Stretch.UniformToFill);

        ImageVM = new ImageViewModel(ImagePath, Stretch.None);

        CurrentStretch = StretchList[0];

    }

      

My Xaml looks like this:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid Grid.Row="0" Grid.Column="0" >
        <Grid.Resources>
            <local:MultiConverter x:Key="multiC"/>
        </Grid.Resources>
        <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
            <Image Source="{Binding ImagePath}" Name="PhotoImage">
                <Image.Stretch>
                    <MultiBinding Converter="{StaticResource multiC}">
                        <Binding Path="ImageVM" />
                        <Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="ActualWidth"/>
                        <Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="ActualHeight"/>
                    </MultiBinding>
                </Image.Stretch>
                <Image.LayoutTransform>
                    <ScaleTransform ScaleX="{Binding ImageVM.Scale}" ScaleY="{Binding ImageVM.Scale}"
                        CenterX="0.5" CenterY="0.5" />
            </Image.LayoutTransform>
        </Image>
        </ScrollViewer>
    </Grid>
    <ComboBox Grid.Row="2" Grid.Column="0" ItemsSource="{Binding StretchList}" SelectedItem="{Binding CurrentStretch}" DisplayMemberPath="."/>
</Grid>

      

As you can see, I used a multi-value converter that takes 3 arguments: the current image model and the window width and height. These arguments were used to calculate the current size of the area in which the image is filled. Also I used ScaleTransform

to scale this area to the calculated size. This is the code for a multi-value converter:

public class MultiConverter : IMultiValueConverter
{
    public object Convert(
        object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] is ImageViewModel)
        {
            var imageVm = (ImageViewModel)values[0];

            // if user selects UniformToFill
            if (imageVm.Stretch == Stretch.UniformToFill)
            {
                var windowWidth = (double)values[1];
                var windowHeight = (double)values[2];

                var scaleX = windowWidth / (double)imageVm.Width;
                var scaleY = windowHeight / (double)imageVm.Height;

                // since it "uniform" Max(scaleX, scaleY) is used for scaling in both horizontal and vertical directions
                imageVm.Scale = Math.Max(scaleX, scaleY);

                // "UniformToFill" is actually "Uniform + Proper Scaling"
                return Stretch.Uniform;
            }
            // if user selects other stretch types
            // remove scaling
            imageVm.Scale = 1;
            return imageVm.Stretch;
        }
        return Binding.DoNothing;
    }

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

      

+2


source


The problem can come from the rest of your layout. If the grid is contained in an infinitely resizable container (Grid Column / Row set to Auto

, StackPanel, another ScrollViewer ...) it will grow with the image. And this is what the ScrollViewer will do instead of activating the scrollbars.

0


source







All Articles