RenderTargetBitmap + Resource'd VisualBrush = incomplete image - wpf

I've found a new twist on the "Visual to RenderTargetBitmap" question!
I'm rendering previews of WPF stuff for a designer. That means I need to take a WPF visual and render it to a bitmap without that visual ever being displayed. Got a nice little method to do it like to see it here it goes
private static BitmapSource CreateBitmapSource(FrameworkElement visual)
{
Border b = new Border { Width = visual.Width, Height = visual.Height };
b.BorderBrush = Brushes.Black;
b.BorderThickness = new Thickness(1);
b.Background = Brushes.White;
b.Child = visual;
b.Measure(new Size(b.Width, b.Height));
b.Arrange(new Rect(b.DesiredSize));
RenderTargetBitmap rtb = new RenderTargetBitmap(
(int)b.ActualWidth,
(int)b.ActualHeight,
96,
96,
PixelFormats.Pbgra32);
// intermediate step here to ensure any VisualBrushes are rendered properly
DrawingVisual dv = new DrawingVisual();
using (var dc = dv.RenderOpen())
{
var vb = new VisualBrush(b);
dc.DrawRectangle(vb, null, new Rect(new Point(), b.DesiredSize));
}
rtb.Render(dv);
return rtb;
}
Works fine, except for one leeetle thing... if my FrameworkElement has a VisualBrush, that brush doesn't end up in the final rendered bitmap. Something like this:
<UserControl.Resources>
<VisualBrush
x:Key="LOLgo">
<VisualBrush.Visual>
<!-- blah blah -->
<Grid
Background="{StaticResource LOLgo}">
<!-- yadda yadda -->
Everything else renders to the bitmap, but that VisualBrush just won't show. The obvious google solutions have been attempted and have failed. Even the ones that specifically mention VisualBrushes missing from RTB'd bitmaps.
I have a sneaky suspicion this might be caused by the fact that its a Resource, and that lazy resource isn't being inlined. So a possible fix would be to, somehow(???), force resolution of all static resource references before rendering. But I have absolutely no idea how to do that.
Anybody have a fix for this?

You have two problems:
You didn't set a PresentationSource on your visual so Loaded events won't fire.
You didn't flush the Dispatcher queue. Without flushing the Dispatcher queue, any functionality that uses Dispatcher callbacks won't work.
The immediate cause of your problem is failure to flush the Dispatcher queue, since VisualBrush uses it, but you will probably run into the PresentationSource problem before long so I would fix both of these.
Here is how I do it:
// Create the container
var container = new Border
{
Child = contentVisual,
Background = Brushes.White,
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(1),
};
// Measure and arrange the container
container.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
container.Arrange(new Rect(container.DesiredSize));
// Temporarily add a PresentationSource if none exists
using(var temporaryPresentationSource = new HwndSource(new HwndSourceParameters()) { RootVisual = (VisualTreeHelper.GetParent(container)==null ? container : null) })
{
// Flush the dispatcher queue
Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() => { }));
// Render to bitmap
var rtb = new RenderTargetBitmap((int)b.ActualWidth, (int)b.ActualHeight, 96, 96, PixelFormats.Pbgra32);
rtb.Render(container);
return rtb;
}
FYI, StaticResource lookup is never delayed under any circumstances: It is processed the moment the XAML is loaded and immediately replaced with the value retrieved from the ResourceDictionary. The only way StaticResource could possibly be related is if it picked up the wrong resource because two resources had the same key. I just thought I should explain this -- it has nothing to do with your actual problem.

Well to inline it, you could just do something like this:
<Grid>
<Grid.Background>
<VisualBrush>
<VisualBrush.Visual>
<!-- blah blah -->
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
</Grid>
If that doesn't work, my guess would be that it must be something specific with the Visual instance you are using (and that will require further code to better diagnose).

Related

WPF Cropping an imagesource in DrawingVisual

I need to display a cropped region of an image source within a DrawingVisual, but for the past few hours I've just drawn blanks for what should be a simple task.
My plan is to implement timed animation (for a HMI system) which will take a big image containing various frames. Depending on a state variable, the DrawingVisual will extract from this big image, the current frame to show - hence the need to crop.
My code stands as follows:
int width = Convert.ToInt32(_imageSource.Width / 2);
int height = Convert.ToInt32(_imageSource.Height);
BitmapImage bm = _imageSource.Clone() as BitmapImage;
bm.BaseUri = BaseUriHelper.GetBaseUri(this);
CroppedBitmap c = new CroppedBitmap(bm, new Int32Rect(0, 0, width, height));
ImageDrawing id = new ImageDrawing(c, new Rect(0, 0, width, height));
dg.Children.Add(id);
dc.DrawDrawing(dg);
However, upon creating the cropped image, I receive
"value does not fall within expected range"
BTW The image is a png loaded within a resource dictionary (in the current assembly) as
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<BitmapImage x:Key="elbowSmallBlueBL" UriSource="./Assets/HMI/Elbows/Small/Elbow Blue BottomLeft.png" />
and then assigned to the DrawingVisual using (ImageSource)FindResource("...
Firstly I don't understand the error message and can't find an satisfactory explanation anywhere and secondly, why is it sooooo difficult to perform a simple task?
Thanks for any help.
p.s. I think this due to not being able to locate the resource. But if the original imagesource is fine why would the cloned version behave any differently?
p.p.s Removing the line "bm.BaseUri = ..." results in same error.
To simplify things, I've created a test project that only has a button which attempts to create CroppedBitmap when clicked.
The button click =
private void Button_Click(object sender, RoutedEventArgs e)
{
BitmapSource i = (BitmapSource)FindResource("elbowSmallBlueBL");
try
{
CroppedBitmap c = new CroppedBitmap(i, new Int32Rect(0, 0, 100, 100));
}
catch (Exception ex)
{
}
}
}
The resource dictionary references the bitmap :
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Elbows -->
<BitmapImage x:Key="elbowSmallBlueBL" UriSource="./Assets/HMI/Elbows/Small/Elbow Blue BottomLeft.png" />
and included the App.xaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="rdHMIControls.xaml" />
The BitmapSource (i) is valid and the call to CroppedBitmap throws the ArgumentException, "Value does not fall within the accepted range".
Creating, or trying to create a cropped image surely can't be any simpler than this?
Another example which I've taken off MSDN
BitmapSource i = (BitmapSource)this.FindResource("test");
try
{
// Create an Image element.
Image croppedImage = new Image();
croppedImage.Width = 200;
croppedImage.Margin = new Thickness(5);
// Create a CroppedBitmap based off of a xaml defined resource.
CroppedBitmap cb = new CroppedBitmap(
i,
new Int32Rect(30, 20, 105, 50)); //select region rect
croppedImage.Source = cb; //set image source to cropped
Again new CroppedBitmap throws InvalidArgument exceptin
Any thoughts?
Thanks

Visual brush using control which isn't rendered?

I'm playing around with an idea at the moment and I've hit a brick wall. I'm using a console app to create a visual control (DevExpress chartcontrol to be precise) in memory, I'm then trying to save that control to an image using a VisualBrush but it won't work because (I assume) the control isn't drawn to the screen.
I've put my code in below so you know where I am at the moment. Does anyone know how I could possibly save this control to an image (ideally jpg, but anything will do...) using a console app? I really don't want to have to render it to the screen even for a millisecond just to be able to save it...
static void sl_CreateDetail(FrameworkElement chartControl1, CreateAreaEventArgs e)
{
var brush = new VisualBrush(chartControl1);
var visual = new DrawingVisual();
DrawingContext context = visual.RenderOpen();
context.DrawRectangle(brush, null,
new Rect(0, 0, chartControl1.ActualWidth, chartControl1.ActualHeight));
context.Close();
var bmp = new RenderTargetBitmap((int)chartControl1.ActualWidth,
(int)chartControl1.ActualHeight, 96, 96, PixelFormats.Pbgra32);
bmp.Render(visual);
e.Data = bmp;
}
Before rendering the control you would have to manually do its layout by calling Measure and Arrange. This requires that you specify the desired size of the control, e.g. by explicitly setting its Width and Height properties.
There is no need for VisualBrush and DrawingVisual, you can directly render the control to the RenderTargetBitmap.
chartControl1.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
chartControl1.Arrange(new Rect(0, 0, chartControl1.Width, chartControl1.Height));
chartControl1.UpdateLayout();
var bmp = new RenderTargetBitmap((int)chartControl1.ActualWidth,
(int)chartControl1.ActualHeight, 96, 96, PixelFormats.Pbgra32);
bmp.Render(chartControl1);
If the control calculates a preferred size during layout (in Measure), you could perhaps use its DesiredSize property for rendering.
chartControl1.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
chartControl1.Arrange(new Rect(new Point(), chartControl1.DesiredSize));
chartControl1.UpdateLayout();
Note also that the rendering thread's ApartmentState must be STA. In a console application you could simply apply the STAThread attribute to the Main method.
[STAThread]
static void Main(string[] args)
{
...
}
I tried Measure(), Arrange(), etc, then discovered that these DO work if the Visual has a parent! In my case I was removing the Visual from one container, updating its properties (colour, etc), then trying to use it as a VisualBrush and it wasn't getting updated. Leaving it in the original container for the duration of Measure() and Arrange() fixed it (even though it was all done offscreen).

WPF custom shader effect property binding for animation

I'm implementing transitions in a WPF application.
First I "save" my 2 FrameworkElement in 2 ImageBrush.
Then I set the Input & Back (Brush) properties of my shader Effect with them.
CustomEffect s = new CustomEffect();
RenderTargetBitmap rtb = new RenderTargetBitmap((int)SourceFrameWorkElement.ActualWidth, (int)SourceFrameWorkElement.ActualHeight, 96, 96, PixelFormats.Pbgra32);
rtb.Render(SourceFrameWorkElement);
ImageBrush ib = new ImageBrush(rtb);
s.Input = ib;
rtb.Render(TargetFrameWorkElement);
ib.ImageSource = rtb;
s.Back = ib;
SourceFrameWorkElement.Effect = s;
Now that all is set up, I want to animate the Time property of my shader, and i've tried this:
DoubleAnimation refDoubleAnimation = new DoubleAnimation(0.0, 1.0, Duration);
Storyboard.SetTarget(refDoubleAnimation, SourceFrameWorkElement);
Storyboard.SetTargetProperty(refDoubleAnimation, new PropertyPath("(Effect).(CustomEffect.Time)");
refStoryboard.Children.Add(refDoubleAnimation);
refStoryboard.Completed += new EventHandler(OnStoryboardCompleted);
refStoryboard.Begin(SourceFrameWorkElement, true);
and i get an InvalidOperationException on the begin method with this message:
"Cannot resolve all property references in the property path '(Effect).(CustomEffect.Time)'.
Verify that applicable objects supports the properties."
But when I use a built in Effect like BlurEffect, it works....
Can someone tell me where i'm wrong ?
Edit:
I've also tried
SourceElement.Effect.BeginAnimation(SlideInEffect.TimeProperty, refDoubleAnimation)
instead of using the storyboard, I don't get an exception but the second image pop instantly and the animation is not playing
The solution was to use BeginAnimation ^^
In fact, I had the second image with opacity to 1, and the animation was playing behind
(i checked if the time elapsed to get in my OnAnimationCompleted eventHandler matched with the transition Duration)
so i've created a second animation on the TargetElement opacity with 2 DiscreteDoubleKeyFrames to do the trick and now it works ^^
Maybe the Storyboard thing could work if i add the namespace in the PropertyPath but i have no time to test it so give it a try if you want, and update the post ^^.

WPF, C#: Draw a line onto existing bitmap in image control

I have a bitmap image in an image control
I need to draw a red line on the bitmap each time I click with the mouse onto it, at the place where I clicked the mouse.
I first thought of creating a Line object, but found out, that I cannot add the Line. I would need a canvas. But if I put my image in a canvas, my bitmap does not stretch over the whole canvas (I found out, that the coordinates of the bitmap determine the place on the canvas, so my bitmap is wrongly displayed.)
Then I tried using graphics
Graphics graphics = Graphics.FromImage(bitmapImg);
graphics.DrawLine(new System.Drawing.Pen(System.Drawing.Color.Red), 0, 0, bitmapImg.Width, bitmapImg.Height); //not the line yet, just for testing
graphics.DrawImage(bitmapImg, 0, 0, bitmapImg.Width,bitmapImg.Height);
graphics.Dispose();
However, I don`t get anything painted onto my bitmap........
Now I think, I probably have to get the bitmap into an array and then change the pixel color to get the line in the bitmap. I believe, that this would be very slow.
I am now trying something with visualDrawing, however, I have not got it to work yet:-(
What is a good way to get a line onto an existing bitmap in WPF C#???? and how to remove it?
I would be glad for any help! Thank you! I posted it already on the MS forum page, but no answer so far.
When you do Graphics.FromImage, this Graphics class (and also the System.Drawing.Pen) do not belong to WPF, they are part from WinForms and they are internally using Windows' GDI+ calls to draw and cannot draw on top of WPF.
If you didn't got an error when compiling the first line of your code, then probably your bitmapImg is a System.Drawing.Image (from WinForms) not an Image control from WPF (System.Window.Controls.Image).
As adrianm mentioned, the easiest way will probably be to use a Grid:
<Grid>
<Image Source="your image" />
<Line Name="line" Visibility="Hidden" Stroke="Red" StrokeThickness="1" />
</Grid>
Then, in your click event handler you can make the line visible and give it the coordinates you want:
line.Visibility = Visible;
line.X1 = mouse_x;
line.Y1 = mouse_y;
line.X2 = ...;
line.Y2 = ...;
You can place a canvas with the background as transparent on top of your BitmapImage and then draw the line as required.
Code from xaml file:
<Grid>
<Image Source="C:\Users\sm143444\Desktop\download.jpg" />
<Canvas Background="Transparent" x:Name="draw" />
</Grid>
Code from Xaml.cs:
public MainWindow()
{
InitializeComponent();
Point startPoint = new Point(50, 50);
Line newLine = new Line();
newLine.Stroke = Brushes.Black;
newLine.Fill = Brushes.Black;
newLine.StrokeLineJoin = PenLineJoin.Bevel;
newLine.X1 = startPoint.X;
newLine.Y1 = startPoint.Y;
newLine.X2 = startPoint.X + 100;
newLine.Y2 = startPoint.Y + 100;
newLine.StrokeThickness = 2;
this.draw.Children.Add(newLine);
}
output
Or you can even add a ZIndex to your image and Line so that they are laid on different layers on canvas.

WPF animation problem when using Storyboard from code

I'm working on a 3D carousel of flat, square tiles that will contain information. I'm working on animating this carousel to rotate when a person presses Next and Previous buttons.
I've gotten it to work by using BeginAnimation on the Rotation property of the RotateTransform3D I applied to the carousel, but I can't seem to make a Storyboard version of the same animation work. The reason I need the Storyboard version is for the HandOffBehavior.Compose parameter because without it, multiple clicks of my next and previous buttons results in a misaligned carousel.
Here is the code for the Storyboard:
RotateTransform3D tempTransform = (RotateTransform3D)wheel.Transform;
AxisAngleRotation3D rotation = (AxisAngleRotation3D)tempTransform.Rotation;
Storyboard storyboard = new Storyboard();
DoubleAnimation animation = new DoubleAnimation();
animation.By = defaultAngle;
animation.Duration = TimeSpan.FromSeconds(.5);
Storyboard.SetTarget(animation, rotation);
Storyboard.SetTargetProperty(animation, new PropertyPath("Angle"));
storyboard.Children.Add(animation);
storyboard.Duration = animation.Duration;
storyboard.Begin(new FrameworkContentElement(), HandoffBehavior.Compose);
For some reason, this code results in absolutely nothing. I followed the examples I had to the letter, so I am quite frustrated. Any help is greatly appreciated. I am also completely open to using BeginAnimation if I can replicate HandOffBehavior.Compose.
My experience comes from 2D animation, but I guess the problem is the same.
For some stupid reason (probably relating to an unhealthy focus on XAML), Storyboards can only animate Freezable objects by looking them up by name. (See example in Storyboards Overview.) Thus although you provide a reference to your 'rotation' object when you call Storyboard.SetTarget(animation, rotation), the Storyboard only wants to remember and use a name, which it does not have.
The solution is:
Create a naming scope around the element that will govern the transform.
Call RegisterName() for each Freezable object being animated.
Pass the element to Storyboard.Begin()
Which would make your code look something like this (not tested):
FrameworkContentElement element = new FrameworkContentElement();
NameScope.SetNameScope(element, new NameScope());
RotateTransform3D tempTransform = (RotateTransform3D)wheel.Transform;
AxisAngleRotation3D rotation = (AxisAngleRotation3D)tempTransform.Rotation;
element.RegisterName("rotation", rotation);
Storyboard storyboard = new Storyboard();
DoubleAnimation animation = new DoubleAnimation();
animation.By = defaultAngle;
animation.Duration = TimeSpan.FromSeconds(.5);
Storyboard.SetTarget(animation, rotation);
Storyboard.SetTargetProperty(animation, new PropertyPath("Angle"));
storyboard.Children.Add(animation);
storyboard.Duration = animation.Duration;
storyboard.Begin(element, HandoffBehavior.Compose);
None of this is necessary in XAML because your objects are automatically registered.
EDIT: But then I worked out that you can simplify things by leaving out the Storyboard altogether:
var T = new TranslateTransform(40, 0);
Duration duration = new Duration(new TimeSpan(0, 0, 0, 1, 0);
DoubleAnimation anim = new DoubleAnimation(30, duration);
T.BeginAnimation(TranslateTransform.YProperty, anim);

Resources