c# Drawing with gdi32.dll vs System.Drawing.Graphics

3.4k Views Asked by At

So I have some code that creates a highlight effect on top of a picture box with gdi32.dll and I am wondering if there is a simpler way to do it with System.Drawing.Graphics? Basically with the gdi32.dll, I have to capture a screen shot after drawing, post that to my picturebox and then I am able to draw more stuff and change the color of the pen that I use. If I just try to change the pen thickness and color and draw on the screen again, if changes what I already drew.

Now I have a version of this that uses System.Drawing.Graphics and lots of math with FillPolygon but if I draw over an area I already drew on, it just makes the area I drew on darker. It does not do this with gdi32.dll which justr shades as long as you have not already shaded the area with the mouse. Any suggestions?

public partial class Form9 : Form
{
    private bool is_mouse_down { get; set; } // Will check if the mouse is down or not.

    private Color Pen_Color = new Color();

    private int Pen_Type { get; set; }

    private int Thickness { get; set; }

    private bool Start { get; set; }

    List<Point> Points = new List<Point>();

    public Form9()
    {
        InitializeComponent();

        pictureBox1.Dock = DockStyle.Fill;

        Pen_Color = Color.Blue;
        Pen_Type = 13; // Type = 9 for highlighter, Type = 13 for solid.
        Thickness = 2;
        Start = false;

        pictureBox1.MouseDown += pictureBox1_MouseDown;
        pictureBox1.MouseUp += pictureBox1_MouseUp;
        pictureBox1.MouseMove += pictureBox1_MouseMove;
        pictureBox1.Paint += pictureBox1_OnPaint;
    }

    private void DrawHighlight(Graphics g, Point[] usePoints, int brushSize, int penType, Color brushColor)
    {
        int useColor = System.Drawing.ColorTranslator.ToWin32(brushColor);
        IntPtr pen = GetImage.GDI32.CreatePen(GetImage.GDI32.PS_SOLID, brushSize, (uint)useColor);
        IntPtr hDC = g.GetHdc();
        IntPtr xDC = GetImage.GDI32.SelectObject(hDC, pen);
        GetImage.GDI32.SetROP2(hDC, penType);//GetImage.GDI32.R2_MASKPEN);
        for (int i = 1; i <= usePoints.Length - 1; i++)
        {
            Point p1 = usePoints[i - 1];
            Point p2 = usePoints[i];
            GetImage.GDI32.MoveToEx(hDC, p1.X, p1.Y, IntPtr.Zero);
            GetImage.GDI32.LineTo(hDC, p2.X, p2.Y);
        }
        GetImage.GDI32.SetROP2(hDC, GetImage.GDI32.R2_COPYPEN);
        GetImage.GDI32.SelectObject(hDC, xDC);
        GetImage.GDI32.DeleteObject(pen);
        g.ReleaseHdc(hDC);
    }

    private void pictureBox1_OnPaint(object sender, PaintEventArgs e)
    {
        if (Start)
        {
            base.OnPaint(e);
            if (is_mouse_down)
            {
                DrawHighlight(e.Graphics, Points.ToArray(), Thickness, Pen_Type, Pen_Color);
            }
        }
    }

    private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
    {
        Points.Clear();

        Start = true;
        is_mouse_down = true;
    }

    private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
    {
        is_mouse_down = false;

        using (Image img = CaptureScreen())
        {
            try
            {
                if (System.IO.File.Exists(Program.ProgramPath + @"\Temp\marked.bmp"))
                {
                    System.IO.File.Delete(Program.ProgramPath + @"\Temp\marked.bmp");
                }
            }
            catch (Exception Ex)
            {
                MessageBox.Show("File Delete Error" + Environment.NewLine + Convert.ToString(Ex));
            }

            try
            {
                img.Save(Program.ProgramPath + @"\Temp\marked.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
            }
            catch (Exception Ex)
            {
                MessageBox.Show("Unable to save Screenshot" + Environment.NewLine + Convert.ToString(Ex));
            }
        }

        if (System.IO.File.Exists(Program.ProgramPath + @"\Temp\marked.bmp"))
        {
            using (FileStream fs = new System.IO.FileStream(Program.ProgramPath + @"\Temp\marked.bmp", System.IO.FileMode.Open, System.IO.FileAccess.Read, FileShare.Read))
            {
                pictureBox1.Image = Image.FromStream(fs);
            }
        }

        pictureBox1.Invalidate(); // Refreshes picturebox image.
    }

    public Image CaptureScreen()
    {
        GetImage gi = new GetImage();

        return gi.CaptureWindow(GetImage.User32.GetDesktopWindow());
    }

    private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
    {
        if (is_mouse_down == true) // Check to see if the mouse button is down while moving over the form.
        {
            Points.Add(new Point(e.X, e.Y));

            pictureBox1.Invalidate(); // Refreshes picturebox image.
        }
    }

Here are a couple photos of what I am talking about:

Using System.Drawing.Graphics: Using <code>System.Drawing.Graphics</code>

Using gdi32.dll: Using <code>gdi32.dll</code>

UPDATE

After testing some of your code...I got a some strange stuff.

enter image description here

1

There are 1 best solutions below

7
On BEST ANSWER

This is a way to draw multiple independent stroke of semi-transparent color without piling up the alpha:

enter image description here

It uses tow lists, one for the strokes and one for the current stroke:

List<List<Point>> strokes = new List<List<Point>>();
List<Point> currentStroke = new List<Point>();

They are filled in the usual way

private void canvas_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left))
    {
        currentStroke.Add(e.Location);
        if (currentStroke.Count == 1)
            currentStroke.Add(new Point(currentStroke[0].X + 1, 
                                        currentStroke[0].Y));
        canvasInvalidate();

    }
}

private void canvas_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left))
    {
        currentStroke.Add(e.Location);
        canvas.Invalidate();
    }
}

private void canvas_MouseUp(object sender, MouseEventArgs e)
{
    if (currentStroke.Count > 1)
    {
        strokes.Add(currentStroke.ToList());
        currentStroke.Clear();
    }
    canvas.Invalidate();
}

In this version we avoid overlay effects of overlapping strokes by drawing all pixels in only one call. All the pixels are painted by creating a GraphicsPath from the strokes and and filling it:

private void canvas_Paint(object sender, PaintEventArgs e)
{
    if (strokes.Count > 0 || currentStroke.Count > 0)
    {
        GraphicsPath gp = new GraphicsPath();
        gp.FillMode = FillMode.Winding;
        if (currentStroke.Count > 0)
        {
            gp.AddCurve(currentStroke.ToArray());
            gp.CloseFigure();
        }

        foreach (var stroke in strokes)
        {
            gp.AddCurve(stroke.ToArray());
            gp.CloseFigure();
        }
        using (SolidBrush b = new SolidBrush(Color.FromArgb(77, 177, 99, 22)))
        {
            e.Graphics.FillPath(b, gp);
        }
    }
}

Note that you should take care not to move back onto the current stroke while drawing it or else the croosing path parts will create holes!

A Clear or Save Button are simple, the former Clears the two list and invalidates, the latter would use DrawToBitmap to save the control..

Note: To avoid flicker do make sure the canval Panel is DoubleBuffered!

Update:

Here is another way that uses a Pen to draw the overlay. To avoid the piling up of alpha and changed color values (depending on the PixelFormat) it uses a fast function to modify all set pixels in the overlay to have the same overlay color:

The stroke collection code is the same. The Paint is reduced to calling a function to create an overlay bitmap and drawing it:

private void canvas_Paint(object sender, PaintEventArgs e)
{
    using (Bitmap bmp = new Bitmap(canvas.ClientSize.Width, 
                                   canvas.ClientSize.Height, PixelFormat.Format32bppPArgb))
    {
        PaintToBitmap(bmp);
        e.Graphics.DrawImage(bmp, 0, 0);
    }

The first function does the drawing, pretty much like before, but with simple pen strokes:

private void PaintToBitmap(Bitmap bmp)
{
    Color overlayColor = Color.FromArgb(77, 22, 99, 99);
    using (Graphics g = Graphics.FromImage(bmp))
    using (Pen p = new Pen(overlayColor, 15f))
    {
        p.MiterLimit = p.Width / 2;
        p.EndCap = LineCap.Round;
        p.StartCap = LineCap.Round;
        p.LineJoin = LineJoin.Round;
        g.SmoothingMode = SmoothingMode.AntiAlias;
        if (currentStroke.Count > 0)
        {
            g.DrawCurve(p, currentStroke.ToArray());
        }

        foreach (var stroke in strokes)
            g.DrawCurve(p, stroke.ToArray());
    }
    SetAlphaOverlay(bmp, overlayColor);
}

It also calls the function that 'flattens' all set pixels to the overlay color:

void SetAlphaOverlay(Bitmap bmp, Color col)
{
    Size s = bmp.Size;
    PixelFormat fmt = bmp.PixelFormat;
    Rectangle rect = new Rectangle(Point.Empty, s);
    BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, fmt);
    int size1 = bmpData.Stride * bmpData.Height;
    byte[] data = new byte[size1];
    System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, data, 0, size1);
    for (int y = 0; y < s.Height; y++)
    {
        for (int x = 0; x < s.Width; x++)
        {
            int index = y * bmpData.Stride + x * 4;
            if (data[index + 0] + data[index + 1] + data[index + 2] > 0)
            {

                data[index + 0] = col.B;
                data[index + 1] = col.G;
                data[index + 2] = col.R;
                data[index + 3] = col.A;
            }
        }
    }
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bmpData.Scan0, data.Length);
    bmp.UnlockBits(bmpData);
}

It uses LockBits, so it is pretty fast..

Here it is in action:

enter image description here

Update 2:

Just for the fun of it here is an extension of only a few lines that adds the option of drawing filled curves:

The fill mode is stored in a cheapo hack by having the 1st element twice. These are the changes:

In the MouseDown:

        currentStroke.Add(e.Location);
        if (cbx_Fill.Checked) 
            currentStroke.Add(e.Location);

And in the PaintToBitmap:

        g.SmoothingMode = SmoothingMode.AntiAlias;
        if (currentStroke.Count > 0)
        {
            if (cbx_Fill.Checked)
                g.FillClosedCurve(b,  currentStroke.ToArray());
            else
                g.DrawCurve(p, currentStroke.ToArray());
        }

        foreach (var stroke in strokes)
            if (stroke[0]==stroke[1])
                g.FillClosedCurve(b,  stroke.ToArray());
            else
                g.DrawCurve(p, stroke.ToArray());

And one more demo:

enter image description here