Displaying multi-page tiff files using the ImageBox control and C#
A brief article showing how to display individual pages from a multi-page tiff file in the ImageBox control.
The ImageBox
control is already a versatile little control and
I use it for all sorts of tasks. One of the features I recently
wanted was to allow users to be able to select a source region,
then adjust this as needed. The control already allows you to
draw a selection region, but if you need to adjust that ...
well, you can't. You can only draw a new region.
This article describes how to extend the ImageBox
to include
the ability to resize the selection region. A older
demonstration which shows how to drag the selection around has
also been incorporated, in a more tidy fashion than the demo.
Note: The code presented in this article has not been added to the core
ImageBox
control. Mostly this is because I don't want to clutter the control with bloat (something users of the oldPropertiesList
control might wish I'd done!) and partly because I don't want to add changes to the control that I'll regret down the line - I don't need another mess like the Color Picker Controls where every update seems to be a breaking change! It most likely will be added to the core control after it's been dog-fooded for a while with different scenarios.
As I mentioned above, this isn't part of the core control (yet)
and so has been added to a new ImageBoxEx
control. Not the
most imaginative of names, but with it's current status of
internal demonstration code, it matters not.
In addition to this new sub-classed control, we also need some helper classes. First amongst these is a new enum to describe the drag handle anchors, so we know which edges to resize.
internal enum DragHandleAnchor
{
None,
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight
}
Next we have the class that describes an individual drag handle
- nothing special here, although I have added Enabled
and
Visible
properties to allow for more advanced scenarios, such
as locking an edge, or only showing some handles.
internal class DragHandle
{
public DragHandle(DragHandleAnchor anchor)
: this()
{
this.Anchor = anchor;
}
protected DragHandle()
{
this.Enabled = true;
this.Visible = true;
}
public DragHandleAnchor Anchor { get; protected set; }
public Rectangle Bounds { get; set; }
public bool Enabled { get; set; }
public bool Visible { get; set; }
}
The final support class is a collection for our drag handle
objects - we could just use a List<>
or some other generic
collection but as a rule it's best not to expose these in a
public API (and this code will be just that eventually) so we'll
create a dedicated read-only collection.
internal class DragHandleCollection : IEnumerable<DragHandle>
{
private readonly IDictionary<DragHandleAnchor, DragHandle> _items;
public DragHandleCollection()
{
_items = new Dictionary<DragHandleAnchor, DragHandle>();
_items.Add(DragHandleAnchor.TopLeft, new DragHandle(DragHandleAnchor.TopLeft));
_items.Add(DragHandleAnchor.TopCenter, new DragHandle(DragHandleAnchor.TopCenter));
_items.Add(DragHandleAnchor.TopRight, new DragHandle(DragHandleAnchor.TopRight));
_items.Add(DragHandleAnchor.MiddleLeft, new DragHandle(DragHandleAnchor.MiddleLeft));
_items.Add(DragHandleAnchor.MiddleRight, new DragHandle(DragHandleAnchor.MiddleRight));
_items.Add(DragHandleAnchor.BottomLeft, new DragHandle(DragHandleAnchor.BottomLeft));
_items.Add(DragHandleAnchor.BottomCenter, new DragHandle(DragHandleAnchor.BottomCenter));
_items.Add(DragHandleAnchor.BottomRight, new DragHandle(DragHandleAnchor.BottomRight));
}
public int Count
{
get { return _items.Count; }
}
public DragHandle this[DragHandleAnchor index]
{
get { return _items[index]; }
}
public IEnumerator<DragHandle> GetEnumerator()
{
return _items.Values.GetEnumerator();
}
public DragHandleAnchor HitTest(Point point)
{
DragHandleAnchor result;
result = DragHandleAnchor.None;
foreach (DragHandle handle in this)
{
if (handle.Visible && handle.Bounds.Contains(point))
{
result = handle.Anchor;
break;
}
}
return result;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
Again, there's not much special about this class. As it is a
custom class it does give us more flexibility, such as
initializing the required drag handles, and providing a
convenient HitTest
method so we can check if a given point is
within the bounds of a DragHandle
.
The ImageBox
control includes a nice bunch of helper methods,
such as PointToImage
, GetOffsetRectangle
and more, which are
very useful for adding scalable elements to an ImageBox
instance. Unfortunately, they are all virtually useless for the
drag handle code due to the fact that the handles themselves
must not scale - the positions of course must update and
resizing must be accurate whether at 100% zoom or not, but the
size must not. This means we can't rely on the built in methods
and must manually recalculate the handles whenever the control
changes.
private void PositionDragHandles()
{
if (this.DragHandles != null && this.DragHandleSize > 0)
{
if (this.SelectionRegion.IsEmpty)
{
foreach (DragHandle handle in this.DragHandles)
{
handle.Bounds = Rectangle.Empty;
}
}
else
{
int left;
int top;
int right;
int bottom;
int halfWidth;
int halfHeight;
int halfDragHandleSize;
Rectangle viewport;
int offsetX;
int offsetY;
viewport = this.GetImageViewPort();
offsetX = viewport.Left + this.Padding.Left + this.AutoScrollPosition.X;
offsetY = viewport.Top + this.Padding.Top + this.AutoScrollPosition.Y;
halfDragHandleSize = this.DragHandleSize / 2;
left = Convert.ToInt32((this.SelectionRegion.Left * this.ZoomFactor) + offsetX);
top = Convert.ToInt32((this.SelectionRegion.Top * this.ZoomFactor) + offsetY);
right = left + Convert.ToInt32(this.SelectionRegion.Width * this.ZoomFactor);
bottom = top + Convert.ToInt32(this.SelectionRegion.Height * this.ZoomFactor);
halfWidth = Convert.ToInt32(this.SelectionRegion.Width * this.ZoomFactor) / 2;
halfHeight = Convert.ToInt32(this.SelectionRegion.Height * this.ZoomFactor) / 2;
this.DragHandles[DragHandleAnchor.TopLeft].Bounds = new Rectangle(left - this.DragHandleSize, top - this.DragHandleSize, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.TopCenter].Bounds = new Rectangle(left + halfWidth - halfDragHandleSize, top - this.DragHandleSize, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.TopRight].Bounds = new Rectangle(right, top - this.DragHandleSize, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.MiddleLeft].Bounds = new Rectangle(left - this.DragHandleSize, top + halfHeight - halfDragHandleSize, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.MiddleRight].Bounds = new Rectangle(right, top + halfHeight - halfDragHandleSize, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.BottomLeft].Bounds = new Rectangle(left - this.DragHandleSize, bottom, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.BottomCenter].Bounds = new Rectangle(left + halfWidth - halfDragHandleSize, bottom, this.DragHandleSize, this.DragHandleSize);
this.DragHandles[DragHandleAnchor.BottomRight].Bounds = new Rectangle(right, bottom, this.DragHandleSize, this.DragHandleSize);
}
}
}
The code is fairly straightforward, but we need to call it from a few places, so we have a bunch of overrides similar to the below.
protected override void OnScroll(ScrollEventArgs se)
{
base.OnScroll(se);
this.PositionDragHandles();
}
We call PositionDragHandles
from the constructor, and the
Scroll
, SelectionRegionChanged
, ZoomChanged
and Resize
events.
Painting the handles is simple enough - after normal painting has occurred, we draw our handles on top.
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (this.AllowPainting && !this.SelectionRegion.IsEmpty)
{
foreach (DragHandle handle in this.DragHandles)
{
if (handle.Visible)
{
this.DrawDragHandle(e.Graphics, handle);
}
}
}
}
protected virtual void DrawDragHandle(Graphics graphics, DragHandle handle)
{
int left;
int top;
int width;
int height;
Pen outerPen;
Brush innerBrush;
left = handle.Bounds.Left;
top = handle.Bounds.Top;
width = handle.Bounds.Width;
height = handle.Bounds.Height;
if (handle.Enabled)
{
outerPen = SystemPens.WindowFrame;
innerBrush = SystemBrushes.Window;
}
else
{
outerPen = SystemPens.ControlDark;
innerBrush = SystemBrushes.Control;
}
graphics.FillRectangle(innerBrush, left + 1, top + 1, width - 2, height - 2);
graphics.DrawLine(outerPen, left + 1, top, left + width - 2, top);
graphics.DrawLine(outerPen, left, top + 1, left, top + height - 2);
graphics.DrawLine(outerPen, left + 1, top + height - 1, left + width - 2, top + height - 1);
graphics.DrawLine(outerPen, left + width - 1, top + 1, left + width - 1, top + height - 2);
}
As the mouse travels across the control, we need to adjust the cursor accordingly - either to change it to one of the four resize cursors if the mouse is over an enabled handle, or to the drag cursor if it's within the bounds of the selection region. Of course, we also need to reset it if none of these conditions are true.
private void SetCursor(Point point)
{
Cursor cursor;
if (this.IsSelecting)
{
cursor = Cursors.Default;
}
else
{
DragHandleAnchor handleAnchor;
handleAnchor = this.IsResizing ? this.ResizeAnchor : this.HitTest(point);
if (handleAnchor != DragHandleAnchor.None && this.DragHandles[handleAnchor].Enabled)
{
switch (handleAnchor)
{
case DragHandleAnchor.TopLeft:
case DragHandleAnchor.BottomRight:
cursor = Cursors.SizeNWSE;
break;
case DragHandleAnchor.TopCenter:
case DragHandleAnchor.BottomCenter:
cursor = Cursors.SizeNS;
break;
case DragHandleAnchor.TopRight:
case DragHandleAnchor.BottomLeft:
cursor = Cursors.SizeNESW;
break;
case DragHandleAnchor.MiddleLeft:
case DragHandleAnchor.MiddleRight:
cursor = Cursors.SizeWE;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
else if (this.IsMoving || this.SelectionRegion.Contains(this.PointToImage(point)))
{
cursor = Cursors.SizeAll;
}
else
{
cursor = Cursors.Default;
}
}
this.Cursor = cursor;
}
When the user first presses the left mouse button, check to see if the cursor is within the bounds of the selection region, or any visible drag handle. If so, we record the location of the cursor, and it's offset to the upper left corner of the selection region.
The original cursor location will be used as the origin, so once the mouse starts moving, we use this to determine if a move should occur, or a resize, or nothing.
The offset is used purely for moving, so that we reposition the selection relative to the cursor position - otherwise it would snap to the cursor which would look pretty awful.
protected override void OnMouseDown(MouseEventArgs e)
{
Point imagePoint;
imagePoint = this.PointToImage(e.Location);
if (e.Button == MouseButtons.Left && (this.SelectionRegion.Contains(imagePoint) || this.HitTest(e.Location) != DragHandleAnchor.None))
{
this.DragOrigin = e.Location;
this.DragOriginOffset = new Point(imagePoint.X - (int)this.SelectionRegion.X, imagePoint.Y - (int)this.SelectionRegion.Y);
}
else
{
this.DragOriginOffset = Point.Empty;
this.DragOrigin = Point.Empty;
}
base.OnMouseDown(e);
}
Even if the user immediately moves the mouse, we don't want to trigger a move or a resize - the mouse may have just twitched. Instead, we wait until it moves beyond an area centred around the drag origin - once it has, then we trigger the action.
This drag rectangle is determined via the
SystemInformation.DragSize
(MSDN) property.
During a mouse move, as well as triggering a move or resize, we also need to process any in-progress action, as well as update the cursor as described in the previous section.
private bool IsOutsideDragZone(Point location)
{
Rectangle dragZone;
int dragWidth;
int dragHeight;
dragWidth = SystemInformation.DragSize.Width;
dragHeight = SystemInformation.DragSize.Height;
dragZone = new Rectangle(this.DragOrigin.X - (dragWidth / 2), this.DragOrigin.Y - (dragHeight / 2), dragWidth, dragHeight);
return !dragZone.Contains(location);
}
protected override void OnMouseMove(MouseEventArgs e)
{
// start either a move or a resize operation
if (!this.IsSelecting && !this.IsMoving && !this.IsResizing && e.Button == MouseButtons.Left && !this.DragOrigin.IsEmpty && this.IsOutsideDragZone(e.Location))
{
DragHandleAnchor anchor;
anchor = this.HitTest(this.DragOrigin);
if (anchor == DragHandleAnchor.None)
{
// move
this.StartMove();
}
else if (this.DragHandles[anchor].Enabled && this.DragHandles[anchor].Visible)
{
// resize
this.StartResize(anchor);
}
}
// set the cursor
this.SetCursor(e.Location);
// perform operations
this.ProcessSelectionMove(e.Location);
this.ProcessSelectionResize(e.Location);
base.OnMouseMove(e);
}
Although I'm not going to include the code here as this article
is already very code heavy, the StartMove
and StartResize
methods simply set some internal flags describing the control
state, and store a copy of the SelectionRegion
property - I'll
explain why towards the end of the article. They also raise
events, both to allow the actions to be cancelled, or to allow
the application to update the user interface in some fashion.
Performing the move is simple - we calculate the new position of the selection region according to the cursor position, and including the offset from the original drag for a smooth move.
We also check to ensure that the full bounds of the selection region fit within the controls client area, preventing the user from dragging out outside the bounds of the underlying image/virtual size.
private void ProcessSelectionMove(Point cursorPosition)
{
if (this.IsMoving)
{
int x;
int y;
Point imagePoint;
imagePoint = this.PointToImage(cursorPosition, true);
x = Math.Max(0, imagePoint.X - this.DragOriginOffset.X);
if (x + this.SelectionRegion.Width >= this.ViewSize.Width)
{
x = this.ViewSize.Width - (int)this.SelectionRegion.Width;
}
y = Math.Max(0, imagePoint.Y - this.DragOriginOffset.Y);
if (y + this.SelectionRegion.Height >= this.ViewSize.Height)
{
y = this.ViewSize.Height - (int)this.SelectionRegion.Height;
}
this.SelectionRegion = new RectangleF(x, y, this.SelectionRegion.Width, this.SelectionRegion.Height);
}
}
The resize code is also reasonably straight forward. We decide which edges of the selection region we're going to adjust based on the drag handle. Next, we get the position of the cursor within the underlying view - snapped to fit within the bounds, so that you can't size the region outside the view.
The we just update the edges based on this calculation. However, we also ensure that the selection region is above a minimum size. Apart from the fact that if the drag handles overlap it's going to be impossible to size properly, you probably want to force some minimum size constraints.
private void ProcessSelectionResize(Point cursorPosition)
{
if (this.IsResizing)
{
Point imagePosition;
float left;
float top;
float right;
float bottom;
bool resizingTopEdge;
bool resizingBottomEdge;
bool resizingLeftEdge;
bool resizingRightEdge;
imagePosition = this.PointToImage(cursorPosition, true);
// get the current selection
left = this.SelectionRegion.Left;
top = this.SelectionRegion.Top;
right = this.SelectionRegion.Right;
bottom = this.SelectionRegion.Bottom;
// decide which edges we're resizing
resizingTopEdge = this.ResizeAnchor >= DragHandleAnchor.TopLeft && this.ResizeAnchor <= DragHandleAnchor.TopRight;
resizingBottomEdge = this.ResizeAnchor >= DragHandleAnchor.BottomLeft && this.ResizeAnchor <= DragHandleAnchor.BottomRight;
resizingLeftEdge = this.ResizeAnchor == DragHandleAnchor.TopLeft || this.ResizeAnchor == DragHandleAnchor.MiddleLeft || this.ResizeAnchor == DragHandleAnchor.BottomLeft;
resizingRightEdge = this.ResizeAnchor == DragHandleAnchor.TopRight || this.ResizeAnchor == DragHandleAnchor.MiddleRight || this.ResizeAnchor == DragHandleAnchor.BottomRight;
// and resize!
if (resizingTopEdge)
{
top = imagePosition.Y;
if (bottom - top < this.MinimumSelectionSize.Height)
{
top = bottom - this.MinimumSelectionSize.Height;
}
}
else if (resizingBottomEdge)
{
bottom = imagePosition.Y;
if (bottom - top < this.MinimumSelectionSize.Height)
{
bottom = top + this.MinimumSelectionSize.Height;
}
}
if (resizingLeftEdge)
{
left = imagePosition.X;
if (right - left < this.MinimumSelectionSize.Width)
{
left = right - this.MinimumSelectionSize.Width;
}
}
else if (resizingRightEdge)
{
right = imagePosition.X;
if (right - left < this.MinimumSelectionSize.Width)
{
right = left + this.MinimumSelectionSize.Width;
}
}
this.SelectionRegion = new RectangleF(left, top, right - left, bottom - top);
}
}
So far, we've used the MouseDown
and MouseMove
events to
control the initializing and processing of the actions. Now,
we've use the MouseUp
event to finish things off - to reset
flags that describe the control state, and to raise events.
protected override void OnMouseUp(MouseEventArgs e)
{
if (this.IsMoving)
{
this.CompleteMove();
}
else if (this.IsResizing)
{
this.CompleteResize();
}
base.OnMouseUp(e);
}
Assuming the user has started moving the region or resizes it,
and then changes their mind. How to cancel? The easiest way is
to press the Escape
key - and so that's what we'll implement.
We can do this by overriding ProcessDialogKey
, checking for
Escape
and then resetting the control state, and restoring the
SelectionRegion
property using the copy we started at the
start of the operation.
protected override bool ProcessDialogKey(Keys keyData)
{
bool result;
if (keyData == Keys.Escape && (this.IsResizing || this.IsMoving))
{
if (this.IsResizing)
{
this.CancelResize();
}
else
{
this.CancelMove();
}
result = true;
}
else
{
result = base.ProcessDialogKey(keyData);
}
return result;
}
That covers most of the important code for making these
techniques work, although it's incomplete, so please download
the latest version for the full source. And I hope you find this
addition to the ImageBox
component useful!
Like what you're reading? Perhaps you like to buy us a coffee?
# Lacko
# Richard Moss
# Lacko
# Richard Moss
# Lacko
# Richard Moss
# Lacko
# Richard Moss
# wmjordan
# Richard Moss
# Joe
# Richard Moss
# Joe
# Koepisch
# Richard Moss
# Rodolfo
# Richard Moss
# ali
# Richard Moss
# Alessio