Convert WPF Control to BitmapSource - wpf

This is kind of a two part question- First, why doesn't this code work?
Canvas canvas = new Canvas { Width = 640, Height = 480 };
System.Windows.Size size = new System.Windows.Size( canvas.Width, canvas.Height);
//Measure and arrange the surface
canvas.Measure( size );
canvas.Arrange( new Rect( size ) );
canvas.Background = new SolidColorBrush( Colors.Purple );
RenderTargetBitmap bitmap = new RenderTargetBitmap( (int)canvas.Width, (int)canvas.Height, 96d, 96d, PixelFormats.Pbgra32 );
bitmap.Render( canvas );
BitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add( BitmapFrame.Create( bitmap ) );
using ( MemoryStream outStream = new MemoryStream() )
{
encoder.Save( outStream );
outStream.Seek( 0, SeekOrigin.Begin );
BitmapImage bmp = new BitmapImage { CacheOption = BitmapCacheOption.OnLoad };
bmp.BeginInit();
bmp.StreamSource = outStream;
bmp.EndInit();
}
When I write the image to disk, all I see is a black image- I've done this before and had no problems, but now something is escaping me... I've checked the width and height and the buffer data in the MemoryStream and everything look okay...
This is just a test, the real goal would be to create a BitmapSource from the Canvas visual image. The Canvas is getting drawn on with Shapes (polylines etc) in code. I then need to pass this BitmapSource to an Image in xaml, at a rate of about 60 frames per second. I noticed that the Image.Source is using CachedBitmap if I create a mock BitmapSource, but it is rebinding to my BitmapImage everytime I update my (black) Bitmap.
Suggestions on how to create a Canvas in memory at 60fps and create a BitmapSource from it that is seen by Image.Source as a CachedBitmap?

Makubex is correct - you need to wait until things get loaded up before the visuals are in a state where they've actually rendered anything capable of being copied; that said, while I'm not on a computer where I've got Studio installed, I do have LINQPad installed....
void Main()
{
mainWindow = new Window(){ Width = 640, Height = 480, Title = "Main Window" };
canvas = new Canvas { Width = 640, Height = 480 };
System.Windows.Size size = new System.Windows.Size( canvas.Width, canvas.Height );
// Measure and arrange the surface
canvas.Measure( size );
canvas.Arrange( new Rect( size ) );
canvas.Background = new SolidColorBrush( Colors.Purple );
mirrorTimer = new DispatcherTimer(
TimeSpan.FromMilliseconds(1000.0 / 60.0),
DispatcherPriority.Background,
CopyToMirror,
Dispatcher.CurrentDispatcher);
updateTimer = new DispatcherTimer(
TimeSpan.FromMilliseconds(1000.0 / 60.0),
DispatcherPriority.Background,
DrawSomething,
Dispatcher.CurrentDispatcher);
mainWindow.Loaded +=
(o,e) =>
{
mirrorWindow = new Window { Width = 640, Height = 480, Title = "Mirror Window" };
mirrorWindow.Show();
mirrorWindow.Loaded +=
(o2,e2) =>
{
mirrorTimer.Start();
};
};
mainWindow.Closed +=
(o,e) =>
{
if(mirrorTimer != null)
{
mirrorTimer.Stop();
mirrorWindow.Close();
}
};
mainWindow.Content = canvas;
mainWindow.Show();
}
Window mainWindow;
Window mirrorWindow;
Canvas canvas;
DispatcherTimer mirrorTimer;
DispatcherTimer updateTimer;
Random rnd = new Random();
private void DrawSomething(object sender, EventArgs args)
{
canvas.Children.Clear();
canvas.Background = Brushes.White;
var blob = new Ellipse() { Width = rnd.Next(0, 20), Height = rnd.Next(0, 20) };
blob.Fill = new SolidColorBrush(Color.FromArgb(255, (byte)rnd.Next(0,255), (byte)rnd.Next(0,255), (byte)rnd.Next(0,255)));
Canvas.SetLeft(blob, (int)rnd.Next(0, (int)canvas.ActualWidth));
Canvas.SetTop(blob, (int)rnd.Next(0, (int)canvas.ActualHeight));
canvas.Children.Add(blob);
}
private void CopyToMirror(object sender, EventArgs args)
{
var currentImage = (mirrorWindow.Content as Image);
if(currentImage == null)
{
currentImage = new Image(){ Width = 640, Height = 480 };
mirrorWindow.Content = currentImage;
}
RenderTargetBitmap bitmap = new RenderTargetBitmap( (int)canvas.Width, (int)canvas.Height, 96d, 96d, PixelFormats.Pbgra32 );
bitmap.Render( canvas );
BitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add( BitmapFrame.Create( bitmap ) );
BitmapImage bmp = new BitmapImage() { CacheOption = BitmapCacheOption.OnLoad };
MemoryStream outStream = new MemoryStream();
encoder.Save(outStream);
outStream.Seek(0, SeekOrigin.Begin);
bmp.BeginInit();
bmp.StreamSource = outStream;
bmp.EndInit();
currentImage.Source = bmp;
}

If you are still having issues with the black image move your CacheOption set to just after the BeingInit call:
bmp.BeginInit();
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.StreamSource = outStream;
bmp.EndInit();

Related

WPF Rotate and return BitmapSource by any angle

Hei, I tried this:
public static BitmapSource RotateImage(Image b, float angle)
{
BitmapSource rotita = (BitmapSource)b.Source;
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
var transform = new RotateTransform(angle);
drawingContext.PushTransform(transform);
drawingContext.DrawImage(rotita, new Rect(0,0, rotita.PixelWidth, rotita.PixelHeight));
drawingContext.Pop();
}
RenderTargetBitmap bmp = new RenderTargetBitmap(rotita.PixelWidth, rotita.PixelHeight, 96, 96, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
rotita = bmp;
return rotita;
}
But this does not work fine. I have this image at 0 degree and after rotation at 30 degrees this image.
What could I make the picture to be complete after rotation?
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(back, new Rect(0, 0, imageWidth, imageHeight));
drawingContext.DrawImage(element, new Rect(x,y, elementWidth, elementHeight));
}
RenderTargetBitmap bmp = new RenderTargetBitmap(imageWidth, imageHeight, 96, 96, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
image.Source = bmp;
Element is the rotated image
The following method creates a composed bitmap from two others, where the second one is rotated around their common center point.
The two crucial parts of this method are the calculation of the transformed bounds of the rotated bitmap, and the alignment of the two bitmaps at their common center point.
private BitmapSource ComposeImage(
BitmapSource image1, BitmapSource image2, double rotationAngle)
{
var rotation = new RotateTransform(rotationAngle);
var size1 = new Size(image1.PixelWidth, image1.PixelHeight);
var size2 = new Size(image2.PixelWidth, image2.PixelHeight);
var center1 = new Vector(size1.Width / 2, size1.Height / 2);
var center2 = new Vector(size2.Width / 2, size2.Height / 2);
var rotatedSize = rotation.TransformBounds(new Rect(size2)).Size;
var totalSize = new Size(
Math.Max(size1.Width, rotatedSize.Width),
Math.Max(size1.Height, rotatedSize.Height));
var center = new Point(totalSize.Width / 2, totalSize.Height / 2);
rotation.CenterX = center.X;
rotation.CenterY = center.Y;
var dv = new DrawingVisual();
using (var dc = dv.RenderOpen())
{
dc.DrawImage(image1, new Rect(center - center1, size1));
dc.PushTransform(rotation);
dc.DrawImage(image2, new Rect(center - center2, size2));
}
var rtb = new RenderTargetBitmap(
(int)totalSize.Width, (int)totalSize.Height, 96, 96, PixelFormats.Default);
rtb.Render(dv);
return rtb;
}

How to render line to PNG bitmap i WPF?

I new to WPF and I am trying to render simple line to bitmap and save it to the PNG file. But I got empty Bitmap instead.
What I am doing wrong?
void RenderLineToFile()
{
var bitmap = RenderBitMap();
SaveImageToFile("image.png", bitmap);
}
RenderTargetBitmap RenderBitMap()
{
int bitmapWidth = 100;
int bitmapHeight = 100;
double dpiX = 72;
double dpiY = 72;
RenderTargetBitmap bm = new RenderTargetBitmap(bitmapWidth, bitmapHeight, dpiX, dpiY, PixelFormats.Pbgra32);
DrawingVisual drawing_visual = new DrawingVisual();
using (DrawingContext drawing_context = drawing_visual.RenderOpen())
{
Pen penBlack = new Pen(Brushes.Black, 1.0);
drawing_context.DrawLine(penBlack, new Point(0, 0), new Point(100, 100));
bm.Render(drawing_visual);
}
return bm;
}
public static void SaveImageToFile(string filePath, BitmapSource image)
{
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(image));
encoder.Save(fileStream);
}
}
According to MSDN,
A DrawingContext must be closed before its content can be rendered...
Try taking bm.Render(drawing_visual); outside of the using clause.

Programmatically setting the icon size

On WPF I am creating a menu item dynamically at run time.
I set the icon from a StreamGeometry that's stored on a ResourceDictionary. Everything works OK but: how do I set the size of the icon?
MenuItem menExit = new MenuItem();
menExit.Header = "Exit"; // will be changedlater
menExit.Command = UICommands.CmdExit;
menExit.CommandBindings.Add(new CommandBinding(UICommands.CmdExit, CmdExitExecute, CmdExitCanExecute));
menExit.Icon = (StreamGeometry)FindResource("ImgExit");
//SET THE SIZE HERE????????
// Eventually, how do I set the fill color?
menu.Items.Add(menExit);
Note, I am doing all this at run time and not in xalm
I suggest you to create a path on which you can specify Height, Width and Fill and set your StreamGeometry as the Data of the path. Then put this Path as the icon of the MenuItem.
var path = new Path
{
Height = 20,
Width = 20,
Fill = new SolidColorBrush(Colors.Blue),
Data = (StreamGeometry) FindResource("ImgExit")
};
menExit.Icon = path;
You can always try this:
/// <summary>
/// Convert Geometry to ImageSource, Draws the Geometry on a bitmap surface and centers it.
/// </summary>
/// <param name="geometry"></param>
/// <param name="TargetSize"></param>
/// <returns></returns>
ImageSource Geometry_To_ImageSource(Geometry geometry, int TargetSize)
{
var rect = geometry.GetRenderBounds(new Pen(Brushes.Black, 0));
var bigger = rect.Width > rect.Height ? rect.Width : rect.Height;
var scale = TargetSize / bigger;
Geometry scaledGeometry = Geometry.Combine(geometry, geometry, GeometryCombineMode.Intersect, new ScaleTransform(scale, scale));
rect = scaledGeometry.GetRenderBounds(new Pen(Brushes.Black, 0));
Geometry transformedGeometry = Geometry.Combine(scaledGeometry, scaledGeometry, GeometryCombineMode.Intersect, new TranslateTransform(((TargetSize - rect.Width) / 2) - rect.Left, ((TargetSize - rect.Height) / 2) - rect.Top));
RenderTargetBitmap bmp = new RenderTargetBitmap(TargetSize, TargetSize, 96, 96, PixelFormats.Pbgra32);
DrawingVisual viz = new DrawingVisual();
using (DrawingContext dc = viz.RenderOpen())
{
dc.DrawGeometry(Brushes.Black, null, transformedGeometry);
}
bmp.Render(viz);
var mem = new MemoryStream();
PngBitmapEncoder pngEncoder = new PngBitmapEncoder();
pngEncoder.Frames.Add(BitmapFrame.Create(bmp));
pngEncoder.Save(mem);
var itm = GetImg(mem);
return itm;
}
BitmapImage GetImg(MemoryStream ms)
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = ms;
bmp.EndInit();
return bmp;
}

Add TransformGroup to a FramworkElement when rendering WPF to a PNG

I've got an app that turns some XAML Usercontrols into PNGs - this has worked really well up to now, unfortunately I now need to double the size of the images.
My method (that doesn't work!) was to add a ScaleTransform to the visual element after I've loaded it ...
This line is the new line at the top of the SaveUsingEncoder method.
visual.RenderTransform = GetScaleTransform(2);
The PNG is the new size (3000 x 2000) - but the XAML is Rendered at 1500x1000 in the centre of the image.
Can anyone assist please?
private void Load(string filename)
{
var stream = new FileStream(filename), FileMode.Open);
var frameworkElement = (FrameworkElement)(XamlReader.Load(stream));
var scale = 2;
var encoder = new PngBitmapEncoder();
var availableSize = new Size(1500 * scale, 1000 * scale);
frameworkElement.Measure(availableSize);
frameworkElement.Arrange(new Rect(availableSize));
name = name.Replace(" ", "-");
SaveUsingEncoder(frameworkElement, string.Format(#"{0}.png", name), encoder, availableSize);
}
private TransformGroup GetScaleTransform(int scale)
{
var myScaleTransform = new ScaleTransform {ScaleY = scale, ScaleX = scale};
var myTransformGroup = new TransformGroup();
myTransformGroup.Children.Add(myScaleTransform);
return myTransformGroup;
}
private void SaveUsingEncoder(FrameworkElement visual, string fileName, BitmapEncoder encoder, Size size)
{
visual.RenderTransform = GetScaleTransform(2);
var bitmap = new RenderTargetBitmap(
(int) size.Width,
(int) size.Height,
96,
96,
PixelFormats.Pbgra32);
bitmap.Render(visual);
var frame = BitmapFrame.Create(bitmap);
encoder.Frames.Add(frame);
using (var stream = File.Create(fileName))
{
encoder.Save(stream);
}
}
Called visual.UpdateLayout before rendering into the RenderTargetBitmap
(Thanks to Clemens for this answer - but he put it as a comment!)

passing a vector drawing to the UI thread

Is there any way to draw actual WPF vectorgraphics (DrawingContext, VisualBrush, DrawingBrush, RenderTargetBitmap etc.) with Freezables in a separate thread offscreen?
The following solution almost has it, excpet that the drawing is a bitmap and is not scalable when this.label becomes big you'll the the pixels.
private void Draw()
{
this.dispatcher = Dispatcher.CurrentDispatcher;
Thread t = new Thread(this.DrawAsync);
t.SetApartmentState(ApartmentState.STA);
t.Start();
}
private void DrawAsync(object state)
{
var b1 = new Button
{
Width = 50,
Height = 50,
Content = new TextBlock
{
FontSize = 16, FontFamily = new FontFamily("Arial"), FontWeight = FontWeights.Bold, Text = "Hello"
}
};
b1.Measure(new Size(50, 50));
b1.Arrange(new Rect(0, 0, 50, 50));
PixelFormat pixelFormat = PixelFormats.Default;
var elementBrush = new VisualBrush(b1);
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
// preferably I'd like to draw controls too, but shapes and text would suffice too
dc.DrawRectangle(elementBrush, null, new Rect(0, 0, 50, 50));
dc.DrawEllipse(Brushes.Green, new Pen(Brushes.Black, 2), new Point(75, 25), 25, 15);
dc.Close();
}
var bitmap = new RenderTargetBitmap(100, 50, 96, 96, pixelFormat);
bitmap.Render(visual);
bitmap.Freeze();
var br = new ImageBrush(bitmap) { Stretch = Stretch.Uniform };
br.Freeze();
this.dispatcher.Invoke((ThreadStart)delegate { this.label.Background = br; });
}
You could use DrawingImage which is a type of freezeable. Load or fill it in a BackgroundWorker, freeze it and pass it to an Image in the Completed Event.

Resources