Make a montage editor in C#, Part 2


The post Make a montage editor in C#, Part 1 explains how the montage editor loads, draws, and saves images. This post explains how the program lets you manipulate images.

When you move the mouse over an image’s body, it displays a four-way north/south/east/west cursor. When you move the mouse over an image’s corner, the program displays an appropriate resizing cursor. For example, if you place the mouse over an image’s upper right corner, the program displays a northeast/southwest cursor to indicate that you can drag that corner to the northeast or southwest.

The key to displaying the correct cursor is the following FindImageAt method.

// Return the image under the mouse and the hit type.
private void FindImageAt(Point point, out ImageInfo image,
    out ImageInfo.HitTypes hit_type)
    // See if we hit an image.
    for (int i = Images.Count - 1; i >= 0; i--)
        hit_type = Images[i].HitType(point);
        if (hit_type != ImageInfo.HitTypes.None)
            image = Images[i];

    image = null;
    hit_type = ImageInfo.HitTypes.None;

This method loops through the loaded ImageInfo objects and calls each object’s HitType method. If it finds a hit, the method saves the object below the mouse and returns the hit type.

The following SetMouseCursor method uses FindImageAt to display the appropriate cursor.

// Variables to remember what the mouse is over.
private ImageInfo MouseImage = null;
private ImageInfo.HitTypes MouseHitType = ImageInfo.HitTypes.None;

// Set the correct mouse cursor.
private void SetMouseCursor(Point point)
    // See if the mouse is over an image.
    FindImageAt(point, out MouseImage, out MouseHitType);

    switch (MouseHitType)
        case ImageInfo.HitTypes.None:
            picImages.Cursor = Cursors.Default;
        case ImageInfo.HitTypes.Body:
            picImages.Cursor = Cursors.SizeAll;
        case ImageInfo.HitTypes.NwCorner:
        case ImageInfo.HitTypes.SeCorner:
            picImages.Cursor = Cursors.SizeNWSE;
        case ImageInfo.HitTypes.NeCorner:
        case ImageInfo.HitTypes.SwCorner:
            picImages.Cursor = Cursors.SizeNESW;

This code simply calls FindImageAt and then sets the appropriate cursor depending on the hit type. Notice that this call saves the image hit and the hit type in the class-level variables MouseImage and MouseHitType.

The PictureBox‘s MouseMove event handler (shown shortly) calls SetMouseCursor to display the correct cursor if no drag is in progress. If a drag is in progress, if the user is moving or resizing an image, the event handler deals with the drag.

When the user presses the mouse down, the following MouseDown event handler starts a drag.

// The type if drag in progress.
private ImageInfo.HitTypes DragType = ImageInfo.HitTypes.None;

// The position where a drag started.
private Point StartPoint;
private Rectangle StartRect;

// Start dragging a corner or an image.
private void picImages_MouseDown(object sender, MouseEventArgs e)
    // If we're not over anything, do nothing.
    if (MouseHitType == ImageInfo.HitTypes.None) return;

    // Bring the image to the top.

    // Save the location and drag type.
    StartPoint = e.Location;
    StartRect = MouseImage.DestRect;
    DragType = MouseHitType;

If the current mouse hit type (saved in variable MouseHitType by the SetMouseCursor method) is None, then the event handler simply exits.

Otherwise the mouse is over an image. In that case the event handler removes its ImageInfo object from the Images list and then re-adds the item. That puts the object at the end of the list so it is drawn last and therefore above the other images. The code refreshes the PictureBox so you can see the item in its new position on top.

Next the code saves the mouse’s current location in variable StartPoint. It saves the image’s current drawing location in StartRect and saves the hit type in DragType.

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

// Display an appropriate mouse pointer.
private void picImages_MouseMove(object sender, MouseEventArgs e)
    // See if a drag is in progress.
    if (DragType == ImageInfo.HitTypes.None)
        // No drag is in progress. Set the appropriate cursor.
        if (DragType == ImageInfo.HitTypes.Body)
            // Just move it.
            int dx = e.X - StartPoint.X;
            int dy = e.Y - StartPoint.Y;
            MouseImage.DestRect.X = StartRect.X + dx;
            MouseImage.DestRect.Y = StartRect.Y + dy;
            // Get the desired new width and height.
            int new_wid, new_hgt;
            if ((DragType == ImageInfo.HitTypes.NwCorner) ||
                (DragType == ImageInfo.HitTypes.SwCorner))
                new_wid = StartRect.Right - e.X;
                new_wid = e.X - StartRect.Left;

            if ((DragType == ImageInfo.HitTypes.NwCorner) ||
                (DragType == ImageInfo.HitTypes.NeCorner))
                new_hgt = StartRect.Bottom - e.Y;
                new_hgt = e.Y - StartRect.Top;

            // Fix the aspect ratio.
            if (new_hgt != 0)
                float orig_aspect =
                    MouseImage.SourceRect.Width /
                float new_aspect = new_wid / (float)new_hgt;

                if (new_aspect > orig_aspect)
                    // Too short and wide. Make taller.
                    new_hgt = (int)(new_wid / orig_aspect);
                else if (new_aspect < orig_aspect)
                    // Too tall and thin. Make wider.
                    new_wid = (int)(new_hgt * orig_aspect);

            // Update the destination rectangle.
            int right = MouseImage.DestRect.Right;
            int bottom = MouseImage.DestRect.Bottom;
            if ((DragType == ImageInfo.HitTypes.NwCorner) ||
                (DragType == ImageInfo.HitTypes.SwCorner))
                MouseImage.DestRect.X = right - new_wid;
            if ((DragType == ImageInfo.HitTypes.NwCorner) ||
                (DragType == ImageInfo.HitTypes.NeCorner))
                MouseImage.DestRect.Y = bottom - new_hgt;
            MouseImage.DestRect.Width = new_wid;
            MouseImage.DestRect.Height = new_hgt;

        // Redraw.

If no drag is in progress, the code calls SetMouseCursor to display the appropriate cursor.

If the user is dragging an image’s body, the code calculates the distance the mouse has moved since the drag started. It uses that distance to update the upper left corner of the image’s DestRect.

If the user is dragging an image corner, the code calculates the width and height from the current mouse position to the image’s opposite corner. For example, suppose the user is dragging the upper right corner. Then the image’s desired width and height are given by the distances from the mouse to the image’s lower left corner.

Having calculated the new desired width and height, the program fixes the new aspect ratio. For example, if the original image was square but the new height is much larger than the new width, then the new size is too tall and thin. In that case, the code calculates a new width to make the image square again.

After it fixes the new width and height to get the right aspect ratio, the code sets the image’s X, Y, Width, and Height values to give the image the correct size but leave the corner opposite the mouse stationary. For example, suppose the user is dragging the image’s upper left corner. Then its lower right corner should remain stationary while the upper left corner moves.

After it has finished moving and resizing the image, the code refreshes the PictureBox to show the result.

The program only contains a few other pieces of code and they’re pretty simple. When the user releases the mouse, the following code ends the drag.

// Stop dragging.
private void picImages_MouseUp(object sender, MouseEventArgs e)
    DragType = ImageInfo.HitTypes.None;

When you select the File menu’s New command, the following code empties out the image list and redraws.

// Remove all images.
private void mnuFileNew_Click(object sender, EventArgs e)
    Images = new List();

The only other even vaguely interesting piece of code is the following Click event handler, which is called by all of the Scale menu’s commands.

// Scale all images.
private void mnuScale_Click(object sender, EventArgs e)
    ToolStripMenuItem mnu = sender as ToolStripMenuItem;
    float scale = float.Parse(mnu.Tag.ToString());

    foreach (ImageInfo info in Images)
        info.DestRect = new Rectangle(
            (int)(info.SourceRect.Width * scale),
            (int)(info.SourceRect.Height * scale));

Each of the Scale commands has its Tag property set to its scale. For example, the 1/2 command has Tag set to 0.5.

This event handler gets the menu item as a ToolStripMenuItem and parses its Tag property to get the desired scale. It then loops through the images and sets the DestRect size for each to the image’s original size times the scale. When it’s finished the code redraws.

There are lots of other features you could add to this simple program. For example you could:

  • Include different kinds of borders for selected images
  • Resize images without preserving their aspect ratios
  • Remove selected images
  • Set the scale for selected images (instead of all images)
  • Crop images
  • Undo and redo changes
  • Add captions
  • Apply image processing effects such as blurring or contrast enhancement

However, this program does a pretty good job without being too complicated. It lets you load images, resize and rearrange them, and save the resulting montages in an image file.

Download Example   Follow me on Twitter   RSS feed   Donate

About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in graphics, image processing and tagged , , , , , , , , , , , , , , . Bookmark the permalink.

One Response to Make a montage editor in C#, Part 2

  1. Paul Matthews says:

    Thanks for posting the example.
    I am having real difficulty with images so this working example I hope will help me to understand the things I am currently doing wrong.

Leave a Reply

Your email address will not be published. Required fields are marked *