1

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

image

the saved image looks like this

image

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;
        }
    }
  • 1
    How do you 'draw' ? [DrawToBitmap()](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.drawtobitmap) has limitations, but I guess you are simply *drawing* it wrong (e.g. not in paint event). – Sinatr Jun 26 '19 at 11:42
  • @Sinatr I added the drawing code in the question – Adrianna Tyszkiewicz Jun 26 '19 at 11:46
  • I would expect to see a `Bitmap` somewhere in your code, into which you [draw](https://stackoverflow.com/q/11402862/1997232) and then this bitmap is simply bitblt on screen in `Paint` event of some control. Not sure what is `g` and how (where) is it used. Internally [DrawToBitmap](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,4441da5479e7a6f8) is sending [WM_PRINT](https://learn.microsoft.com/en-us/windows/desktop/gdi/wm-print) and supplying custom context into which `pnl_Draw` has to draw, but it doesn't. – Sinatr Jun 26 '19 at 13:09
  • What happens if you draw something the way you're doing it, you minimize the Form and then restore it? Is all you have drawn still there? – Jimi Jun 26 '19 at 13:21
  • See the notes here: [How to call a method that uses PaintEventArgs](https://stackoverflow.com/a/53708936/7444103). Search SO for these terms, you'll find a host of questions/answers about this topic. Don't consider any answer that implies the use of `CreateGraphics()` to draw shapes. – Jimi Jun 26 '19 at 13:28
  • You can't cache a Graphics object. The only way to get a vaild one is Paint e.Graphics. It will not stay valid after the Paint event finishes. – TaW Jun 26 '19 at 18:35

1 Answers1

-1

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.

sktpin
  • 317
  • 2
  • 15
  • Now, let's say you want to remove one of the shapes you've drawn and preserve the others, how to preceed? Or (slightly more complicated) you want to select a shape and stretch/resize/move it... – Jimi Jun 26 '19 at 16:11
  • That'd be an advanced version of the program ;) I have no idea whether, and if, how, this is done in the OP's code - maybe it's not a requirement. I guess a dead simple, if rather memory consuming way would be to allow a limited number of undows, like 10..20 or so, and just make copies of the whole bitmap and stash it somewhere, and just pop the latest one off it, when you "UNDO". You could do this in a less memory consuming way like stashing away only small rectangular areas where the change happened - like I'm suggesting with the Invalidate rectangle. – sktpin Jun 26 '19 at 16:18
  • Either way, re-drawing the app window from a bitmap when you drag stuff around will be way baster and less ridden with visual artifacts than drawing a bazillion objects newly directly onto a panel. – sktpin Jun 26 '19 at 16:18
  • If you call it "shapes" that you want to manage, not pixel data - you could store everything as vectors. Still, redrawing all vectors onto the panel directly when it gets a redraw call from the system is not an optimal thing to do, no matter what your underlying drawing or storage mechanics are - it seems to me that way anyway. Depending on the exact application requirements, just setting the DoubleBuffer style to true, for the control drawn onto, might also help. But IIRC you need to drive your own class from such a control to call SetStyle on it, or make a new UserControl to start with. – sktpin Jun 26 '19 at 16:22
  • It's not the drawing surface that matters, but the way you handle the objects you're drawing. Drawing on a Panel (configured correctly; yes, you need to set the appropriate styles in the constructor of a Custom Control) or a Bitmap is the same. It's the number of operations that can cause a problem if you don't handle it properly. But you have to, otherwise no undo/delete/color change/whatever is possible. – Jimi Jun 26 '19 at 16:25
  • Yeah, well, my post was intended with this in mind: The OP seems not to know which parts of the program are all important to post to really understand what's going on. I can't comment yet with my rep. So I thought I give a reply of a procedure that I know has worked for me, that will get the OP something that at least basically works, and to build upon for more advanced features. I am aware that this is not a diagnosis of what's actually going wrong with her program. – sktpin Jun 26 '19 at 16:28
  • As you want. My suggestion is to remove this: `Graphics gfx;` (bad thing, also not needed) and see what `g.DrawImage( drawingBitmap, g.ClipRectangle.X, g.ClipRectangle.Y, sourceBounds );` actually does, if that overload is available and mayby explain where this: `private void pnl_Paint(object sender, Graphics g)` comes from, since this is not a recognized event delegate signature (you should have `PaintEventArgs e` there). – Jimi Jun 26 '19 at 16:41
  • _re-drawing the app window from a bitmap when you drag stuff around will be way faster and less ridden with visual artifacts than drawing a bazillion objects newly directly onto a panel._ a) not true for gazillion < 100k b) don't draw onto `Panels` even when they are Double-Buffered. Draw onto `PictureBoxes` or `Labels`(!) !! __Both__ are __meant__ for drawing and therefore __both are `DoubleBuffered`__ by default..!!! – TaW Jun 27 '19 at 05:43
  • @TaW: The panel was not my suggestion per se, I just took it from what the OP already had. Also, you are wrong to say that PictureBox is meant for drawing into - no, it is meant for displaying pictures assigned to it, hence the name... And, gee... nothing says "meant for drawing" more than "Label" I guess. – sktpin Jul 01 '19 at 08:38
  • @Jimi good catch, I corrected the PaintEventArgs thing. I don't see what's wrong with the DrawImage call, I had looked up the specific overload, to paint a portion of the overall bitmap onto a portion of the panel, i.e. refresh only a portion of the whole thing. This is example code, though, not necessarily copy & paste code. – sktpin Jul 01 '19 at 08:45
  • @sktpin You are mistaken. Labels are indeed well-prepared for owner-drawing, which is what all drawing really amounts to. Pictureboxes can combine two images with drawings. Panels otoh are Containers and not prepared for owner-drawing at all. – TaW Jul 01 '19 at 08:52
  • You said *meant for*, not well prepared. Stop twisting words. Anyway, I myself only ever use custom controls for painting, as I see everything else as rather pointless/limited, if one wants to draw everything oneself. As for the drawing speed / artifacts - I may have had a bit of bad bias lately, doing some stuff with the Mono implementation of GDI+, on a very weak embedded Linux system, which is probably not what the OP has. – sktpin Jul 01 '19 at 08:55
  • _You said meant for, not well prepared._ Ain't no diff'rence 'tween the two. – TaW Jul 01 '19 at 09:10
  • Are you serious? "meant for" suggests intention and one should expect corresponding parts of the API documentation e.g. "use feature X of component Y for purpose Z", whereas the fact that you happened to find out that control so-and-so are implemented with doublebuffers does not give any idea of intent from the API designers. I.e. you seem to be suggesting to rely on knowledge of implementation details instead of API reference, which is a huge no no, much worse to tell to a beginner. – sktpin Jul 02 '19 at 11:38