Draw round circles in scaled coordinate systems in C#

In other posts I explain how you can use transformations to draw objects in a coordinate system other than the one provided “natively” by a PictureBox, Bitmap, and other objects that support drawing. Basically a transformation (represented by a Matrix object) maps from “world” coordinates to “device” coordinates.

This makes it easy to work in the coordinate system that makes sense to you, but it has a couple of weird side effects. For example, if you scale by different amounts in the X and Y directions, then anything you draw is stretched or squashed. That’s what you want if you’re drawing a graph, curve, or other shape that should be drawn in the world coordinate system, but suppose you want to draw a circle. It won’t be round because it will be scaled by the transformation.

Even stranger, the pen you use to draw is also scaled so the lines that make up curves will be thicker in the X direction than in the Y direction (or vice versa).

Fixing this is kind of confusing and depends on exactly what you want to do. This example demonstrates three techniques for dealing with these scaling issues. In the left PictureBox, the program draws circles with 1 unit thick lines (in world coordinates). You can easily see that the circles are stretched more horizontally than they are vertically. If you look closely, you can also see that the circle’s edges are thicker horizontally than vertically.

In the middle PictureBox, the program draws the same circles in world coordinates but it uses a pen with thickness 0 so the edges of the circles are not scaled.

In the right PictureBox, the program uses an untransformed coordinate system to draw thick circles in device coordinates. Because it doesn’t use a scaling transformation, the circles are not stretched and their edges all have the same thicknesses.

The following sections explain how the program creates its transformations, records the points clicked by the user, and draws the three pictures.

Transformations

The following code shows how the program creates transformations to map between world and device coordinates.

// The drawing transformation and its inverse.
private Matrix Transform, Inverse;

// Initialize the drawing transformation.
private const float XScale = 20;
private const float YScale = 10;
private void Form1_Load(object sender, EventArgs e)
{
    // Make the drawing transformation.
    Transform = new Matrix();
    Transform.Scale(XScale, YScale);

    // Make the inverse transformation.
    Inverse = Transform.Clone();
    Inverse.Invert();
}

The code declares two Matrix variables named Transform and Inverse. The form’s Load event handler sets Transform to scale by a factor of 20 in the X direction and 10 in the Y direction. This provides the mapping from world coordinates to device coordinates.

For example, when the program applies the transformation (you’ll see how that works later) and draws at the point (2, 3), the result is drawn on the PictureBox at pixel location (2 × 20, 3 × 10) = (40, 30).

The program then makes a copy of the transformation and uses its Invert method to create an inverse transformation. This maps back from device coordinates to world coordinates. For example, if you use this Matrix to transform the point (40, 30), you’ll get the world coordinate point (2, 3) back. (You’ll see how to do this later.)

The program uses both of these transformations (Transform and Inverse) later.

Saving Points

The following code shows how the program saves new points. All three PictureBox controls use the same MouseClick event handler so they all execute the same code.

// The clicked points.
private List<PointF> PointsClicked = new List<PointF>();

// Save a clicked point.
private void pic_MouseClick(object sender, MouseEventArgs e)
{
    // Transform the point to convert it from
    // device coordinates to world coordinates.
    PointF[] points = { new PointF(e.X, e.Y) };
    Inverse.TransformPoints(points);

    // Save the point's world coordinates.
    PointsClicked.Add(points[0]);

    // Redraw to show the new point.
    picNormal.Refresh();
    picThinPen.Refresh();
    picRound.Refresh();
}

The code first declares the PointsClicked list to hold points that the user clicks.

The MouseClick event (and other mouse events) provide information about the mouse’s position in device coordinates measured in pixels. The point (0, 0) is in the upper left corner of the PictureBox and coordinates increase to the right and down.

The program create an array of PointF to hold that point, and then uses the Inverse transformation to transform the point from device coordinates into world coordinates.

Unfortunately the TransformPoints method takes an array as a parameter so you need to make this array even if it contains only one point. You could add an extension method to the Matrix class to take a single Point or PointF as a parameter if you like, but I’ll skip that in this example.

The program adds the transformed point (in world coordinates) to the PointsClicked list. It then refreshes the three PictureBox controls to make them redraw to show the new point.

Drawing in World Coordinates

The following code shows how the program draws the left PictureBox.

// Draw the grid and any clicked points.
private void picNormal_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.Transform = Transform;

    // Draw the grid.
    DrawGrid(picNormal, e.Graphics);

    // If there are no points, do nothing else.
    if (PointsClicked.Count == 0) return;

    // Draw the points.
    foreach (PointF point in PointsClicked)
    {
        e.Graphics.DrawEllipse(Pens.Red,
            point.X - 1, point.Y - 1, 2, 2);
    }
}

The code first sets the Graphics object’s SmoothingMode property to produce a smooth result. It then sets the object’s Transform property to the transformation matrix created earlier. That lets the program draw in the world coordinate system and the result is appropriately scaled.

Next the code calls the DrawGrid method (shown shortly) to draw a blue grid on the PictureBox. If the PointsClicked list is empty, there are no points to draw so the method returns.

If there are points, the code draws them. Remember that the points were stored in world coordinates, so the transformation transforms them appropriately before they appear on the PictureBox.

The following code shows the DrawGrid method.

// Draw a scaled grid.
private void DrawGrid(PictureBox pic, Graphics gr)
{
    // Draw the grid.
    int hgt = (int)(1 + pic.ClientSize.Height / YScale);
    int wid = (int)(1 + pic.ClientSize.Width / XScale);
    using (Pen thin_pen = new Pen(Color.Blue, 0))
    {
        for (int x = 0; x < wid; x++)
            gr.DrawLine(thin_pen, x, 0, x, hgt);
        for (int y = 0; y < hgt; y++)
            gr.DrawLine(thin_pen, 0, y, wid, y);
    }
}

This code simply loops through X and Y coordinates drawing vertical and horizontal lines at every unit. For example, X values include 0, 1, 2, 3, and so on until they reach the right edge of the PictureBox. The Graphics object’s transformation scales the values to give a grid. (Otherwise, with the X and Y values so close together, the PictureBox would be completely filled with blue.)

Drawing Thin Circles

The following code shows how the program draws the middle picture.

// Draw the grid and any clicked points.
private void picThinPen_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.Transform = Transform;

    // Draw the grid.
    DrawGrid(picThinPen, e.Graphics);

    // If there are no points, do nothing else.
    if (PointsClicked.Count == 0) return;

    // Draw the points.
    using (Pen thin_pen = new Pen(Color.Red, 0))
    {
        foreach (PointF point in PointsClicked)
            e.Graphics.DrawEllipse(thin_pen,
                point.X - 1, point.Y - 1, 2, 2);
    }
}

This code is similar to the previous code that draws on the left picture. The only difference is that it uses a Pen with thickness 0. When it sees a Pen with thickness 0, the drawing software automatically draws the thinnest line possible. That’s a one pixel wide line whose thickness is not transformed by the Graphics object’s transformation.

However, the shape of the circle is transformed. The result is a stretched circle (an ellipse) with edges of constant thickness 1.

Drawing Round Circles

The following code shows how the program draws the right picture.

// Draw the grid and any clicked points.
private void picRound_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.Transform = Transform;

    // Draw the grid.
    DrawGrid(picNormal, e.Graphics);

    // If there are no points, do nothing else.
    if (PointsClicked.Count == 0) return;

    // Reset the Graphics object's transformation.
    e.Graphics.ResetTransform();

    // Draw the points.
    using (Pen thick_pen = new Pen(Color.Red, 3))
    {
        // Use the inverse transform to convert
        // from world coordinates to device coordinates.
        PointF[] points = PointsClicked.ToArray();
        Transform.TransformPoints(points);

        // Draw the points' circles in device coordinates.
        foreach (PointF point in points)
        {
            e.Graphics.DrawEllipse(thick_pen,
                point.X - 5, point.Y - 5, 10, 10);
        }
    }
}

This code is somewhat similar to the previous drawing code, but there are some important differences. Like the earlier versions, the code starts by setting the Graphics object’s SmoothingMode and Transform properties. It calls DrawGrid and returns if there are no points to draw as before.

Next the code calls the Graphics object’s ResetTransform method to remove the transformation. Anything drawn after this point is drawn in device coordinates not in world coordinates.

The code then creates a Pen with thickness 3. Because there is no transformation active, this Pen will be drawn 3 pixels wide in both the X and Y directions. In other words, it won’t have thick and thin parts like the circles in the left picture do.

Next the code draws circles around the selected points. However, the points are stored in world coordinates and we are now drawing in device coordinates. To figure out where the points belong in device coordinates, the program uses the Transform matrix. That transformation maps the points from world coordinates to device coordinates. After performing this mapping, the code simply draws each point (in device coordinates).

Conclusion

Working with transformations can be confusing until you get used to it. With a little practice, however, it’s not all that bad. Just keep track of the coordinate system used by each value and the coordinate system you’re using to draw. Then you can use transformations to map back and forth between world and device coordinates as needed.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in drawing, graphics, mathematics and tagged , , , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *