Title: Let the user draw rotated polygons with right angles in C#
This example lets the user draw rotated polygons where all edges are either parallel or perpendicular to a defined baseline direction. As a result, all of the polygon's angles are right angles. (And yes, I know that the polygons aren't actually defined and then rotated. It just seemed like the best way to describe them was to call them rotated polygons.)
The idea was to let you draw rotated polygons over a layout of a collection of buildings. To make that easier, the program lets you load an image of a map showing the buildings so you can draw over them.
The following section explains how you can use the program to draw rotated polygons. The rest of this post explains how the program works.
Using the Program
The program provides the following menu commands.
- File
- Open - This command lets you open an image file to use as a background. That lets you define the baseline so it lines up with a direction on the image.
- New - This clears the picture's background image and removes all currently defined rotated polygons.
- Exit - This does exactly what you would expect.
- Drawing
- Set Baseline - This lets you click and drag to define a new baseline.
- Draw Polygon - The command lets you start drawing a new polygon. I'll explain how that works next.
- Clear Polygons - This removes all currently defined rotated polygons.
To create a rotated polygon, press Ctrl+P or select the Drawing menu's Draw Polygon command. Then left click to define the polygon's vertices. The program automatically adjusts the location of the next point so the line between it and the previous point is either parallel or perpendicular to the baseline.
For example, consider the picture on the right. Here I have defined two of the polygon's vertices and the mouse is at the black cross. To find the vertex location for this mouse position, we consider the two dashed lines leading from the mouse position, one parallel to the baseline and the other perpendicular to it. The dashed lines extend until they are as close as possible to the polygon's last vertex. The goal is to place the new vertex at the end of the shorter of the two dashed line. In this example, the dashed line that is parallel to the baseline is shorter. The program has drawn the polygon's new tentative edge in green. (But the new tentative vertex isn't drawn in red because I haven't clicked the mouse yet to fix that point.)
After you have defined all of the polygon's vertices, right-click to finish the polygon. At that point the program adjusts the final point so the line between it and the polygon's first point is parallel or perpendicular to the baseline. For that to produce a correct result, the number of polygon points that you define before right-clicking should be even and at least four. If you define fewer than four points, the program discards the points.
If you define an odd number of points, then the program cannot easily adjust the final point so the lines it makes with the previous and first points are parallel and perpendicular to the baseline. Rather than trying to figure out what the program should do in this situation, I just decided that you should just select an even number of points. If you place the final point close to where it should go, then the program adjusts it properly and all is well.
Defining the Baseline
Instead of using a complicated set of MouseDown, MouseMove, and MouseUp event handlers to handle all mouse operations, this program installs and uninstalls those event handlers as they are needed to perform different tasks.
This section describes the mouse event handlers that the program uses to let you define the baseline. It's pretty basic mouse event handling, so if you already know how to let the user select a line segment, you may want to skim or even skip the rest of this section.
The program stores the baseline's start and end points in the following variables.
// The baseline start and end points.
private Point BaselineStart = new Point(0, 00);
private Point BaselineEnd = new Point(200, 100);
When you select the Drawing menu's Set Baseline command, the program executes the following code.
// Let the user draw the baseline.
private void mnuDrawingSetBaseline_Click(object sender, EventArgs e)
{
picCanvas.MouseDown += DrawBaseline_MouseDown;
picCanvas.Cursor = Cursors.Cross;
}
This code registers the following DrawBaseline_MouseDown method to catch MouseDown events and changes the cursor to a crosshair.
private void DrawBaseline_MouseDown(object sender, MouseEventArgs e)
{
picCanvas.MouseDown -= DrawBaseline_MouseDown;
picCanvas.MouseMove += DrawBaseline_MouseMove;
picCanvas.MouseUp += DrawBaseline_MouseUp;
BaselineStart = e.Location;
BaselineEnd = e.Location;
picCanvas.Refresh();
}
When you press the mouse down, the event handler uninstalls itself so it no longer catches MouseDown events. It then installs the MouseMove and MouseUp event handlers that I'll show you next.
It also saves the mouse's current location in the BaselineStart and BaselineEnd variables and refreshes the program's PictureBox. (I'll show you its Paint event handler later.)
The following code shows the baseline's MouseMove and MouseUp event handlers.
private void DrawBaseline_MouseMove(object sender, MouseEventArgs e)
{
BaselineEnd = e.Location;
picCanvas.Refresh();
}
private void DrawBaseline_MouseUp(object sender, MouseEventArgs e)
{
picCanvas.MouseMove -= DrawBaseline_MouseMove;
picCanvas.MouseUp -= DrawBaseline_MouseUp;
picCanvas.Cursor = Cursors.Default;
}
The MouseMove event handler saves the mouse's current position in variable BaselineEnd. It then refreshes the PictureBox to draw the current baseline selection.
The MouseUp event handler uninstalls the MouseMove and MouseUp event handlers and then resets the mouse cursor to the default.
Defining Rotated Polygons
The baseline is defined by two points, so you can define it by a single press/drag/release operation. The polygon could include any number of points, so the program needs to use a different selection mechanism. It lets you click multiple times to define the polygon's vertices.
The program stores information about the new polygon while you are drawing it in the following two variables.
// The new polygon while under construction.
private List<PointF> NewPolygon = null;
private PointF LastPoint;
Variable NewPolygon is a list containing the vertices that are currently defined for the new rotated polygon. Variable LastPoint indicates the position that would be added to the polygon if you were to click the mouse now. (If this were a normal polygon and not one with edges parallel and perpendicular to the baseline, then LastPoint would simply be the mouse's position.)
When you select the Drawing menu's Draw Polygon command, the following code executes.
// Let the user draw a polygon.
private void mnuDrawingDrawPolygon_Click(object sender, EventArgs e)
{
NewPolygon = new List<PointF>();
picCanvas.MouseClick += DrawPolygon_MouseClick;
picCanvas.MouseMove += DrawPolygon_MouseMove;
picCanvas.Cursor = Cursors.Cross;
}
This code sets variable NewPolygon to a new list of PointF objects. If the polygon's vertices were where you clicked the mouse, then their and Y coordinates could be integers so you could store them in a list of Point instead of PointF. However, the vertices are adjusted to make the rotated polygon's sides parallel or perpendicular to the baseline, so the vertex coordinates are not necessarily integers.
After initializing the NewPolygon list, the code installs event handlers to catch MouseClick and MouseMove events and sets the cursor to the crosshair.
When you click on a point, the following event handler executes.
private void DrawPolygon_MouseClick(object sender, MouseEventArgs e)
{
// See if we are done with this polygon.
if (e.Button == MouseButtons.Right)
{
// End this polygon.
picCanvas.MouseClick -= DrawPolygon_MouseClick;
picCanvas.MouseMove -= DrawPolygon_MouseMove;
picCanvas.Cursor = Cursors.Default;
// Is we have at least four points,
// save the new polygon.
if (NewPolygon.Count > 3)
{
SaveNewPolygon();
}
// Reset the new polygon.
NewPolygon = null;
}
else
{
// Continue this polygon.
PointF adjusted_point = AdjustPoint(e.Location);
NewPolygon.Add(adjusted_point);
LastPoint = adjusted_point;
}
picCanvas.Refresh();
}
If you clicked the right mouse button, then you are trying to end the new polygon. The code uninstalls the MouseClick and MouseMove event handlers and restores the cursor to the default. If you have defined at least four vertices, the code calls the SaveNewPolygon method (described shortly) to save the new polygon. The code finishes by setting the NewPolygon list to null.
If you did not click the right mouse button, then you are adding a new point to the polygon. In that case, the code calls the AdjustPoint method (described later) to move the point that you clicked so the line between it and the polygon's previous point is either parallel or perpendicular to the baseline. The code adds the adjusted point to the new polygon and sets LastPoint equal to the adjusted point. (You'll see how that is used later in the Paint event handler.)
When you move the mouse, the following event handler executes.
private void DrawPolygon_MouseMove(object sender, MouseEventArgs e)
{
if (NewPolygon.Count == 0) return;
LastPoint = AdjustPoint(e.Location);
picCanvas.Refresh();
}
If the new polygon has no points yet, this code simply returns. Otherwise the code adjusts the mouse's current location and saves it in variable LastPoint. It then refreshes the PictureBox to show the partially completed rotated polygon.
Saving the Polygon
The program stores finished rotated polygons in the following Polygons list.
// The polygons.
private List<List<PointF>> Polygons = new List<List<PointF>>();
The following SaveNewPolygon method finishes the new polygon and adds it to that list.
// Fix the new polygon's last point so the final
// segment forms a right angle with the first segment.
private void SaveNewPolygon()
{
NewPolygon[NewPolygon.Count - 1] =
AdjustPoints(
NewPolygon[NewPolygon.Count - 1],
NewPolygon[0]);
Polygons.Add(NewPolygon);
}
This code calls the AdjustPoints method described in the next section to adjust the new polygon's last point so it lines up properly with the polygon's first point. The method then adds the new polygon to the Polygons list.
Adjusting Points
By far the most interesting part of this program is the code that adjusts a point so it lines up properly with the new polygon's existing vertices. The following AdjustPoint method adjusts a point so it lines up with the polygon's last vertex.
// Adjust this point so it is perpendicular
// to the previous point in the new polygon.
private PointF AdjustPoint(PointF point)
{
if (NewPolygon == null) return point;
if (NewPolygon.Count == 0) return point;
// Adjust the point to the last point
// that is currently in the new polygon.
return AdjustPoints(point, NewPolygon[NewPolygon.Count - 1]);
}
If the new polygon is null or has no vertices, then this method simply returns the original point unchanged. If neither of those conditions is true, then the code simply calls the AdjustPoints method described shortly to adjust the point and returns the result. That method decides which dashed line to follow in the earlier picture and where the adjusted point should lie.
One approach to adjusting the point would be to find the distance between the point to adjust and the reference point as multiples of vectors parallel and perpendicular to the baseline vector. That probably wouldn't be quite as hard as it sounds, but there's an easier way. (Or at least a way that's easier to understand.)
The code first rotates the points so the baseline is parallel to the X axis. Then finding the correct adjusted point is simply a matter of determining whether the two points differ less in their X or Y coordinates.
For example, take a look at the picture below.
The picture on the left shows the original rotated polygon in progress. The picture on the right has been rotated so the baseline is parallel to the X axis. In that picture it's easy to see that the dashed line that we want to follow is the horizontal one because it is shorter than the vertical one.
The length of the vertical dashed segment is the difference between the two rotated points' Y coordinates. Similarly the length of the horizontal dashed segment is the difference between the two rotated points' X coordinates. To see which dashed segment is shorter, we simply calculate those lengths and compare them.
Finding the location of the adjusted point is also easy in the rotated picture. For example, to use the horizontal dashed segment (which we should in this example), the adjusted point has the X coordinate of the polygon's current last vertex and the Y coordinate of the mouse's position.
That gives us the following algorithm for adjusting a point.
- Find a transformation that rotates the baseline so it is perpendicular to the X axis.
- Rotate the point to adjust and the reference point.
- Subtract X and Y coordinates to see which dashed segment we should follow.
- Use the coordinates of the point to adjust and the reference point to find the adjusted point's rotated location.
- Reverse the earlier transformation to move the adjusted point where it belongs in the original drawing.
The following AdjustPoints method follows those steps.
// Adjust a point so it is perpendicular
// to a reference point.
private PointF AdjustPoints(PointF point_to_adjust, PointF reference_point)
{
if (NewPolygon == null) return point_to_adjust;
if (NewPolygon.Count == 0) return point_to_adjust;
// Transform the last point in the new polygon
// and this point.
Matrix matrix = GetTransform();
PointF[] points =
{
reference_point,
point_to_adjust,
};
matrix.TransformPoints(points);
// Fix the transformed point.
float dx = Math.Abs(points[1].X - points[0].X);
float dy = Math.Abs(points[1].Y - points[0].Y);
if (dx <= dy)
points[1].X = points[0].X;
else
points[1].Y = points[0].Y;
// Untransform the result.
matrix.Invert();
matrix.TransformPoints(points);
return points[1];
}
If the new polygon is null or has no vertices, then this method simply returns the original point unchanged. If neither of those conditions is true, then the program calls the GetTransform method described shortly to get a transformation matrix that rotates the baseline so it is parallel to the X axis. The code makes an array holding the point to adjust and the reference point, and uses the matrix to rotate them.
Next the code determines whether the rotated points are closer in the X or Y directions and adjusts the point to adjust accordingly. The method then inverts the rotation matrix (so it reverses the rotation) and applies the inverted matrix to the points. The method finishes by returning the unrotated adjusted point.
The last piece of code that deals with adjusting points is the following GetTransform method.
// Return a transformation matrix that rotates
// the baseline so it is parallel to the X axis.
private Matrix GetTransform()
{
float dx = BaselineStart.X - BaselineEnd.X;
float dy = BaselineStart.Y - BaselineEnd.Y;
double angle = -Math.Atan2(dy, dx) * 180 / Math.PI;
Matrix matrix = new Matrix();
matrix.Rotate((float)angle);
return matrix;
}
This method is actually fairly straightforward. It calculates the difference in X and Y coordinates between the baseline's two end points. It then uses the Math.Atan2 method to calculate the angle that the baseline makes with respect to the X axis.
Next the code creates a new Matrix object and calls its Rotate method, passing that method the negative of the baseline angle. The method then returns the resulting matrix.
Drawing
The program's Paint event handler, which is shown in the following code, is somewhat involved but not very complicated.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen pen = new Pen(Color.Red, 2))
{
// Draw the baseline.
e.Graphics.DrawLine(pen, BaselineStart, BaselineEnd);
pen.Color = Color.Yellow;
pen.DashPattern = new float[] { 3, 3 };
e.Graphics.DrawLine(pen, BaselineStart, BaselineEnd);
// Draw the defined polygons.
pen.Color = Color.Blue;
pen.DashStyle = DashStyle.Solid;
using (Brush brush = new SolidBrush(Color.FromArgb(128, Color.LightBlue)))
{
foreach (List<PointF> points in Polygons)
{
e.Graphics.FillPolygon(brush, points.ToArray());
e.Graphics.DrawPolygon(pen, points.ToArray());
}
}
// Draw the new polygon if there is one.
if (NewPolygon != null)
{
pen.Color = Color.Green;
pen.DashStyle = DashStyle.Solid;
if (NewPolygon.Count > 1)
e.Graphics.DrawLines(pen, NewPolygon.ToArray());
e.Graphics.DrawLine(pen,
NewPolygon[NewPolygon.Count - 1],
LastPoint);
foreach (PointF point in NewPolygon)
e.Graphics.FillEllipse(Brushes.Red,
point.X - 3, point.Y - 3, 6, 6);
}
}
}
This code sets the e.Graphics object to draw anti-aliased shapes and then creates a thick, red pen. It uses the pen to draw the baseline, changes the pen so it is a dashed yellow pen, and draws the baseline again. The result is a thick baseline that alternates dashes of red and yellow.
Next the code makes the pen solid blue. It then loops through any polygons that are stored in the Polygons list. Each of those entries is itself a list of points. The code calls the points list's ToArray method to convert the points into an array and uses the result to fill and outline the polygon.
After it has drawn any existing polygons, the code draws the new polygon if one is under construction. To do that, the code makes the pen green. If the new polygon contains more than one vertex, the code uses the Graphics object's DrawLines method to draw the edges that connect those vertices. It then draws a line from the last vertex to the point stored in LastPoint. (Recall that LastPoint holds the adjusted mouse position. That is where the polygon's next vertex will go if you click the mouse now.) The method finishes by looping through the vertices again, this time drawing a red circle at each.
Conclusion
This is a fairly specialized application, but it does demonstrate a few reusable techniques. It shows how you can install and uninstall mouse event handlers to perform different operations.
The example also shows how you can let the user draw rotated polygons. You may never need to do that, but it might be useful to let the user draw polygons that have edges parallel to the X and Y axes. You can do that by using a horizontal baseline. Or you can simplify the program by removing the rotation and unrotation transformations.
Download the example to see additional details including the code that can optionally draw the dashed lines shown in some of the pictures above.
Download the example to experiment with it and to see additional details.
|