Make a hexagonal montage of pictures in C#

[hexagonal montage]

This example combines techniques from several earlier posts to make a hexagonal montage. It uses the techniques demonstrated in the post Draw a hexagonal grid in C# to define the grid and to make positions on the form two and from grid rows and columns. It uses clipping techniques used by the post Clip an image to a polygon in C# to clip images to the hexagons. Finally it uses the general approach demonstrated by the post Make an improved diagonal picture montage in C# to make the hexagonal montage.

This is a fairly long example so I won’t describe all of the techniques described in those earlier posts. See those posts for the details.

This example defines a static Hex class to contain methods that work with hexagons. Those methods include methods that convert points to and from locations in the grid, draw the grid, get points defining a hexagon, and draw pictures inside a hexagon.

However I will briefly explain what happens when you click on the program’s picGrid PictureBox.

Selecting Images

The program defines the following Hexagon class to keep track of the image that should be drawn in a particular hexagon.

public class Hexagon
{
    public int Row, Column;
    public Bitmap Picture;
    public string FileName;
    public Hexagon(int row, int column, Bitmap picture, string file_name)
    {
        Row = row;
        Column = column;
        Picture = picture;

        // Remove the file path.
        FileInfo file_info = new FileInfo(file_name);
        FileName = file_info.Name;
    }
}

When you click on the picGrid PictureBox, the following code executes.

// Add the clicked hexagon to the Hexagons list.
private void picGrid_MouseClick(object sender, MouseEventArgs e)
{
    // Get the row and column clicked.
    int row, col;
    Hex.PointToHex(e.X, e.Y, HexHeight, out row, out col);

    // Remove any existing record for this cell.
    RemoveHexagon(row, col);

    // Let the user select a new picture.
    if (ofdFile.ShowDialog() == DialogResult.OK)
    {
        Bitmap bm = LoadBitmapUnlocked(ofdFile.FileName);
        Hexagons.Add(new Hexagon(row, col, bm, ofdFile.FileName));
    }

    // Redraw.
    picGrid.Refresh();
}

This event handler uses the Hex class’s static PointToHex method to find the row and column of the point that you clicked in the hexagonal grid. It then calls the RemoveHexagon method described shortly to remove any existing hexagon that is currently at that position.

Next the code displays an OpenFileDialog to let you select an image file. If you pick a file, the code uses it to create a new Hexagon object for the selected row and column and containing the image that you selected. The code finishes by refreshing the PictureBox to redraw the grid.

The following code shows the RemoveHexagon method.

// Remove the Hexagon at this position if there is one.
private void RemoveHexagon(int row, int col)
{
    int index = FindHexagon(row,col);
    if (index >= 0) Hexagons.RemoveAt(index);
}

The RemoveHexagon method calls the following FindHexagon method to find the index of the hexagon at the clicked row and column, if there is already a hexagon there. If the method finds a hexagon at that position, the RemoveHexagon method removes it from the Hexagons list.

// Find the Hexagon at this position if there is one.
private int FindHexagon(int row, int col)
{
    for (int i = 0; i < Hexagons.Count; i++)
    {
        if ((Hexagons[i].Row == row) &&
            (Hexagons[i].Column == col))
                return i;
    }
    return -1;
}

This method simply loops through the objects in the Hexagons list and returns the index of any object that has the desired row and column. If it finds no such object, the method returns -1.

Drawing the Grid

The picGrid PictureBox control’s Paint event handler calls the following DrawGrid method to draw the grid.

private void DrawGrid(Graphics gr, int xmax, int ymax)
{
    gr.Clear(picBackgroundColor.BackColor);
    gr.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw the selected hexagons.
    float xmin = BorderThickness / 2f;
    foreach (Hexagon hexagon in Hexagons)
    {
        PointF[] points = Hex.HexToPoints(HexHeight,
            hexagon.Row, hexagon.Column, xmin, xmin);

        if (points[3].X > xmax) continue;
        if (points[4].Y > ymax) continue;

        Hex.DrawImageInPolygon(gr,
            Hex.HexToPoints(HexHeight,
                hexagon.Row, hexagon.Column, xmin, xmin),
                hexagon.Picture);
    }

    // Draw the grid.
    using (Pen pen = new Pen(picBorderColor.BackColor, BorderThickness))
    {
        Hex.DrawHexGrid(gr, pen,
            xmin, xmax, xmin, ymax, HexHeight);
    }
}

[hexagonal montage]

This code loops through the Hexagon objects in the Hexagons list. For each object, it uses the Hex class’s HexToPoints method to get points that define the object’s hexagon. The HexToPoints method returns the points in the order shown in the picture on the right, so the code looks at the points with indices 3 and 4 to determine whether the hexagon lies partly outside of the PictureBox. If the hexagon does not fit, the code skips it.

If the hexagon fits, the code calls the Hex.DrawImageInPolygon method to draw the hexagon’s image.

After it has drawn all of the images, the code calls the Hex.DrawHexGrid method to draw the hexagonal outlines.

Loading Many Files

The program includes one other interesting technique that I want to discuss. If you open the File menu and select Open Files in Directory, then the program executes the following code.

// Load the files from a directory.
private void mnuFileOpenDirectoryFiles_Click(object sender, EventArgs e)
{
    if (ofdDirectoryFiles.ShowDialog() == DialogResult.OK)
    {
        // Get a list of the files in the directory.
        FileInfo info = new FileInfo(ofdDirectoryFiles.FileName);
        DirectoryInfo dir_info = info.Directory;
        List<FileInfo> file_infos = new List<FileInfo>();
        foreach (FileInfo file_info in dir_info.GetFiles())
        {
            string ext = file_info.Extension.ToLower().Replace(".", "");
            if ((ext == "bmp") || (ext == "png") ||
                (ext == "jpg") || (ext == "jpeg") ||
                (ext == "gif") || (ext == "tiff"))
            {
                file_infos.Add(file_info);
            }
        }

        // Calculate the number of rows and columns.
        int num_rows = (int)Math.Sqrt(file_infos.Count);
        int num_cols = num_rows;
        if (num_rows * num_cols < file_infos.Count)
            num_cols++;
        if (num_rows * num_cols < file_infos.Count)
            num_rows++;

        // Load the files.
        Hexagons = new List<Hexagon>();
        int index = 0;
        for (int row = 0; row < num_rows; row++)
        {
            for (int col = 0; col < num_cols; col++)
            {
                string name = file_infos[index].Name;
                string full_name = file_infos[index].FullName;
                Bitmap bm = LoadBitmapUnlocked(full_name);
                Hexagons.Add(new Hexagon(row, col, bm, name));

                index++;
                if (index >= file_infos.Count) break;
            }
            if (index >= file_infos.Count) break;
        }

        picGrid.Refresh();
    }
}

This code displays an OpenFileDialog to let you select a file in a directory that contains images. If you select a file, the code gets a DirectoryInfo object representing the file’s directory. It calls that object’s GetFiles method to loop through the file in the directory and adds those that are image files to the file_infos list.

Next the code calculates a reasonable number of rows and columns to hold the files’ images. It starts by making the number of rows and columns equal to the square root of the number of images truncated to an integer. If that doesn’t give enough room, the method adds an extra column. If that still doesn’t give enough room, the method adds another row.

The code then loops through the rows and columns. For each position in the grid, the program loads the next image file from the file_infos list and creates a Hexagon object for that position.

After it finishes loading all of the files into Hexagon objects, the code refreshes the picGrid PictureBox to show the new grid.

Conclusion

That’s about all there is to the example that’s new. There are lots of other details such as how the program loads images without locking their files, how the program saves the resulting montage, how the Hex class calculates the positions of grid cells, and how the program displays the name of the image file under mouse. See the previous examples and download this example to see those details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing, tools | Tagged , , , , , , , , , , , , , , | Leave a comment

Find drawn characters under the mouse in C#

[drawn characters]

The following examples find the positions of drawn characters in a string that is drawn by using the Graphics object’s DrawString method.

This example does the opposite: it finds the drawn characters at given positions.

The program uses the following variables to keep track of the text and the positions of the drawn characters.

// The text.
private string TheText =
    "When in the course of human events it " +
    "becomes necessary for the quick brown " +
    "fox to jump over the lazy dog...";

// The character locations.
private List<RectangleF> TheRects;

When it starts, the program uses the following code to draw its text.

// Draw the text.
private void Form1_Load(object sender, EventArgs e)
{
    // Make a Bitmap to hold the text.
    Bitmap bm = new Bitmap(
        picText.ClientSize.Width,
        picText.ClientSize.Height);
    using (Graphics gr = Graphics.FromImage(bm))
    {
        gr.Clear(Color.White);

        // Don't use TextRenderingHint.AntiAliasGridFit.
        gr.TextRenderingHint = TextRenderingHint.AntiAlias;

        // Make a font to use.
        using (Font font = new Font("Times New Roman", 16, FontStyle.Regular))
        {
            // Draw the text.
            gr.DrawString(TheText, font, Brushes.Blue, 4, 4);

            // Measure the characters.
            TheRects = MeasureCharacters(gr, font, TheText);
        }
    }

    // Display the result.
    picText.Image = bm;
}

This code creates a bitmap and draws the text on it. It also calls the MeasureCharacters method described in the post Measure character positions when drawing long strings in C# to find the locations of the drawn characters. It finishes by displaying the bitmap on the picText PictureBox control

When the mouse moves over the picText control, the following event handler executes.

private void picText_MouseMove(object sender, MouseEventArgs e)
{
    // See if the mouse is over one of the character rectangles.
    string new_text = "";
    for (int i = 0; i < TheText.Length; i++)
    {
        if (TheRects[i].Contains(e.Location))
        {
            new_text =
                "Character " + i.ToString() + ": " + TheText[i];
            break;
        }
    }
    if (lblChar.Text != new_text) lblChar.Text = new_text;
}

This code loops through the TheRects list checking the rectangles that contain the drawn characters to see if any contains the mouse’s position. If the mouse position lies inside one of the rectangles, the program displays the index and value of the corresponding character.

See the earlier posts and download the example to see additional details.

Unfortunately this method doesn’t work for strings drawn in more complex ways. For example, if you use a formatting rectangle and a StringFormat object to draw wrapped text with ellipses, then the string measuring methods won’t work. In that case you might want to display the text in a TextBox or RichTextBox control and use a TextRenderer object to measure character positions.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, drawing, fonts, graphics, strings | Tagged , , , , , , , , , , , , , , , , | Leave a comment

Clip an image to a polygon in C#

[clip an image]

This example lets you select a polygon and then uses it to clip an image to it. It uses the technique described in my previous post Build a polygon selector class in C# to let you select the polygon.

The following section describes how the program lets you select a polygon. The section after that explains how the program clips images.

Selecting Polygons

When the program starts, the following code prepares a PolygonSelector object to let you select polygons.

// The polygon selector.
private PolygonSelector Selector;
private PointF[] Polygon = null;

// 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 field Selector holds a references to a PolygonSelector. The Polygon array will hold the points that define the polygon.

The form’s Load event handler creates the PolygonSelector and registers the Selector_PolygonSelected method to catch the selector’s PolygonSelected event.

The selector is attached to the picCanvas control. When you press the mouse down on that control, the selector automatically takes over and lets you select a polygon. See the previous post for an explanation of how the selector works.

When you finish selecting a polygon, the selector raises its PolygonSelected event and the following event handler catches it.

// The user has selected a polygon. Save it.
void Selector_PolygonSelected(object sender, PolygonEventArgs args)
{
    Polygon = PointsToPointFs(args.Points);
    picCanvas.Refresh();
}

This code gets the polygon’s points from the event handler’s e.Points list. It calls the PointsToPointFs method to convert the Point values into an array of PointF values, saves the result in the Polygon field, and refreshes the picCanvas PictureBox to show the new image.

The following code shows the PointsToPointFs method.

// Convert Point data into a PointF array.
private PointF[] PointsToPointFs(IEnumerable<Point> points)
{
    var query = from Point point in points select (PointF)point;
    return query.ToArray();
}

This method uses LINQ to convert an IEnumerable<Point> into a PointF array. The method creates a query that iterates over the values in the points enumerable and casts the values into PointF values. The method then calls the query’s ToArray method and returns the result.

The code saves the PointF array in the Polygon field and refreshes the picCanvas PictureBox to show the image clipped in the polygon.

Drawing the Image

The following code shows the Paint event handler that draws the clipped image.

private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.Clear(picCanvas.BackColor);

    if (Polygon != null)
    {
        DrawImageInPolygon(e.Graphics,
            Polygon, Properties.Resources.Smiley_with_bg);
        using (Pen pen = new Pen(Color.Green, 3))
        {
            e.Graphics.DrawPolygon(pen, Polygon.ToArray());
        }
    }
}

This code sets the Graphics object’s SmoothingMode and clears the picture. If the Polygon field is not null, the code calls the DrawImageInPolygon method described shortly to clip an image to the polygon and draw it. It finishes by creating a thick, green pen and using the pen to draw the polygon.

The following code shows the DrawImageInPolygon method.

// Draw an image so it fills the polygon.
public static void DrawImageInPolygon(Graphics gr,
    PointF[] points, Image image)
{
    // Get the polygon's bounds and center.
    float xmin, xmax, ymin, ymax;
    GetPolygonBounds(points, out xmin, out xmax, out ymin, out ymax);
    float wid = xmax - xmin;
    float hgt = ymax - ymin;
    float cx = (xmin + xmax) / 2f;
    float cy = (ymin + ymax) / 2f;

    // Calculate the scale needed to make
    // the image fill the polygon's bounds.
    float xscale = wid / image.Width;
    float yscale = hgt / image.Height;
    float scale = Math.Max(xscale, yscale);

    // Calculate the image's scaled size.
    float width = image.Width * scale;
    float height = image.Height * scale;
    float rx = width / 2f;
    float ry = height / 2f;

    // Find the source rectangle and destination points.
    RectangleF src_rect = new RectangleF(0, 0,
        image.Width, image.Height);
    PointF[] dest_points =
    {
        new PointF(cx - rx,  cy - ry),
        new PointF(cx + rx,  cy - ry),
        new PointF(cx - rx,  cy + ry),
    };

    // Clip the drawing area to the polygon and draw the image.
    GraphicsPath path = new GraphicsPath();
    path.AddPolygon(points);
    GraphicsState state = gr.Save();
    gr.SetClip(path);   // Comment out to not clip.
    gr.DrawImage(image, dest_points, src_rect, GraphicsUnit.Pixel);
    gr.Restore(state);
}

This code first calls the GetPolygonBounds method to find the largest and smallest X and Y coordinates used by the polygon’s points. That method simply loops through the array’s points and keeps track of the largest and smallest values. It’s relatively straightforward so it isn’t shown here.

The DrawImageInPolygon method then calculates the width and height of the points’ bounds, and finds the center of those bounds.

The method then calculates a scale factor that it should use to make the image fill the points’ bounds. First it calculates the amounts by which it should scale the image to make it fill the bounds horizontally and vertically. It then uses the larger of the two scales to ensure that the image completely fills the bounds. The code uses the scale factor to calculate the image’s scaled size.

Next the code creates a Rectangle that defines the image’s entire area. It also creates an array of PointF values that indicate where the image should be drawn. That area is the same as the polygon’s bounds.

The code then creates a GraphicsPath object and uses its AddPolygon method to add the polygon’s points to it. The code saves the Graphics object’s state and calls its SetClip method to clip future drawing to the path. The method draws the image, now clipped to the polygon, and then restores the Graphics object’s state to remove the clipping. (That isn’t necessary in this example but would be if the program performed additional drawing after drawing the clipped polygon.)

Summary

The key to this example is the Graphics object’s SetClip method, which restricts the object’s drawing to a GraphicsPath. This example sets the path equal to the polygon that you selected. For other programs, you could make the path much more complicated so it contained rectangles, ellipses, and other shapes.

After you set the Graphics object’s clipping region, you simply draw as usual.

See the previous post to see how the PolygonSelector works. Download this example to experiment with it and to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing | Tagged , , , , , , , , , | 1 Comment

Build a polygon selector class in C#

[polygon selector]

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 Example   Follow me on Twitter   RSS feed   Donate




Posted in classes, drawing, graphics | Tagged , , , , , , , , | 1 Comment

Arrange images on the corners of a polygon in C#


[arrange images]

This post shows a way to arrange images on top of a background image as shown in the image above.

How to Use the Program

Enter the names of the background and foreground image files, or use the File menu’s Foreground Image and Background Image commands to select the files. Next, enter the final size that you want the composite image to have. The program will use an area with this size from the upper left corner of the background image to produce the final result, so make sure this image is at least as large as the values you enter here. If the background image is too small, parts of the result image will be blank. (You could modify the program to scale the background image if necessary.)

Now enter the width that you want the foreground image to have. The program scales that image uniformly so it calculates an appropriate image height from the width you enter.

Next enter a radius to determine the distance between the center of the result image and the centers of the foreground images. Finally enter the number of copies of the foreground image that you want to display. For example, if you enter 6, then the images are placed on the corners of a hexagon.

How the Program Works

The program is relatively straightforward. The basic idea is to make an angle theta loop around a circle, incrementing by the number of radians between the vertices of the polygon. For example, if we’re building a hexagon, then the values of theta are 2π/6 radians apart. For each value of theta, the program finds a point in the direction of theta from the origin at the distance given by the radius value. It then adds that point’s coordinates to the center of the image to center the result. (This is a common approach for doing something to points on a circle. For example, you could connect the points to draw the polygon.)

As it generates its points, the program draws copies of the foreground image at them.

To simplify the discussion somewhat, I’ll describe the program’s code in two pieces. The following code shows how the first piece, which prepares the foreground image.

private void btnGo_Click(object sender, EventArgs e)
{
    try
    {
        Bitmap fg_image = new Bitmap(txtFgImage.Text);
        Bitmap bg_image = new Bitmap(txtBgImage.Text);
        int width = int.Parse(txtWidth.Text);
        int height = int.Parse(txtHeight.Text);
        int fg_width = int.Parse(txtFgWidth.Text);
        int radius = int.Parse(txtRadius.Text);
        int num_images = int.Parse(txtNumImages.Text);

        // Scale the foreground image.
        float scale = fg_width / (float)fg_image.Width;
        int fg_height = (int)(fg_image.Height * scale);
        fg_image = new Bitmap(fg_image, new Size(fg_width, fg_height));

This code gets the images and the parameters that you entered. It then scales the foreground image. To do that, it sets variable scale equal to the foreground image width that you entered divided by the foreground image’s actual width. It then uses that scale factor to calculate the appropriate foreground image height.

The program then sets variable fg_image to a new Bitmap. It passes the constructor the original foreground image and the new size that it should have. This is perhaps the easiest way to resize an image.

The following shows the rest of the code that produces the result image. This piece draws the background image and then draws the foreground image on top of it.

        // Draw the final result.
        Bitmap bm = new Bitmap(width, height);
        using (Graphics gr = Graphics.FromImage(bm))
        {
            gr.InterpolationMode = InterpolationMode.High;
            gr.DrawImage(bg_image, 0, 0);
            int cx = width / 2;
            int cy = height / 2;
            int rx = fg_width / 2;
            int ry = fg_height / 2;
            RectangleF src_rect =
                new RectangleF(0, 0, fg_width, fg_height);

            double theta = -Math.PI / 2;
            double dtheta = 2 * Math.PI / num_images;
            for (int i = 0; i < num_images; i++)
            {
                float x = (float)(radius * Math.Cos(theta));
                float y = (float)(radius * Math.Sin(theta));
                PointF[] dest_points =
                {
                    new PointF(cx + x - rx, cy + y - ry),
                    new PointF(cx + x + rx, cy + y - ry),
                    new PointF(cx + x - rx, cy + y + ry),
                };
                gr.DrawImage(fg_image, dest_points, src_rect, GraphicsUnit.Pixel);
                theta += dtheta;
            }

            // Redraw the left side of the first image.
            src_rect =
                new RectangleF(0, 0, fg_width / 2f, fg_height);

            theta = -Math.PI / 2;
            {
                float x = (float)(radius * Math.Cos(theta));
                float y = (float)(radius * Math.Sin(theta));
                PointF[] dest_points =
                {
                    new PointF(cx + x - rx, cy + y - ry),
                    new PointF(cx + x, cy + y - ry),
                    new PointF(cx + x - rx, cy + y + ry),
                };
                gr.DrawImage(fg_image, dest_points, src_rect, GraphicsUnit.Pixel);
            }
        }
        picResult.Image = bm;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

This code creates a bitmap with the final size that you entered. It makes an associated Graphics object and draws the background image onto it. The only parameters that the code passes into the DrawImage method are the image and the coordinates (0, 0) were the image’s upper left corner should be drawn. The image is drawn at full scale and truncated if it cannot fit on the bitmap.

Next the code calculates the coordinates of the center of the bitmap. It also makes a RectangleF sized to fit the foreground image.

The code then makes variable theta loop through angles around a circle. It starts at angle π/2 so the first image is directly above the center of the bitmap.

For each angle, the code calculates the point (x, y) distance radius from the origin. It adds those values to the coordinates at the center of the result image. It also adds +/-rx and +/-ry to find the corners where the new copy of the foreground image should be placed. Finally the code calls the Graphics object’s DrawImage method to draw a copy of the foreground image in that position.

If the program left it at that, the first image would be below the last one as shown in the following picture.


[arrange images]

To make the final image lies partly below the first one, the code redraws the left side of the first image again. Note that this doesn’t work if the images are so close together that images before the last one also overlap with the first image.

Conclusion

This example lets you arrange images at the corners of a polygon. You may not need this in a line-of-business application, but it produces an interesting effect that you may find useful. It’s also an worthwhile exercise in image processing.

Note that this program does not rotate the copies of the foreground image. You can see this if you look closely at the images above. It’s easier to see in the following image.


[arrange images]

If you want the images to rotate so they all have their bottoms closer to the center of the result, you can use techniques described in my earlier post Draw an image spiral in C#.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in graphics, image processing, mathematics | Tagged , , , , , , , , , | Leave a comment

Make an improved diagonal picture montage in C#


[picture montage]

My earlier example Make a Pinterest-style diagonal picture montage in C# showed how you could make a picture montage showing parts of images rotated by a desired angle. While using that program today I found a small bug. The areas between the images are not always completely filled with the divider color.

The following picture shows the program displaying a picture montage that it created. The red circles show places where pieces of images show between the cells’ borders.


[picture montage]

[picture montage]

The picture on the right shows aa closeup of one of the problem areas.

Fortunately the solution to this problem is simple. The Cell class’s Draw method draws a cell’s picture and border. The following code shows the part of that method that draws the border. The new statement highlighted in blue fixes the problem.


// Draw the cell.
public void Draw(Graphics gr, Pen pen, float cell_width, float cell_height)
{
    // Draw the cell's picture.
    ...

    // Outline the cell.
    gr.DrawRectangle(pen, Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height);
    GraphicsPath path = MakeRoundedRect(Bounds,
        2 * pen.Width, 2 * pen.Width, true, true, true, true);
    gr.DrawPath(pen, path);
}

The new statement draws a rectangle around the image before the code draws the image’s rounded rectangle. The new rectangle fills in the problem areas where the rounded rectangles’ corners don’t quite meet the adjacent rectangles.

See the Make a Pinterest-style diagonal picture montage in C#previous post for more details about how the program works. Download this example to experiment with it.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing, tools | Tagged , , , , , , , , , , , , , | Leave a comment

Use VBA to randomize Excel selections

[randomize Excel selections]

The post Use VBA to randomize cells in Excel explains how you can randomize a simple selection’s rows. Unfortunately that method cannot randomize Excel selections in general. In particular it doesn’t work if the current selection includes multiple areas on the worksheet.

To understand why the method doesn’t work, see the post Understand range areas in Excel VBA. This post demonstrates a new approach that can randomize Excel selections even if they include multiple areas.

When you click the Randomize Selection button, the following code executes.

Sub RandomizeSelection()
Dim rng As Range
Dim num_values As Integer
Dim values() As Variant
Dim cell As Range
Dim i As Integer
Dim j As Integer
Dim temp As Variant

    ' Make sure the selection is a range.
    If Not (TypeOf Application.Selection Is Range) Then
        MsgBox "The current selection is not a range."
        Exit Sub
    End If
   
    ' Get the values.
    Set rng = Application.Selection
    num_values = rng.Count
    ReDim values(1 To num_values)
    i = 1
    For Each cell In rng
        values(i) = cell.Value
        i = i + 1
    Next cell
   
    ' Randomize the values.
    For i = 1 To num_values - 1
        ' Pick a random index between i and num_values.
        j = Int((num_values - 1 + 1) * Rnd + 1)
                
        ' Swap indices i and j.
        temp = values(i)
        values(i) = values(j)
        values(j) = temp
    Next i
    
    ' Copy the randomized values back into the cells.
    i = 1
    For Each cell In rng
        cell.Value = values(i)
        i = i + 1
    Next cell
End Sub

This code first verifies that the current selection is a range. It then sets Range variable rng equal to the selection (for convenience) and gets the number of cells in the range.

Next the subroutine creates an array named values. It then loops through the range’s cells and copies their values into the array.

After copying the values into the array, the code randomizes the array. To do that, it loops through each position i in the array except the last position. For each position i, the code picks a random index j between i and num_values. The code then swaps the values at positions i and j. When it is finished, the array is randomized.

The For i lop does not include the last index in the array because that step would make the code swap the last item with itself. That wouldn’t hurt anything, but it would be a wasted step.

After it finishes randomizing the values array, the code loops through the range’s cells again, this time copying the randomized values into the range’s cells.

Conclusion

That’s all there is to the example. Download it to experiment with it. For example, you can verify that the method works if you select a single cell, cells in a contiguous block, or cells in multiple blocks.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in Excel, VBA | Tagged , , , , , , , , , , | Leave a comment

Understand range areas in Excel VBA


[range areas]

Most of my VBA examples assume that the current selection is a single block of cells. If the selection includes a single area, then you can easily loop through the cells that it contains.

Unfortunately if the selection includes multiple range areas, then trying to loop through the selection’s cells may give strange results where cells seem to not be part of the selection.

This example shows three ways that you can loop through a selection’s cells. When you click the Show Values button, the example loops through the current selection’s cells in three ways. The following code shows the outline of the macro that the button executes.

Sub ForLoop()
Dim col1 As Integer
Dim col2 As Integer
Dim col3 As Integer
Dim rng As Range
Dim cell As Range
Dim area As Range
Dim sheet As Worksheet
Dim num_values As Integer
Dim i As Integer
Dim j As Integer

    ' Display headers.
    Set sheet = Application.Sheets(1)
    Set rng = Application.Selection
    col1 = 4
    col2 = 5
    col3 = 6
    For i = col1 To col3
        sheet.Columns(i).ClearContents
    Next i
    sheet.Columns(col1).Interior.Color = RGB(255, 182, 193)
    sheet.Columns(col2).Interior.Color = RGB(144, 238, 144)
    sheet.Columns(col3).Interior.Color = RGB(173, 216, 230)
    ...
End Sub

This code declares a bunch of variables. It then sets variables sheet and rng equal to the workbook’s first worksheet and current selection so they are easier to use.

Next the code sets variables col1, col2, and col3 to the columns where it should place output. It loops through those columns clearing any previous results and then gives those columns pink, light green, and light blue backgrounds.

The previous code snippet omits the looping code, which it the part that’s really interesting. The following code shows the next piece, which loops through the range’s cells in the most obvious but incorrect way.

' Loop through range indexes.
sheet.Cells(1, col1).Value = "For i"
num_values = rng.Count
For i = 1 To num_values
    sheet.Cells(i + 1, col1).Value = rng(i).Address + ": " + rng(i).Value
Next i

This code sets num_values to the number of cells in the range and then makes variable i loop through the values 1 to num_values. Inside the loop, the code uses i as an index into the range and displays that cell’s address and value.

If the range includes a single area, then this works. For example, the following picture shows the results when the selection includes only cells A5 through A7.


[range areas]

You can see in columns D, E, and F that all three methods find the same cells.

However, if the selection includes multiple range areas, then this technique of accessing the range’s cells by index doesn’t work. In that case the indexes are in relation to the selection’s first area. After the index passes the end of that area, the other accessed cells just drop off the end off that area. Look at the

The following code shows a second method for accessing the range’s cells.

' Loop through the range's cells.
sheet.Cells(1, col2).Value = "For Each"
i = 2
For Each cell In rng
    sheet.Cells(i, col2).Value = cell.Address + ": " + cell.Value
    i = i + 1
Next cell

This code uses a For Each loop to loop through the range. Each of the loop’s items is a Range representing one of the range’s cells. Using an index as in rng(i) doesn’t work with multiple range areas, but this loop does. That’s somewhat counterintuitive because both seem to be accessing the range’s default Item property.

Anyway, you can see that this version works in the light green column in the picture at the top of the post.

The main drawback to this method is that you cannot access an arbitrary cell in the range. For example, this method won’t let you easily change the value of the range’s fourth cell. If you need that kin of access, loop through the cells and copy references to the cells into an array. Then you can access the cells through the array.

The following code shows the last method that this example uses to access the range’s cells.

' Loop through range's areas.
sheet.Cells(1, col3).Value = "By Areas"
i = 2
For Each area In rng.Areas
    sheet.Cells(i, col3).Value = "Area " & area.Address
    i = i + 1
    num_values = area.Cells.Count
    For j = 1 To num_values
        sheet.Cells(i, col3).Value = "    " & _
            area.Cells(j).Address & ": " & area.Cells(j).Value
        i = i + 1
    Next j
Next area>

This code loops through the range’s Areas collection. Each area is a simple Range so you can loop through it by using the simpler For i loop if you like.

Summary

The most important thing to take away from this example is the fact that you can’t use a simple For i loop to loop over the items in a range that includes multiple range areas. You can use a For Each loop or you can loop through the range’s areas, particularly if you want to handle each area separately.

In my next post I’ll show how you can modify my earlier example that randomizes items in an Excel worksheet so it can handle multi-area ranges.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in Excel, VBA | Tagged , , , , , , , , , | 1 Comment

Let the user draw rotated skewed polygons in C#

[skewed polygons]

The example Let the user draw rotated polygons with right angles in C# draws polygons with edges that are parallel or perpendicular to a baseline. This example is very similar except its polygons have edges that are parallel to one of two baselines. The result is a rotated and skewed polygon.

The two programs are extremely similar. The only difference is in the GetTransform method. Before I describe the new version of that method, let me recap how the previous example works.

The Previous Example

The previous example uses a GetTransform method to make a transformation matrix that rotates polygon points so the polygon’s edges are parallel to the X and Y axes. It then transforms the new polygon point and compares it to the polygon’s previous point. If the transformed points differ by less in the X direction than in the Y direction, the program gives the new point same X coordinate as the old point. Conversely if the points differ by less in the Y direction than in the X direction, the program gives the new point same Y coordinate as the old point.

After it has adjusted the new point’s transformed version, it inverts the transformation matrix and uses it to move the updated point back into the original frame of reference. The result is the final adjusted point.

Here’s a summary of the steps.

  1. Transform the new point and the previous one into a rotated coordinate system where the polygon’s edges are parallel to the X and Y axes.
  2. Determine whether the transformed new point and the transformed previous point are closer in the X or Y direction and update the transformed new point accordingly.
  3. Invert the previous transformation and use it to move the updated point back into the original coordinate system.

This is a fairly straightforward technique, but it’s not an intuitive way to think so it may be hard to understand. If you don’t see how it works, you may want to revisit the previous example.

GetTransform

The current example uses the same technique. The only difference is in the transformation that maps points from the original skewed coordinate system into a coordinate system that’s easier to work with.

Unfortunately there’s no direct way to make a transformation that maps from a skewed coordinate system to a rectangular one. Fortunately there is an easy way to map in the other direction. One of the Matrix class’s constructors takes as parameters a source rectangle and an array of three points that indicate where the upper left, upper right, and lower left corners of the rectangle should be mapped. The fourth corner’s position is implied by the positions of the other three corners.

[skewed polygons]

The picture on the right shows how the corners of the rectangle on the left map to corners on a parallelogram on the right.

But we want to do the opposite. We want to map from a skewed coordinate system to a rectangular one. To do that, we first make a transformation matrix that maps from a rectangular system to the system defined by the program’s baselines. We then invert that transformation matrix to get the transformation that we need.

The following code shows the new GetTransform method.

private Matrix GetTransform()
{
    float xdx = XAxisEnd.X - XAxisStart.X;
    float xdy = XAxisEnd.Y - XAxisStart.Y;
    float xlen = (float)Math.Sqrt(xdx * xdx + xdy * xdy);

    float ydx = YAxisEnd.X - YAxisStart.X;
    float ydy = YAxisEnd.Y - YAxisStart.Y;
    float ylen = (float)Math.Sqrt(ydx * ydx + ydy * ydy);

    RectangleF rect = new RectangleF(0, 0, xlen, ylen);
    PointF[] dest_points =
    {
        XAxisStart,
        XAxisEnd,
        new PointF(XAxisStart.X + ydx, XAxisStart.Y + ydy),
    };
    Matrix matrix = new Matrix(rect, dest_points);

    // Invert the matrix.
    matrix.Invert();
    return matrix;
}

The program stores the end points of the two baseline axes in variables XAxisStart, XAxisEnd, YAxisStart, and YAxisEnd. The GetTransform method first calculates the lengths of those axes. It then makes a rectangle with width equal to the length of the X baseline and height equal to the length of the Y baseline. Think of this rectangle as living in normal rectangular coordinates.

Next the code creates an array indicating where the rectangle’s upper left, upper right, and lower left corners should be mapped. It maps the upper left and upper right corners to the end points of the X baseline. It maps the lower left corner to the point you get when you add the Y baseline to the start of the X baseline. If the two baselines start at the same point, as they do in the picture at the top of this post, then that point is at the end of the Y baseline.

The method then uses the rectangle and points to create a transformation matrix to make the rectangle to the skewed coordinates. Finally it inverts that matrix and returns the result.

The rest of the program is the same as the previous example. It uses the GetTransform method to map from the drawing coordinate system to a rectangular system and then adjusts the new point in the easier-to-use coordinate system.

Conclusion

This example and the previous one demonstrate a powerful technique. They both map from an inconvenient coordinate system to one that is easier to use. Although it’s a powerful technique, it’s probably one that you won’t need to use very often.

Download the new example to see additional details and to experiment with it. For example, see what happens if you draw a polygon, change the X and Y baselines, and then draw a new polygon.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, drawing, graphics, mathematics, transformations | Tagged , , , , , , , , , , , , | Leave a comment

Let the user draw rotated polygons with right angles in C#

[rotated polygons]

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.

[rotated polygons]

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.


[rotated polygons]

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.

  1. Find a transformation that rotates the baseline so it is perpendicular to the X axis.
  2. Rotate the point to adjust and the reference point.
  3. Subtract X and Y coordinates to see which dashed segment we should follow.
  4. Use the coordinates of the point to adjust and the reference point to find the adjusted point’s rotated location.
  5. 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 Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, drawing, graphics, mathematics, transformations | Tagged , , , , , , , , , , , , | Leave a comment