Select rectangular areas in an image in WPF and C#


[select rectangular areas]

This is another fine example of WPF’s unofficial slogan: Twice as flexible and only five times as hard. Practically everything about this example is harder than it is in Windows forms: drawing the rectangle, selecting the area, saving the results into a file, building a menu item with a shortcut, arranging the controls, and even simply displaying the original image.

I’m going to gloss over some of those topics and focus mostly on the two issues that are most central to the example: how to select rectangular areas and how to save the result into a file. Download the example program to see additional details.

Before I get to those topics, however, you should probably see the XAML code.

XAML Code

The following XAML code builds the program’s user interface.

<Window x:Class="wpf_select_image_rectangle.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="wpf_select_image_rectangle"
    Height="410" Width="624"
    Background="LightBlue">
    
    <Window.CommandBindings>
        <CommandBinding Command="SaveAs"
            CanExecute="SaveAsCommand_CanExecute"
            Executed="SaveAsCommand_Executed" />
    </Window.CommandBindings>
    
    <Window.InputBindings>
        <KeyBinding Gesture="Ctrl+S" Command="SaveAs" />
    </Window.InputBindings>
    
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_File">
                <MenuItem Command="SaveAs" InputGestureText="Ctrl+S" />
            </MenuItem>
        </Menu>
        <StackPanel Margin="10" Orientation="Horizontal">
            <Canvas Name="canDraw" Width="479" Height="328"
                VerticalAlignment="Top" HorizontalAlignment="Left"
                MouseDown="canDraw_MouseDown">
                <Image Name="imgOriginal"
                    HorizontalAlignment="Left" VerticalAlignment="Top" 
                    Stretch="None" Source="cows.jpg"
                    Cursor="Cross"/>
            </Canvas>
            <Image Name="imgResult" Stretch="None" Margin="10,0,0,0"
                VerticalAlignment="Top" HorizontalAlignment="Left" />
        </StackPanel>
    </DockPanel>
</Window>

In this code you can see how the program creates a command binding that binds the SaveAs command to the SaveAsCommand_CanExecute and SaveAsCommand_Executed event handlers.

The code then creates an input binding so pressing Ctrl+S triggers the SaveAs command. The code also creates the File menu’s Save As command, which also uses the Ctrl+S gesture. That gesture doesn’t actually take any action; it just makes the menu item display the text Ctrl+S. (I guess in case you want to have a menu item that displays an input gesture but doesn’t actually do anything with it. You know, for users who like bafflingly inconsistent user interfaces.)

The key controls that let you select rectangular areas are the canvas named canDraw and the Image control that it contains. The canDraw control’s size is set to the same as the image.

One way to make the Image control display an image is to use Project > Properties > Resources > Add Resource > Add Existing File, find the file, and add it to the project. Then use the Properties window to set its Build Action property to Resource. Now you can set the Image control’s Source property to the image file’s name.

Rectangle Code

The XAML code shown earlier sets the canDraw control’s MouseDown event handler. When you press the mouse down on that control, the following code executes.

private Rectangle DragRectangle = null;
private Point StartPoint, LastPoint;

private void canDraw_MouseDown(object sender, MouseButtonEventArgs e)
{
    StartPoint = Mouse.GetPosition(canDraw);
    LastPoint = StartPoint;
    DragRectangle = new Rectangle();
    DragRectangle.Width = 1;
    DragRectangle.Height = 1;
    DragRectangle.Stroke = Brushes.Red;
    DragRectangle.StrokeThickness = 1;
    DragRectangle.Cursor = Cursors.Cross;

    canDraw.Children.Add(DragRectangle);
    Canvas.SetLeft(DragRectangle, StartPoint.X);
    Canvas.SetTop(DragRectangle, StartPoint.Y);

    canDraw.MouseMove += canDraw_MouseMove;
    canDraw.MouseUp += canDraw_MouseUp;
    canDraw.CaptureMouse();
}

This code first gets the mouse position relative to the canDraw control and saves that position. It then creates a Rectangle object and sets some of its properties.

The code then adds the rectangle to the canDraw control’s Children collection. It sets the rectangle’s Left and Top attached properties to position the rectangle inside the Canvas control.

Next the event handler registers event handlers for the MouseMove and MouseUp events. It finishes by capturing the mouse for the Canvas control so future MouseMove and MouseUp events go to that control.

The following code executes when you move the mouse after you have pressed it down on the canDraw control.

private void canDraw_MouseMove(object sender, MouseEventArgs e)
{
    LastPoint = Mouse.GetPosition(canDraw);
    DragRectangle.Width = Math.Abs(LastPoint.X - StartPoint.X);
    DragRectangle.Height = Math.Abs(LastPoint.Y - StartPoint.Y);
    Canvas.SetLeft(DragRectangle, Math.Min(LastPoint.X, StartPoint.X));
    Canvas.SetTop(DragRectangle, Math.Min(LastPoint.Y, StartPoint.Y));
}

This code gets the mouse’s current position and uses that position and the start position to calculate the size and location of the rectangle. It then updates the rectangle’s size and position.

When you release the mouse button, the following code executes.

private void canDraw_MouseUp(object sender, MouseButtonEventArgs e)
{
    canDraw.ReleaseMouseCapture();
    canDraw.MouseMove -= canDraw_MouseMove;
    canDraw.MouseUp -= canDraw_MouseUp;
    canDraw.Children.Remove(DragRectangle);

    if (LastPoint.X < 0) LastPoint.X = 0;
    if (LastPoint.X >= canDraw.Width) LastPoint.X = canDraw.Width - 1;
    if (LastPoint.Y < 0) LastPoint.Y = 0;
    if (LastPoint.Y >= canDraw.Height) LastPoint.Y = canDraw.Height - 1;

    int x = (int)Math.Min(LastPoint.X, StartPoint.X);
    int y = (int)Math.Min(LastPoint.Y, StartPoint.Y);
    int width = (int)Math.Abs(LastPoint.X - StartPoint.X) + 1;
    int height = (int)Math.Abs(LastPoint.Y - StartPoint.Y) + 1;

    // Note that the CroppedBitmap object's SourceRect
    // is immutable so we must create a new CroppedBitmap.
    BitmapSource bms = (BitmapSource)imgOriginal.Source;
    CroppedBitmap cropped_bitmap =
        new CroppedBitmap(bms, new Int32Rect(x, y, width, height));
    imgResult.Source = cropped_bitmap;
    
    DragRectangle = null;
}

This code first releases the mouse capture, uninstalls the MouseMove and MouseUp event handlers, and removes the rectangle from the canDraw control.

Next the code calculates the selected area’s size and location. This time it ensures that the end point’s coordinates lie within the image’s bounds. WPF is happy to draw a rectangle that extends beyond the Canvas control’s edges, but we cannot copy parts of the image that don’t exist.

The code then displays the selected area in the CroppedBitmap object. It would be nice if you could update the CroppedBitmap object’s selected area, but that value is immutable so you cannot change it after you initially create the CroppedBitmap. Instead you must create a whole new object. The code does that and then makes the imgResult control display the cropped bitmap.

Finally, the code sets DragRectangle to null so the garbage collector can dispose of the rectangle.

To summarize this part:

  • The MouseDown event handler saves the mouse position, creates a Rectangle, and installs the MouseMove and MouseUp event handlers.
  • The MouseMove event handler updates the selection rectangle.
  • The MouseUp event handler uninstalls the MouseMove and MouseUp event handlers, makes the CroppedBitmap represent the selected area, and displays the CroppedBitmap in the imgResult control.

Image Saving Code

When you select the File menu’s Save As command (or press Ctrl+S), the following code executes.

SaveFileDialog sfdImage = new SaveFileDialog();
private void SaveAsCommand_Executed(object sender,
    ExecutedRoutedEventArgs e)
{
    sfdImage.Filter =
        "Image Files|*.bmp;*.gif;*.jpg;*.png;*.tif|All files (*.*)|*.*";
    sfdImage.DefaultExt = "png";
    if (sfdImage.ShowDialog().Value)
    {
        (imgResult.Source as CroppedBitmap).SaveImage(sfdImage.FileName);
    }
}

This code displays a save file dialog to let the user pick the image file where the selected part of the image should be stored. If the user picks a file, the code treats the imgResult control’s source as a CroppedBitmap and calls its SaveImage extension method. The following code shows that method.

// Save an image of the element into a
// graphic file with an appropriate file type.
public static void SaveImage(
    this CroppedBitmap cropped_bitmap,
    string filename)
{
    FileInfo file_info = new FileInfo(filename);
    BitmapEncoder encoder = null;
    switch (file_info.Extension.ToLower())
    {
        case ".bmp":
            encoder = new BmpBitmapEncoder();
            break;
        case ".gif":
            encoder = new GifBitmapEncoder();
            break;
        case ".jpg":
        case ".jpeg":
            encoder = new JpegBitmapEncoder();
            break;
        case ".png":
            encoder = new PngBitmapEncoder();
            break;
        case ".tif":
            encoder = new TiffBitmapEncoder();
            break;
    }
    encoder.Frames.Add(BitmapFrame.Create(cropped_bitmap));

    using (FileStream stream =
        new FileStream(filename, FileMode.Create))
    {
        encoder.Save(stream);
    }
}

This method creates a FileInfo object for the desired filename. It checks the file’s extension and sets the encoder variable to the appropriate encoder type.

The code then uses the cropper bitmap to create a bitmap frame and adds it to the encoder’s Frames list. Finally the method creates a file stream and makes the encoder save its contents into the stream.

Conclusion

This example lets you select a rectangular area on an image, view that area in a CroppedBitmap, and save the CroppedBitmap contents into an image file. Several pieces of the example are awkward due to the way WPF handles images, but probably the trickiest part was figuring out exactly how to perform those tasks.

Download the example to give it a try and to see additional details.


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, wpf and tagged , , , , , , , , , , , . Bookmark the permalink.