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.
Related
I have this code to add bitmap to a picturebox
pictureBoxTemp.Image = finalImage;
Where finalImage variable is a bitmap holding the image. When I use this method. The result is good. Here is the image for this code:
And then, I decided to add zoom functionality so it is necessary to add a paint event on my picturebox.. This is my code for the paint event:
protected void pictureBox_Paint(object sender, PaintEventArgs e, ref ZoomInfo imageZoom)
{
//Conditions to avoid to proceed further.
if (((PictureBox)sender).Image == null) { return; }
float futurePosX = (imageZoom.translateX) * (1 - imageZoom.zoom) / imageZoom.zoom;
float futurePosY = (imageZoom.translateY) * (1 - imageZoom.zoom) / imageZoom.zoom;
Graphics g = e.Graphics;
g.Clear(Color.Transparent);
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
g.ScaleTransform(imageZoom.zoom, imageZoom.zoom);
g.DrawImage(((PictureBox)sender).Image, futurePosX, futurePosY);
}
By default, futurePosY and futurePosX are both 0 so g.DrawImage() will draw it in 0,0 position. When paint is called, image is changed to this:
Notice that there is now a black region on the bottom of the image which means that the image is clipped.
I don't know for now why DrawImage() is clipping my image but when use pictureBox.Image = Bitmap again, the image fits perfectly on pictureBox. Any suggestions how to correct this?
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.
if i draw some rectangles in panel , how can i select one of them and delete it. My code here i have write it inside panel_OnClick event :
g = panel1.CreateGraphics();
Pen p = new Pen(Color.Black);
p.Width = 2;
g.DrawRectangle(p, e.X, e.Y, 100, 60);
p.Dispose();
g.Dispose();
Drawing on the panel is like drawing on a piece of paper - they are etched in and are no longer a rectangle, but a collection of pixels. Even though you could draw a rectangle over the one you want to clear using the background color, you won't be "removing the rectangle", you'll just draw a rectangle over the existing one.
Rectangle will have a Region.
You will need to subscribe to one of the following: MouseClick, MouseDown, MouseUp.
// assuming you keep a reference of the rectangle
void OnMouseClick(object sender, MouseEventArgs e) {
if(myRect.Region.IsVisible(e.Location) {
// perform action on myRect ...
// have window Invalidate(myRect)
// Refresh() the invalidated area.
}
}
This snippet assumes that no Rectangles overlap. You can also create a GraphicsPath from the points of the Rectangle and then from that path, I believe you can create a Region that enables the actual lines of the rectangle to be selected.
Update per comment
Region
GraphicsPath
I checked and I didn't see the Region property for Rectangle. So, to create the Region do the following:
var gPath = new GraphicsPath();
gPath.AddRectangle(rectangle);
var region = new Region(gPath);
I currently have a windows form app with a pictureBox in the middle of it which i am drawing various images too. The images are drawing fine except for the fact that they are all being scaled up by exactly 25%. I should also add that i am drawing everything inside a Paint method, using the PaintEventArgs to get the graphics device.
Ive made sure the SizeMode is set to Normal, ive checked over and over that the scale factor of the graphics object is 1 and all the image objects that i pass to the paint method are of the size they should be, but when they get drawn they are a different size.
I have until now just been calling g.drawImage(image, Rectangle) and passing the width and height of the image as the width and height of the Rectangle so that they are forced to be drawn at the correct size but i feel that this should be a short term fix and i am overlooking something simple.
Any help would be great, thanks in advance.
Code is as follows (only the important bits):
public class Level : PictureBox
{
...
private Image image;
...
public Level(TabPage parent, Panel propertiesPanel, ItemManager items, string levelName)
{
...
image = Image.FromFile(#"Levels/" + levelName);
Size = image.Size;
SizeMode = PictureBoxSizeMode.Normal;
MouseClick += new MouseEventHandler(level_MouseClick);
MouseMove += new MouseEventHandler(level_MouseMove);
Paint += new PaintEventHandler(level_Paint);
Invalidate();
}
private void level_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
//With the rectangle fix (drawing to correct size)
g.DrawImage(image, new Rectangle(0, 0, image.Size.Width, image.Size.Height));
////Without the fix (as i thought it should be be this is where it scales it)
//g.DrawImage(image, new Point(0, 0));
drawPlacedItems(g);
drawItemPreview(g);
}
This sounds like the HorizontalResolution and VerticalResolution properties of your image are being applied when you don't want them to, modify your code as per Jeremy's link to Image sizing issue in bitmap that ensures that HorizontalResolution and VerticalResolution are reset or ignored before calling DrawImage.
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.