Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an Image Viewer in C# Part 5: Selecting Part of an Image

0.00/5 (No votes)
20 Jun 2012 1  
Selecting part of an image

Part 4 of this series (by far, the most popular article on cyotek.com) was supposed to be the end, but recently I was asked if it was possible to select part of an image for saving it to a file. After implementing the new functionality and lacking ideas for a new post on other matters, here we are with a new part!

Getting Started

If you aren't already familiar with the ImageBox component, you may wish to view parts 1, 2, 3 and 4 for the original background and specification of the control.

First thing is to add some new properties, along with backing events. These are:

  • SelectionMode - Determines if selection is available within the control
  • SelectionColor - Primary color for drawing the selection region
  • SelectionRegion - The currently selected region.
  • LimitSelectionToImage - This property allows you to control if the selection region can be drawn outside the image boundaries.
  • IsSelecting - This property returns if a selection operation is in progress

If the SelectionMode property is set, then the AutoPan and AllowClickZoom properties will both be set to false to avoid conflicting actions.

We also need a couple of new events not directly tried to properties.

  • Selecting - Occurs when the user starts to draw a selection region and can be used to cancel the action
  • Selected - Occurs when the user completes drawing a selection region

These events are called when setting the IsSelecting property:

[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public virtual bool IsSelecting
{
  get { return _isSelecting; }
  protected set
  {
    if (_isSelecting != value)
    {
      CancelEventArgs args;

      args = new CancelEventArgs();

      if (value)
        this.OnSelecting(args);
      else
        this.OnSelected(EventArgs.Empty);

      if (!args.Cancel)
        _isSelecting = value;
    }
  }
}

Drawing the Selection Highlight

Before adding support for defining the selection region, we'll add the code to draw it - that way, we'll know the code to define the region works! To do this, we'll modify the existing OnPaint override, and insert a call to a new method named DrawSelection:

protected override void OnPaint(PaintEventArgs e)
{
  /* Snipped existing code for brevity */

  // draw the selection
  if (this.SelectionRegion != Rectangle.Empty)
    this.DrawSelection(e);

  base.OnPaint(e);
}

The DrawSelection method itself is very straightforward. First, it fills the region with a translucent variant of the SelectionColor property, then draws a solid outline around this. A clip region is also applied to avoid overwriting the controls borders.

As with most of the methods and properties in the ImageBox control, it has been marked as virtual to allow you to override it and provide your own drawing implementation if required, without needing to redraw all of the control.

protected virtual void DrawSelection(PaintEventArgs e)
{
  RectangleF rect;

  e.Graphics.SetClip(this.GetInsideViewPort(true));

  rect = this.GetOffsetRectangle(this.SelectionRegion);

  using (Brush brush = new SolidBrush(Color.FromArgb(128, this.SelectionColor)))
    e.Graphics.FillRectangle(brush, rect);

  using (Pen pen = new Pen(this.SelectionColor))
    e.Graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);

  e.Graphics.ResetClip();
}

The GetOffsetRectangle method will be described a little further down this article.

Defining the Selection Region

Currently, the selection region can only be defined via the mouse; there is no keyboard support. To do this, we'll do the usual overriding of MouseDown, MouseMove and MouseUp.

protected override void OnMouseDown(MouseEventArgs e)
{
  base.OnMouseDown(e);

  /* Snipped existing code for brevity */

  if (e.Button == MouseButtons.Left && this.SelectionMode != ImageBoxSelectionMode.None)
    this.SelectionRegion = Rectangle.Empty;
}

protected override void OnMouseMove(MouseEventArgs e)
{
  base.OnMouseMove(e);

  if (e.Button == MouseButtons.Left)
  {
    /* Snipped existing code for brevity */
  
    this.ProcessSelection(e);
  }
}

protected override void OnMouseUp(MouseEventArgs e)
{
  base.OnMouseUp(e);

  if (this.IsPanning)
    this.IsPanning = false;

  if (this.IsSelecting)
    this.IsSelecting = false;
}

OnMouseDown and OnMouseUp aren't being used for much in this case, the former is used to clear an existing selection region, the later to notify that the selection is no longer being defined. OnMouseMove calls the ProcessSelection method which is where all the action happens.

protected virtual void ProcessSelection(MouseEventArgs e)
{
  if (this.SelectionMode != ImageBoxSelectionMode.None)
  {
    if (!this.IsSelecting)
    {
      _startMousePosition = e.Location;
      this.IsSelecting = true;
    }

First, we check to make sure a valid selection mode is set. Then, if a selection operation hasn't been initiated, we attempt to set the IsSelecting property. As noted above, this property will call the Selecting event allowing the selection to be cancelled if required by the implementing application.

if (this.IsSelecting)
{
  float x;
  float y;
  float w;
  float h;
  Point imageOffset;

  imageOffset = this.GetImageViewPort().Location;

  if (e.X < _startMousePosition.X)
  {
    x = e.X;
    w = _startMousePosition.X - e.X;
  }
  else
  {
    x = _startMousePosition.X;
    w = e.X - _startMousePosition.X;
  }

  if (e.Y < _startMousePosition.Y)
  {
    y = e.Y;
    h = _startMousePosition.Y - e.Y;
  }
  else
  {
    y = _startMousePosition.Y;
    h = e.Y - _startMousePosition.Y;
  }

  x = x - imageOffset.X - this.AutoScrollPosition.X;
  y = y - imageOffset.Y - this.AutoScrollPosition.Y;

If selection was allowed, we construct the co-ordinates for a rectangle, automatically switching values around to ensure that the rectangle will always have a positive width and height. We'll also offset the co-ordinates if the image has been scrolled or if it has been centred (or both!).

x = x / (float)this.ZoomFactor;
y = y / (float)this.ZoomFactor;
w = w / (float)this.ZoomFactor;
h = h / (float)this.ZoomFactor;

As this is the zoomable scrolling image control, we also need to rescale the rectangle according to the current zoom level. This ensures the SelectionRegion property always returns a rectangle that describes the selection at 100% zoom.

      if (this.LimitSelectionToImage)
      {
        if (x < 0)
          x = 0;

        if (y < 0)
          y = 0;

        if (x + w > this.Image.Width)
          w = this.Image.Width - x;

        if (y + h > this.Image.Height)
          h = this.Image.Height - y;
      }

      this.SelectionRegion = new RectangleF(x, y, w, h);
    }
  }
}

The final step is to constrain the rectangle to the image size if the LimitSelectionToImage property is set, before assigning the final rectangle to the SelectionRegion property.

And that's pretty much all there is to it.

Scaling and Offsetting

When using the control in our own products, it's very rarely to display a single image, but rather to display multiple items, be it sprites in a sprite sheet or tiles in a map. These implementations therefore often require the ability to get a single item, for example to display hover effects. This can be tricky with a control that scrolls, zooms and centres the image. Rather than repeat ZoomFactor calculations (and worse AutoScrollPosition) everywhere, we added a number of helper methods named GetOffset* and GetScaled*. Calling these with a "normal" value, will return that value repositioned and rescaled according to the current state of the control. An example of this is the DrawSelection method described above which needs ensure the current selection region is rendered correctly.

public virtual RectangleF GetScaledRectangle(RectangleF source)
{
  return new RectangleF
    (
      (float)(source.Left * this.ZoomFactor),
      (float)(source.Top * this.ZoomFactor),
      (float)(source.Width * this.ZoomFactor),
      (float)(source.Height * this.ZoomFactor)
    );
}

public virtual RectangleF GetOffsetRectangle(RectangleF source)
{
  RectangleF viewport;
  RectangleF scaled;
  float offsetX;
  float offsetY;

  viewport = this.GetImageViewPort();
  scaled = this.GetScaledRectangle(source);
  offsetX = viewport.Left + this.Padding.Left + this.AutoScrollPosition.X;
  offsetY = viewport.Top + this.Padding.Top + this.AutoScrollPosition.Y;

  return new RectangleF(new PointF(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size);
}

Versions of these methods exist for the following structures:

  • Point
  • PointF
  • Size
  • SizeF
  • Rectangle
  • RectangleF

These methods can come in extremely useful depending on how you are using the control!

Cropping an Image

The demonstration program displays two ImageBox controls, the first allows you to select part of an image, and the second displays the cropped selection. I didn't add any sort of crop functionality to the control itself, but the following snippets shows how the demonstration program creates the cropped version.

Rectangle rect;

if (_previewImage != null)
  _previewImage.Dispose();

rect = new Rectangle((int)imageBox.SelectionRegion.X, (int)imageBox.SelectionRegion.Y, 
       (int)imageBox.SelectionRegion.Width, (int)imageBox.SelectionRegion.Height);

_previewImage = new Bitmap(rect.Width, rect.Height);

using (Graphics g = Graphics.FromImage(_previewImage))
  g.DrawImage(imageBox.Image, new Rectangle(Point.Empty, rect.Size), rect, GraphicsUnit.Pixel);
}

previewImageBox.Image = _previewImage;

Finishing Touches

We'll finish off by adding a couple of helper methods that implementers can call:

public virtual void SelectAll()
{
  if (this.Image == null)
    throw new InvalidOperationException("No image set");

  this.SelectionRegion = new RectangleF(PointF.Empty, this.Image.Size);
}

public virtual void SelectNone()
{
  this.SelectionRegion = RectangleF.Empty;
}

Known Issues

Currently, if you try and draw the selection bigger than the visible area of the control, it will work, but it will not scroll the control for you. I was also going to add the ability to move or modify the selection but ran out of time for this particular post.

As always, if you have any comments or questions, please contact us!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here