Question

When I try to change a CroppedBitmap's SourceRect property at runtime, nothing happens. There's no error, and the property value doesn't actually get changed.

I'm trying to do sprite animation. I have a BitmapSource that contains a spritesheet, which is a single bitmap containing a grid of different poses for the sprite. Then I have a CroppedBitmap that has the spritesheet as its Source, and a SourceRect that pulls one of the poses out of the spritesheet. At runtime, when I want to animate, I'm trying to change the CroppedBitmap's SourceRect property, to pull a different pose out of the larger bitmap; but, as noted above, the new property value simply doesn't stick. It's the weirdest thing.

Here's some sample XAML:

<UserControl.Resources>
    <BitmapImage x:Key="spritesheet" UriSource="Sprites/elf.png"/>
</UserControl.Resources>
<Image>
    <Image.Source>
        <CroppedBitmap x:Name="image" Source="{StaticResource spritesheet}"
                       SourceRect="240 640 240 320"/>
    </Image.Source>
</Image>

And the codebehind tries to do this:

var newRect = new Int32Rect(...);
Debug.WriteLine("             Before: " + image.SourceRect);
Debug.WriteLine("Assigning new value: " + newRect);
image.SourceRect = newRect;
Debug.WriteLine("              After: " + image.SourceRect);

That gives me this debug output:

             Before: 240,640,240,320
Assigning new value: 240,0,240,320
              After: 240,640,240,320

So it's actually assigning the new rectangle (with Y=0) into the property; there's no exception; but afterward, the property value simply didn't change (Y is still 640).

Any ideas about why this happens, and how to fix it?

Was it helpful?

Solution

I eventually found the answer. From the documentation for CroppedBitmap:

CroppedBitmap implements the ISupportInitialize interface to optimize initialization on multiple properties. Property changes can occur only during object initialization. Call BeginInit to signal that initialization has begun and EndInit to signal that initialization has completed. After initialization, property changes are ignored. (emphasis mine)

Just for fun, I tried adding BeginInit()..EndInit() calls in my method, to see if that would make it modifiable. Not surprisingly, I got an InvalidOperationException ("Cannot set the initializing state more than once").

So CroppedBitmap is effectively immutable. (But they ignored their own Freezable system, which would have thrown an exception to tell me I was doing something wrong, and implemented something more surprising instead.)

Which means, no-go on changing the SourceRect property. I'll need to create a separate CroppedBitmap instance for each sub-image within the spritesheet.

OTHER TIPS

Here is an alternate way to deal with this:
Instead of using a CroppedBitmap, use the full source image, but:

  1. Set the image.RenderTransform to adjust the viewable area.
  2. Set an Image.Clip if necessary, to avoid showing portions of the image that are unwanted.

This means that you don't need to keep making new CroppedBitmaps, you can just adjust the transform.
In my testing, I saw no difference in speed doing it either way.

For completeness, here's how I'd adjust your code to do what I'm suggesting:

<Image RenderTransform="1, 0, 0, 1, -240, -640">
  <!-- Still include your Image.Source here, just not as a CroppedBitmap -->
  <Image.Clip>
    <RectangleGeometry Rect="0, 0, 240, 320" />
  </Image.Clip>
</Image>

Then the later call to do the equivalent of adjusting the SourceRect is:

image.RenderTransform = new MatrixTransform(1d, 0d, 0d, 1d, -240d, 0d);

Here is a way using IMultiValueConverter:

<Image>
    <Image.Source>
        <MultiBinding Converter="{x:Static local:SourceAndRectToCroppedBitmapConverter.Default}">
            <Binding Path="FileName" />
            <Binding Path="CropRect" />
        </MultiBinding>
    </Image.Source>

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public class SourceAndRectToCroppedBitmapConverter : IMultiValueConverter
{
    public static readonly SourceAndRectToCroppedBitmapConverter Default = new SourceAndRectToCroppedBitmapConverter();

    private static readonly ImageSourceConverter Converter = new ImageSourceConverter();

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] is string text)
        {
            return new CroppedBitmap((BitmapSource)Converter.ConvertFrom(values[0]), (Int32Rect)values[1]);
        }

        return new CroppedBitmap((BitmapSource)values[0], (Int32Rect)values[1]);
    }

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

Potentially poor perf.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top