Map device coordinates to world coordinates in C#

Sometimes it's convenient to draw in one coordinate system (called world coordinates) and map those coordinates to the screen's device coordinates. The example, Use transformations to map points from one coordinate system to another when drawing in C# shows how to do this in C#.

For example, the picture shown here draws ellipses. The axes show the X and Y coordinate systems used. For example, the purple ellipse is about 7.75 units wide and 1 unit tall.

The program uses a transformation to scale and translate the drawing so the ellipses are centered and drawn at a reasonable size. Without the transformation, the ellipses would be tiny little marks just a few pixels in size in the PictureBox's upper left corner.

That much is described by the earlier example. The new feature here is that the program allows the user to click and drag to define new ellipses. The reason this is not trivial is that the picture is drawn with the transformation but the PictureBox's mouse events use normal device coordinates. If you use those coordinates, then any new ellipses would be huge and not centered property after they were transformed.

The solution to this problem is to transform the mouse coordinates by using the inverse of the transformation used to draw the ellipses. For example, the drawing transformation enlarges the ellipses so they have a reasonable size. The inverse transformation reduces the mouse coordinates during a click and drag so the resulting ellipse is small enough to draw correctly when modified by the drawing transformation.

That's the theory. Here's the code.

The following code shows how the program stores information about the ellipses.

// The user's ellipses.
private List Ellipses = new List();
private List Colors = new List();

// Used while drawing a new ellipse.
private bool Drawing = false;
private PointF StartPoint, EndPoint;

// The transformations.
private Matrix Transform = null, InverseTransform = null;
private const float DrawingScale = 50;

// The world coordinate bounds.
private float Wxmin, Wxmax, Wymin, Wymax;

This code defines lists to hold the ellipses and their colors. The Drawing, StartPoint, and EndPoint variables are used to let the user click and drag to create a new ellipse.

The Transform and InverseTransform variables are the matrices used to transform the drawing and inverse transform mouse coordinates.

Finally Wxmin, Wxmax, Wymin, and Wymax store the world coordinates used to draw the ellipses.

When the picCanvas PictureBox resizes, the following code executes.

// Create new transformations to center the drawing.
private void picCanvas_Resize(object sender, EventArgs e)
{
    CreateTransforms();
    picCanvas.Refresh();
}

This code calls the following CreateTransforms method and then refreshes the PictureBox.

// Create the transforms.
private void CreateTransforms()
{
    // Make the draw transformation. (World --> Device)
    Transform = new Matrix();
    Transform.Scale(DrawingScale, DrawingScale);
    float cx = picCanvas.ClientSize.Width / 2;
    float cy = picCanvas.ClientSize.Height / 2;
    Transform.Translate(cx, cy, MatrixOrder.Append);

    // Make the inverse transformation. (Device --> World)
    InverseTransform = Transform.Clone();
    InverseTransform.Invert();

    // Calculate the world coordinate bounds.
    Wxmin = -cx / DrawingScale;
    Wxmax = cx / DrawingScale;
    Wymin = -cy / DrawingScale;
    Wymax = cy / DrawingScale;
}

This method makes a new Matrix object named Transform. It uses the object's Scale method to apply a scaling transformation to enlarge the drawing. The code then uses the object's Translate method to add another transformation to the Matrix to center the drawing in the PictureBox.

That completes the drawing transformation. It first scales and then translates the drawing.

Now the code makes a clone of the drawing transformation and calls the new Matrix object's Invert method to invert it. This is the transformation that maps from mouse coordinates into world coordinates. (Basically it does the opposite of whatever the drawing transformation does.)

The method finishes by calculating the minimum and maximum X and Y coordinates that will appear in the drawing area. (It uses them to decide how long to draw the axes.)

The following code shows how the program uses the drawing transformation.

// Draw.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    // If we don't have the transforms yet, get them.
    if (Transform == null) CreateTransforms();

    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.Transform = Transform;

    // Use a pen that isn't scaled.
    using (Pen thin_pen = new Pen(Color.Black, 0))
    {
        // Draw the axes.
        float tic = 0.25f;
        thin_pen.Width = 2 / DrawingScale;
        e.Graphics.DrawLine(thin_pen, Wxmin, 0, Wxmax, 0);
        for (int x = (int)Wxmin; x <= Wxmax; x++)
            e.Graphics.DrawLine(thin_pen, x, -tic, x, tic);
        e.Graphics.DrawLine(thin_pen, 0, Wymin, 0, Wymax);
        for (int y = (int)Wymin; y <= Wymax; y++)
            e.Graphics.DrawLine(thin_pen, -tic, y, tic, y);

        // Draw the ellipses.
        thin_pen.Width = 0;
        for (int i = 0; i < Ellipses.Count; i++)
        {
            using (Brush brush = new SolidBrush(Color.FromArgb(128, Colors[i])))
            {
                e.Graphics.FillEllipse(brush, Ellipses[i]);
            }
            thin_pen.Color = Colors[i];
            e.Graphics.DrawEllipse(thin_pen, Ellipses[i]);
        }

        // Draw the new ellipse.
        if (Drawing)
        {
            thin_pen.Color = Color.Black;
            e.Graphics.DrawEllipse(thin_pen,
                Math.Min(StartPoint.X, EndPoint.X),
                Math.Min(StartPoint.Y, EndPoint.Y),
                Math.Abs(StartPoint.X - EndPoint.X),
                Math.Abs(StartPoint.Y - EndPoint.Y));
        }
    }
}

If the program hasn't created the transformations yet, it calls CreateTransforms to do so now.

Next the program sets the Graphics object's SmoothingMode property to get a smooth picture. It also sets the object's Transform property to the drawing transformation matrix.

The code then creates a Pen with width 0. That width tells the program to draw with the thinnest line possible. (If you don't do this, then the pen is scaled by the drawing transformation so the ellipses are drawn with huge edges.)

The rest of the method is reasonably straightforward. It draws the axes and then loops through the ellipses drawing them. If the program is in the middle of drawing a new ellipse, the method finishes by drawing it.

The following code shows how the program uses the inverse transformation to map from mouse (device) coordinates to world coordinates.

// Convert from device coordinates to world coordinates.
private PointF DeviceToWorld(PointF point)
{
    PointF[] points = { point };
    InverseTransform.TransformPoints(points);
    return points[0];
}

The Matrix class provides a TransformPoints method that transforms an array of points by applying the its transformation. The DeviceToWorld method takes a point in device coordinates as a parameter. It creates an array holding that point and calls the inverse transformation matrix's TransformPoints method to transform the point to world coordinates. It then returns the converted point.

The rest of the program's code is fairly straightforward. The mouse events that let the user click and drag call the DeviceToWorld method to convert mouse coordinates into device coordinates. For example, the following code shows the PictureBox's MouseDown event handler.

// Let the user draw a new ellipse.
private void picCanvas_MouseDown(object sender, MouseEventArgs e)
{
    Drawing = true;

    // Get the start and end points.
    StartPoint = DeviceToWorld(e.Location);
    EndPoint = StartPoint;
}

This is just like any other click and drag mouse event except it calls DeviceToWorld.

Download the example to see the rest of the code.

   

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments
  • No comments exist for this post.
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.