Title: 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 Map points between coordinate systems 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 blue ellipse is about 1 unit wide and 3 units 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 control'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 control'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 shows how the program stores information about the ellipses.
// The user's ellipses.
private List<RectangleF> Ellipses = new List<RectangleF>();
private List<Color> Colors = new List<Color>();
// 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 to find the inverse points for mouse coordinates.
Finally Wxmin, Wxmax, Wymin, and Wymax store the world coordinates used to draw the ellipses.
When the form resizes, the following code executes.
// Create new transformations to center the drawing.
private void Form1_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 device (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 one-pixel-wide lines no matter what transformations are in effect. (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 because the mouse is down, 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 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 into 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 use the DeviceToWorld method to convert mouse coordinates into device coordinates. For example, the following code shows the PictureBox control'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 experiment with it and to see additional details.
|