I am trying to draw dotted lines using the DrawLine
method. However, it adds a slight offset to the ends which makes the lines ugly. How can prevent adding offset to the ends?
protected override void OnPaint (PaintEventArgs e) {
Graphics g = e.Graphics;
g.DrawLine (Dot, new Point (10, 50), new Point (70, 50));
g.DrawLine (Dot, new Point (70, 50), new Point (70, 100));
}
Pen Dot = new Pen (Brushes.Black, 2) { DashStyle = DashStyle.Dot };
Output
Intended Result
The goal is simple and plausible:
- We want our lines to start end end in a dot.
And the rules of the game are also rather simple:
- Both the dots and the gaps are by default squares.
- All lines are drawn with
PenAlignment.Center
.
Unfortunately the consequences of the combination are rather complicated, so bear with me...
Let's first look at the 1st rule; let's ignore other DashStyles
and stay with DashStyle.Dot
. By default each dot and each gap have Pen.Width
pixels for both sides. This leads right into the first issues:
- If the width of our lines can't be divided by
Pen.Width
we are in trouble.
- To begin and end in a dot we want to have
n
dots and n-1
gaps.
There is more but let's next look into the 2nd rule; to illustrate it I drew this 10x upscaled image:
This is the code that created the colored parts:
g.FillRectangle(Brushes.Yellow, 15, 15, 10, 10);
g.DrawRectangle(Pens.Orange, 10, 10, 10, 10);
g.DrawLine(Pens.OrangeRed, 10, 5, 40, 5);
using (Pen pen = new Pen(Color.Red, 2f) { DashStyle = DashStyle.Dot })
g.DrawLine(pen, 10, 30, 48, 30);
using (Pen pen = new Pen(Color.Crimson, 2f))
g.DrawLine(pen, 10, 40, 48, 40);
using (Pen pen = new Pen(Color.DarkMagenta, 3f))
g.DrawLine(pen, 10, 50, 48, 50);
Look closely to see how the lines are drawn!
(Aside: You may also want to watch the difference of DrawRectangle
and FillRectangle
!)
- The horizontal lines start and end at the right coordinates but they expand either downward (if their Pen.Width = 1) or above and below the y-coodinate.
- Vertical lines would do the same, of course.
The problem is that they just won't fit together at the (outer) edges.
So what can we do? I don't think a DashOffset
can help. But there is another option to tweak a Pen
: We can set its DashPattern
to use custom values.
The values we need are two floats
, containing the scaling for the dots and the gaps. By default both values are 1f
. I decided to keep the dots square and modify only the gaps. Here is a function which solves the issue by
- expanding the line width by half a pen width on both sides so the outer edges meet
- expanding the gaps as needed to fit in the line length
Here is the line drawing function; it takes the Graphics
object, a Pen
, the two end Points
and a byte
that tells us if the line is meant to stand alone or will connect to other lines, as in our example cases.
To make a good connection, that will work well with semi-tranparent brushes we need the ability to skip a dot a at the beginning or end or even both, e.g. when we want to insert a diaogonal line as in my test below.
The skip values are 0
to skip none, 1 or 2
to skip the 1st or last dot and 3
to skip both. Of course you may want to use an enumeration
instead.
void DrawDottedLine(Graphics g, Pen pen_, Point pa_, Point pb, byte skipDots)
{
float pw = pen_.Width;
float pw2 = pen_.Width / 2f;
Pen pen = (Pen)pen_.Clone();
// find out directions:
int sigX = Math.Sign(pb_.X - pa_.X);
int sigY = Math.Sign(pb_.Y - pa_.Y);
// move the end points out a bit:
PointF pa = new PointF(pa_.X - pw2 * sigX, pa_.Y - pw2 * sigY);
PointF pb = new PointF(pb_.X + pw2 * sigX, pb_.Y + pw2 * sigY);
// find line new length:
float lw = (float)(Math.Abs(pb.X - pa.X));
float lh = (float)(Math.Abs(pb.Y - pa.Y));
float ll = (float)(Math.Sqrt(lw * lw + lh * lh));
// dot length:
float dl = ll / pw;
// dot+gap count: round to nearest odd int:
int dc = (int)(2 * Math.Round((dl + 1) / 2) - 1);
// gap count:
int gc = dc / 2 ;
// dot count:
dc = gc + 1;
// gap scaling
float gs = (ll - dc * pw) / (pw * gc);
// our custom dashpattern
pen.DashPattern = new float[] { 1, gs };
// maybe skip 1st and/or last dots:
if (skipDots % 2 == 1) pa = new PointF(pa_.X + pw * sigX, pa_.Y + pw * sigY);
if (skipDots > 1) pb = new PointF(pb_.X - pw * sigX, pb_.Y - pw * sigY);
// finally we can draw the line:
g.DrawLine(pen, pa, pb);
// dispose of pen clone
pen.Dispose();
}
After some obvious preparations I move the points out a bit and then calculate the number of dots and of gaps for vertical or horizontal lines. Then I calculate the modified the gap scale.
Here is the result, scaled up 4x, of drawing four lines to form a rectangle with varying pen widths going from 1/3 - 10/3
:
This is the testbed I used; note the use of semi-transparent black to illustrate how the corners are drawn correctly, i.e. non-overlapping:
Pen Dot = new Pen(Color.Black, 1f);
Point pa = new Point(10, 50);
Point pb = new Point(70, 50);
Point pc = new Point(70, 100);
Point pd = new Point(10, 100);
for (int i = 1; i < 10; i++ )
{
Dot = new Pen(Color.FromArgb(128, Color.Black), i / 3f){ DashStyle = DashStyle.Dot };
g.TranslateTransform(10, 10);
DrawDottedLine(g, Dot, pa, pb, 2);
DrawDottedLine(g, Dot, pb, pc, 2);
DrawDottedLine(g, Dot, pc, pd, 2);
DrawDottedLine(g, Dot, pd, pa, 2);
DrawDottedLine(g, Dot, pd, pb, 3);
}
I really wish one could avoid the connection problems by simply using DrawLines
, but this didn't work and after figuring out this solution I'm not really amazed it didn't..
The coordinates you provide are the point to the top-left positions of the resulting line. So when you are drawing a line that is two pixels wide, you should calculate your begin- and endpoints to include the width of that line.
In this case, you would want to offset the vertical line slightly more to the left and top (by borderwith
to be precise).
So, by adding (or subtracting) an offset equal to the width of the line, the result is this:
protected override void OnPaint (PaintEventArgs e) {
Graphics g = e.Graphics;
g.DrawLine (Dot, new Point (10, 50), new Point (70, 50));
g.DrawLine (Dot, new Point (69, 49), new Point (69, 100));
}
Pen Dot = new Pen (Brushes.Black, 2) { DashStyle = DashStyle.Dot };