using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace NodeNetwork.Views.Controls { public class DragCanvas : Canvas { #region Position public static readonly DependencyProperty DragOffsetProperty = DependencyProperty.Register(nameof(DragOffset), typeof(Point), typeof(DragCanvas), new PropertyMetadata(new Point(), DragOffsetChanged)); /// /// Gets or sets the current canvas drag offset. /// public Point DragOffset { get => (Point)GetValue(DragOffsetProperty); set => SetValue(DragOffsetProperty, value); } private static void DragOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var canvas = (DragCanvas)d; if (e.NewValue is Point position) { canvas.ApplyDragToChildren(position.X - canvas._previousDragOffset.X, position.Y - canvas._previousDragOffset.Y); } } #endregion #region Dragging /// /// Triggered when the user clicks and moves the canvas, starting a drag /// /// The dragcanvas that triggered this event /// The mouseevent that triggered this event public delegate void DragStartEventHandler(object sender, MouseEventArgs args); public event DragStartEventHandler DragStart; /// /// Triggered when the user drags the canvas /// /// The dragcanvas that triggered this event /// Contains the distance traveled since the last drag move or drag start event public delegate void DragMoveEventHandler(object sender, DragMoveEventArgs args); public event DragMoveEventHandler DragMove; /// /// Triggered when the user releases the mouse and the drag stops. /// /// The dragcanvas that triggered this event /// Contains the total distance traveled public delegate void DragEndEventHandler(object sender, DragMoveEventArgs args); public event DragEndEventHandler DragStop; public bool IsDraggingEnabled { get; set; } = true; #region StartDragGesture public static readonly DependencyProperty StartDragGestureProperty = DependencyProperty.Register(nameof(StartDragGesture), typeof(MouseGesture), typeof(DragCanvas), new PropertyMetadata(new MouseGesture(MouseAction.LeftClick))); /// /// This mouse gesture starts a drag on the canvas. Left click by default. /// public MouseGesture StartDragGesture { get => (MouseGesture)GetValue(StartDragGestureProperty); set => SetValue(StartDragGestureProperty, value); } #endregion /// /// Used when the mousebutton is down to check if the initial click was in this element. /// This is useful because we dont want to assume a drag operation when the user moves the mouse but originally clicked a different element /// private bool _userClickedThisElement; /// /// Is a drag operation currently in progress? /// private bool _dragActive; /// /// The position of the mouse (screen co-ordinate) where the mouse was clicked down. /// private Point _originScreenCoordPosition; /// /// The position of the mouse (screen co-ordinate) when the previous DragDelta event was fired /// private Point _previousMouseScreenPos; private Point _previousDragOffset; /// /// This event puts the control into a state where it is ready for a drag operation. /// protected override void OnMouseDown(MouseButtonEventArgs e) { if (IsDraggingEnabled && StartDragGesture.Matches(this, e)) { _userClickedThisElement = true; _previousMouseScreenPos = _originScreenCoordPosition = e.GetPosition(this); Focus(); CaptureMouse(); //All mouse events will now be handled by the dragcanvas } } /// /// Trigger a dragging event when the user moves the mouse while the left mouse button is pressed /// protected override void OnMouseMove(MouseEventArgs e) { if (_userClickedThisElement && !_dragActive) { _dragActive = true; DragStart?.Invoke(this, e); } if (_dragActive) { Point curMouseScreenPos = e.GetPosition(this); if (!curMouseScreenPos.Equals(_previousMouseScreenPos)) { double xDelta = curMouseScreenPos.X - _previousMouseScreenPos.X; double yDelta = curMouseScreenPos.Y - _previousMouseScreenPos.Y; var dragEvent = new DragMoveEventArgs(e, xDelta, yDelta); DragMove?.Invoke(this, dragEvent); this.DragOffset = new Point(_previousDragOffset.X + xDelta, _previousDragOffset.Y + yDelta); _previousMouseScreenPos = curMouseScreenPos; } } base.OnMouseMove(e); } /// /// Stop dragging when the user releases the mouse button /// protected override void OnMouseUp(MouseButtonEventArgs e) { _userClickedThisElement = false; ReleaseMouseCapture(); //Stop absorbing all mouse events if (_dragActive) { _dragActive = false; Point curMouseScreenPos = e.GetPosition(this); double xDelta = curMouseScreenPos.X - _originScreenCoordPosition.X; double yDelta = curMouseScreenPos.Y - _originScreenCoordPosition.Y; DragStop?.Invoke(this, new DragMoveEventArgs(e, xDelta, yDelta)); } } private void ApplyDragToChildren(double deltaX, double deltaY) { foreach (UIElement cur in Children) { double prevLeft = Canvas.GetLeft(cur); if (Double.IsNaN(prevLeft)) { prevLeft = 0; } double prevTop = Canvas.GetTop(cur); if (Double.IsNaN(prevTop)) { prevTop = 0; } Canvas.SetLeft(cur, prevLeft + (deltaX)); Canvas.SetTop(cur, prevTop + (deltaY)); } _previousDragOffset = new Point(_previousDragOffset.X + deltaX, _previousDragOffset.Y + deltaY); } #endregion #region Zoom public event EventHandler Zoom; private double _wheelOffset = 6; #region ZoomFactor public static readonly DependencyProperty ZoomFactorProperty = DependencyProperty.Register(nameof(ZoomFactor), typeof(double), typeof(DragCanvas), new PropertyMetadata(1d, OnZoomFactorPropChanged, ZoomFactorValueCoerce)); public double ZoomFactor { get => (double)GetValue(ZoomFactorProperty); set => SetValue(ZoomFactorProperty, value); } private bool isUpdatingZoomFactor; private static void OnZoomFactorPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DragCanvas dc = (DragCanvas)d; if (!dc.isUpdatingZoomFactor) { dc.SetZoomImpl(new Point(dc.ActualWidth / 2, dc.ActualHeight / 2), (double)e.OldValue, (double)e.NewValue, null); } } private static object ZoomFactorValueCoerce(DependencyObject d, object baseValue) { if (baseValue is double doubleValue) { var canvas = (DragCanvas)d; if (doubleValue < canvas.MinZoomFactor) { return canvas.MinZoomFactor; } else if (doubleValue > canvas.MaxZoomFactor) { return canvas.MaxZoomFactor; } } return baseValue; } #endregion #region MaxZoomFactor public static readonly DependencyProperty MaxZoomFactorProperty = DependencyProperty.Register(nameof(MaxZoomFactor), typeof(double), typeof(DragCanvas), new FrameworkPropertyMetadata(2.5d, MaxZoomFactorChanged)); public double MaxZoomFactor { get { return (double)GetValue(MaxZoomFactorProperty); } set { SetValue(MaxZoomFactorProperty, value); } } private static void MaxZoomFactorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var doubleValue = (double)e.NewValue; if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue) || doubleValue <= 0) { throw new ArgumentException("MaxZoomFactor can not be NaN, Infinity or less than zero"); } var canvas = (DragCanvas)d; var binding = BindingOperations.GetBindingExpression(canvas, ZoomFactorProperty); binding?.UpdateTarget(); } #endregion #region MinZoomFactor public static readonly DependencyProperty MinZoomFactorProperty = DependencyProperty.Register(nameof(MinZoomFactor), typeof(double), typeof(DragCanvas), new FrameworkPropertyMetadata(0.15d, MinZoomFactorChanged)); public double MinZoomFactor { get { return (double)GetValue(MinZoomFactorProperty); } set { SetValue(MinZoomFactorProperty, value); } } private static void MinZoomFactorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var doubleValue = (double)e.NewValue; if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue) || doubleValue <= 0) { throw new ArgumentException("MinZoomFactor can not be NaN, Infinity or less than zero"); } var canvas = (DragCanvas)d; var binding = BindingOperations.GetBindingExpression(canvas, ZoomFactorProperty); binding?.UpdateTarget(); } #endregion private Rect ZoomView(Rect curView, double curZoom, double newZoom, Point relZoomPoint) //curView in content space, relZoomPoint is relative to view space { double zoomModifier = curZoom / newZoom; Size newSize = new Size(curView.Width * zoomModifier, curView.Height * zoomModifier); Point zoomCenter = new Point(curView.X + (curView.Width * relZoomPoint.X), curView.Y + (curView.Height * relZoomPoint.Y)); double newX = zoomCenter.X - (relZoomPoint.X * newSize.Width); double newY = zoomCenter.Y - (relZoomPoint.Y * newSize.Height); Point newPos = new Point(newX, newY); return new Rect(newPos, newSize); } protected override void OnMouseWheel(MouseWheelEventArgs e) { e.Handled = true; //Calculate new scaling factor var wheelOffset = _wheelOffset + (e.Delta / 120); double newScale = Math.Log(1 + (wheelOffset / 10d)) * 2d; if (newScale < MinZoomFactor) { newScale = MinZoomFactor; } else if (newScale > MaxZoomFactor) { newScale = MaxZoomFactor; } Point zoomCenter = e.GetPosition(this); SetZoom(zoomCenter, newScale, e); } public void SetZoom(Point zoomCenter, double newScale, MouseEventArgs parentEvent) { SetZoomImpl(zoomCenter, ZoomFactor, newScale, parentEvent); isUpdatingZoomFactor = true; ZoomFactor = newScale; isUpdatingZoomFactor = false; } private void SetZoomImpl(Point zoomCenter, double oldScale, double newScale, MouseEventArgs parentEvent) { //Calculate current viewing window onto the content Point topLeftContentSpace = TranslatePoint(new Point(0, 0), Children[0]); Point bottomRightContentSpace = TranslatePoint(new Point(ActualWidth, ActualHeight), Children[0]); Rect curView = new Rect { Location = topLeftContentSpace, Size = new Size(bottomRightContentSpace.X - topLeftContentSpace.X, bottomRightContentSpace.Y - topLeftContentSpace.Y) }; //Mouse position as a fraction of the view size Point relZoomPoint = new Point { X = zoomCenter.X / this.ActualWidth, Y = zoomCenter.Y / this.ActualHeight }; //Calculate new viewing window Rect newView = ZoomView(curView, oldScale, newScale, relZoomPoint); //Calculate new content offset based on the new view Point newOffset = new Point(-newView.X * newScale, -newView.Y * newScale); //Calculate new viewing window scale ScaleTransform newScaleTransform = new ScaleTransform { ScaleX = newScale, ScaleY = newScale }; var zoomEvent = new ZoomEventArgs(parentEvent, new ScaleTransform(oldScale, oldScale), newScaleTransform, newOffset); Zoom?.Invoke(this, zoomEvent); ApplyZoomToChildren(zoomEvent); DragOffset = new Point(zoomEvent.ContentOffset.X, zoomEvent.ContentOffset.Y); _wheelOffset = 10d * Math.Pow(Math.E, newScale / 2) - 10; } private void ApplyZoomToChildren(ZoomEventArgs e) { foreach (UIElement cur in this.Children) { cur.RenderTransform = e.NewScale; } } #endregion #region Viewport /// /// Centers the canvas and sets the minimum zoom factor for the specified viewport. /// /// The desired viewport. public void SetViewport(Rect viewport) { // Get current view size var topLeftContentSpace = TranslatePoint(new Point(0, 0), Children[0]); var bottomRightContentSpace = TranslatePoint(new Point(ActualWidth, ActualHeight), Children[0]); var curViewSize = new Size(bottomRightContentSpace.X - topLeftContentSpace.X, bottomRightContentSpace.Y - topLeftContentSpace.Y); // Calc new scale var oldZoom = ZoomFactor; var newScaleX = oldZoom * curViewSize.Width / viewport.Width; var newScaleY = oldZoom * curViewSize.Height / viewport.Height; // Calc new zoom var zoom = Math.Min(newScaleX, newScaleY); ZoomFactor = zoom; this.UpdateLayout(); var boundingCenter = new Point(viewport.TopLeft.X + viewport.Width / 2d, viewport.TopLeft.Y + viewport.Height / 2d); // Update current view size topLeftContentSpace = TranslatePoint(new Point(0, 0), Children[0]); bottomRightContentSpace = TranslatePoint(new Point(ActualWidth, ActualHeight), Children[0]); curViewSize = new Size(bottomRightContentSpace.X - topLeftContentSpace.X, bottomRightContentSpace.Y - topLeftContentSpace.Y); // Calc new position offset var viewOffset = new Point(boundingCenter.X - curViewSize.Width / 2d, boundingCenter.Y - curViewSize.Height / 2d); this.DragOffset = new Point(-viewOffset.X * ZoomFactor, -viewOffset.Y * ZoomFactor); } #endregion } public class DragMoveEventArgs : EventArgs { public MouseEventArgs MouseEvent { get; } public double DeltaX { get; } public double DeltaY { get; } public DragMoveEventArgs(MouseEventArgs mouseEvent, double deltaX, double deltaY) { this.MouseEvent = mouseEvent; this.DeltaX = deltaX; this.DeltaY = deltaY; } } public class ZoomEventArgs : EventArgs { public MouseEventArgs MouseEvent { get; } public ScaleTransform OldScaleScale { get; } public ScaleTransform NewScale { get; } public Point ContentOffset { get; } public ZoomEventArgs(MouseEventArgs e, ScaleTransform oldScale, ScaleTransform newScale, Point contentOffset) { this.MouseEvent = e; this.OldScaleScale = oldScale; this.NewScale = newScale; this.ContentOffset = contentOffset; } } }