Trying to change a CroppedBitmap's SourceRect at runtime - wpf

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?

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.

Here is an alternate way to deal with this:
Instead of using a CroppedBitmap, use the full source image, but:
Set the image.RenderTransform to adjust the viewable area.
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.

Related

Is it possible to concatenate an imagesourece Uri in xaml

Hi I've been looking at ways to dynamically change an image Uri in xaml and in my research came across the following answer, which has certainly given me hope that what I really want to do might just be possible. In the original question the questioner wanted to swap the image itself, in my case I want to swap the directory where the image is located.
So when one looks at the answer that #Clemens provided one ends up with an images source being bound to a dependency property that is dynamically set when the form loads.
What I'd like to know is whether it would be feasible to set the location part of the uri dynamically (as per the logic that #Clemens is advocating and then simply append the Image name in the actual binding statement so that it might look something like this:
<Image Source="{Binding ImageUri & myImage.png}"/>
Essentially I have a number of buttons to which I would like to assign a default image og a size to be determined by the end user. To that end the Images would be stored in different folders in the application (in fact its a custom control) and then the relevant path bit of the URI would be set as per the suggestion in the referenced answer and I'd just append the Image name (which would be the same for the button irrespective of the size) and it would then have the correct image to display.
MainWindow.xaml.cs :
namespace MainWindowNamespace
{
public sealed class ImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
string fullPath = Path.GetFullPath((string)value);
return new BitmapImage(new Uri(fullPath));
}
catch { return null; }
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ throw new NotImplementedException(); }
}
}
MainWindow.xaml :
<Window
xmlns:imgConvert="clr-namespace:MainWindowNamespace">
<Window.Resources>
<imgConvert:ImageConverter x:Key="ImageConverter" />
</Window.Resources>
<Image ImageSource="{Binding ImagePath, Converter={StaticResource ImageConverter}}" />
</Window>

Using resources as conversion results in a bind converter

When I try to bind a valueconverter from a defined enum Status to brush, I get an error in my XAML designer:
'OKStatus' resource not found.
The application works fine runtime, but I'm not able to see my GUI in the designer.
My resources are defined in the color.xaml file, which is read at run time.
All code is within the same namespace
My XAML:
xmlns:config="clr-namespace:App.MyNamespace"
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="c:\Skins\Colors.xaml" />
<ResourceDictionary Source="c:\Skins\Common.xaml" />
</ResourceDictionary.MergedDictionaries>
<config:StatusConverter x:Key="StateConverter" />
<config:BoolConverter x:Key="BoolConverter" />
<config:BooleanConverter x:Key="BooleanConverter" />
</ResourceDictionary>
</UserControl.Resources>
and
Status
My converter:
[ValueConversion(typeof(bool), typeof(Brush))]
public class BoolConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
bool state = (bool)value;
FrameworkElement FrameElem = new FrameworkElement();
if (state == true)
return (FrameElem.FindResource("OKStatus") as Brush);
else
return (FrameElem.FindResource("ErrorStatus") as Brush);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
In this code the frameElem wont have any knowledge of the resources I have defined I guess, so I need a way to get access to my resources during design.
Is this possible?
Yes, it is possible, and your guess is correct. Resource finding starts with the logical tree, and creating a new FrameworkElement() doesn't satisfy this. It's completely disconnected.
What you can do (and what you may have to do if N8's suggestion doesn't work), is to hand your converter a reference to the UserControl as the FrameworkElement to call FindResource() on.
The reason N8's suggestion probably won't work is that Application.Current.FindResource() probably starts at application-level resources and then goes to system resources, but the resources you're after are in the UserControl's resources. If they were placed in App.xaml's resources, it would work. However, I think Application.Current may be null at design-time.
The easiest way I can think of to do this is in your UserControl's constructor:
public MyUserControl(){
var boolconv = new BoolConverter();
boolconv.FrameworkElement = this;
this.Resources.Add( "BoolConverter", boolconv );
InitializeComponent();
}
I'm pretty sure it goes before InitializeComponent(), rather than after.
Doing this in XAML would be more complicated, as you probably have to add a DependencyProperty to your converter so that you could bind the UserControl to it. I think that would be going overboard.
Another way is to put TrueBrush and FalseBrush properties on your converter and assign them in XAML, which is what I tend to do so that my converters are vague and general-use. (NB: Names are slightly different.)
<config:BoolToBrushConverter x:Key="Bool2Brush"
TrueBrush="{StaticResource OKStatusBrush}"
FalseBrush="{StaticResource ErrorStatusBrush}" />
I think the issue is that you are trying to find the resource out of a framework element not in the visual tree. Could you try the following instead?
Application.Current.FindResource("OKStatus") as Brush;
As I have learned by TechNet Wiki, there is necessary to use MultiValue Converter and MultiValueBinding to get correct registred converter and correct FrameworkElement by the UserControl.
XAML Example:
<TextBlock x:Name="tb1" Margin="20">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource MyConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type UserControl}}"/>
<Binding Path="MyValue"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Then the converter declaration can looks :
public class MyConverter : IMultiValueConverter
{
FrameworkElement myControl;
object theValue;
public object Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
myControl = values[0] as FrameworkElement;
theValue = values[1];
return myControl.FindResource(">>resource u need<<");
}
public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
.....
}
}
The detail explanation is:
https://social.technet.microsoft.com/wiki/contents/articles/12423.wpfhowto-pass-and-use-a-control-in-it-s-own-valueconverter-for-convertconvertback.aspx
I have run into this problem as well. I think that calling Application.Current is the best way to get at resources from an IValueConverter, since they are not defined on a per window, or page or control, level. This would require the resources to be at least application-level, as stated above.
However, since the Application.Current reference is set to null in the designer, this method will always break the designer. What you appear to have done is given something for the designer to display, while you have given your running application access to resources in the converter.
For all of you out there with this issue, you don't need to implement the Kludge that lewi implemented; this is only if you want the designer surface to load. It does not affect your application while running as the Application.Current call has something to do.
Actually what I ended up doing (for now) was to change from FindResource to TryFindResource, and put the statements in a try/catch block.
This seems to work so far.
try
{
if (state == true)
return (FrameElem.TryFindResource("OKStatus") as Brush);
else
return (FrameElem.TryFindResource("ErrorStatus") as Brush);
}
catch (ResourceReferenceKeyNotFoundException)
{
return new SolidColorBrush(Colors.LightGray);
}

WPF Image Source binding with StringFormat

I'm new to WPF and MVVM (started this week experimenting with it) and trying to bind image resources at runtime. The items I'm trying to display contain an enumerate property that indicates the type or state of the item:
public class TraceEvent
{
/// <summary>
/// Gets or sets the type of the event.
/// </summary>
/// <value>The type of the event.</value>
public TraceEventType EventType { get; set; }
}
As far as I known the Source attribute of Image has a value converter that takes strings and returns Uri objects.
<Image Source="{Binding Path=EventType, StringFormat={}/AssemblyName;component/Images/{0}Icon.ico}" />
So why doesn't the above work? If I enter the uri directly (without binding) the image is shown perfectly. In fact, if I do the binding in a TextBlock and use the result of that value in the Image also shown without problem:
<TextBlock Visibility="Collapsed" Name="bindingFix" Text="{Binding Path=EventType, StringFormat={}/AssemblyName;component/Images/{0}Icon.ico}"/>
<Image Source="{Binding ElementName=bindingFix, Path=Text}" />
I'm pretty sure I'm doing something terrible wrong for such an obvious thing to do with images.
Thanks.
StringFormat is only used if the target property is actually a string - the Image.Source property is a Uri so the binding engine won't apply the StringFormat.
One alternative is to use a Value Converter. Either write a general StringFormatConverter that takes the string format in the ConverterParameter, or a more specific ImageSourceConverter e.g.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return string.Format( "/Images/{0}Icon.ico", value );
}
Note that if your images live in the same assembly as they are used, then you shouldn't need to specify the assembly name in the URI and the above syntax should work.
i'm not sure about this one but it seems that you are passing the image's source property a string where it expects a uri. so, you have to convert your string into a uri object

Using file-path to images stored within the Application Settings

I am trying to develop an application that uses a number of images that are stored in a seperate remote file location. The file-paths to the UI elements are stored within the Application Settings. Although I understand how to access and use the file-path from Settings in C# (Properties.Settings.Default.pathToGridImages + "OK.png"), I am at a loss to figure out how to utilize the Settings paths in WPF, and can only seem to access the file if I include the file-path, such as:
<Grid.Background>
<ImageBrush ImageSource="C:\Skins\bottomfill.png" TileMode="Tile" />
</Grid.Background>
I would have thought that concatenating "Properties.Settings.Default.pathToGridImages" with "bottomfill.png" in WPF could be done much like it can be done in C#. Can anyone please point me in the right direction?
You can do this using a MultiBinding and a value converter. To start with, use a multibinding to bind your image source to the base path, and the image name:
<ImageBrush>
<ImageBrush.ImageSource>
<MultiBinding Converter="{StaticResource MyConverter}">
<Binding Source="{StaticResource MySettings}" Path="Default.FilePath" />
<Binding Source="ImageName.png"></Binding>
</MultiBinding>
</ImageBrush.ImageSource>
</ImageBrush>
You then need to have a converter that implements IMultiValueConverter and combines the two parts of the path and creates the image using either an ImageSourceConverter or by creating a new BitmapImage:
class MyConverter: IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Concatenate the values.
string filename = Path.Combine(values[0].ToString(), values[1].ToString());
// You can either use an ImageSourceConverter
// to create your image source from the path.
ImageSourceConverter imageConverter = new ImageSourceConverter();
return imageConverter.ConvertFromString(filename);
// ...or you can create a new bitmap with the combined path.
return new BitmapImage(new Uri(filename, UriKind.RelativeOrAbsolute));
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
// No need to implement the convert back as this will never be used in two way binding.
throw new NotImplementedException();
}
}
Obviously, you need to declare namespaces and resource to the CLR stuff in the XAML so you can access it (If you've called your settings and converter classes something different, make sure you change this to match up):
...
xmlns:local ="clr-namespace:WpfApplication1">
<Window.Resources>
<local:MyConverter x:Key="MyConverter"></local:MyConverter>
<local:MySettings x:Key="MySettings"></local:MySettings>
</Window.Resources>
I've tested it out and it works fine.
[An alternative way would be just to bind the ImageSource property to a property on your data context that combined the paths in C# code, but that would depend on how you've got your datacontexts set up, so may be undesirable in many cases.]

How to add data into RichTextBox from two datasourses in WPF

I need to put data from two different datasourses in the same textbox. The text that comes from the first one have to be bolded and the secound normal.
It's there a possibility to do this in WPF?
You cannot bind (or multibind) to Document property of RichTextBox, because it is NOT a DependencyProperty (strange!!!)!!! See this link for a really easy way of subclassing RichTextBox to create your own BindableRichTextBox or this post for another workaround.
Now you can use MultiBinding with a custom IMultiValueConverter to achieve the results. Since you have not given much details of your problem, I can only give you an overall idea of what you should do:
<!--NOTE: Include xmlns:local=" .. " appropriately for your project-->
<Window.Resources>
<sys:String x:Key="SourceA">This text will be normal..</sys:String>
<sys:String x:Key="SourceB">This text will be Bold!!!</sys:String>
</Window.Resources>
And now you can do like this:
<local:BindableRichTextBox>
<!--<local:BindableRichTextBox.Document>-->
<MultiBinding Converter="{x:Static local:MySourceBToBoldConverter.Instance}">
<Binding Source="{StaticResource SourceA}" />
<Binding Source="{StaticResource SourceB}" />
</MultiBinding>
<!--</local:BindableRichTextBox.Document>-->
</local:BindableRichTextBox>
And then create a class MySourceBToBoldConverter that inherits from IMultiValueConverter like this:
public class MySourceBToBoldConverter : IMultiValueConverter
{
public static readonly MySourceBToBoldConverter Instance = new MySourceBToBoldConverter();
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//Now you'll get value from Source A as value[0]
// and value from Source B as value[1]
//Do whatever you want like bold etc...
//and return the result
string normalText = values[0] as string;
string boldText = values[1] as string;
Bold bold = new Bold();
bold.Inlines.Add(boldText);
Paragraph para = new Paragraph();
para.Inlines.Add(normalText);
para.Inlines.Add(bold);
FlowDocument rtbDocument = new FlowDocument();
rtbDocument.Blocks.Add(para);
return rtbDocument;
}
public object[] ConvertBack(object value, ... )
{
//Convert the object returned by Convert() back
//to its original form if it's possible;
//otherwise throw not supported exception ;)
throw new NotImplementedException();
}
}
Currently I don't have my work PC with me that has VS installed, so I can't give you a working example, but go ahead and search google/msdn/stackoverflow 4 MultiBinding and IMultiValueConverter and you'll find some good examples out there.
Check the working example here.
Regards,
Mihir Gokani

Resources