使用 ScrollViewer 和动态拉伸的 Wpf 滚动图像

Wpf Scroll Image using ScrollViewer with dynamic Stretch

我正在开发一个简单的图像查看器应用程序。我根据 ViewModel 属性.

控制绑定上的 Stretch 属性

当我更改基于 'Combobox' 的 Stretch 属性时,绑定到 ViewModel,并且在使用 'UniformToFill' 时图像 'cuts off' 是宽图像的角时出现问题。因此要使用 ScrollViewer 来滚动图像内容。

问题是 ScrollViewer 似乎没有显示滚动条让我能够滚动。

WPF 标记:

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

我知道如果我为 ScrollViewer 和 Image 设置固定的高度和宽度,它会起作用。但我想动态地做:

可以用ActualHeight,ActualWidth来解决吗?还有 DependecyProperty?

问题可能来自布局的其余部分 - 如果 Grid 包含在可无限调整大小的容器中(Grid Column/Row 设置为 Auto、StackPanel、另一个 ScrollViewer... ), 它会随着图像一起成长。 ScrollViewer 也会这样做,而不是激活滚动条。

这几乎是不可能的,或者我应该说期望 ScrollViewer 知道图像与 Stretch = UniformToFill 的边界没有多大意义。根据 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).

所以我认为我们真正需要的是使用Uniform + Proper Scaling而不是UniformToFill

解决方法是当Stretch设置为UniformToFill时必须设置为Uniform然后Image.Width = image actual width * scalingParamImage.Height= image actual height * scalingParam,其中scalingParam = Grid.Width (or Height) / image actual width (or Height).这样 ScrollViewer 边界将与图像缩放后的大小相同。

我已经提供了一个可行的解决方案来给你一个想法,我不确定它是否适合你的情况,但它是:

首先,我为图像定义了一个简单的视图模型:

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

上面代码中BitmapFrame用于判断图片的实际大小。 然后我在我的Mainwindow(或主视图模型)中做了一些初始化:

    // 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];

    }

我的 Xaml 看起来像这样:

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

如您所见,我使用了一个带有 3 个参数的多值转换器:当前图像视图模型和 window 宽度和高度。此参数用于计算图像填充区域的当前大小。我还使用 ScaleTransform 将该区域缩放到计算的大小。这是多值转换器的代码:

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

所以最终我与一些同事进行了讨论,我们同意我们需要先解决问题再修复。换句话说,将 Stretch 属性与 scrollviewer 结合使用更强大的支持扩展能力的东西。

我想出的解决方案暂时可行,下一个 scrum sprint 将针对整个问题制定更好的解决方案。


解决方案 将根据元素上当前存在的拉伸属性控制宽度和高度的自定义依赖属性。

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

依赖关系属性

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