// 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 }