3

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

The problem occurs when I change the Stretch attribute based on a 'Combobox', bound to ViewModel, and the image 'cuts off' the corners of a wide image when using 'UniformToFill'. Hence to use of a ScrollViewer to be able to scroll the image content.

The problem is the ScrollViewer doesn't seem to show up scrollbars for me to be able to 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 if I set a fixed Height and Width to ScrollViewer and Image, it will work. But I want to do it Dynamically:

  • The ScrollView Will have Height and Width from Parent 'Grid(Contaioner)' Control.
  • The Image will have Height and Width from itself, but take Stretch to account in that calculation.

Possible to solve with ActualHeight, ActualWidth? And a DependecyProperty?

3 Answers 3

2

This is almost impossible, Or I should say it doesn't make a lot of sense to expect ScrollViewer to know the boundaries of an image with Stretch = UniformToFill. According to MSDN:

UniformToFill: The content (your Image) is resized to fill the destination dimensions (window or grid) while it preserves its native aspect ratio. If the aspect ratio of the destination rectangle differs from the source, the source content is clipped to fit in the destination dimensions (Therefore the image will be cutted off).

So I think what we really need here is to use Uniform + Proper Scaling instead of UniformToFill.

The solution is when Stretch is set to UniformToFill it must 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). This way ScrollViewer boundaries will be the same as the image scaled size.

I've provided a working solution to give you an Idea, I'm not sure how suitable would it be for your case but here it is:

First I 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");
        }
    }
}

In the above code BitmapFrame is 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've used a multi-value converter that takes 3 arguments: current image view-model and window width and height. This arguments were used to calculate current size of the area that image fills. Also I've used ScaleTransform to scale that area to the calculated size. This is the code for 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's "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();
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Very interesting solution. I'm kinda stuck in the swamp of my on thoughts so this is very nice. The framework i'm working in, in-house, prevent me from doing a explicit OnPropertyChanged. specifically to the whole VM. I will take some moments and maybe convert this solution to some mix of what I have and see if that can be a nicer solution.
The solution just gives an idea, the highlights are 1. to use Uniform + proper scaling instead of UniformToFill and 2. calculating the scaling index with the help of BitmapFrame.Create method which gives the actual size of the image.
Yea. I currently working on something that would fit in our framework. Thanks for all the help.
1

So ultimately i took a discussion with some co-workers and we agreed that we need to fix the problem before a fix. In other words replace Stretch attribute combined with scrollviewer with something more robust that will support extent ability.

The solution I came up with will work for now, and a better solution to the whole problem will be preformed next scrum sprint.


Solution A custom dependencyproperty that will control width and height depending on stretch attribute currently present on 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 Comment

Update, As of now we have implementet the zoombox extension from xceed. Works really well. with some minor tweaks from the default settings.
0

The problem may come from the rest of your layout - If the Grid is contained in an infinitely resizable container (a Grid Column/Row set to Auto, a StackPanel, another ScrollViewer...), it will grow with the Image. And so will do the ScrollViewer, instead of activating the scroll bars.

4 Comments

Well yes, The grid is infinitly resizable. And i want to keep it that way. If i somehow can set the width/height depending on Stretch attribute. I think I have a solution going, need some time to edit it before i post.
You want your grid to be infinitely resizable, as in growing past your Window border? :/ Then, why a ScrollViewer? And why would you want to change that behavior depending on the Image's Stretch property?
There's a difference between your grid being dynamically resizeable (stretching to occupy all the available space), and infinitely resizeable (growing freely past its container boundaries). The first would allow your ScrollViewer to work, the second is what you have now and what you wanna avoid.
Ok..Right now the Columndefinition is set to 'Auto' and the RowDefinition to '*' . If i set the width/height on the Image element (if they are larger than the grid size) the scrollviewer works. And as I was saying in OP the user will select stretch attributes from a combobox that will reflect on the image. The 'uniform to fill' attribute will chop of the image. Therefore i have now a dependency property that listens to the SizeChanged event on the image, and will calculate the ActualWidth/height and set it to the element.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.