[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Build a polygon selector class in C#

[Build a polygon selector class in C#]

This example shows how to make a polygon selector class that makes it easy to let the user select a polygon. I call this kind of class that provides a service for another class a symbiont. In a symbiotic relationship, the smaller of the organisms is called the symbiont and the larger is called the host.

The goal is to make the symbiont do ass much of the work as possible so the host can use it easily. In this example, the polygon selector installs event handlers on the host to let it handle the mouse events that the user produces to select the polygon.

PolygonEventArgs

When the user finishes selecting a polygon, the selector raises a PolygonSelected event. It passes that event the new polygon's points through an object from the following PolygonEventArgs class.

public class PolygonEventArgs { public List<Point> Points; public PolygonEventArgs(List<Point> points) { Points = points; } }

This class simply holds a public list of the points that make up the polygon. The class's constructor initializes that list.

PolygonSelector

The PolygonSelector class is relatively simple. However, it's rather long so I'll describe it in pieces.

The following code shows the the first part of the polygon selector class.

public class PolygonSelector { // The control on which the polygon will be selected. private Control Host; // The pen used while selecting the polygon. private Pen PolygonPen; // The points in the polygon. private List<Point> PolygonPoints; public PolygonSelector(Control host, Pen pen) { Host = host; PolygonPen = pen; // Install a MouseDown event handler. Host.MouseDown += Host_MouseDown; }

The class stores a reference to the host control in its Host field. The field has type Control so the host could be any type of control. In practice, however, it is intended to be a form or PictureBox.

The PolygonPen field holds the pen that the polygon selector should use while selecting a new polygon. The PolygonPoints list holds the selected points.

The class's constructor first saves the host and pen. It then installs the following event handler to catch the host's MouseDown events.

// On left mouse down, start selecting a polygon. private void Host_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left) return; // Uninstall picCanvas_MouseDown. Host.MouseDown -= Host_MouseDown; // Add the first point and a copy to be the last point. PolygonPoints = new List<Point>(); PolygonPoints.Add(e.Location); PolygonPoints.Add(e.Location); // Install an event handler to catch clicks. Host.Paint += Host_Paint; Host.MouseMove += Host_MouseMove; Host.MouseClick += Host_MouseClick; }

If the user has not pressed the left mouse button, the event handler exits.

If the user did press the left mouse button, the code uninstalls the MouseDown event handler so it does not execute while the user is selecting this polygon. It then creates a new PolygonPoints list and adds the mouse's location to the list twice. The first instance is the first point in the polygon. The second instance is the initial last point in the polygon. That point will be updated when the mouse moves.

Next the code installs Paint, MouseMove, and MouseClick event handlers on the host. The following code shows the Paint event handler.

// Draw the polygon so far. private void Host_Paint(object sender, PaintEventArgs e) { if (PolygonPoints.Count > 1) { e.Graphics.DrawLines(PolygonPen, PolygonPoints.ToArray()); } }

If there are at least two points in the PolygonPoints list, this code draws the lines that they define.

Note that the host will probably also have a Paint event handler installed. The polygon selector installs its Paint event handler when drawing starts, so will be installed after any others (barring any weird programming shenanigans). That means it will execute last so the new polygon will be drawn on top of anything else that is drawn on the host.

When the user moves the mouse, the following event handler executes.

// Update the last point's position and redraw. private void Host_MouseMove(object sender, MouseEventArgs e) { PolygonPoints[PolygonPoints.Count - 1] = e.Location; Host.Refresh(); }

This code moves the last point in the PolygonPoints list to the mouse's current position. It then refreshes the host to raise its Paint event. The host's Paint event handler executes followed by the polygon selector's event handler.

When the user clicks the mouse, the following event handler executes.

// Add a point to the polygon or end the polygon. private void Host_MouseClick(object sender, MouseEventArgs e) { int num_points = PolygonPoints.Count; // See which button was clicked. if (e.Button == MouseButtons.Right) { // Right button. End the polygon. // Remove the last point. PolygonPoints.RemoveAt(num_points - 1); // Uninstall our event handlers. Host.Paint -= Host_Paint; Host.MouseMove -= Host_MouseMove; Host.MouseClick -= Host_MouseClick; // Raise the PolygonSelected event. OnPolygonSelected(); // Reinstall the MouseDown event handler. Host.MouseDown += Host_MouseDown; } else { // Make the last point permanent. // If the last point is different from the // one before, add a new last point. if (PolygonPoints[num_points - 1] != PolygonPoints[num_points - 2]) { PolygonPoints.Add(e.Location); Host.Refresh(); } } }

This code performs two different tasks depending on whether the user clicked the left or right mouse button. If the user clicked the right button, this code finishes the current polygon. If the user clicked the left button, the code adds a new point to the polygon.

To finish the current polygon, the code removes the final point from the points list. It then uninstalls the polygon selector's Paint, MouseMove, and MouseClick event handlers. It calls the OnPolygonSelected method described shortly to raise the PolygonSelected event. It finishes by reinstalling the MouseDown event handler so it can start a new polygon.

If the user clicked the left mouse button, the event handler tries to add a new point to the points list. If the last two points are the same, then the code does nothing and the current last point in the list remains the last point. This prevents the polygon selector from adding duplicate points to the polygon.

If the last two points are different, then the code adds the mouse's current location to the end of the list. That point should be the same as the previous last point as updated by the MouseMove event handler. Future MouseMove events will update this new point's position.

After it has added the new last point, the code refreshes the host to draw the updated polygon.

The following code shows how the polygon selector raises its PolygonSelected event.

// Event to raise when a polygon is selected. public delegate void PolygonSelectedEventHandler( object sender, PolygonEventArgs args); public event PolygonSelectedEventHandler PolygonSelected; // Raise the event. protected virtual void OnPolygonSelected() { if ((PolygonSelected == null) || (PolygonPoints.Count < 3)) { Host.Refresh(); } else { PolygonEventArgs args = new PolygonEventArgs(PolygonPoints); PolygonSelected(this, args); } }

This code first declares a delegate type named PolygonSelectedEventHandler that is a void method that takes as parameters an object and a PolygonEventArgs. It then declares the PolygonSelected event to be of that delegate type.

The OnPolygonSelected method raises the event. It first checks whether the PolygonSelected event is null. This is C#'s goofy way of determining whether any event handlers are registered to catch the event. If no event handlers are registered, or if the polygon does not contain at least three points, then the code simply refreshes the host to redraw without the new partially selected polygon.

If there are event handlers ready to catch the event, presumably in the host, and if the polygon contains at least three points, then the code creates a new PolygonEventArgs object holding the new polygon's points. It then invokes the event handler passing it the current polygon selector object and the PolygonEventArgs.

Disabling the Polygon Selector

There's one odd potential problem with the polygon selector class. Suppose the user selects a new tool or something on the program's form and the host needs to disable the polygon selector. The obvious thing to do is to release any references to the polygon selector object so it would go away and stop working. Unfortunately that object would probably keep its Host_MouseDown event handler installed. If the user later pressed the mouse down, it would start selecting a new polygon.

Eventually the garbage collector would run and kill the polygon selector, but you don't know when that might happen.

What we need is a way to explicitly make the polygon selector stop. The IDisposable interface seems like a good approach. However, that interface is intended to free unmanaged resources when an object is being destroyed and that's not what's happening here. That approach would work, but to avoid possible confusion I used another method.

Instead of using IDisposable, I gave the class the following Enabled property.

// Enable or disable the selector. private bool IsEnabled = true; public bool Enabled { get { return IsEnabled; } set { if (value == IsEnabled) return; IsEnabled = value; if (IsEnabled) { Host.MouseDown += Host_MouseDown; } else { Host.MouseDown -= Host_MouseDown; Host.MouseMove -= Host_MouseMove; Host.MouseClick -= Host_MouseClick; Host.Paint -= Host_Paint; } } }

The IsEnabled field keeps track of whether the polygon selector is enabled. The Enabled property gets and sets that value. The get accessor simply returns the value of IsEnabled.

The set accessor first compares IsEnabled to the property's new value and returns without doing anything if the values are the same.

If the value is changing, the code saves the new value in the IsEnabled field. If the polygon selector should be enabled, the code then installs the Host_MouseDown event handler so the selector is ready to select a polygon.

If the polygon selector should be disabled, the code uninstalls all of the event handlers that the selector might have installed. One nice thing about C#'s event handlers is that you can uninstall an event handler even if it is not installed without anything bad happening. For example, if the Host_Paint event handler is not installed, then uninstalling it won't hurt the program. (This is good because C# does not provide a way to determine whether an event handler is installed. If this safety feature were not true, then we would need to write code to keep track of which event handlers were installed.)

The Main Program

Building the polygon selector takes a little work, but all of that work makes using the selector relatively simple. The following code shows how the example program creates its selector.

// The polygons. private List<Point[]> Polygons = new List<Point[]>(); // The polygon selector. private PolygonSelector Selector; // Prepare the selector. private void Form1_Load(object sender, EventArgs e) { Selector = new PolygonSelector(picCanvas, new Pen(Color.Red, 3)); Selector.PolygonSelected += Selector_PolygonSelected; }

The Polygons field holds a list of arrays of points. Each of the arrays contains the points that define a polygon.

The Selector field holds the polygon selector.

The form's Load event handler creates the selector object. Its host is the picCanvas PictureBox control. The code also registers the following event handler to catch the selector's PolygonSelected event.

// The user has selected a polygon. Save it. void Selector_PolygonSelected(object sender, PolygonEventArgs args) { // Save the new polygon. Polygons.Add(args.Points.ToArray()); // Redraw. picCanvas.Refresh(); }

This code converts the new polygon's point list into an array and adds it to the Polygons list. It then refreshes the picCanvas control to draw the new polygon.

The following code shows the picCanvas control's Paint event handler.

// Draw any existing polygons. private void picCanvas_Paint(object sender, PaintEventArgs e) { e.Graphics.Clear(picCanvas.BackColor); e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using (Pen pen = new Pen(Color.Green, 3)) { foreach (Point[] polygon in Polygons) { e.Graphics.DrawPolygon(pen, polygon); } } }

This code clears the drawing surface and prepares to draw smooth lines. It then loops through the polygons in the Polygons list and draws them.

When the polygon selector refreshes its host control, this event handler executes before the one defined by the polygon selector. That means any partially selected polygon is drawn in top of the polygons drawn by the preceding code. This also means the partially selected polygon is drawn with smooth lines because this code has already set the Graphics object's SmoothingMode property.

The last piece of the main program is the following code, which executes when the user checks or unchecks the Draw Polygons checkbox.

private void chkDrawPolygons_CheckedChanged(object sender, EventArgs e) { Selector.Enabled = chkDrawPolygons.Checked; }

This event handler simply updates the polygon selector's Enabled property appropriately.

Conclusion

Defining the polygon selector symbiont takes a bit of work, but using it is relatively easy. This class allows you to add polygon selection to a control without rewriting the same event handlers over and over. It also lets you avoid cluttering up your form code with those event handlers.

Better still, you can write other selector classes to let the user do other things such as drawing lines, rectangles, ellipses, or performing just about any other sequence of mouse events. Each of the symbionts encapsulates its event handlers to keep your form clean and simple. The most complicated thing you need to do is to ensure that only one symbiont is enabled at a given time.

Download the example to see additional details, to try building new drawing symbionts, or to use this symbiont in your programs. In my next post, I'll use the PolygonSelector to show how to clip an image to fit within a polygon.

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.