I'm using a thread to get an image from a website and shoot it back to the parent form (WPF) to display. I ran into an issue and have managed to debug it to this example:
public void Watch()
{
while (true)
{
Bitmap bmp = new Bitmap(1, 1);
BitmapImage bmpImg = new BitmapImage();
this.SetImage(bmp, bmpImg);
}
}
public delegate void SetImageCallback(Bitmap bmp, BitmapImage bmpImg);
private void SetImage(Bitmap bmp, BitmapImage bmpImg)
{
if (!this.imgVideo.Dispatcher.CheckAccess())
{
SetImageCallback del = new SetImageCallback(SetImage);
this.Dispatcher.Invoke(del, bmp, bmpImg);
}
else
{
Bitmap bitmap = bmp;
BitmapImage bitmapImage = bmpImg;
}
}
Keep in mind that Watch() runs on its own thread. If I use the bitmap object (which I can use with PictureBox in Window Forms) everything works great. That is, debugging this code, when I get to the line
Bitmap bitmap = bmp;
And inspect the variable bmp, everything is great and works as expected. HOWEVER, when I get to the next line
BitmapImage bitmapImage = bmpImg;
And inpsect the variable bmpImage, I get a ton of System.InvalidOperationException's. When this is in practice and gets assigned to a WPF Image object, it says that "The calling thread cannot access this object because a different thread owns it." Why am I running into this issue with WPF BitmapImages (which are required to set an ImageSource) but NOT in Windows Forms Bitmap objects (which can be used to set a PictureBox)? How do I fix this in WPF?
Most objects in WPF are of this category: they cannot be shared between different threads. However certain low-level resources such as brushes and bitmaps are derived from a special class called Freezable that if frozen can be shared between different threads. Of course once an object is frozen is can no longer be modified in any way. To freeze a freezable object simply call Freeze and this will prevent cross-thread exceptions.
Instead of
if (!this.imgVideo.Dispatcher.CheckAccess())
{
SetImageCallback del = new SetImageCallback(SetImage);
this.Dispatcher.Invoke(del, bmp, bmpImg);
}
try using :
if (!App.Current.Dispatcher.CheckAccess())
App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action<CustomObject>(SetImage),CustomeObjectInstance );
Here Cutom object will be a wrapper class wrapping
Bitmap bmp, BitmapImage bmpImg
Obviously, your SetImage signature will change to
SetImage(CutomObject custObj)
I have not tested the code but this may solve the issue.
Let us know if this works so that some poor soul can be benefitted from this post.
All the best!
Sid
Related
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.
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 some code where I draw a function graph into a PictureBox graphics. The code is implemented in the Paint event. At some point I want to save a bitmap of the content of the graphics in a file.
I already read the answers to this question, and didn't found what I was looking for.
What I need is both to draw in the PictureBox (or any other control that you suggest) so that I don't loose the drawing when the control is hidden or something (so I think I cannot CreateGraphics()) and be able to save that drawing in a button's click event.
I'm willing to put the drawing logic out of the Paint event if necesary.
Thanks in advance.
I went ahead and answered the question based on my assumption,
I created a new Winforms application
I added a panel and a button
I created a Bitmap named buffer, and in the Form1 constructor, initialized it to the size of the panel. Instead of drawing directly to the panel, I draw to the Bitmap, then set the panels background image buffer; This will add persistance to your graphics. If you truly wish to write to a file, I can show you that. Just ask.
To write to file you will need this namespace reference :
using System.IO;
I added the ImageToDisc function you were asking for.
Here is the Code :
Bitmap buffer;
public Form1()
{
InitializeComponent();
panel1.BorderStyle = BorderStyle.FixedSingle;
buffer = new Bitmap(panel1.Width,panel1.Height);
}
private void button1_Click(object sender, EventArgs e)
{
using (Graphics g = Graphics.FromImage(buffer))
{
g.DrawRectangle(Pens.Red, 100, 100,100,100);
}
panel1.BackgroundImage = buffer;
//writes the buffer Bitmap to a binary file, only neccessary if you want to save to disc
ImageToDisc();
//just to prove that it did write it to file and can be loaded I set the mainforms background image to the file
this.BackgroundImage=FileToImage();
}
//Converts the image to a byte[] and writes it to disc
public void ImageToDisc()
{
ImageConverter converter = new ImageConverter();
File.WriteAllBytes(#"c:\test.dat", (byte[])converter.ConvertTo(buffer, typeof(byte[])));
}
//Converts the image from disc to an image
public Bitmap FileToImage()
{
ImageConverter converter = new ImageConverter();
return (Bitmap)converter.ConvertFrom(File.ReadAllBytes(#"c:\test.dat"));
}
I want to set a background image for my form/window like this guy but instead of an image file on disk I've got a System.Drawing.Bitmap in memory.
I need to do something like this:
this.Background = new ImageBrush(new BitmapImage(bmp));
Except BitmapImage won't take a Bitmap, nor will ImageBrush and I'm not sure if any of the others will. There's one called BitmapCacheBrush but I don't know what that does.
Nevermind, I figured it out.
public static Brush CreateBrushFromBitmap(Bitmap bmp)
{
return new ImageBrush(Imaging.CreateBitmapSourceFromHBitmap(bmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()));
}
credit
My scenario is the following. I am creating a little math quiz application for my son and wanted to dynamically change the background ImageBrush of my canvas after each question is answered. I proceeded to embed my images (pngs) into a resource file and figured I would load them into an array and then randomly choose one and load it into the canvas.
The first problem I ran into was of course the fact that in the resource file the images were being stored as Bitmaps. So after some looking around on the Internet I finally figured out how to get them converted from Bitmap to BitmapImage objects using the following helper method:
private BitmapImage FromResourceBitmap(Bitmap bitmap)
{
var result = new BitmapImage();
using(var stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Png);
stream.Position = 0;
result.BeginInit();
result.StreamSource = stream;
result.EndInit();
}
return result;
}
From there I created an ImageBrush from the BitmapImage and assigned it to the Background property of the canvas:
var brush = new ImageBrush {ImageSource = m_Media.Screens[0]}; // m_Media.Screens[x] returns a BitmapImage...obviously.
QuestionCanvas.Background = brush;
Unfortunately, this doesn't seem to work. When the application runs the background is pure white. My XAML doesn't describe any backgrounds and...well I'm confused. Any help would be greatly appreciated! Thank you!
I'm wondering if perhaps your canvas is transparent, or perhaps you have another element on top of the canvas. I would take a look at Snoop on Codeplex to look at your visual tree and identify exactly what is going on. Also consider using triggers or codebinding to set the image for you when you move to the next question. Then you could just bind the Background to the template with the trigger, or the item holding the image and have it automatically update.
After looking through the various ImageBrush related classes, especially that of BitmapImage I started to think that the StreamSource property was simply referencing the stream instead of making a local copy within the BitmapImage object. So, the using statement within my helper method was in effect releasing the stream and therefore making the StreamSource of the BitmapImage null. The canvas then fell back to a plain white (#FFFFFFFF) SolidColorBrush (Thanks James for reminding me of Snoop).
So, to fix this I instead created a private List to hold the references to the various image streams and then point my BitmapImages to those references. I implemented IDisposable to release the various MemoryStreams when the GC came along. Here is the abbreviated code:
public class Media : IDisposable
{
private readonly List<BitmapImage> m_Screens = new List<BitmapImage>();
private readonly List<MemoryStream> m_BackingStreams = new List<MemoryStream>();
public BitmapImage MainScreen { get; private set; }
public List<BitmapImage> Screens
{
get
{
return m_Screens;
}
}
public Media()
{
LoadScreens();
}
private void LoadScreens()
{
m_BackingStreams.Add(FromResourceBitmap(Properties.Resources.Screen_01));
m_BackingStreams.Add(FromResourceBitmap(Properties.Resources.Screen_02));
m_BackingStreams.Add(FromResourceBitmap(Properties.Resources.Screen_03));
m_BackingStreams.Add(FromResourceBitmap(Properties.Resources.Screen_04));
m_BackingStreams.Add(FromResourceBitmap(Properties.Resources.Screen_05));
foreach (var stream in m_BackingStreams)
{
m_Screens.Add(FromResourceStream(stream));
}
}
private BitmapImage FromResourceStream(Stream stream)
{
var result = new BitmapImage();
result.BeginInit();
result.StreamSource = stream;
result.EndInit();
return result;
}
private MemoryStream FromResourceBitmap(Bitmap bitmap)
{
var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream;
}
public void Dispose()
{
if (m_BackingStreams.Count == 0 || m_BackingStreams == null) return;
foreach (var stream in m_BackingStreams)
{
stream.Close();
stream.Flush();
}
}
And here is what it looked like when I actually set the background of my Canvas during runtime:
MainMenuCanvas.Background = new ImageBrush(m_Media.Screens[0]);
That fixed it, as inelegant as it may be.
While researching I did come across this little bit of info in the BitmapImage.StreamSource documentation page on MSDN:
Set the CacheOption property to
BitmapCacheOption.OnLoad if you wish
to close the stream after the
BitmapImage is created. The default
OnDemand cache option retains access
to the stream until the bitmap is
needed, and cleanup is handled by the
garbage collector.
( http://bit.ly/bitmapimagestreamsource )
However, when I tried to use the original solution with the CacheOption enum set to BitmapCacheOption.OnLoad it resulted in the same problem. I may be missing something here, but the obvious isn't so obvious I guess. :)