// Copy(left)right Nik Radford. 2008.
// Contact Email : nik@terminaldischarge.net
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License (Version 2) as published by
// the Free Software Foundation.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// Please see http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt for exact details
//
using System;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
namespace NikRadford.Forms
{
///
/// A picture box that allows drag selecting areas and panning of the image
///
public class SelectablePictureBox : Control
{
#region Internals
///
/// The image that the picture box is supposed to display
///
private Image m_Image;
///
/// The factor by which the image is zoomed
///
private float m_ZoomFactor;
///
/// Indicates whether or not an area is being selected
///
private bool m_Selecting;
///
/// Indicates whether the control is currently panning
///
private bool m_Panning;
///
/// Gets the top left co-ordinate of the view point
///
private Point m_ViewPoint;
///
/// The horizontal scroll bar
///
private HScrollBar m_HorizontalScrollbar;
///
/// The vertical scroll bar
///
private VScrollBar m_VerticalScrollbar;
///
/// The start point of the select box
///
private Point m_SelectStartPoint;
///
/// The end point of the select box
///
private Point m_SelectEndPoint;
///
/// The start point of the pan
///
private Point m_PanStartPoint;
///
/// The hold point of the pan
///
private Point m_PanHoldPoint;
///
/// Indicates whether pan is enabled or not
///
private bool m_CanPan;
///
/// Indicates whether drag select is enabled or not
///
private bool m_CanSelect;
///
/// The button used for panning
///
private MouseButtons m_PanButton;
///
/// The button used for drag selecting
///
private MouseButtons m_SelectButton;
#endregion
#region Constructor
///
/// Default Constructor
///
public SelectablePictureBox()
{
this.m_Panning = false;
this.m_Image = null;
this.m_ZoomFactor = 1;
this.m_Selecting = false;
this.m_CanPan = true;
this.m_CanSelect = true;
this.m_PanButton = MouseButtons.Left;
this.m_SelectButton = MouseButtons.Right;
InitializeComponent();
}
#endregion
#region Events
///
/// Fired when the scroll bars positions are changed
///
public event ScrollEventHandler Scroll;
///
/// Fired when the zoom factor changes
///
public event EventHandler Zoomed;
///
/// Fired when the user does a drag select to select an area of pixels
///
public event EventHandler DragSelect;
#endregion
#region Public Methods
///
/// Zooms the image by a given factor
///
///
public void Zoom(float factor)
{
if (factor > this.MaxZoomFactor)
throw new InvalidOperationException(string.Format("Can not zoom in by factors greater than {0}", this.MaxZoomFactor));
if (factor <= 0.0)
throw new InvalidOperationException("Zoom factor has to be greater than 0.0");
ZoomEventArgs ze = new ZoomEventArgs(m_ZoomFactor, factor);
m_ZoomFactor = factor;
this.OnZoom(ze);
}
///
/// Zooms the image to fit the control width
///
public void ZoomToFit()
{
if (Image != null)
{
int controlWidth = this.ClientSize.Width - 20;
int controlHeight = this.ClientSize.Height - 20;
float zoomFactorX = (float) controlWidth / (float) Image.Width;
float zoomFactorY = (float)controlHeight / (float)Image.Height;
float zoomFactor = zoomFactorX < zoomFactorY ? zoomFactorX : zoomFactorY;
this.Zoom(zoomFactor);
}
}
///
/// Sets the top left corner of the view point
///
/// The point in pixel co-ordinates of the image
public void SetViewPoint(Point point)
{
if (Image == null)
return;
//check that point is indeed within the image size
if (point.X < 0 || !(point.X < this.Image.Width) ||
point.Y < 0 || !(point.Y < this.Image.Height))
throw new ArgumentOutOfRangeException("point", "Point was outside the bounds of the image.");
this.m_ViewPoint = new Point(Convert.ToInt32(point.X * m_ZoomFactor), Convert.ToInt32(point.Y * m_ZoomFactor));
this.HorizontalScroll.Value = m_ViewPoint.X < this.HorizontalScroll.Maximum ? m_ViewPoint.X : this.HorizontalScroll.Maximum;
this.VerticalScroll.Value = m_ViewPoint.Y < this.VerticalScroll.Maximum ? m_ViewPoint.Y : this.VerticalScroll.Maximum;
this.Invalidate();
}
///
/// Gets the current view point
///
/// The point in pixel co-ordinates of the image
public Point GetViewPoint()
{
if (Image == null)
return new Point();
return new Point(
Convert.ToInt32(this.m_ViewPoint.X / m_ZoomFactor),
Convert.ToInt32(this.m_ViewPoint.Y / m_ZoomFactor)
);
}
#endregion
#region Protected Methods
#region Event Handlers
///
/// Draws the control
///
/// Standard PainEventArgs
protected override void OnPaint(PaintEventArgs e)
{
this.DoubleBuffered = true;
base.OnPaint(e);
Graphics g = e.Graphics;
Pen p = new Pen(this.BackColor);
//paint client area one color
RectangleF clientRectangle = new RectangleF(
0,
0,
this.ClientSize.Width >= 1 ? this.ClientSize.Width : 1,
this.ClientSize.Height >= 1 ? this.ClientSize.Height : 1
);
g.FillRectangle(p.Brush, clientRectangle);
if (this.Image != null)
{
//Work out destination rectangle
RectangleF destRectangle = new RectangleF(
0f,
0f,
(Image.Width * ZoomFactor) < this.ClientSize.Width ? (Image.Width * ZoomFactor) : this.ClientSize.Width,
(Image.Height * ZoomFactor) < this.ClientSize.Height ? (Image.Height * ZoomFactor) : this.ClientSize.Height
);
//Source Rectangle is the top left position we are currently scrolled to
//to the (modified by zoom value) width of the client
RectangleF sourceRectangle = new RectangleF(
//as the scroll value is modified by the zoom factor we have to reverse it
this.m_ViewPoint.X / m_ZoomFactor,
//As above
this.m_ViewPoint.Y / m_ZoomFactor,
this.ClientSize.Width / m_ZoomFactor < Image.Width ? this.ClientSize.Width / m_ZoomFactor : Image.Width,
this.ClientSize.Height / m_ZoomFactor < Image.Height ? this.ClientSize.Height / m_ZoomFactor : Image.Height
);
//Now we draw the image
g.DrawImage(this.Image, destRectangle, sourceRectangle, GraphicsUnit.Pixel);
}
if (this.m_Selecting)
{
//Draw select rectangle
Pen dashedRed = new Pen(Color.Red, 1f);
dashedRed.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
Rectangle rect = GetUntransformedSelection();
g.DrawRectangle(dashedRed, rect);
}
//Fill in the section between the scrollbars
if (this.Image != null && (HorizontalScroll.Visible || VerticalScroll.Visible))
g.FillRectangle(SystemBrushes.Control, new Rectangle(this.ClientSize.Width - 20, this.ClientSize.Height - 20, 20, 20));
}
///
/// Called when the control is zoomed
///
/// Event arguments with specified arguments
protected virtual void OnZoom(ZoomEventArgs ze)
{
//okay so we have the old and the new
//lets get the current scrollbar positions
int horizontalPosition = this.HorizontalScroll.Value;
int verticalPosition = this.VerticalScroll.Value;
//okay update scroll values
UpdateScrollValues();
//convert from current position at old zoom factor to new position at new zoom factor
int hValue = Convert.ToInt32((horizontalPosition / ze.OldFactor) * ze.NewFactor);
this.HorizontalScroll.Value = hValue < this.HorizontalScroll.Maximum ? hValue : this.HorizontalScroll.Maximum;
int vValue = Convert.ToInt32((verticalPosition / ze.OldFactor) * ze.NewFactor);
this.VerticalScroll.Value = vValue < this.VerticalScroll.Maximum ? vValue : this.VerticalScroll.Maximum;
//set the view point
this.m_ViewPoint.X = this.HorizontalScroll.Value;
this.m_ViewPoint.Y = this.m_VerticalScrollbar.Value;
//repaint the control
this.Invalidate();
//Fire zoom event
if (this.Zoomed != null)
this.Zoomed(this, ze);
}
///
/// Handles the resize event
///
///
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
UpdateScrollbars();
}
///
/// Handles the size changed event
///
///
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
OnResize(EventArgs.Empty);
}
///
/// Handles the scroll event
///
///
protected virtual void OnScroll(ScrollEventArgs se)
{
switch (se.ScrollOrientation)
{
case ScrollOrientation.HorizontalScroll:
this.m_ViewPoint.X = se.NewValue;
break;
case ScrollOrientation.VerticalScroll:
this.m_ViewPoint.Y = se.NewValue;
break;
}
this.Invalidate();
if (this.Scroll != null)
this.Scroll(this, se);
}
///
/// Handles mouse down event
///
///
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (this.m_CanSelect && e.Button == this.m_SelectButton)
{
this.m_SelectStartPoint = e.Location;
}
if (this.CanPan && e.Button == m_PanButton)
{
this.Cursor = Cursors.Hand;
this.m_PanStartPoint = e.Location;
this.m_PanHoldPoint = Cursor.Position;
}
}
///
/// Handles mouse move event
///
///
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (this.m_CanSelect && e.Button == this.m_SelectButton)
{
m_SelectEndPoint = e.Location;
m_Selecting = true;
this.Invalidate();
}
if (this.m_CanPan && e.Button == this.m_PanButton)
{
if (this.m_Panning != true)
{
this.m_Panning = true;
Cursor.Hide();
}
int moveX = e.Location.X - m_PanStartPoint.X;
int moveY = e.Location.Y - m_PanStartPoint.Y;
if (moveX != 0 || moveY != 0)
{
//move horizontally
m_ViewPoint.X -= moveX;
if (m_ViewPoint.X < 0)
m_ViewPoint.X = 0;
if (m_ViewPoint.X > m_HorizontalScrollbar.Maximum)
m_ViewPoint.X = m_HorizontalScrollbar.Maximum;
m_HorizontalScrollbar.Value = m_ViewPoint.X;
//move vertically
m_ViewPoint.Y -= moveY;
if (m_ViewPoint.Y < 0)
m_ViewPoint.Y = 0;
if (m_ViewPoint.Y > m_VerticalScrollbar.Maximum)
m_ViewPoint.Y = m_VerticalScrollbar.Maximum;
m_VerticalScrollbar.Value = m_ViewPoint.Y;
//m_PanStartPoint = e.Location;
Cursor.Position = m_PanHoldPoint;
System.Diagnostics.Debug.Write("Panning - move\r\n");
this.Invalidate();
}
}
}
///
/// Handles the mouse up event
///
///
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (this.m_CanSelect && m_Selecting == true && e.Button == MouseButtons.Right)
{
m_SelectEndPoint = e.Location;
m_Selecting = false;
DragSelectEventArgs de = new DragSelectEventArgs(GetUntransformedSelection(), GetTransformedSelection());
OnDragSelect(de);
this.Invalidate();
}
if (this.m_CanPan && m_Panning == true && e.Button == MouseButtons.Left)
{
m_Panning = false;
Cursor.Position = m_PanHoldPoint;
Cursor.Show();
this.Invalidate();
}
}
///
/// Handles the drag select event
///
///
protected virtual void OnDragSelect(DragSelectEventArgs de)
{
if (DragSelect != null)
{
DragSelect(this, de);
}
}
#endregion
#endregion
#region Private Methods
///
/// Updates the values associated with the scroll bars
///
private void UpdateScrollValues()
{
//work out minimum and maximum values for the scroll bars
this.HorizontalScroll.Minimum = 0;
int hMaxValue = this.Image == null ? 0 : Convert.ToInt32((this.Image.Width * m_ZoomFactor) - (this.ClientSize.Width - 20));
this.HorizontalScroll.Maximum = hMaxValue > 0 ? hMaxValue : 0;
//work out our X view point
this.m_ViewPoint.X = this.m_HorizontalScrollbar.Maximum == 0 ? 0 : this.m_ViewPoint.X;
//set the change values
this.HorizontalScroll.SmallChange = 1;
this.HorizontalScroll.LargeChange = Convert.ToInt32(this.HorizontalScroll.Maximum * 0.1);
//set the scroll bar current value
this.HorizontalScroll.Value = 0;
//same again with the vertical scrollbar
this.VerticalScroll.Minimum = 0;
int vMaxValue = this.Image == null ? 0 : Convert.ToInt32((this.Image.Height * m_ZoomFactor) - (this.ClientSize.Height - 20));
this.VerticalScroll.Maximum = vMaxValue > 0 ? vMaxValue : 0;
this.m_ViewPoint.Y = this.m_VerticalScrollbar.Maximum == 0 ? 0 : this.m_ViewPoint.Y;
this.VerticalScroll.SmallChange = 1;
this.VerticalScroll.LargeChange = Convert.ToInt32(this.VerticalScroll.Maximum * 0.1);
this.VerticalScroll.Value = 0;
//now update the scroll bars
this.UpdateScrollbars();
}
///
/// Updates wheter to show / enable the scroll bars and also positions them
///
private void UpdateScrollbars()
{
//Position and size the horizontal scroll bar
m_HorizontalScrollbar.Size = new Size(this.Width - 20, 20);
m_HorizontalScrollbar.Location = new Point(0, this.ClientSize.Height - 20);
//Determine if visable (whether we have an image loaded or not)
m_HorizontalScrollbar.Visible = Image == null ? false : true;
//Determine
m_HorizontalScrollbar.Enabled = m_HorizontalScrollbar.Maximum > 0;
m_VerticalScrollbar.Size = new Size(20, this.Height - 20);
m_VerticalScrollbar.Location = new Point(this.Size.Width - 20, 0);
m_VerticalScrollbar.Visible = Image == null ? false : true;
m_VerticalScrollbar.Enabled = m_VerticalScrollbar.Maximum > 0;
}
///
/// Initalizes child controls
///
private void InitializeComponent()
{
//Create scrollbars
m_HorizontalScrollbar = new HScrollBar();
m_VerticalScrollbar = new VScrollBar();
//Suspend the controls layout
this.SuspendLayout();
//Set up the inital scroll values
UpdateScrollValues();
//wire up events
this.m_VerticalScrollbar.Scroll += new ScrollEventHandler(m_VerticalScrollbar_Scroll);
this.m_HorizontalScrollbar.Scroll += new ScrollEventHandler(m_HorizontalScrollbar_Scroll);
//add the scrollbars as child controls
Controls.Add(m_HorizontalScrollbar);
Controls.Add(m_VerticalScrollbar);
//resume layout
this.ResumeLayout();
}
///
/// Gets the current selection as untransformed co-ordinates on the control
///
/// Rectangle of the current untransformed selection
private Rectangle GetUntransformedSelection()
{
//from the start and end point, we create a rectangle (left, top, width, height)
Rectangle rect = new Rectangle(
//Find which is the left most co-ordinate
m_SelectStartPoint.X < m_SelectEndPoint.X ? m_SelectStartPoint.X : m_SelectEndPoint.X,
//Find which is the top most co-ordinate
m_SelectStartPoint.Y < m_SelectEndPoint.Y ? m_SelectStartPoint.Y : m_SelectEndPoint.Y,
//Depending on which was the left most co-ordinate, figure out the width
m_SelectStartPoint.X < m_SelectEndPoint.X ? m_SelectEndPoint.X - m_SelectStartPoint.X : m_SelectStartPoint.X - m_SelectEndPoint.X,
//Depending on which was the top most co-ordinate, figure out the height
m_SelectStartPoint.Y < m_SelectEndPoint.Y ? m_SelectEndPoint.Y - m_SelectStartPoint.Y : m_SelectStartPoint.Y - m_SelectEndPoint.Y
);
//return
return rect;
}
///
/// Gets the current selection transformed to the co-ordinates on the image
///
/// RectangleF containing the selected area
private RectangleF GetTransformedSelection()
{
//Get the untransformed selection
Rectangle untransformed = GetUntransformedSelection();
//Change it image co-ordinates
RectangleF transformed = new RectangleF(
((untransformed.X + m_ViewPoint.X) / m_ZoomFactor),
((untransformed.Y + m_ViewPoint.Y) / m_ZoomFactor),
untransformed.Width / m_ZoomFactor,
untransformed.Height / m_ZoomFactor
);
//return it
return transformed;
}
#region Child Control Event Handlers
///
/// Handles the event when horizontal scroll bar position is changed
///
///
///
private void m_HorizontalScrollbar_Scroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
}
///
/// Handles the event when the vertical scroll bar position is changed.
///
///
///
private void m_VerticalScrollbar_Scroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
}
#endregion
#endregion
#region Properties
///
/// Gets or sets the image in use by the selectable picture box
///
public Image Image
{
get { return this.m_Image; }
set
{
this.m_Image = value;
this.m_ViewPoint = new Point();
this.UpdateScrollValues();
this.Invalidate();
}
}
///
/// Gets or sets the zoom factor
///
public float ZoomFactor
{
get { return m_ZoomFactor; }
set { this.Zoom(value); }
}
///
/// Gets the information for the Horizontal Scroll bar
///
public ScrollBar HorizontalScroll
{
get { return m_HorizontalScrollbar; }
}
///
/// Gets the information for the Vertical Scroll bar
///
public ScrollBar VerticalScroll
{
get { return m_VerticalScrollbar; }
}
///
/// Gets the maximum zoom factor supported by the control
///
public float MaxZoomFactor
{
get { return 256; }
}
///
/// Gets or set whether drag select is enabled
///
public bool CanDragSelect
{
get { return m_CanSelect; }
set { m_CanSelect = value; }
}
///
/// Gets or sets whether pan is enabled
///
public bool CanPan
{
get { return m_CanPan; }
set { m_CanPan = value; }
}
///
/// Gets or sets the mouse button used for drag selecting
///
public MouseButtons DragSelectMouseButton
{
get { return m_SelectButton; }
set { m_SelectButton = value; }
}
///
/// Gets or sets the mouse button used for panning
///
public MouseButtons PanMouseButton
{
get { return m_PanButton; }
set { m_PanButton = value; }
}
#endregion
}
#region EventArgs
///
/// Used to return arguments on an zoom event
///
public class ZoomEventArgs : EventArgs
{
#region Internals
///
/// The zoom factor prior to the zoom event
///
private float m_OldFactor;
///
/// The zoom factor after the zoom event
///
private float m_NewFactor;
#endregion
#region Constructors
///
/// Constructor
///
/// The zoom factor prior to the zoom event
/// The zoom factor after the zoom event
public ZoomEventArgs(float oldFactor, float newFactor)
{
this.m_OldFactor = oldFactor;
this.m_NewFactor = newFactor;
}
#endregion
#region Properties
///
/// Gets the old zoom factor
///
public float OldFactor
{
get { return m_OldFactor; }
}
///
/// Gets the new zoom factor
///
public float NewFactor
{
get { return m_NewFactor; }
}
#endregion
}
public class DragSelectEventArgs : EventArgs
{
#region Internals
private Rectangle m_Untransformed;
private RectangleF m_Transformed;
#endregion
#region Constructor
public DragSelectEventArgs(Rectangle untransformed, RectangleF transformed)
{
this.m_Untransformed = untransformed;
this.m_Transformed = transformed;
}
#endregion
#region Properties
///
/// Gets the untransformed rectangle that was selected
///
public Rectangle Untransformed
{
get { return this.m_Untransformed; }
}
///
/// Gets the transformed rectangle that was selected
///
public RectangleF Transformed
{
get { return this.m_Transformed; }
}
#endregion
}
#endregion
}