Drawing Dashed Lines and Curves

Introduction

It can be difficult to create a dashed or dotted line when the pen width is greater than 1 using
the Windows API. This is especially true for Win95/98 where video drivers are not required
to implement such functionality. Almost inevitably your carefully crafted wide dashed lines in WinNT will come out as solid lines
in Win95/98. Interestingly, some printer drivers for Win95/98 do not have this limitation.

Acknowledgements

This code was inspired by the article of
Jean-Claude Lanz to whom
the author is very grateful. His code shows how dashed lines can be simulated using short line
segments corresponding to each dash or dot. In other words, one has to do in user mode what the driver or operating
system does for you automatically under Windows NT. This contribution extends that insight so that essentially all
of what can be accomplished under Windows NT with non-solid (e.g. dotted or dashed) pens can also be
seen in Win95/98. This includes, for example, the drawing of ellipses with dotted or dashed borders.

The logic of the code drawing straight lines comes essentially from Jean-Claude Lanz. Some
limitations of his code have been removed and extra functions have been added. However,
the class presented here is not a updated replacement of Mr. Lanz's efforts. In particular,
the design philosophy is slightly different.

Mr. Lanz's class is easier to use than equivalent MFC drawing code because all the necessary pen
creation and selection is automatic and hidden. For my purposes, this takes away too much flexibility,
besides being slower than using a combination of simpler atomic functions. Using my class, requires no less
(but no more effort) than straight MFC code. In the worst case, what requires just three lines of code
with Mr. Lanz's class needs up to eighteen with mine! The tradeoffs are thus between simplicity,
and speed and flexibility.

Speed

The algorithms described here have proven much faster than I thought. However, one must be aware
that, especially on legacy systems, it is much slower to draw dotted and dashed lines than solid ones.
The greater the number of dashes drawn, the slower it will be; so that rather counter-intuitively,
the thinest lines may take longer to draw than thicker ones. This is true of NT's GDI dashed pens
as well. However, for lines thinner than 3 pixels, it may be noticeably faster to use the native
dashed pen styles if available than to emulate them.

A general rule of thumb may be that very thin dashed lines are appropriate for general drawing
but less satisfactory for animation purposes on slower systems with legacy graphics cards.

Drawing Straight Lines

To draw dashed straight lines, you need to create a CLine object with the required pattern of dashes
and dots. The pattern is specified by an array of unsigned integers pairs where the first number of
each pair indicates the length of the line segments. The second number represents the gap to the next
line segment. For example to draw a line with a "dash-dot-dot" pattern, you would replace your normal:

How it works

Essentially, a single CLine::LineTo(...) call is broken up into multiple CDC::MoveTo(...) and
CDC::LineTo(...) calls for the multiple line segments making up the dashed line. Using Bresenham's
classical algorithm, the original line is traversed pixel by pixel. The appropriate CDC::MoveTo(...)
or CDC::LineTo(...) calls are inserted when the requisite number of pixels representing the
"dashes" or the "gaps" have been counted past. The position in the pattern is saved between calls
to CLine::LineTo(...) so that the dashes and dots "wrap" properly around corners when drawing joined
lines or polygons.

Drawing Dashed Curves

Bezier curves can be drawn in exactly the same way as the code for straight lines above but
with a call to BezierTo(...) instead of LineTo(...). Note that you can not
pass more than three points to this function. In other words, you must break up GDI PolyBezierTo(...)
into multiple calls to CLine::BezierTo(...).

As with CLine::LineTo(...), the position in the pattern is saved between calls, so that the dashes
and dots wrap properly around corners and joins. Mixed calls to BezierTo(...) and LineTo(...) also work properly.

How it works

Unfortunately, I could not find a simple equivalent to Bresenham's algorithm for walking along
bezier curves pixel by pixel, and which would also allow the bezier curve to be truncated at any
specified pixel. However, it is very easy to split the curve recursively by linear interpolation
using the de Casteljau algorithm. The bezier curve is eventually sub-divided into segments
short enough to be approximated by straight lines. The total length of a bezier curve, for example,
can be calculated by adding the lengths of these straight lines together (see LBezier::Length());

It is a similar exercise to divide bezier curves into multiple shorter curve segments to make up the
dashes or dots. Cubic bezier curves are usually described in standard parametric representation where
t ranges from 0.0-1.0. The bezier curve can be split at any particular value of t
(see LBezier::TSplit(...)).
The calculation of the t at a point is similar to the length calculation above except that
only approximating line segments up to the required length are needed. After each linear
interpolation the parameter t is halved. So by noting the level of recursion in the de
Casteljau algorithm, one knows the various t values of the approximating segments.
These can then be summed up to give the total t for the parameter of the bezier curve up to
the required length (see LBezier::TAtLength).

If all this sounds very involved, you may be thankful that all the calculation is encapsulated in the
LBezier and CLine classes.
The algorithm itself is very fast. I have therefore not bothered to make further optimizations, such
as removing tail end-recursion. My original plan was to output a series of bezier control points which
could be saved between calls to BezierTo(...). However, this procedure is so fast that the calculations
can be reproduced at each invocation without penalty, making the class interface much simpler.

The efficiency of this algorithm also led me to abandon my search for more direct and sophisticated
means of parameterization by length. The other candidates proved too slow or too complex (i.e.
either the logic or the mathematics proved impenetrable .)

Why not use straight line segments to simulate bezier curves?

Using bezier segments rather than "polylines" to represent the dashes in bezier curves has the following
advantages:

Bezier segments can be scaled without (as many) gross artefacts. This can be important, for example,
if you are drawing to an exported metafile, especially in OLE.

Fancy effects with round or square dots and dashes

Dashed or Dotted Geometric pens under NT can be created with the
PS_ENDCAP_ROUND | PS_JOIN_ROUND,
or with the
PS_ENDCAP_SQUARE | PS_JOIN_MITER or
PS_ENDCAP_FLAT | PS_JOIN_MITER attributes.
Using the first will produce round dots
and dashes while the others creates square dots and dashes. Under Windows 95/98, the "end cap" and
"line join" styles can only be specified drawing Paths. Thus to draw square dots, one must wrap
the calls in a Path Envelope and call ::StrokePen(). As always, using Paths mean that a variety
of special effects can be employed. For example, to fill the lines with a pattern brush, gradient
fills or fractals, one can call WidenPath(...) and then set the clipping region to the
resulting Path via SelectClipPath(...). Remember to call CloseFigure(...) before closing the path.

The patterns required to reproduce the NT
PS_DASH, PS_DOT, PS_DASHDOT and PS_DASHDOTDOT styles depend on the pen size and whether you want
round or square dots. The CLine::GetPattern(...) helper function returns the appropriate patterns.

If an opaque background mode is specified (CDC::SetBkMode(OPAQUE)), the gaps between dashed lines
is normally filled with the background colour (CDC::SetBkColor(Colour)). To reproduce the same effect using
CLine, you can draw solid lines first in the desired background colour before drawing the dashed
lines on top using CLine calls. This alas requires a little bit more work, but is much faster
than keeping exchanging pens to draw the gaps.

Why bother?

Why did I go to so much trouble, particularly in the calculation of dashed bezier curves? The point
is that all of the other Windows GDI primitives (polygons, rectangles, round rectangles, ellipses)
can be reproduced using sequences of straight lines and bezier curves. All of these simulated figures
unlike their GDI equivalents can be freely rotated and skewed and do not have to be axis aligned.

The calls to CLine::MoveTo, CLine::BezierTo and CLine::LineTo can only draw the outlines of the figures
of course. To draw the interiors, one once more can have recourse to Windows Paths (FillPath(...) or using
SelectClipPath(...)). Though it may seem tedious to draw the outline and fill in separate passes, this
does give great additional flexibility. Not only can you display various special effects but you can escape
the limitations of standard GDI, for example by having the outline behind the fill, rather than in front
of it.

Some caveats in using path functions

Wrapping each GDI call in a BeginPath()/EndPath() might seem to be very expensive. The
costs are negligible on my system. If in doubt, benchmark.

You may be tempted to have multiple GDI function calls within each Path Bracket.
If so, be sure to take care that separate filled shapes ( such as rectangles and
polygons and ellipses) do not intersect when you are using the wrong PolyFillMode.

Under Win95/98 some GDI calls cannot be used to construct Paths. The invalid
calls include AngleArc, Arc, ArcTo, Chord, Ellipse, RoundRect and Rectangle.

Possible enhancements

It may be a good idea to add ::PolylineTo(...) and ::PolyBezierTo(...) to the CLine class which
in turn call ::LineTo(...) and BezierTo(...). I am not yet convinced that it would be a good idea
to add equivalents for other GDI calls such as Rectangle(...) etc.

Further acknowledgements

The original idea for calculating bezier lengths came from Jens Gravesen.

See Jens Gravesen: "Adaptive subdivision and the length of Bezier curves"
mat-report no. 1992-10, Mathematical Institute, The Technical
University of Denmark, or in "Graphics Gems V" (Editor Alan Paeth).

Sample Code

The demo project shows a rotating gradient filled ellipse with a dotted outline.
For clarity and so that you might see how fast or slow typically complex operations can be,
the drawing code has not been carefully optimized.

Top White Papers and Webcasts

U.S. companies are desperately trying to recruit and hire skilled software engineers and developers, but there is simply not enough quality talent to go around. Tiempo Development is a nearshore software development company. Our headquarters are in AZ, but we are a pioneer and leader in outsourcing to Mexico, based on our three software development centers there. We have a proven process and we are experts at providing our customers with powerful solutions. We transform ideas into reality.

When individual departments procure cloud service for their own use, they usually don't consider the hazardous organization-wide implications. Read this paper to learn best practices for setting up an internal, IT-based cloud brokerage function that service the entire organization. Find out how this approach enables you to retain top-down visibility and control of network security and manage the impact of cloud traffic on your WAN.