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.
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!