3

Let me preface this with a real life product; You may remember in Elementary school, they had scratch paper which basically consisted of a rainbow-colored sheet of paper with a black film on top. You would take a sharp object and peel away the black film to expose the colored paper.

I am attempting to do the same thing using images in a picture box.

My idea consists of these things:

  • A textured image.
  • A black rectangle the size of the picture box.
  • A circle image.

What I am trying to achieve is to open a program, have an image drawn to a picture box with the black rectangle on top of it. Upon clicking the picture box it uses the circle to invert the alpha of the rectangle where I click using the circle as a reference.

  • My Problem- I cannot figure out any way to erase (set the transparency of) a part of the black rectangle where I click.

For the life of me, I do not know of any method to cut a window in an image. It is almost like a reverse crop, where I keep the exterior elements rather than the interior, exposing the textured image below.

Can WinForms not do this? Am I crazy? Should I just give up?

I should mention that I prefer not to have to change alpha on a pixel per pixel basis. It would slow the program down far too much to be used as a pseudo-painter. If that is the only way, however, feel free to show.

Here is an image of what I'm trying to achieve:

enter image description here

TaW
  • 53,122
  • 8
  • 69
  • 111
  • Windows Forms is probably not going to help you implement this, though you can always custom draw a control. WPF may give you better tools for the job, though I'm not sure about this specific task. – Eric J. Sep 03 '15 at 03:44

1 Answers1

7

This is not really hard:

  • Set the colored image as a PictureBox's BackgroundImage.
  • Set a black image as its Image.
  • And draw into the image using the normal mouse events and a transparent Pen..

enter image description here

We need a point list to use DrawCurve:

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

We need to prepare and clear the the black layer:

private void ClearSheet()
{
    if (pictureBox1.Image != null) pictureBox1.Image.Dispose();
    Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);
    using (Graphics G = Graphics.FromImage(bmp)) G.Clear(Color.Black);
    pictureBox1.Image = bmp;
    currentLine.Clear();
}

private void cb_clear_Click(object sender, EventArgs e)
{
    ClearSheet();
}

To draw into the Image we need to use an associated Graphics object..:

void drawIntoImage()
{
    using (Graphics G = Graphics.FromImage(pictureBox1.Image))
    {
        // we want the tranparency to copy over the black pixels
        G.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
        G.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        G.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;

        using (Pen somePen = new Pen(Color.Transparent, penWidth))
        {
            somePen.MiterLimit = penWidth / 2;
            somePen.EndCap = System.Drawing.Drawing2D.LineCap.Round;
            somePen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
            somePen.StartCap = System.Drawing.Drawing2D.LineCap.Round;
            if (currentLine.Count > 1)
                G.DrawCurve(somePen, currentLine.ToArray());
        }

    }
    // enforce the display:
    pictureBox1.Image = pictureBox1.Image;
}

The usual mouse events:

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    currentLine.Add(e.Location);
}

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        currentLine.Add(e.Location);
        drawIntoImage();
    }  
}

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    currentLine.Clear();
}

That's all that's needed. Make sure to keep the PB's SizeMode = Normal or else the pixels won't match..!

Note that there are a few challenges when you want to get soft edges, more painting tools, letting a simple click paint a dot or an undo or other finer details to work. But the basics are not hard at all..

Btw, changing Alpha is not any different from changing the color channels.

As an alternative you may want to play with a TextureBrush:

TextureBrush brush = new TextureBrush(pictureBox1.BackgroundImage);

using (Pen somePen = new Pen(brush) )
{
  // basically 
  // the same drawing code.. 
}

But I found this to be rather slow.

Update:

Using a png-file as a custom tip is a little harder; the main reason is that the drawing is reversed: We don't want to draw the pixels, we want to clear them. GDI+ doesn't support any such composition modes, so we need to do it in code.

To be fast we use two tricks: LockBits will be as fast as it gets and restricting the area to our custom brush tip will prevent wasting time.

Let's assume you have a file to use and load it into a bitmap:

string stampFile = @"yourStampFile.png";
Bitmap stamp = null;

private void Form1_Load(object sender, EventArgs e)
{
    stamp = (Bitmap) Bitmap.FromFile(stampFile);
}

Now we need a new function to draw it into our Image; instead of DrawCurve we need to use DrawImage:

void stampIntoImage(Point pt)
{
    Point point =  new Point(pt.X - stamp.Width / 2, pt.Y - stamp.Height / 2);
    using (Bitmap stamped = new Bitmap(stamp.Width, stamp.Height) )
    {
        using (Graphics G = Graphics.FromImage(stamped))
        {
            stamp.SetResolution(stamped.HorizontalResolution, stamped.VerticalResolution);
            G.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
            G.DrawImage(pictureBox1.Image, 0, 0, 
                        new Rectangle(point, stamped.Size), GraphicsUnit.Pixel);
            writeAlpha(stamped, stamp);
        }
        using (Graphics G = Graphics.FromImage(pictureBox1.Image))
        {
            G.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
            G.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            G.CompositingQuality = 
               System.Drawing.Drawing2D.CompositingQuality.HighQuality;
            G.DrawImage(stamped, point);
        }
    }
    pictureBox1.Image = pictureBox1.Image;
}

A few notes: I found that I hat to do an explicit SetResolution since the stamp file I photoshopped was 72dpi and the default Bitmaps in my program were 120dpi. Watch out for these differences!

I start the Bitmap to be drawn by copying the right part of the current Image.

Then I call a fast routine that applies the alpha of the stamp to it:

void writeAlpha(Bitmap target, Bitmap source)
{
   // this method assumes the bitmaps both are 32bpp and have the same size
    int Bpp = 4;  
    var bmpData0 = target.LockBits(
                    new Rectangle(0, 0, target.Width, target.Height),
                    ImageLockMode.ReadWrite, target.PixelFormat);
    var bmpData1 = source.LockBits(
                    new Rectangle(0, 0, source.Width, source.Height),
                    ImageLockMode.ReadOnly, source.PixelFormat);

    int len = bmpData0.Height * bmpData0.Stride;
    byte[] data0 = new byte[len];
    byte[] data1 = new byte[len];
    Marshal.Copy(bmpData0.Scan0, data0, 0, len);
    Marshal.Copy(bmpData1.Scan0, data1, 0, len);

    for (int i = 0; i < len; i += Bpp)
    {
        int tgtA = data0[i+3];        // opacity
        int srcA = 255 - data1[i+3];  // transparency
        if (srcA > 0) data0[i + 3] = (byte)(tgtA < srcA ? 0 : tgtA - srcA);
    }
    Marshal.Copy(data0, 0, bmpData0.Scan0, len);
    target.UnlockBits(bmpData0);
    source.UnlockBits(bmpData1);
}

I use a simple rule: Reduce target opacity by the source transparency and make sure we don't get negative.. You may want to play around with it.

Now all we need is to adapt the MouseMove; for my tests I have added two RadioButtons to switch between the original round pen and the custom stamp tip:

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        if (rb_pen.Checked)
        {
            currentLine.Add(e.Location);
            drawIntoImage();
        }
        else if (rb_stamp.Checked) { stampIntoImage(e.Location); };
    }
}

I didn't use a fish but you can see the soft edges:

enter image description here

Update 2: Here is a MouseDown that allows for simple clicks:

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
   if (rb_pen.Checked) currentLine.Add(e.Location);
   else if (rb_stamp.Checked)
   {
       { stampIntoImage(e.Location); };
   }
}
TaW
  • 53,122
  • 8
  • 69
  • 111
  • This is just about everything I was looking for with one exception. Is there a way to use a PNG image as a "brush tip"? For example, use a fish-shaped brush to cut away instead of a round-pen. I would settle for it just to be a single cut-out per click of the mouse. – DJ Schrecker Sep 03 '15 at 13:21
  • This is perfect. Thanks a lot for the help. I could hug you right now. – DJ Schrecker Sep 03 '15 at 16:19