Drawing Dashed Lines and Curves

Download demo project - 34 KB

Download Source code - 6 KB

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:

    dc.MoveTo(point1); dc.LineTo(point2);

with:

    unsigned Pattern[] = {30, 10, 10, 10, 10, 10}; // Dash gap dot gap dot gap CDashLine Line(dc, Pattern, sizeof(Pattern)/sizeof(unsigned)); Line.MoveTo(point1); Line.LineTo(point2);

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(...).

    unsigned Pattern[] = {30, 10, 10, 10, 10, 10}; // Dash gap dot gap dot gap CDashLine Line(dc, Pattern, sizeof(Pattern)/sizeof(unsigned)); Line.MoveTo(point1); Line.BezierTo(&points234); Line.LineTo(point5); Line.BezierTo(&points678);

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:
  1. Postponing rasterization reduces errors and takes advantage of hardware acceleration where available.
  2. 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

  1. 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.
  2. 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.
  3. 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

Download demo project - 34 KB

Download Source code - 6 KB

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.



Comments

  • http://www.ucancode.net

    Posted by Legacy on 08/01/2003 12:00am

    Originally posted by: MFC Diagramming Source Code Library

    Best

    Reply
  • Effect of pen width on speed

    Posted by Legacy on 04/18/2003 12:00am

    Originally posted by: swathi

    Does increasing or decreasing the pen width effect the time taken to draw???

    Reply
  • Adding text to it

    Posted by Legacy on 05/27/2001 12:00am

    Originally posted by: Aharon Tam

    Nice work,

    I tried to add text to it via TextOut or DrawText.
    The text does not show up.

    Why??

    Reply
  • Drawing Arcs and Lines

    Posted by Legacy on 04/18/2001 12:00am

    Originally posted by: Murgan

    Problem:
    
    1st segment is a straight line: x1,y1 and "X" long.

    2nd segment is an arc: starting at the end of the above line with a radius "R" and spans for "A" degrees.

    3rd segment is a straight line: starting at the end of the arc and "Y" long.

    I have a great problem in trying to simulate this in VB. Someone, please help.

    Thank you.
    Murgan

    Reply
  • Download Links interchanged

    Posted by Legacy on 08/26/1999 12:00am

    Originally posted by: Shashidhar BS

    The links to download source code and demo project have been interchanged.

    Reply
  • Asserting the wrong argument

    Posted by Legacy on 06/29/1999 12:00am

    Originally posted by: Robin Walsby

    Asserting the wrong thing, looks to me like:

    void
    CDashLine::SetPattern(unsigned* pattern, unsigned count)
    {
    // Must be an even number of dash and gaps
    ASSERT(m_Count >=0);
    ASSERT(!(m_Count % 2));


    Should be

    void
    CDashLine::SetPattern(unsigned* pattern, unsigned count)
    {
    // Must be an even number of dash and gaps
    ASSERT(count >=0); // CHANGED
    ASSERT(!(count % 2)); // CHANGED

    Apart from that, nice class.

    Robin

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds