I'm trying to do the following:
Create a Livechart Cartesian chart in memory
Add the chart to a grid
Add Labels to the same grid
Add the grid to a Viewbox
Render the Viewbox as a PNG
Save the PNG to disk
The above should be run from a different thread in the background in order to allow UI reponsiveness.
However simple this may seem, I've been struggling to get a proper working solution. The following issues are relevant:
The Livechart (which is inside the Viewbox) takes time to render
Thus the chart needs to be given time to complete rendering before trying to save it as an image
I have found code which makes use of HwndSource, however it is not working all the time (works about 95% of the time). Without the HwndSource modification it NEVER works (always gets a chart with nothing on it)
Running the Run() function in a different UI thread does not work, as I get the following error message: WPF Dispatcher {“The calling thread cannot access this object because a different thread owns it.”}
So my questions are:
What is the right way to wait for the Livechart/Grid/ViewBox combination to finish rendering before saving it as an image? Maybe make use of the Loaded event? Note that I have tried to impelment it but cannot get it to work as I hit the 'threading' issue.
How can I run the entire process in a different UI thread?
See below for code
public void Run()
(
//Create Livechart which is a child of a Grid control
Grid gridChart = Charts.CreateChart();
//Creates a ViewBox control which has the grid as its child
Viewbox viewBox = WrapChart(gridChart,1400,700);
//Creates and saves the image
CreateAndSaveImage(viewBox ,path,name);
)
Below is the function which creates the Viewbox and add the grid as a child
public Viewbox viewBox WrapChart(Grid grid,int width,int height)
{
chart.grid.Width = width;
chart.grid.Height = height;
viewbox.Child = chart.grid;
viewbox.Width = width;
viewbox.Height = height;
viewbox.Measure(new System.Windows.Size(width, height));
viewbox.Arrange(new Rect(0, 0, width, height));
viewbox.UpdateLayout();
}
Function below creates and saves the image
public void CreateAndSaveImage(Viewbox viewbox,string folderPath,string fileName)
{
var x = HelperFunctions.GetImage(viewbox);
System.IO.FileStream stream = System.IO.File.Create(folderPath + fileName);
HelperFunctions.SaveAsPng(x, stream);
stream.Close();
}
The following code renders the viewbox to an image. Note that this is the only code that I could find which waits for the chart to finish loading. I have no idea how it works, but it works 95% of the time. Sometimes a chart still does not finish loading.
public static RenderTargetBitmap GetImage(Viewbox view)
{
using (new HwndSource(new HwndSourceParameters())
{
RootVisual =
(VisualTreeHelper.GetParent(view) == null
? view
: null)
})
{
Size size = new Size(view.ActualWidth, view.ActualHeight);
if (size.IsEmpty)
return null;
int actualWidth = Convert.ToInt32(size.Width);
int requiredWidth = Convert.ToInt32(size.Width * 1);
int actualHeight = Convert.ToInt32(size.Height);
int requiredHeight = Convert.ToInt32(size.Height * 1);
// Flush the dispatcher queue
view.Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() => { }));
var renderBitmap = new RenderTargetBitmap(requiredWidth, requiredHeight,
96d * requiredWidth / actualWidth, 96d * requiredHeight / actualHeight,
PixelFormats.Pbgra32);
DrawingVisual drawingvisual = new DrawingVisual();
using (DrawingContext context = drawingvisual.RenderOpen())
{
context.DrawRectangle(new VisualBrush(view), null, new Rect(new Point(), size));
context.Close();
}
renderBitmap.Render(view);
renderBitmap.Freeze();
return renderBitmap;
}
}
The following code saves the bitmap as a picture to file
public static void SaveAsPng(BitmapSource src, Stream outputStream)
{
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(src));
encoder.Save(outputStream);
}
The following code is what I use to run the entire thing in a different thread. Note that it is not working, as I get the following error message:
WPF Dispatcher {“The calling thread cannot access this object because
a different thread owns it.”}.
Note that if I execute Run() normally (without any separate threads) it works, however sometimes the chart does not render properly (as explained previously).
Thread thread = new Thread(() =>
{
Run();
System.Windows.Threading.Dispatcher.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Try call this line for the chart:
this.chart.Model.Updater.Run(false, true);
This line updates the chart and always is visible when save to image.
Related
I have class BBox, which repesents Rectangle - it contains cooridinates(x, y, width, height) and color of rectangle. Then I have List of these BBoxes, which contains circa 4000 of them. I need to draw all boxes which are stored in a List on a Canvas as fast as possible. What is the most effective way?
private List<BBox> FoundBoxes { get; set; }
public void DrawBoxes(Canvas canvas)
{
foreach (var box in FoundBoxes)
{
var brush = box.getColor();
System.Windows.Shapes.Rectangle rect;
rect = new System.Windows.Shapes.Rectangle
{
Stroke = brush,
Height = box.Height,
Width = box.Width,
StrokeThickness = 1
};
Canvas.SetLeft(rect, box.TopLeftX);
Canvas.SetTop(rect, box.TopLeftY);
canvas.Children.Add(rect);
}
}
Code above takes more than 1 second, which is slow for my application. I am sure that there must be a way to do it in parallel. So I tried this:
Parallel.ForEach(FoundBoxes, box =>
{
...same method body...
});
but it throws
System.InvalidOperationException: 'The calling thread must be STA, because many UI components require this.'
I am aware why this exceprtion occurs, and I tried to find solution, but nothing works for me. For example I tried running it using Dispatcher.Invoke(() => or running it using new Thread as stated here:
Thread t = new Thread(delegate ()
{
Parallel.ForEach(FoundBoxes, box =>
{
...same method body...
});
});
t.SetApartmentState(ApartmentState.STA);
t.IsBackground = true;
t.Start();
but it still throws mentioned exception. How can I solve this? Or is there any better / more effective way to draw multiple objects on canvas? Thank you.
Allright I solved my problem using WriteableBitmapEx as Clemens recommended. Now, 4000+ boxes are drawn in real-time.
I am applying a blur in a background thread to increase performance. This function is returning a RenderTargetBitmap. When this is done I'm invoking through the Dispatcher an update on the image and add it as content to the page. This is done like the following:
System.Windows.Controls.Image image = new System.Windows.Controls.Image();
Thread screenshotThread = new Thread(new ThreadStart(delegate()
{
RenderTargetBitmap img = CaptureScreen(0, 0, actualWidth, actualHeight);
//System.Windows.Controls.Image image = imgBlur;
//image = new System.Windows.Controls.Image();
Application.Current.Dispatcher.Invoke(() =>
{
image.Source = img;
image.Width = actualWidth;
image.Height = actualHeight;
PageContainer.Children.Add(image);
});
}));
screenshotThread.SetApartmentState(ApartmentState.STA);
screenshotThread.Start();
I'm adding the image to the PageContainer, this is a Grid. After running this piece of code, the image has been added to the page. However, the imagesource is null.. No image is currently visible. How can I make this image appear?
You have created the Imagecontrol and the RenderTargetBitmap on different thread. I am surprised you did not get an exception. Try adding img.Freeze(); before setting it to the Image.Source.
I'm creating and application that needs to add and remove a lot of UIElement to a Canvas.
Basically a Canvas contains a collection of UIElement and automatically renders/updates it on the screen depending what it contains.
In order to avoid having a tons of UIElements who overlap each others on the screen I prefer to add all of them on a secondary Canvas then create a Image from it (thanks to WritableBitmap). Finally I add this Image on my current Canvas.
By allowing to have only few image on my Canvas I expect to have better performance.
Unfortunately it seems I can't delete completely the WritableBitmap, even if I set it to null.
The following code illustrates it :
//My constructor
public WP8Graphics()
{
//Here my collection DataBinded with the Canvas from the Mainpage
this.UIElements = new ObservableCollection<UIElement>();
//The secondary Canvas
GraphicCanvas = new Canvas();
GraphicCanvas.Height = MainPage.CurrentCanvasHeight;
GraphicCanvas.Width = MainPage.CurrentCanvasWidth;
}
///This method can be hit thousand times, it basically create a rectangle
public void fillRect(int x, int y, int width, int height)
{
// some code
// CREATE THE RECTANGLE rect
GraphicCanvas.Children.Add(rect); // My secondary Canvas
WriteableBitmap wb1 = new WriteableBitmap(GraphicCanvas, null);
wb1.Invalidate();
WriteableBitmap wb2 = new WriteableBitmap((int)MainPage.CurrentCanvasWidth, (int)MainPage.CurrentCanvasHeight);
for (int i = 0; i < wb2.Pixels.Length; i++)
{
wb2.Pixels[i] = wb1.Pixels[i];
}
wb2.Invalidate();
wb1 = null;
Image thumbnail = new Image();
thumbnail.Height = MainPage.CurrentCanvasHeight;
thumbnail.Width = MainPage.CurrentCanvasWidth;
thumbnail.Source = wb2;
this.UIElements.Add(thumbnail);
}
After something like 24 WriteableBitmap created a OutOfMemoryException appears.
I read many articles about this problem and in my case it seems the WriteableBitmap depends on my GraphicCanvas and remains because there still have a reference to it. I can't delete my Graphic Canvas nor set myImage source to null.
I have 2 question :
Is there another way to create an image from Canvas or a collection of UIElements ?
Is it possible to remove the reference who keeps that WriteableBitmap alive ?
I hope to be enough clear and easy to read.
Thank you for reading.
EDITED with atomaras suggestion, but still the same problem
WriteableBitmap wb1 = new WriteableBitmap(GraphicCanvas, null);
This line still throws OutOfMemoryException.
You need to copy the pixels of the original writeablebitmap (that will hold on to the GraphicsCanvas) to a new writeablebitmap.
Take a look at this great post http://www.wintellect.com/blogs/jprosise/silverlight-s-big-image-problem-and-what-you-can-do-about-it
Also why do you keep all of the writeablebitmaps in the UIElements collection? Wouldn't the latest one suffice? Cant you clear the UIElements collection right before adding the latest/new bitmap?
I have a PDF Export in my Application (migradoc). To avoid freezing the GUI i want to run this Export as seperate Thread.
The PDF has also Charts embedded in it. To make those Charts look like the ones i use in my application i create and render them in Code. (visifire)
My Thread is already STA, but i get an Exception when running the WPF render Commands:
The calling thread cannot access this object because a different thread owns it
My Code:
chart.Measure(new Size(311, 180));
chart.Arrange(new Rect(0, 0, 311, 180));
chart.UpdateLayout();
ExportToPng(new Uri("C:\\" + i + "c.png"), chart);
public void ExportToPng(Uri path, Chart surface)
{
if (path == null) return;
// Save current canvas transform
Transform transform = surface.LayoutTransform;
// reset current transform (in case it is scaled or rotated)
surface.LayoutTransform = null;
// Create a render bitmap and push the surface to it
var renderBitmap =
new RenderTargetBitmap(
(int) surface.Width,
(int) surface.Height,
96d,
96d,
PixelFormats.Pbgra32);
renderBitmap.Render(surface);
// Create a file stream for saving image
using (var outStream = new FileStream(path.LocalPath, FileMode.Create))
{
// Use png encoder for our data
var encoder = new PngBitmapEncoder();
// push the rendered bitmap to it
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
// save the data to the stream
encoder.Save(outStream);
}
// Restore previously saved layout
surface.LayoutTransform = transform;
}
I already tried to dispatch this Commands, but i still keep getting the same Error.
DispatcherHelper.UIDispatcher.BeginInvoke((Action)(() =>
{
chart.Measure(new Size(311, 180));
chart.Arrange(new Rect(0, 0, 311, 180));
chart.UpdateLayout();
ExportToPng(new Uri("C:\\" + i + "c.png"), chart);
}));
you need to pass copy of any object which is the part of GUI thread as the GUI thread own them and that's why they can't be access in other thread. like you chart object you need to create a copy of chart object and then pass it into the thread so the owner of the object is your new thread.
If you need to render these on the same GUI thread then only chance is to render these on the same thread and wait to operation to complete.
I have a fair few images that I'm loading into a ListBox in my WPF application. Originally I was using GDI to resize the images (the originals take up far too much memory). That was fine, except they were taking about 400ms per image. Not so fine. So in search of another solution I found a method that uses TransformedBitmap (which inherits from BitmapSource). That's great, I thought, I can use that. Except I'm now getting memory leaks somewhere...
I'm loading the images asynchronously using a BackgroundWorker like so:
BitmapSource bs = ImageUtils.ResizeBitmapSource(ImageUtils.GetImageSource(photo.FullName));
//BitmapSource bs = ImageUtils.GetImageSource(photo.FullName);
bs.Freeze();
this.dispatcher.Invoke(new Action(() => { photo.Source = bs; }));
GetImageSource just gets the Bitmap from the path and then converts to BitmapSource.
Here's the code snippet for ResizeBitmapSource:
const int thumbnailSize = 200;
int width;
int height;
if (bs.Width > bs.Height)
{
width = thumbnailSize;
height = (int)(bs.Height * thumbnailSize / bs.Width);
}
else
{
height = thumbnailSize;
width = (int)(bs.Width * thumbnailSize / bs.Height);
}
BitmapSource tbBitmap = new TransformedBitmap(bs,
new ScaleTransform(width / bs.Width,
height / bs.Height, 0, 0));
return tbBitmap;
That code is essentially the code from:
http://rongchaua.net/blog/c-wpf-fast-image-resize/
Any ideas what could be causing the leak?
edit:
Here's the code for GetImageSource, as requested
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
{
using (var bmp = Image.FromStream(stream, false, false))
{
// Use WPF to resize
var bitmapSource = ConvertBitmapToBitmapSource(bmp);
bitmapSource = ResizeBitmapSource(bitmapSource);
return bitmapSource;
}
}
I think you misunderstood how the TransformedBitmap works. It holds onto a reference to the source bitmap, and transforms it in memory. Maybe you could encode the transformed bitmap into a memory stream, and read it right back out. I'm not sure how fast this would be, but you wouldn't then be holding on to the full sized bitmap.
I found this blog post that returned a WriteableBitmap with the TransformedBitmap as the source. The WriteableBitmap will copy the pixel data to a memory buffer in the initializer, so it doesn't actually hold on to a reference to the TransformedBitmap, or the full sized image.
At a guess, from looking at your code you might need to dispose of the bitmap returned by the call to ImageUtils.GetImageSource(photo.FullName).
I have also noted on the blog you pointed out that the author has added an update (11th of March) about inserting a using statement to prevent memory leaks.