Creating an image viewer in C# Part 5: 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 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 controlSelectionColor
- Primary color for drawing the selection regionSelectionRegion
- 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 also was 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!
Update History
- 2012-05-30 - First published
- 2020-11-21 - Updated formatting
Related articles you may be interested in
- Displaying multi-page tiff files using the ImageBox control and C#
- Adding drag handles to an ImageBox to allow resizing of selection regions
- ImageBox 1.1.4.0 update
- ImageBox and TabList update's - virtual mode, pixel grid, bug fixes and more!
- ImageBox update, version 1.1.0.0
- Zooming to fit a region in a ScrollableControl
- Zooming into a fixed point on a ScrollableControl
- Arcade explosion generator
- Extending the ImageBox component to display the contents of a PDF file using C#
- Creating a scrollable and zoomable image viewer in C# Part 4
- Creating a scrollable and zoomable image viewer in C# Part 3
- Creating a scrollable and zoomable image viewer in C# Part 2
- Creating a scrollable and zoomable image viewer in C# Part 1
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
ImageBoxSample-Part5.zip
|
Sample project which shows extending the ImageBox control to support selection regions. |
30/05/2012 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Rafael Vasco
#
Hi about the checker flickering i said in part 4 it's still present in this version. As i said to fix it, (haven't tested with border but it could work too), in this line (680): e.Graphics.TranslateTransform(this.AutoScrollPosition.X, this.AutoScrollPosition.Y); // transform tile drawing e.Graphics.FillRectangle(_texture, fillRectangle); // line 680 e.Graphics.ResetTransform(); //reset transform to identity
What happens without this two lines is that when scrolling , the image scrolls but the tile grid on back doesn't. It's not the expected behavior at least in my case. Anyway, thank u very much for this code, immensely helpful for me :)
JR
#
Thanks a lot for sharing your knowledge. It lets increase the c# skills of many beginners and even advanced programmers around the world. You contribute to make this world a better place to live. Best regards, JR
sudhakar
#
thanks for this valuable solution, One more thing about picture box is that please provide rotation of image with drag handle if possible or any idea that can forward me to right direction to achieve this.......
sudhakar
#
one more issue in this imagebox control , when i applied rotaion of image then imagebox control break the corner of image that means space is not available for rotaion, how it will be resolve?
Richard Moss
#
Hello,
The
ImageBox
control wasn't really designed with that purpose in mind. Your best option is probably to enableVirtualMode
, and set theVirtualSize
property to be large enough to handle the image and any angle it is rotated at. This means you would have to draw the image yourself though using theVirtualPaint
event, but all in all it shouldn't be too much trouble - certainly easier than the actual act of rotating the image.Regards; Richard Moss