Title: Make a montage editor in C#, Part 1
A while ago I wanted to make a montage showing a bunch of different pictures. I got a reasonable result by using MS Paint but it was a huge amount of work. After resizing and positioning a picture in the montage, it was very hard to move it.
To make creating this kind of montage easier, I wrote this program. Use the File menu's Open Image File command to open one or more image files. Then click and drag them into position. You can drag an image's corners to resize it while making it keep its original aspect ratio. You can also click on an image to bring it to the top.
Use the Scale menu's commands to resize all of the loaded images. Use the Colors menu's Background Color command to set the montage's background color.
When you're happy with the result, use the File menu's Save As command to save the result into an image file.
This is a moderately complicated program because it does a lot. In this post, I'll explain the program's file handling features. In my next post I'll explain its image manipulation features.
Because the program allows you to resize images, it's useful to keep track of each image's original size. That way you can easily restore images to their original size or give them all the same scale. That also prevents rounding errors from distorting an image if you resize it several times.
The following code shows the ImageInfo class that the program uses to store image information.
public class ImageInfo
{
// The image we will draw.
public Bitmap Picture = null;
// The source and destination for drawing.
public Rectangle SourceRect, DestRect;
// Constructor. Open the image file.
public ImageInfo(string filename)
{
using (Bitmap temp = new Bitmap(filename))
{
Picture = new Bitmap(temp);
SourceRect = new Rectangle(0, 0,
Picture.Width, Picture.Height);
DestRect = new Rectangle(10, 10,
Picture.Width, Picture.Height);
}
}
// Draw the image.
private const int half_gap = 4;
private const int gap = half_gap * 2;
public void Draw(Graphics gr, bool with_border)
{
gr.DrawImage(Picture, DestRect, SourceRect,
GraphicsUnit.Pixel);
if (with_border)
{
using (Pen pen = new Pen(Color.White, 4))
{
Rectangle rect = DestRect;
if (rect.Width < 0)
{
rect.X += rect.Width;
rect.Width = -rect.Width;
}
if (rect.Height < 0)
{
rect.Y += rect.Height;
rect.Height = -rect.Height;
}
gr.DrawRectangle(pen, rect);
pen.Color = Color.Black;
pen.CompoundArray = new float[]
{ 0.0f, 0.25f, 0.75f, 1.0f };
gr.DrawRectangle(pen, rect);
}
foreach (Rectangle rect in CornerRects())
{
gr.FillRectangle(Brushes.White, rect);
gr.DrawRectangle(Pens.Black, rect);
}
}
}
// Return an array representing the picture's
// corners in order NW, NE, SW, SE.
private Rectangle[] CornerRects()
{
return new Rectangle[]
{
new Rectangle(DestRect.Left - half_gap,
DestRect.Top - half_gap, gap, gap),
new Rectangle(DestRect.Right - half_gap,
DestRect.Top - half_gap, gap, gap),
new Rectangle(DestRect.Left - half_gap,
DestRect.Bottom - half_gap, gap, gap),
new Rectangle(DestRect.Right - half_gap,
DestRect.Bottom - half_gap, gap, gap),
};
}
// Return a value indicating whether the position
// is over the image.
public enum HitTypes
{
None,
NwCorner,
NeCorner,
SwCorner,
SeCorner,
Body,
}
public HitTypes HitType(Point point)
{
Rectangle[] rects = CornerRects();
if (rects[0].Contains(point)) return HitTypes.NwCorner;
if (rects[1].Contains(point)) return HitTypes.NeCorner;
if (rects[2].Contains(point)) return HitTypes.SwCorner;
if (rects[3].Contains(point)) return HitTypes.SeCorner;
if (DestRect.Contains(point)) return HitTypes.Body;
return HitTypes.None;
}
}
The class first declares a Bitmap to hold the original image.
The SourceRect rectangle indicates the location from which the image will be copied. It will include the entire original bitmap.
The DestRect rectangles indicates the location where the image will be drawn on the montage. This will change when you move and resize the image.
The class's constructor loads an image file and saves a copy of the resulting bitmap. It saves a copy and not the original so the program doesn't lock the image file. It then sets SourceRect to the image's entire area. It sets DestRect to display the image at position (10, 10) at full scale.
The Draw method draws the image. If with_border is true (it is in this program), it draws a border around the image that has a white center and 1 pixel black edges. It also draws grab handles at the image's corners.
To make drawing the grab handles easier, the class defines a CornerRects method that simply returns rectangles representing the grab handles.
Next the class defines the HitTypes enumeration. The HitType method determines whether a point hits the image and returns the appropriate HitTypes value to indicate whether the point hit the image's corner or body.
Now that you understand the ImageInfo class, you can see the program's file manipulation code.
The following code shows the list the program uses to store information about its loaded images.
// The loaded images.
private List<ImageInfo> Images =
new List<ImageInfo>();
The following code shows how the program draws the loaded images.
// Redraw the images.
private void picImages_Paint(object sender, PaintEventArgs e)
{
DrawPictures(e.Graphics, true);
}
// Draw the pictures.
private void DrawPictures(Graphics gr, bool with_border)
{
gr.InterpolationMode = InterpolationMode.High;
gr.Clear(picImages.BackColor);
foreach (ImageInfo info in Images)
info.Draw(gr, with_border);
}
The picImages PictureBox's Paint event handler simply calls the DrawPictures method. That method sets the Graphics object's InterpolationMode. It clears the drawing with the current background color and then loops through the ImageInfo objects, calling each object's Draw method.
The following code shows how the program loads images.
// Select images to add to the montage.
// Note that I set the dialog's Multiselect property
// to True at design time.
private void mnuFileOpen_Click(object sender, EventArgs e)
{
if (ofdImage.ShowDialog() == DialogResult.OK)
{
try
{
foreach (string filename in ofdImage.FileNames)
{
ImageInfo image = new ImageInfo(filename);
Images.Add(image);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
picImages.Refresh();
}
}
This code displays an OpenFileDialog to let the user select image files. If the user selects files and clicks OK, the program loops through them, uses each file to create an ImageInfo object, and adds the resulting objects to the Images list.
The following code shows how the program saves the montage in a file.
// Save the result in a file.
private void mnuFileSaveAs_Click(object sender, EventArgs e)
{
if (sfdMontage.ShowDialog() == DialogResult.OK)
{
// Make a Bitmap to hold the result.
using (Bitmap bm = new Bitmap(
picImages.ClientSize.Width,
picImages.ClientSize.Height))
{
// Draw the pictures.
using (Graphics gr = Graphics.FromImage(bm))
{
DrawPictures(gr, false);
}
// Save the file.
try
{
SaveImage(bm, sfdMontage.FileName);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
}
This code displays a SaveFileDialog to let the user pick the image file that should hole the montage. If the user selects a file and clicks OK, the code creates a bitmap that's the same size as the picImages PictureBox. It calls DrawPictures to draw on the bitmap and then uses the following SaveImage method to save the result into the selected file.
// Save the file with the appropriate format.
public void SaveImage(Image image, string filename)
{
string extension = Path.GetExtension(filename);
switch (extension.ToLower())
{
case ".bmp":
image.Save(filename, ImageFormat.Bmp);
break;
case ".exif":
image.Save(filename, ImageFormat.Exif);
break;
case ".gif":
image.Save(filename, ImageFormat.Gif);
break;
case ".jpg":
case ".jpeg":
image.Save(filename, ImageFormat.Jpeg);
break;
case ".png":
image.Save(filename, ImageFormat.Png);
break;
case ".tif":
case ".tiff":
image.Save(filename, ImageFormat.Tiff);
break;
default:
throw new NotSupportedException(
"Unknown file extension " + extension);
}
}
This method simply saves the file using the format that's appropriate for its extension.
In my next post, I'll explain the rest of this example. I'll explain how the program lets you scale all of the images at once, and how it lets you move and resize images individually.
Download the example to experiment with it and to see additional details.
|