Bitmap repaint in picturebox lagging - c#

I have application in which you can draw some shapes, catch them by vertices and move the vertice. I store vertices of a shapes in the List and repaint whole list of the objects (when the vertice is catch and mouse moves)in the bitmap which is assigned to PictureBox.Image. When I add more than 5 shapes, the moving vertice is lagging. Here is a piece of code:
private void DrawFullList()
{
if (pictureBox2.Image != null)
{
pictureBox2.Image.Dispose();
g.Dispose();
}
graphic = new Bitmap(pictureBox2.Width, pictureBox2.Height);
g = Graphics.FromImage(graphic);
pictureBox2.Image = graphic;
for (int i = 0; i < PointsList.Count; i++)
Draw(BrushList[i], PointsList[i]);
}
private void Draw(Brush brush, Point[] points)
{
Pen PathPen = new Pen(brush);
PathPen.Width = 3;
if (points.Length == 2)
g.DrawLine(PathPen, points[0], points[1]);
else
g.FillPolygon(brush,points);
pictureBox2.Image = graphic;
}
If there anyway to imporve it? I was trying to graphic.Clear(Color.Transparent) but there was no way to change the size of bitmap ( the function is used when we resizing the window).
Any tips?

I found simple mistake what actually makes the lags. pictureBox2.Image = graphic; was executed two times in a row, when PointsList.Count != 0 what was creating laggs.

Your code looks overcomplicated and ineffective. Also your code rely too hard on garbage collector (it is a good practice to dispose Graphics, Brush and Pen classes right after use).
I think in your case best idea is to avoid creating and disposing bitmaps completely. You can replace your PictureBox with, for example, Panel class, subscribe to its Paint event and paint your shapes inside this method. When position of your vertices changes simply call Invalidate method to repaint your shapes inside panel.

Related

Panel to Image (DrawToBtimap doesn't work) [duplicate]

This question already has an answer here:
DrawToBitmap returning blank image
(1 answer)
Closed 3 years ago.
I'm trying to make an application similar to Paint.
Everything works well, but I have a problem with saving the image to a file.
The function for saving works okay, the file saves in the selected location, but it is empty when something is drawn.
It works only when I change the background color, then the image saves with this color.
When I 'draw' something like that
the saved image looks like this
Code:
private void saveToolStripMenuItem_Click(object sender, EventArgs e)
{
int width = pnl_Draw.Width;
int height = pnl_Draw.Height;
Bitmap bm = new Bitmap(width, height);
SaveFileDialog sf = new SaveFileDialog();
sf.Filter = "Bitmap Image (.bmp)|*.bmp|Gif Image (.gif)|*.gif|JPEG Image (.jpeg)|*.jpeg|Png Image (.png)|*.png|Tiff Image (.tiff)|*.tiff|Wmf Image (.wmf)|*.wmf";
sf.ShowDialog();
var path = sf.FileName;
pnl_Draw.DrawToBitmap(bm, new Rectangle(0, 0, width, height));
bm.Save(path, ImageFormat.Jpeg);
}
Drawing:
private void pnl_Draw_MouseMove(object sender, MouseEventArgs e)
{
if(startPaint)
{
//Setting the Pen BackColor and line Width
Pen p = new Pen(btn_PenColor.BackColor,float.Parse(cmb_PenSize.Text));
//Drawing the line.
g.DrawLine(p, new Point(initX ?? e.X, initY ?? e.Y), new Point(e.X, e.Y));
initX = e.X;
initY = e.Y;
}
}
Where is your "g" (System.Drawing.Graphics?) object come from when you DrawLine on it? I also can't see where you fill the background color, but I have the suspiction that your drawing gets discarded/overwritten - but given the little code visible here, it's hard to tell.
I'd just suggest what worked for me in the past:
Use a Bitmap object to draw your lines etc into, not the panel directly. And when you want to save it, just save the bitmap.
To make the drawing visible on your panel, call Invalidate(...) on your panel after a new line stroke was made, with the bounding rectangle around the line stroke as the update-rectangle passed to Invalidate.
In the OnPaint handler of your panel, then make sure to only draw that portion that's new, e.g. that rectangle I mentioned. This will be passed as the clip bounds of the OnPaint call. If only the changed portion of the whole image is drawn in the OnPaint handler, it will be much faster than always drawing the whole bitmap onto the panel.
For that, you need to creage a graphics object from the drawn-to bitmap, and keep both the bitmap and the graphics object alive throughout your drawing session (i.e. don't let it get garbage collected by not having references to them somewhere)
Something roughly like this:
// assuming you're all doing this directly in your main form, as a simple experimental app:
// Somewhere in your form class:
Bitmap drawingBitmap;
Graphics gfx;
// in your form class' constructor, AFTER the InitializeComponent() call, so the sizes are known:
// using a pixelformat which usually yields good speed vs. non-premultiplied ones
drawingBitmap = new Bitmap( pnl.Width, pnl.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb );
gfx = Graphics.FromImage( drawingBitmap );
private void pnl_Draw_MouseMove(object sender, MouseEventArgs e)
{
if(startPaint)
{
//Setting the Pen BackColor and line Width
Pen p = new Pen(btn_PenColor.BackColor,float.Parse(cmb_PenSize.Text));
//Drawing the line.
var p1 = new Point(initX ?? e.X, initY ?? e.Y);
var p2 = new Point(e.X, e.Y);
gfx.DrawLine( p, p1, p2 ); // !!! NOTE: gfx instance variable is used
initX = e.X;
initY = e.Y;
// makes the panel's Paint handler being called soon - with a clip rectangle just covering your new little drawing stroke, not more.
pnl.Invalidate( new Rectangle( p1.X, p1.Y, 1+p2.X-p1.X, 1+p2.Y-p1.Y ));
}
}
private void pnl_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
// use the clip bounds of the Graphics object to draw only the relevant area, not always everything.
var sourceBounds = new RectangleF( g.ClipRectangle.X, g.ClipRectangle.Y, g.ClipRectangle.Width, g.ClipRectangle.Height );
// The bitmap and drawing panel are assumed to have the same size, to make things easier for this example.
// Actually you might want have a drawing bitmap of a totally unrelated size, and need to apply scaling to a setting changeable by the user.
g.DrawImage( drawingBitmap, g.ClipRectangle.X, g.ClipRectangle.Y, sourceBounds );
}
Here is a little bit about that, although pretty old article. But GDI+ hasn't change that much since then, as it's a wrapper around the Win32 drawing stuff.
https://www.codeproject.com/Articles/1355/Professional-C-Graphics-with-GDI
NOTE: What I don't have time right now to check and do not remember is whether you are allowed to, at the same time:
have an "open" graphics object of a bitmap and draw into the bitmap
paint that same bitmap onto something else (like the panel)
save the bitmap to file
You'd have to check that, and manage the existence of the graphics object with its claws on your drawing bitmap accordingly.

C# Picturebox Image is null when I draw a shape in Paint event [duplicate]

in a UserControl I have a PictureBox and some other controls. For the user control which contains this picturebox named as Graph I have a method to draw a curve on this picture box:
//Method to draw X and Y axis on the graph
private bool DrawAxis(PaintEventArgs e)
{
var g = e.Graphics;
g.DrawLine(_penAxisMain, (float)(Graph.Bounds.Width / 2), 0, (float)(Graph.Bounds.Width / 2), (float)Bounds.Height);
g.DrawLine(_penAxisMain, 0, (float)(Graph.Bounds.Height / 2), Graph.Bounds.Width, (float)(Graph.Bounds.Height / 2));
return true;
}
//Painting the Graph
private void Graph_Paint(object sender, PaintEventArgs e)
{
base.OnPaint(e);
DrawAxis(e);
}
//Public method to draw curve on picturebox
public void DrawData(PointF[] points)
{
var bmp = Graph.Image;
var g = Graphics.FromImage(bmp);
g.DrawCurve(_penAxisMain, points);
Graph.Image = bmp;
g.Dispose();
}
When application starts, the axis are drawn. but when I call the DrawData method I get the exception that says bmp is null. What can be the problem?
I also want to be able to call DrawData multiple times to show multiple curves when user clicks some buttons. What is the best way to achive this?
Thanks
You never assigned Image, right? If you want to draw on a PictureBox’ image you need to create this image first by assigning it a bitmap with the dimensions of the PictureBox:
Graph.Image = new System.Drawing.Bitmap(Graph.Width, Graph.Height);
You only need to do this once, the image can then be reused if you want to redraw whatever’s on there.
You can then subsequently use this image for drawing. For more information, refer to the documentation.
By the way, this is totally independent from drawing on the PictureBox in the Paint event handler. The latter draws on the control directly, while the Image serves as a backbuffer which is painted on the control automatically (but you do need to invoke Invalidate to trigger a redraw, after drawing on the backbuffer).
Furthermore, it makes no sense to re-assign the bitmap to the PictureBox.Image property after drawing. The operation is meaningless.
Something else, since the Graphics object is disposable, you should put it in a using block rather than disposing it manually. This guarantees correct disposing in the face of exceptions:
public void DrawData(PointF[] points)
{
var bmp = Graph.Image;
using(var g = Graphics.FromImage(bmp)) {
// Probably necessary for you:
g.Clear();
g.DrawCurve(_penAxisMain, points);
}
Graph.Invalidate(); // Trigger redraw of the control.
}
You should consider this as a fixed pattern.

Existing Graphics into Bitmap

I'm writing a plugin for a trading software (C#, winforms, .NET 3.5) and I'd like to draw a crosshair cursor over a panel (let's say ChartPanel) which contains data that might be expensive to paint. What I've done so far is:
I added a CursorControl to the panel
this CursorControl is positioned over the main drawing panel so that it covers it's entire area
it's Enabled = false so that all input events are passed to the parent
ChartPanel
it's Paint method is implemented so that it draws lines from top to bottom and from left to right at current mouse position
When MouseMove event is fired, I have two possibilities:
A) Call ChartPanel.Invalidate(), but as I said, the underlying data may be expensive to paint and this would cause everything to redraw everytime I move a mouse, which is wrong (but it is the only way I can make this work now)
B) Call CursorControl.Invalidate() and before the cursor is drawn I would take a snapshot of currently drawn data and keep it as a background for the cursor that would be just restored everytime the cursor needs to be repainted ... the problem with this is ... I don't know how to do that.
2.B. Would mean to:
Turn existing Graphics object into Bitmap (it (the Graphics) is given to me through Paint method and I have to paint at it, so I just can't create a new Graphics object ... maybe I get it wrong, but that's the way I understand it)
before the crosshair is painted, restore the Graphics contents from the Bitmap and repaint the crosshair
I can't control the process of painting the expensive data. I can just access my CursorControl and it's methods that are called through the API.
So is there any way to store existing Graphics contents into Bitmap and restore it later? Or is there any better way to solve this problem?
RESOLVED: So after many hours of trial and error I came up with a working solution. There are many issues with the software I use that can't be discussed generally, but the main principles are clear:
existing Graphics with already painted stuff can't be converted to Bitmap directly, instead I had to use panel.DrawToBitmap method first mentioned in #Gusman's answer. I knew about it, I wanted to avoid it, but in the end I had to accept, because it seems to be the only way
also I wanted to avoid double drawing of every frame, so the first crosshair paint is always drawn directly to the ChartPanel. After the mouse moves without changing the chart image I take a snapshow through DrawToBitmap and proceed as described in chosen answer.
The control has to be Opaque (not enabled Transparent background) so that refreshing it doesn't call Paint on it's parent controls (which would cause the whole chart to repaint)
I still experience occasional flicker every few seconds or so, but I guess I can figure that out somehow. Although I picked Gusman's answer, I would like to thank everyone involved, as I used many other tricks mentioned in other answers, like the Panel.BackgroundImage, use of Plot() method instead of Paint() to lock the image, etc.
This can be done in several ways, always storing the graphics as a Bitmap. The most direct and efficient way is to let the Panel do all the work for you.
Here is the idea: Most winforms Controls have a two-layered display.
In the case of a Panel the two layers are its BackgroundImage and its Control surface.
The same is true for many other controls, like Label, CheckBox, RadioButton or Button.
(One interesting exception is PictureBox, which in addition has an (Foreground) Image. )
So we can move the expensive stuff into the BackgroundImage and draw the crosshair on the surcafe.
In our case, the Panel, all nice extras are in place and you could pick all values for the BackgroundImageLayout property, including Tile, Stretch, Center or Zoom. We choose None.
Now we add one flag to your project:
bool panelLocked = false;
and a function to set it as needed:
void lockPanel( bool lockIt)
{
if (lockIt)
{
Bitmap bmp = new Bitmap(panel1.ClientSize.Width, panel1.ClientSize.Width);
panel1.DrawToBitmap(bmp, panel1.ClientRectangle);
panel1.BackgroundImage = bmp;
}
else
{
if (panel1.BackgroundImage != null)
panel1.BackgroundImage.Dispose();
panel1.BackgroundImage = null;
}
panelLocked = lockIt;
}
Here you can see the magic at work: Before we actually lock the Panel from doing the expensive stuff, we tell it to create a snapshot of its graphics and put it into the BackgroundImage..
Now we need to use the flag to control the Paint event:
private void panel1_Paint(object sender, PaintEventArgs e)
{
Size size = panel1.ClientSize;
if (panelLocked)
{
// draw a full size cross-hair cursor over the whole Panel
// change this to suit your own needs!
e.Graphics.DrawLine(Pens.Red, 0, mouseCursor.Y, size.Width - 1, mouseCursor.Y);
e.Graphics.DrawLine(Pens.Red, mouseCursor.X, 0, mouseCursor.X, size.Height);
}
// expensive drawing, you insert your own stuff here..
else
{
List<Pen> pens = new List<Pen>();
for (int i = 0; i < 111; i++)
pens.Add(new Pen(Color.FromArgb(R.Next(111),
R.Next(111), R.Next(111), R.Next(111)), R.Next(5) / 2f));
for (int i = 0; i < 11111; i++)
e.Graphics.DrawEllipse(pens[R.Next(pens.Count)], R.Next(211),
R.Next(211), 1 + R.Next(11), 1 + R.Next(11));
}
}
Finally we script the MouseMove of the Panel:
private void panel1_MouseMove(object sender, MouseEventArgs e)
{
mouseCursor = e.Location;
if (panelLocked) panel1.Invalidate();
}
using a second class level variable:
Point mouseCursor = Point.Empty;
You call lockPanel(true) or lockPanel(false) as needed..
If you implement this directly you will notice some flicker. This goes away if you use a double-buffered Panel:
class DrawPanel : Panel
{
public DrawPanel() { this.DoubleBuffered = true; }
}
This moves the crosshair over the Panels in a perfectly smooth way. You may want to turn on & off the Mouse cursor upon MouseLeave and MouseEnter..
Why don't you clone all the graphics in the ChartPanel over your CursorControl?
All the code here must be placed inside your CursorControl.
First, create a property which will hold a reference to the chart and hook to it's paint event, something like this:
ChartPanel panel;
public ChartPanel Panel
{
get{ return panel; }
set{
if(panel != null)
panel.Paint -= CloneAspect;
panel = value;
panel.Paint += CloneAspect;
}
}
Now define the CloneAspect function which will render the control's appearance to a bitmap whenever a Paint opperation has been done in the Chart panel:
Bitmap aspect;
void CloneAspect(object sender, PaintEventArgs e)
{
if(aspect == null || aspect.Width != panel.Width || aspect.Height != panel.Height)
{
if(aspect != null)
aspect.Dispose();
aspect = new Bitmap(panel.Width, panel.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
}
panel.DrawToBitmap(aspect, new Rectangle(0,0, panel.Width, panel.Height);
}
Then in the OnPaint overriden method do this:
public override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawImage(aspect);
//Now draw the cursor
(...)
}
And finally wherever you create the chart and the customcursor you do:
CursorControl.Panel = ChartPanel;
And voila, you can redraw as many times you need without recalculating the chart's content.
Cheers.

Drawing on PictureBox

in a UserControl I have a PictureBox and some other controls. For the user control which contains this picturebox named as Graph I have a method to draw a curve on this picture box:
//Method to draw X and Y axis on the graph
private bool DrawAxis(PaintEventArgs e)
{
var g = e.Graphics;
g.DrawLine(_penAxisMain, (float)(Graph.Bounds.Width / 2), 0, (float)(Graph.Bounds.Width / 2), (float)Bounds.Height);
g.DrawLine(_penAxisMain, 0, (float)(Graph.Bounds.Height / 2), Graph.Bounds.Width, (float)(Graph.Bounds.Height / 2));
return true;
}
//Painting the Graph
private void Graph_Paint(object sender, PaintEventArgs e)
{
base.OnPaint(e);
DrawAxis(e);
}
//Public method to draw curve on picturebox
public void DrawData(PointF[] points)
{
var bmp = Graph.Image;
var g = Graphics.FromImage(bmp);
g.DrawCurve(_penAxisMain, points);
Graph.Image = bmp;
g.Dispose();
}
When application starts, the axis are drawn. but when I call the DrawData method I get the exception that says bmp is null. What can be the problem?
I also want to be able to call DrawData multiple times to show multiple curves when user clicks some buttons. What is the best way to achive this?
Thanks
You never assigned Image, right? If you want to draw on a PictureBox’ image you need to create this image first by assigning it a bitmap with the dimensions of the PictureBox:
Graph.Image = new System.Drawing.Bitmap(Graph.Width, Graph.Height);
You only need to do this once, the image can then be reused if you want to redraw whatever’s on there.
You can then subsequently use this image for drawing. For more information, refer to the documentation.
By the way, this is totally independent from drawing on the PictureBox in the Paint event handler. The latter draws on the control directly, while the Image serves as a backbuffer which is painted on the control automatically (but you do need to invoke Invalidate to trigger a redraw, after drawing on the backbuffer).
Furthermore, it makes no sense to re-assign the bitmap to the PictureBox.Image property after drawing. The operation is meaningless.
Something else, since the Graphics object is disposable, you should put it in a using block rather than disposing it manually. This guarantees correct disposing in the face of exceptions:
public void DrawData(PointF[] points)
{
var bmp = Graph.Image;
using(var g = Graphics.FromImage(bmp)) {
// Probably necessary for you:
g.Clear();
g.DrawCurve(_penAxisMain, points);
}
Graph.Invalidate(); // Trigger redraw of the control.
}
You should consider this as a fixed pattern.

C# Out of Memory when Creating Bitmap

I'm creating an application (Windows Form) that allows the user to take a screenshot based on the locations they choose (drag to select area). I wanted to add a little "preview pane" thats zoomed in so the user can select the area they want more precisely (larger pixels). On a mousemove event i have a the following code...
private void falseDesktop_MouseMove(object sender, MouseEventArgs e)
{
zoomBox.Image = showZoomBox(e.Location);
zoomBox.Invalidate();
bmpCrop.Dispose();
}
private Image showZoomBox(Point curLocation)
{
Point start = new Point(curLocation.X - 50, curLocation.Y - 50);
Size size = new Size(100, 90);
Rectangle rect = new Rectangle(start, size);
Image selection = cropImage(falseDesktop.Image, rect);
return selection;
}
private static Bitmap bmpCrop;
private static Image cropImage(Image img, Rectangle cropArea)
{
if (cropArea.Width != 0 && cropArea.Height != 0)
{
Bitmap bmpImage = new Bitmap(img);
bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat);
bmpImage.Dispose();
return (Image)(bmpCrop);
}
return null;
}
The line that fails and has the Out of Memory exception is:
bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat);
Basically what this does is it takes a 100x90 rectangle around the mouse pointer and pulls that into the zoomBox, which is a picturebox control. However, in the process, i get an Out Of Memory error. What is it that i am doing incorrectly here?
Thanks for your assistance.
Out of memory in C# imaging, is usually sign of wrong rect or point - a bit of red herring. I bet start has negative X or Y when error happens or the Size.Hight + Y or Size.Width + X is bigger than Hight or width of the image.
MSDN explains that an OutOfMemoryException means
rect is outside of the source bitmap bounds
where rect is the first parameter to the Bitmap.Clone method.
So check that the cropArea parameter is not larger than your image.
In GDI+ an OutOfMemoryException does not really mean "out of memory"; the GDI+ error code OufOfMemory has been overloaded to mean different things. The reasons for this are historic and a well described by Hans Passant in another answer.
Use the Bitmap object like this:
using (Bitmap bmpImage = new Bitmap(img))
{
// Do something with the Bitmap object
}
you should check if curLocation.X is larger than 50, otherwise your rectangle will start in the negative area (and of course curLocation.Y)
If the zoom box goes off the edge of the desktop area, then when you try to crop, you are asking the system to make a new image that includes pixels outside of the video memory area. Make sure to limit your zoom box so that none of its extents is less than 0 or greater than the screen edges.
If you are creating new bitmaps over and over, you might need to call GC.Collect(); which will force C# to garbage collect

Categories

Resources