Prevent Alpha Color Pen from Overlapping?

2019-09-17 11:26发布

问题:

I have a simple highlighter using a Pen object. When the size is very large, it creates odd stripes which you can fix by defining its Start & End Cap withSystem.Drawing.Drawing2D.LineCap.Round but it instantly overlaps itself and loses its transparency. A flat cap also loses its transparency when you draw over itself multiple times.

How can I create an opaque pen that does not overlap itself or create stripes with a large pen width?

private static Color baseColor = Color.Yellow;
bool Draw;
Graphics g;
Point start;
Point end;

     private void Form1_Load(object sender, EventArgs e)
    {
        g = this.CreateGraphics();
        Pen1.Color = Color.FromArgb(128, baseColor);
        Pen1.Width = 50;
        Pen1.StartCap = System.Drawing.Drawing2D.LineCap.Flat; //I know it's default, just for clarification's sake
        Pen1.EndCap = System.Drawing.Drawing2D.LineCap.Flat;   //' '''' '' ' '''''''  '''' ''' ''''''''''''' ' ''''
    }

     private void Form1_MouseDown(object sender, MouseEventArgs e)
    {
        Draw = true;
        start = e.Location;
    }

     private void Form1_MouseUp(object sender, MouseEventArgs e)
    {
        Draw = false;
    }

    private void Form1_MouseMove(object sender, MouseEventArgs e)
    {
        if (Draw == true)
        {
            end = e.Location;
            g.DrawLine(Pen1, start, end);
            start = end;
        }
    }

It behaves like this;

Note: Yes, I am aware .CreateGraphics is not a good drawing method, it has a unique purpose that is irrelevant to the question.

回答1:

You have two issues with the drawing objects:

  • You draw each Line separately; this will create ugly artifacts where the common points overlap. Instead draw the lines all in one go with the DrawLines method! (Note the plural!)

  • You did not restrict the Pen.MiterLimit; this creates ugly spikes when the lines directions change sharply. Try to restrict it to around 1/2 of the Pen.Width or less . Setting LineJoin is also recommened as well a the two Caps..

Collect the points of your current line in a List<Point> currentPoints in the MouseMove and collect upon MouseUp the currentList into a List<List<Point>> allPointLists !

then you can draw both in the Paint event..

    foreach (List<Point> points in allPointLists)
        if (points.Count > 1) e.Graphics.DrawLines(Pens.Gold, points.ToArray());
    if (currentPoints.Count > 1) e.Graphics.DrawLines(Pens.Gold, currentPoints.ToArray());

Note that it will pay immediately to do it right, i.e. draw only with a valid graphics object and always rely on the Paint event to make sure that the drawing is always updated as needed! Using control.CreateGraphics is almost always wrong and will hurt as soon as you go beyond a single non-persistent drawing operation..

Here is the full code:

List<Point> currentPoints = new List<Point>();
List<List<Point>> allPointLists = new List<List<Point>>();


private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    currentPoints = new List<Point>();
}

private void Form1_MouseUp(object sender, MouseEventArgs e)
{
    if (currentPoints.Count > 1)
    {
        allPointLists.Add(currentPoints.ToList());
        currentPoints.Clear();
    }
}

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        currentPoints.Add(e.Location);
        Invalidate();
    }
}

Here is the Paint event; note that I use two different Pens for the currently drawn lines and the older ones. Also note that you can use DrawCurve instead of DrawLines to get even smoother results..

Also note that I use a List<T> to be flexible wrt the number of elements and that I convert it to an array in the Draw commands..

private void Form1_Paint(object sender, PaintEventArgs e)
{
    Color c1 = Color.FromArgb(66, 77, 88, 222);
    using (Pen pen = new Pen(c1, 50f))
    {
        pen.MiterLimit = pen.MiterLimit / 12;
        pen.LineJoin = LineJoin.Round;
        pen.StartCap = LineCap.Round;
        pen.EndCap = LineCap.Round;
        foreach (List<Point> points in allPointLists)
            if (points.Count > 1) e.Graphics.DrawLines(pen, points.ToArray());
    }
    Color c2 = Color.FromArgb(66, 33, 111, 222);

    using (Pen pen = new Pen(c2, 50f))
    {
        pen.MiterLimit = pen.MiterLimit / 4;
        pen.LineJoin = LineJoin.Round;
        pen.StartCap = LineCap.Round;
        pen.EndCap = LineCap.Round;
        if (currentPoints.Count > 1) e.Graphics.DrawLines(pen, currentPoints.ToArray());
    }
}

To prevent flicker don't forget to turn on DoubleBuffered for your Form; or use a DoubleBuffered Panel subclass or simply a PictureBox!

Final note: I left out the case of simple clicks; if they are supposed to paint you will have to catch them, probably best in the if (points.Count > 1) check and best do a FillEllipse at the right spot and with the right size..

Update

List<T> is so useful I hardly ever use arrays theses days. Here is how to make use of it to implement a Clear and an Undo button:

private void buttonClear_Click(object sender, EventArgs e)
{
    allPointLists.Clear();
    Invalidate();
}

private void buttonUndo_Click(object sender, EventArgs e)
{
    allPointLists.Remove(allPointLists.Last());
    Invalidate();
}

Note two small corrections in the MouseUp code this has required to handle the currentPoints list properly! The clearing is obvious; the ToList() call is making a copy of the data, so we don't clear the instance we have just added to te List of Lists!