/* * GNU AFFERO GENERAL PUBLIC LICENSE * Version 3, 19 November 2007 * Copyright (C) 2007 Free Software Foundation, Inc. * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ // Port from: https://github.com/cyotek/Cyotek.Windows.Forms.ImageBox to AvaloniaUI using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using Bitmap = Avalonia.Media.Imaging.Bitmap; using Color = Avalonia.Media.Color; using Pen = Avalonia.Media.Pen; using Point = Avalonia.Point; using Size = Avalonia.Size; namespace UVtools.AvaloniaControls; public class AdvancedImageBox : UserControl { #region Bindable Base /// /// Multicast event for property change notifications. /// private PropertyChangedEventHandler? _propertyChanged; public new event PropertyChangedEventHandler PropertyChanged { add => _propertyChanged += value; remove => _propertyChanged -= value; } protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; RaisePropertyChanged(propertyName); return true; } protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { } /// /// Notifies listeners that a property value has changed. /// /// /// Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers /// that support . /// protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) { var e = new PropertyChangedEventArgs(propertyName); OnPropertyChanged(e); _propertyChanged?.Invoke(this, e); } #endregion #region Sub Classes /// /// Represents available levels of zoom in an control /// public class ZoomLevelCollection : IList { #region Public Constructors /// /// Initializes a new instance of the class. /// public ZoomLevelCollection() { List = new SortedList(); } /// /// Initializes a new instance of the class. /// /// The default values to populate the collection with. /// Thrown if the collection parameter is null public ZoomLevelCollection(IEnumerable collection) : this() { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } AddRange(collection); } #endregion #region Public Class Properties /// /// Returns the default zoom levels /// public static ZoomLevelCollection Default => new(new[] { 7, 10, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600, 3200, 6400 }); #endregion #region Public Properties /// /// Gets the number of elements contained in the . /// /// /// The number of elements contained in the . /// public int Count => List.Count; /// /// Gets a value indicating whether the is read-only. /// /// true if this instance is read only; otherwise, false. /// true if the is read-only; otherwise, false. /// public bool IsReadOnly => false; /// /// Gets or sets the zoom level at the specified index. /// /// The index. public int this[int index] { get => List.Values[index]; set { List.RemoveAt(index); Add(value); } } #endregion #region Protected Properties /// /// Gets or sets the backing list. /// protected SortedList List { get; set; } #endregion #region Public Members /// /// Adds an item to the . /// /// The object to add to the . public void Add(int item) { List.Add(item, item); } /// /// Adds a range of items to the . /// /// The items to add to the collection. /// Thrown if the collection parameter is null. public void AddRange(IEnumerable collection) { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } foreach (int value in collection) { Add(value); } } /// /// Removes all items from the . /// public void Clear() { List.Clear(); } /// /// Determines whether the contains a specific value. /// /// The object to locate in the . /// true if is found in the ; otherwise, false. public bool Contains(int item) { return List.ContainsKey(item); } /// /// Copies a range of elements this collection into a destination . /// /// The that receives the data. /// A 64-bit integer that represents the index in the at which storing begins. public void CopyTo(int[] array, int arrayIndex) { for (int i = 0; i < Count; i++) { array[arrayIndex + i] = List.Values[i]; } } /// /// Finds the index of a zoom level matching or nearest to the specified value. /// /// The zoom level. public int FindNearest(int zoomLevel) { int nearestValue = List.Values[0]; int nearestDifference = Math.Abs(nearestValue - zoomLevel); for (int i = 1; i < Count; i++) { int value = List.Values[i]; int difference = Math.Abs(value - zoomLevel); if (difference < nearestDifference) { nearestValue = value; nearestDifference = difference; } } return nearestValue; } /// /// Returns an enumerator that iterates through the collection. /// /// A that can be used to iterate through the collection. public IEnumerator GetEnumerator() { return List.Values.GetEnumerator(); } /// /// Determines the index of a specific item in the . /// /// The object to locate in the . /// The index of if found in the list; otherwise, -1. public int IndexOf(int item) { return List.IndexOfKey(item); } /// /// Not implemented. /// /// The index. /// The item. /// Not implemented public void Insert(int index, int item) { throw new NotImplementedException(); } /// /// Returns the next increased zoom level for the given current zoom. /// /// The current zoom level. /// When positive, constrain maximum zoom to this value /// The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom. public int NextZoom(int zoomLevel, int constrainZoomLevel = 0) { var index = IndexOf(FindNearest(zoomLevel)); if (index < Count - 1) index++; return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index]; } /// /// Returns the next decreased zoom level for the given current zoom. /// /// The current zoom level. /// When positive, constrain minimum zoom to this value /// The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom. public int PreviousZoom(int zoomLevel, int constrainZoomLevel = 0) { var index = IndexOf(FindNearest(zoomLevel)); if (index > 0) index--; return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index]; } /// /// Removes the first occurrence of a specific object from the . /// /// The object to remove from the . /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . public bool Remove(int item) { return List.Remove(item); } /// /// Removes the element at the specified index of the . /// /// The zero-based index of the element to remove. public void RemoveAt(int index) { List.RemoveAt(index); } /// /// Copies the elements of the to a new array. /// /// An array containing copies of the elements of the . public int[] ToArray() { int[] results; results = new int[Count]; CopyTo(results, 0); return results; } #endregion #region IList Members /// /// Returns an enumerator that iterates through a collection. /// /// An object that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } #endregion #region Enums /// /// Determines the sizing mode of an image hosted in an control. /// public enum SizeModes : byte { /// /// The image is displayed according to current zoom and scroll properties. /// Normal, /// /// The image is stretched to fill the client area of the control. /// Stretch, /// /// The image is stretched to fill as much of the client area of the control as possible, whilst retaining the same aspect ratio for the width and height. /// Fit } [Flags] public enum MouseButtons : byte { None = 0, LeftButton = 1, MiddleButton = 2, RightButton = 4 } /// /// Describes the zoom action occurring /// [Flags] public enum ZoomActions : byte { /// /// No action. /// None = 0, /// /// The control is increasing the zoom. /// ZoomIn = 1, /// /// The control is decreasing the zoom. /// ZoomOut = 2, /// /// The control zoom was reset. /// ActualSize = 4 } public enum SelectionModes { /// /// No selection. /// None, /// /// Rectangle selection. /// Rectangle, /// /// Zoom selection. /// Zoom } #endregion #region UI Controls public ScrollBar HorizontalScrollBar { get; } public ScrollBar VerticalScrollBar { get; } public ContentPresenter ViewPort { get; } public Vector Offset { get => new(HorizontalScrollBar.Value, VerticalScrollBar.Value); set { HorizontalScrollBar.Value = value.X; VerticalScrollBar.Value = value.Y; RaisePropertyChanged(); TriggerRender(); } } public Size ViewPortSize => ViewPort.Bounds.Size; #endregion #region Private Members private Point _startMousePosition; private Vector _startScrollPosition; private bool _isPanning; private bool _isSelecting; private Bitmap? _trackerImage; private bool _canRender = true; private Point _pointerPosition; #endregion #region Properties public static readonly DirectProperty CanRenderProperty = AvaloniaProperty.RegisterDirect( nameof(CanRender), o => o.CanRender); /// /// Gets or sets if control can render the image /// public bool CanRender { get => _canRender; set { if (!SetAndRaise(CanRenderProperty, ref _canRender, value)) return; if (_canRender) TriggerRender(); } } public static readonly StyledProperty GridCellSizeProperty = AvaloniaProperty.Register(nameof(GridCellSize), 15); /// /// Gets or sets the grid cell size /// public byte GridCellSize { get => GetValue(GridCellSizeProperty); set => SetValue(GridCellSizeProperty, value); } public static readonly StyledProperty GridColorProperty = AvaloniaProperty.Register(nameof(GridColor), Brushes.Gainsboro); /// /// Gets or sets the color used to create the checkerboard style background /// public ISolidColorBrush GridColor { get => GetValue(GridColorProperty); set => SetValue(GridColorProperty, value); } public static readonly StyledProperty GridColorAlternateProperty = AvaloniaProperty.Register(nameof(GridColorAlternate), Brushes.White); /// /// Gets or sets the color used to create the checkerboard style background /// public ISolidColorBrush GridColorAlternate { get => GetValue(GridColorAlternateProperty); set => SetValue(GridColorAlternateProperty, value); } public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register(nameof(Image)); /// /// Gets or sets the image to be displayed /// public Bitmap? Image { get => GetValue(ImageProperty); set { SetValue(ImageProperty, value); if (value is null) { SelectNone(); } UpdateViewPort(); TriggerRender(); RaisePropertyChanged(nameof(IsImageLoaded)); } } public WriteableBitmap? ImageAsWriteableBitmap { get { if (Image is null) return null; return (WriteableBitmap) Image; } } public bool IsImageLoaded => Image is not null; public static readonly DirectProperty TrackerImageProperty = AvaloniaProperty.RegisterDirect( nameof(TrackerImage), o => o.TrackerImage, (o, v) => o.TrackerImage = v); /// /// Gets or sets an image to follow the mouse pointer /// public Bitmap? TrackerImage { get => _trackerImage; set { if (!SetAndRaise(TrackerImageProperty, ref _trackerImage, value)) return; TriggerRender(); RaisePropertyChanged(nameof(HaveTrackerImage)); } } public bool HaveTrackerImage => _trackerImage is not null; public static readonly StyledProperty TrackerImageAutoZoomProperty = AvaloniaProperty.Register(nameof(TrackerImageAutoZoom), true); /// /// Gets or sets if the tracker image will be scaled to the current zoom /// public bool TrackerImageAutoZoom { get => GetValue(TrackerImageAutoZoomProperty); set => SetValue(TrackerImageAutoZoomProperty, value); } public bool IsHorizontalBarVisible { get { if (Image is null) return false; if (SizeMode != SizeModes.Normal) return false; return ScaledImageWidth > ViewPortSize.Width; } } public bool IsVerticalBarVisible { get { if (Image is null) return false; if (SizeMode != SizeModes.Normal) return false; return ScaledImageHeight > ViewPortSize.Height; } } public static readonly StyledProperty ShowGridProperty = AvaloniaProperty.Register(nameof(ShowGrid), true); /// /// Gets or sets the grid visibility when reach high zoom levels /// public bool ShowGrid { get => GetValue(ShowGridProperty); set => SetValue(ShowGridProperty, value); } public static readonly DirectProperty PointerPositionProperty = AvaloniaProperty.RegisterDirect( nameof(PointerPosition), o => o.PointerPosition); /// /// Gets the current pointer position /// public Point PointerPosition { get => _pointerPosition; private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value); } public static readonly DirectProperty IsPanningProperty = AvaloniaProperty.RegisterDirect( nameof(IsPanning), o => o.IsPanning); /// /// Gets if control is currently panning /// public bool IsPanning { get => _isPanning; protected set { if (!SetAndRaise(IsPanningProperty, ref _isPanning, value)) return; _startScrollPosition = Offset; if (value) { Cursor = new Cursor(StandardCursorType.SizeAll); //this.OnPanStart(EventArgs.Empty); } else { Cursor = Cursor.Default; //this.OnPanEnd(EventArgs.Empty); } } } public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect( nameof(IsSelecting), o => o.IsSelecting); /// /// Gets if control is currently selecting a ROI /// public bool IsSelecting { get => _isSelecting; protected set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value); } /// /// Gets the center point of the viewport /// public Point CenterPoint { get { var viewport = GetImageViewPort(); return new(viewport.Width / 2, viewport.Height / 2); } } public static readonly StyledProperty AutoPanProperty = AvaloniaProperty.Register(nameof(AutoPan), true); /// /// Gets or sets if the control can pan with the mouse /// public bool AutoPan { get => GetValue(AutoPanProperty); set => SetValue(AutoPanProperty, value); } public static readonly StyledProperty PanWithMouseButtonsProperty = AvaloniaProperty.Register(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton | MouseButtons.RightButton); /// /// Gets or sets the mouse buttons to pan the image /// public MouseButtons PanWithMouseButtons { get => GetValue(PanWithMouseButtonsProperty); set => SetValue(PanWithMouseButtonsProperty, value); } public static readonly StyledProperty PanWithArrowsProperty = AvaloniaProperty.Register(nameof(PanWithArrows), true); /// /// Gets or sets if the control can pan with the keyboard arrows /// public bool PanWithArrows { get => GetValue(PanWithArrowsProperty); set => SetValue(PanWithArrowsProperty, value); } public static readonly StyledProperty SelectWithMouseButtonsProperty = AvaloniaProperty.Register(nameof(SelectWithMouseButtons), MouseButtons.LeftButton | MouseButtons.RightButton); /// /// Gets or sets the mouse buttons to select a region on image /// public MouseButtons SelectWithMouseButtons { get => GetValue(SelectWithMouseButtonsProperty); set => SetValue(SelectWithMouseButtonsProperty, value); } public static readonly StyledProperty InvertMousePanProperty = AvaloniaProperty.Register(nameof(InvertMousePan), false); /// /// Gets or sets if mouse pan is inverted /// public bool InvertMousePan { get => GetValue(InvertMousePanProperty); set => SetValue(InvertMousePanProperty, value); } public static readonly StyledProperty AutoCenterProperty = AvaloniaProperty.Register(nameof(AutoCenter), true); /// /// Gets or sets if image is auto centered /// public bool AutoCenter { get => GetValue(AutoCenterProperty); set => SetValue(AutoCenterProperty, value); } public static readonly StyledProperty SizeModeProperty = AvaloniaProperty.Register(nameof(SizeMode), SizeModes.Normal); /// /// Gets or sets the image size mode /// public SizeModes SizeMode { get => GetValue(SizeModeProperty); set { SetValue(SizeModeProperty, value); SizeModeChanged(); RaisePropertyChanged(nameof(IsHorizontalBarVisible)); RaisePropertyChanged(nameof(IsVerticalBarVisible)); } } private void SizeModeChanged() { switch (SizeMode) { case SizeModes.Normal: HorizontalScrollBar.Visibility = ScrollBarVisibility.Auto; VerticalScrollBar.Visibility = ScrollBarVisibility.Auto; break; case SizeModes.Stretch: case SizeModes.Fit: HorizontalScrollBar.Visibility = ScrollBarVisibility.Hidden; VerticalScrollBar.Visibility = ScrollBarVisibility.Hidden; break; default: throw new ArgumentOutOfRangeException(nameof(SizeMode), SizeMode, null); } } public static readonly StyledProperty AllowZoomProperty = AvaloniaProperty.Register(nameof(AllowZoom), true); /// /// Gets or sets if zoom is allowed /// public bool AllowZoom { get => GetValue(AllowZoomProperty); set => SetValue(AllowZoomProperty, value); } public static readonly DirectProperty ZoomLevelsProperty = AvaloniaProperty.RegisterDirect( nameof(ZoomLevels), o => o.ZoomLevels, (o, v) => o.ZoomLevels = v); ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default; /// /// Gets or sets the zoom levels. /// /// The zoom levels. public ZoomLevelCollection ZoomLevels { get => _zoomLevels; set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value); } public static readonly StyledProperty MinZoomProperty = AvaloniaProperty.Register(nameof(MinZoom), 10); /// /// Gets or sets the minimum possible zoom. /// /// The zoom. public int MinZoom { get => GetValue(MinZoomProperty); set => SetValue(MinZoomProperty, value); } public static readonly StyledProperty MaxZoomProperty = AvaloniaProperty.Register(nameof(MaxZoom), 6400); /// /// Gets or sets the maximum possible zoom. /// /// The zoom. public int MaxZoom { get => GetValue(MaxZoomProperty); set => SetValue(MaxZoomProperty, value); } public static readonly StyledProperty ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register(nameof(ConstrainZoomOutToFitLevel), true); /// /// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level. /// public bool ConstrainZoomOutToFitLevel { get => GetValue(ConstrainZoomOutToFitLevelProperty); set => SetValue(ConstrainZoomOutToFitLevelProperty, value); } public static readonly DirectProperty OldZoomProperty = AvaloniaProperty.RegisterDirect( nameof(OldZoom), o => o.OldZoom); private int _oldZoom = 100; /// /// Gets the previous zoom value /// /// The zoom. public int OldZoom { get => _oldZoom; private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value); } public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register(nameof(Zoom), 100); /// /// Gets or sets the zoom. /// /// The zoom. public int Zoom { get => GetValue(ZoomProperty); set { var minZoom = MinZoom; if (ConstrainZoomOutToFitLevel) minZoom = Math.Max(ZoomLevelToFit, minZoom); var newZoom = Math.Clamp(value, minZoom, MaxZoom); var previousZoom = Zoom; if (previousZoom == newZoom) return; OldZoom = previousZoom; SetValue(ZoomProperty, newZoom); UpdateViewPort(); TriggerRender(); RaisePropertyChanged(nameof(IsHorizontalBarVisible)); RaisePropertyChanged(nameof(IsVerticalBarVisible)); } } /// /// Gets if the image have zoom. /// True if zoomed in or out /// False if no zoom applied /// public bool IsActualSize => Zoom == 100; /// /// Gets the zoom factor, the zoom / 100.0 /// public double ZoomFactor => Zoom / 100.0; /// /// Gets the zoom to fit level which shows all the image /// public int ZoomLevelToFit { get { if (!IsImageLoaded) return 100; var image = Image!; double zoom; double aspectRatio; if (image.Size.Width > image.Size.Height) { aspectRatio = ViewPortSize.Width / image.Size.Width; zoom = aspectRatio * 100.0; if (ViewPortSize.Height < image.Size.Height * zoom / 100.0) { aspectRatio = ViewPortSize.Height / image.Size.Height; zoom = aspectRatio * 100.0; } } else { aspectRatio = ViewPortSize.Height / image.Size.Height; zoom = aspectRatio * 100.0; if (ViewPortSize.Width < image.Size.Width * zoom / 100.0) { aspectRatio = ViewPortSize.Width / image.Size.Width; zoom = aspectRatio * 100.0; } } return (int) zoom; } } /// /// Gets the width of the scaled image. /// /// The width of the scaled image. public double ScaledImageWidth => Image?.Size.Width * ZoomFactor ?? 0; /// /// Gets the height of the scaled image. /// /// The height of the scaled image. public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0; public static readonly StyledProperty PixelGridColorProperty = AvaloniaProperty.Register(nameof(PixelGridColor), Brushes.DimGray); /// /// Gets or sets the color of the pixel grid. /// /// The color of the pixel grid. public ISolidColorBrush PixelGridColor { get => GetValue(PixelGridColorProperty); set => SetValue(PixelGridColorProperty, value); } public static readonly StyledProperty PixelGridZoomThresholdProperty = AvaloniaProperty.Register(nameof(PixelGridZoomThreshold), 5); /// /// Gets or sets the minimum size of zoomed pixel's before the pixel grid will be drawn /// /// The pixel grid threshold. public int PixelGridZoomThreshold { get => GetValue(PixelGridZoomThresholdProperty); set => SetValue(PixelGridZoomThresholdProperty, value); } public static readonly StyledProperty SelectionModeProperty = AvaloniaProperty.Register(nameof(SelectionMode), SelectionModes.None); public SelectionModes SelectionMode { get => GetValue(SelectionModeProperty); set => SetValue(SelectionModeProperty, value); } public static readonly StyledProperty SelectionColorProperty = AvaloniaProperty.Register(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255))); public ISolidColorBrush SelectionColor { get => GetValue(SelectionColorProperty); set => SetValue(SelectionColorProperty, value); } public static readonly StyledProperty SelectionRegionProperty = AvaloniaProperty.Register(nameof(SelectionRegion), Rect.Empty); public Rect SelectionRegion { get => GetValue(SelectionRegionProperty); set { SetValue(SelectionRegionProperty, value); //if (!RaiseAndSetIfChanged(ref _selectionRegion, value)) return; TriggerRender(); RaisePropertyChanged(nameof(HaveSelection)); RaisePropertyChanged(nameof(SelectionRegionNet)); RaisePropertyChanged(nameof(SelectionPixelSize)); } } public Rectangle SelectionRegionNet { get { var rect = SelectionRegion; return new Rectangle((int) Math.Ceiling(rect.X), (int)Math.Ceiling(rect.Y), (int)rect.Width, (int)rect.Height); } } public PixelSize SelectionPixelSize { get { var rect = SelectionRegion; return new PixelSize((int) rect.Width, (int) rect.Height); } } public bool HaveSelection => !SelectionRegion.IsEmpty; #endregion #region Constructor public AdvancedImageBox() { InitializeComponent(); FocusableProperty.OverrideDefaultValue(typeof(AdvancedImageBox), true); AffectsRender(ShowGridProperty); HorizontalScrollBar = this.FindControl("HorizontalScrollBar"); VerticalScrollBar = this.FindControl("VerticalScrollBar"); ViewPort = this.FindControl("ViewPort"); SizeModeChanged(); HorizontalScrollBar.Scroll += ScrollBarOnScroll; VerticalScrollBar.Scroll += ScrollBarOnScroll; ViewPort.PointerPressed += ViewPortOnPointerPressed; ViewPort.PointerLeave += ViewPortOnPointerLeave; ViewPort.PointerMoved += ViewPortOnPointerMoved; ViewPort.PointerWheelChanged += ViewPortOnPointerWheelChanged; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } #endregion #region Render methods public void TriggerRender(bool renderOnlyCursorTracker = false) { if (!_canRender) return; if (renderOnlyCursorTracker && _trackerImage is null) return; InvalidateVisual(); } public override void Render(DrawingContext context) { //Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); base.Render(context); var viewPortSize = ViewPortSize; // Draw Grid var gridCellSize = GridCellSize; if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) { // draw the background var gridColor = GridColor; var altColor = GridColorAlternate; var currentColor = gridColor; for (int y = 0; y < viewPortSize.Height; y += gridCellSize) { var firstRowColor = currentColor; for (int x = 0; x < viewPortSize.Width; x += gridCellSize) { context.FillRectangle(currentColor, new Rect(x, y, gridCellSize, gridCellSize)); currentColor = ReferenceEquals(currentColor, gridColor) ? altColor : gridColor; } if (Equals(firstRowColor, currentColor)) currentColor = ReferenceEquals(currentColor, gridColor) ? altColor : gridColor; } } /*else { context.FillRectangle(Background, new Rect(0, 0, Viewport.Width, Viewport.Height)); }*/ var image = Image; if (image is null) return; var imageViewPort = GetImageViewPort(); // Draw iamge context.DrawImage(image, GetSourceImageRegion(), imageViewPort ); var zoomFactor = ZoomFactor; if (HaveTrackerImage && _pointerPosition.X >= 0 && _pointerPosition.Y >= 0) { var destSize = TrackerImageAutoZoom ? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor) : image.Size; var destPos = new Point( _pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2 ); context.DrawImage(_trackerImage, new Rect(destPos, destSize) ); } //SkiaContext.SkCanvas.dr // Draw pixel grid if (zoomFactor > PixelGridZoomThreshold && SizeMode == SizeModes.Normal) { var offsetX = Offset.X % zoomFactor; var offsetY = Offset.Y % zoomFactor; Pen pen = new(PixelGridColor); for (double x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor) { context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom)); } for (double y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor) { context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y)); } context.DrawRectangle(pen, imageViewPort); } if (!SelectionRegion.IsEmpty) { var rect = GetOffsetRectangle(SelectionRegion); var selectionColor = SelectionColor; context.FillRectangle(selectionColor, rect); var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B); context.DrawRectangle(new Pen(color.ToUint32()), rect); } } private bool UpdateViewPort() { if (Image is null) { HorizontalScrollBar.Maximum = 0; VerticalScrollBar.Maximum = 0; return true; } var scaledImageWidth = ScaledImageWidth; var scaledImageHeight = ScaledImageHeight; var width = scaledImageWidth - HorizontalScrollBar.ViewportSize; var height = scaledImageHeight - VerticalScrollBar.ViewportSize; //var width = scaledImageWidth <= Viewport.Width ? Viewport.Width : scaledImageWidth; //var height = scaledImageHeight <= Viewport.Height ? Viewport.Height : scaledImageHeight; bool changed = false; if (Math.Abs(HorizontalScrollBar.Maximum - width) > 0.01) { HorizontalScrollBar.Maximum = width; changed = true; } if (Math.Abs(VerticalScrollBar.Maximum - scaledImageHeight) > 0.01) { VerticalScrollBar.Maximum = height; changed = true; } /*if (changed) { var newContainer = new ContentControl { Width = width, Height = height }; FillContainer.Content = SizedContainer = newContainer; Debug.WriteLine($"Updated ViewPort: {DateTime.Now.Ticks}"); //TriggerRender(); }*/ return changed; } #endregion #region Events and Overrides private void ScrollBarOnScroll(object? sender, ScrollEventArgs e) { TriggerRender(); } /*protected override void OnScrollChanged(ScrollChangedEventArgs e) { Debug.WriteLine($"ViewportDelta: {e.ViewportDelta} | OffsetDelta: {e.OffsetDelta} | ExtentDelta: {e.ExtentDelta}"); if (!e.ViewportDelta.IsDefault) { UpdateViewPort(); } TriggerRender(); base.OnScrollChanged(e); }*/ private void ViewPortOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) { e.Handled = true; if (Image is null) return; if (AllowZoom && SizeMode == SizeModes.Normal) { // The MouseWheel event can contain multiple "spins" of the wheel so we need to adjust accordingly //double spins = Math.Abs(e.Delta.Y); //Debug.WriteLine(e.GetPosition(this)); // TODO: Really should update the source method to handle multiple increments rather than calling it multiple times /*for (int i = 0; i < spins; i++) {*/ ProcessMouseZoom(e.Delta.Y > 0, e.GetPosition(ViewPort)); //} } } private void ViewPortOnPointerPressed(object? sender, PointerPressedEventArgs e) { if (e.Handled || _isPanning || _isSelecting || Image is null) return; var pointer = e.GetCurrentPoint(this); if (SelectionMode != SelectionModes.None) { if (!( pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed && (SelectWithMouseButtons & MouseButtons.RightButton) != 0 ) ) return; IsSelecting = true; } else { if (!( pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0 ) || !AutoPan || SizeMode != SizeModes.Normal ) return; IsPanning = true; } var location = pointer.Position; if (location.X > ViewPortSize.Width) return; if (location.Y > ViewPortSize.Height) return; _startMousePosition = location; } /*protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (e.Handled || _isPanning || _isSelecting || Image is null) return; var pointer = e.GetCurrentPoint(this); if (SelectionMode != SelectionModes.None) { if (!( pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed && (SelectWithMouseButtons & MouseButtons.RightButton) != 0 ) ) return; IsSelecting = true; } else { if (!( pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0 ) || !AutoPan || SizeMode != SizeModes.Normal ) return; IsPanning = true; } var location = pointer.Position; if (location.X > ViewPortSize.Width) return; if (location.Y > ViewPortSize.Height) return; _startMousePosition = location; }*/ protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); if (e.Handled) return; IsPanning = false; IsSelecting = false; } private void ViewPortOnPointerLeave(object? sender, PointerEventArgs e) { PointerPosition = new Point(-1, -1); TriggerRender(true); e.Handled = true; } /*protected override void OnPointerLeave(PointerEventArgs e) { base.OnPointerLeave(e); PointerPosition = new Point(-1, -1); TriggerRender(true); e.Handled = true; }*/ private void ViewPortOnPointerMoved(object? sender, PointerEventArgs e) { if (e.Handled) return; var pointer = e.GetCurrentPoint(ViewPort); PointerPosition = pointer.Position; if (!_isPanning && !_isSelecting) { TriggerRender(true); return; } if (_isPanning) { double x; double y; if (!InvertMousePan) { x = _startScrollPosition.X + (_startMousePosition.X - _pointerPosition.X); y = _startScrollPosition.Y + (_startMousePosition.Y - _pointerPosition.Y); } else { x = (_startScrollPosition.X - (_startMousePosition.X - _pointerPosition.X)); y = (_startScrollPosition.Y - (_startMousePosition.Y - _pointerPosition.Y)); } Offset = new Vector(x, y); } else if (_isSelecting) { var viewPortPoint = new Point( Math.Min(_pointerPosition.X, ViewPort.Bounds.Right), Math.Min(_pointerPosition.Y, ViewPort.Bounds.Bottom)); double x; double y; double w; double h; var imageOffset = GetImageViewPort().Position; if (viewPortPoint.X < _startMousePosition.X) { x = viewPortPoint.X; w = _startMousePosition.X - viewPortPoint.X; } else { x = _startMousePosition.X; w = viewPortPoint.X - _startMousePosition.X; } if (viewPortPoint.Y < _startMousePosition.Y) { y = viewPortPoint.Y; h = _startMousePosition.Y - viewPortPoint.Y; } else { y = _startMousePosition.Y; h = viewPortPoint.Y - _startMousePosition.Y; } x -= imageOffset.X - Offset.X; y -= imageOffset.Y - Offset.Y; var zoomFactor = ZoomFactor; x /= zoomFactor; y /= zoomFactor; w /= zoomFactor; h /= zoomFactor; if (w > 0 && h > 0) { SelectionRegion = FitRectangle(new Rect(x, y, w, h)); } } e.Handled = true; } /*protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); if (e.Handled || !ViewPort.IsPointerOver) return; var pointer = e.GetCurrentPoint(ViewPort); PointerPosition = pointer.Position; if (!_isPanning && !_isSelecting) { TriggerRender(true); return; } if (_isPanning) { double x; double y; if (!InvertMousePan) { x = _startScrollPosition.X + (_startMousePosition.X - _pointerPosition.X); y = _startScrollPosition.Y + (_startMousePosition.Y - _pointerPosition.Y); } else { x = (_startScrollPosition.X - (_startMousePosition.X - _pointerPosition.X)); y = (_startScrollPosition.Y - (_startMousePosition.Y - _pointerPosition.Y)); } Offset = new Vector(x, y); } else if (_isSelecting) { double x; double y; double w; double h; var imageOffset = GetImageViewPort().Position; if (_pointerPosition.X < _startMousePosition.X) { x = _pointerPosition.X; w = _startMousePosition.X - _pointerPosition.X; } else { x = _startMousePosition.X; w = _pointerPosition.X - _startMousePosition.X; } if (_pointerPosition.Y < _startMousePosition.Y) { y = _pointerPosition.Y; h = _startMousePosition.Y - _pointerPosition.Y; } else { y = _startMousePosition.Y; h = _pointerPosition.Y - _startMousePosition.Y; } x -= imageOffset.X - Offset.X; y -= imageOffset.Y - Offset.Y; var zoomFactor = ZoomFactor; x /= zoomFactor; y /= zoomFactor; w /= zoomFactor; h /= zoomFactor; if (w > 0 && h > 0) { SelectionRegion = FitRectangle(new Rect(x, y, w, h)); } } e.Handled = true; }*/ #endregion #region Zoom and Size modes private void ProcessMouseZoom(bool isZoomIn, Point cursorPosition) => PerformZoom(isZoomIn ? ZoomActions.ZoomIn : ZoomActions.ZoomOut, true, cursorPosition); /// /// Returns an appropriate zoom level based on the specified action, relative to the current zoom level. /// /// The action to determine the zoom level. /// Thrown if an unsupported action is specified. private int GetZoomLevel(ZoomActions action) { var result = action switch { ZoomActions.None => Zoom, ZoomActions.ZoomIn => _zoomLevels.NextZoom(Zoom), ZoomActions.ZoomOut => _zoomLevels.PreviousZoom(Zoom), ZoomActions.ActualSize => 100, _ => throw new ArgumentOutOfRangeException(nameof(action), action, null), }; return result; } /// /// Resets the property whilsts retaining the original . /// protected void RestoreSizeMode() { if (SizeMode != SizeModes.Normal) { var previousZoom = Zoom; SizeMode = SizeModes.Normal; Zoom = previousZoom; // Stop the zoom getting reset to 100% before calculating the new zoom } } private void PerformZoom(ZoomActions action, bool preservePosition) => PerformZoom(action, preservePosition, CenterPoint); private void PerformZoom(ZoomActions action, bool preservePosition, Point relativePoint) { Point currentPixel = PointToImage(relativePoint); int currentZoom = Zoom; int newZoom = GetZoomLevel(action); /*if (preservePosition && Zoom != currentZoom) CanRender = false;*/ RestoreSizeMode(); Zoom = newZoom; if (preservePosition && Zoom != currentZoom) { ScrollTo(currentPixel, relativePoint); } } /// /// Zooms into the image /// public void ZoomIn() => ZoomIn(true); /// /// Zooms into the image /// /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. public void ZoomIn(bool preservePosition) { PerformZoom(ZoomActions.ZoomIn, preservePosition); } /// /// Zooms out of the image /// public void ZoomOut() => ZoomOut(true); /// /// Zooms out of the image /// /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. public void ZoomOut(bool preservePosition) { PerformZoom(ZoomActions.ZoomOut, preservePosition); } /// /// Zooms to the maximum size for displaying the entire image within the bounds of the control. /// public void ZoomToFit() { if (!IsImageLoaded) return; Zoom = ZoomLevelToFit; } /// /// Adjusts the view port to fit the given region /// /// The X co-ordinate of the selection region. /// The Y co-ordinate of the selection region. /// The width of the selection region. /// The height of the selection region. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(double x, double y, double width, double height, double margin = 0) { ZoomToRegion(new Rect(x, y, width, height), margin); } /// /// Adjusts the view port to fit the given region /// /// The X co-ordinate of the selection region. /// The Y co-ordinate of the selection region. /// The width of the selection region. /// The height of the selection region. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(int x, int y, int width, int height, double margin = 0) { ZoomToRegion(new Rect(x, y, width, height), margin); } /// /// Adjusts the view port to fit the given region /// /// The rectangle to fit the view port to. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(Rectangle rectangle, double margin = 0) => ZoomToRegion(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height, margin); /// /// Adjusts the view port to fit the given region /// /// The rectangle to fit the view port to. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(Rect rectangle, double margin = 0) { if (margin > 0) rectangle = rectangle.Inflate(margin); var ratioX = ViewPortSize.Width / rectangle.Width; var ratioY = ViewPortSize.Height / rectangle.Height; var zoomFactor = Math.Min(ratioX, ratioY); var cx = rectangle.X + rectangle.Width / 2; var cy = rectangle.Y + rectangle.Height / 2; CanRender = false; Zoom = (int)(zoomFactor * 100); // This function sets the zoom so viewport will change CenterAt(new Point(cx, cy)); // If i call this here, it will move to the wrong position due wrong viewport } /// /// Zooms to current selection region /// public void ZoomToSelectionRegion(double margin = 0) { if (!HaveSelection) return; ZoomToRegion(SelectionRegion, margin); } /// /// Resets the zoom to 100%. /// public void PerformActualSize() { SizeMode = SizeModes.Normal; //SetZoom(100, ImageZoomActions.ActualSize | (Zoom < 100 ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut)); Zoom = 100; } #endregion #region Utility methods /// /// Determines whether the specified point is located within the image view port /// /// The point. /// /// true if the specified point is located within the image view port; otherwise, false. /// public bool IsPointInImage(Point point) => GetImageViewPort().Contains(point); /// /// Determines whether the specified point is located within the image view port /// /// The X co-ordinate of the point to check. /// The Y co-ordinate of the point to check. /// /// true if the specified point is located within the image view port; otherwise, false. /// public bool IsPointInImage(int x, int y) => IsPointInImage(new Point(x, y)); /// /// Determines whether the specified point is located within the image view port /// /// The X co-ordinate of the point to check. /// The Y co-ordinate of the point to check. /// /// true if the specified point is located within the image view port; otherwise, false. /// public bool IsPointInImage(double x, double y) => IsPointInImage(new Point(x, y)); /// /// Converts the given client size point to represent a coordinate on the source image. /// /// The X co-ordinate of the point to convert. /// The Y co-ordinate of the point to convert. /// /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. /// /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point public Point PointToImage(double x, double y, bool fitToBounds = true) => PointToImage(new Point(x, y), fitToBounds); /// /// Converts the given client size point to represent a coordinate on the source image. /// /// The X co-ordinate of the point to convert. /// The Y co-ordinate of the point to convert. /// /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. /// /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point public Point PointToImage(int x, int y, bool fitToBounds = true) { return PointToImage(new Point(x, y), fitToBounds); } /// /// Converts the given client size point to represent a coordinate on the source image. /// /// The source point. /// /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. /// /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point public Point PointToImage(Point point, bool fitToBounds = true) { double x; double y; var viewport = GetImageViewPort(); if (!fitToBounds || viewport.Contains(point)) { x = (point.X + Offset.X - viewport.X) / ZoomFactor; y = (point.Y + Offset.Y - viewport.Y) / ZoomFactor; var image = Image; if (fitToBounds) { x = Math.Clamp(x, 0, image!.Size.Width-1); y = Math.Clamp(y, 0, image.Size.Height-1); } } else { x = 0; // Return Point.Empty if we couldn't match y = 0; } return new(x, y); } /// /// Returns the source repositioned to include the current image offset and scaled by the current zoom level /// /// The source to offset. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(System.Drawing.Point source) { var offset = GetOffsetPoint(new Point(source.X, source.Y)); return new((int)offset.X, (int)offset.Y); } /// /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level /// /// The source X co-ordinate. /// The source Y co-ordinate. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(int x, int y) { return GetOffsetPoint(new System.Drawing.Point(x, y)); } /// /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level /// /// The source X co-ordinate. /// The source Y co-ordinate. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(double x, double y) { return GetOffsetPoint(new Point(x, y)); } /// /// Returns the source repositioned to include the current image offset and scaled by the current zoom level /// /// The source to offset. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(Point source) { Rect viewport = GetImageViewPort(); var scaled = GetScaledPoint(source); var offsetX = viewport.Left + Offset.X; var offsetY = viewport.Top + Offset.Y; return new(scaled.X + offsetX, scaled.Y + offsetY); } /// /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset /// /// The source to offset. /// A which has been resized and repositioned to match the current zoom level and image offset public Rect GetOffsetRectangle(Rect source) { var viewport = GetImageViewPort(); var scaled = GetScaledRectangle(source); var offsetX = viewport.Left - Offset.X; var offsetY = viewport.Top - Offset.Y; return new(new Point(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size); } /// /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been resized and repositioned to match the current zoom level and image offset public Rectangle GetOffsetRectangle(int x, int y, int width, int height) { return GetOffsetRectangle(new Rectangle(x, y, width, height)); } /// /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been resized and repositioned to match the current zoom level and image offset public Rect GetOffsetRectangle(double x, double y, double width, double height) { return GetOffsetRectangle(new Rect(x, y, width, height)); } /// /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset /// /// The source to offset. /// A which has been resized and repositioned to match the current zoom level and image offset public Rectangle GetOffsetRectangle(Rectangle source) { var viewport = GetImageViewPort(); var scaled = GetScaledRectangle(source); var offsetX = viewport.Left + Offset.X; var offsetY = viewport.Top + Offset.Y; return new(new System.Drawing.Point((int)(scaled.Left + offsetX), (int)(scaled.Top + offsetY)), new System.Drawing.Size((int)scaled.Size.Width, (int)scaled.Size.Height)); } /// /// Fits a given to match image boundaries /// /// The rectangle. /// /// A structure remapped to fit the image boundaries /// public Rectangle FitRectangle(Rectangle rectangle) { var image = Image; if (image is null) return Rectangle.Empty; var x = rectangle.X; var y = rectangle.Y; var w = rectangle.Width; var h = rectangle.Height; if (x < 0) { x = 0; } if (y < 0) { y = 0; } if (x + w > image.Size.Width) { w = (int)(image.Size.Width - x); } if (y + h > image.Size.Height) { h = (int)(image.Size.Height - y); } return new(x, y, w, h); } /// /// Fits a given to match image boundaries /// /// The rectangle. /// /// A structure remapped to fit the image boundaries /// public Rect FitRectangle(Rect rectangle) { var image = Image; if (image is null) return Rect.Empty; var x = rectangle.X; var y = rectangle.Y; var w = rectangle.Width; var h = rectangle.Height; if (x < 0) { w -= -x; x = 0; } if (y < 0) { h -= -y; y = 0; } if (x + w > image.Size.Width) { w = image.Size.Width - x; } if (y + h > image.Size.Height) { h = image.Size.Height - y; } return new(x, y, w, h); } #endregion #region Navigate / Scroll methods /// /// Scrolls the control to the given point in the image, offset at the specified display point /// /// The X co-ordinate of the point to scroll to. /// The Y co-ordinate of the point to scroll to. /// The X co-ordinate relative to the x parameter. /// The Y co-ordinate relative to the y parameter. public void ScrollTo(double x, double y, double relativeX, double relativeY) => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); /// /// Scrolls the control to the given point in the image, offset at the specified display point /// /// The X co-ordinate of the point to scroll to. /// The Y co-ordinate of the point to scroll to. /// The X co-ordinate relative to the x parameter. /// The Y co-ordinate relative to the y parameter. public void ScrollTo(int x, int y, int relativeX, int relativeY) => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); /// /// Scrolls the control to the given point in the image, offset at the specified display point /// /// The point of the image to attempt to scroll to. /// The relative display point to offset scrolling by. public void ScrollTo(Point imageLocation, Point relativeDisplayPoint) { //CanRender = false; var zoomFactor = ZoomFactor; var x = imageLocation.X * zoomFactor - relativeDisplayPoint.X; var y = imageLocation.Y * zoomFactor - relativeDisplayPoint.Y; _canRender = true; Offset = new Vector(x, y); /*Debug.WriteLine( $"X/Y: {x},{y} | \n" + $"Offset: {Offset} | \n" + $"ZoomFactor: {ZoomFactor} | \n" + $"Image Location: {imageLocation}\n" + $"MAX: {HorizontalScrollBar.Maximum},{VerticalScrollBar.Maximum} \n" + $"ViewPort: {Viewport.Width},{Viewport.Height} \n" + $"Container: {HorizontalScrollBar.ViewportSize},{VerticalScrollBar.ViewportSize} \n" + $"Relative: {relativeDisplayPoint}");*/ } /// /// Centers the given point in the image in the center of the control /// /// The point of the image to attempt to center. public void CenterAt(System.Drawing.Point imageLocation) => ScrollTo(new Point(imageLocation.X, imageLocation.Y), new Point(ViewPortSize.Width / 2, ViewPortSize.Height / 2)); /// /// Centers the given point in the image in the center of the control /// /// The point of the image to attempt to center. public void CenterAt(Point imageLocation) => ScrollTo(imageLocation, new Point(ViewPortSize.Width / 2, ViewPortSize.Height / 2)); /// /// Centers the given point in the image in the center of the control /// /// The X co-ordinate of the point to center. /// The Y co-ordinate of the point to center. public void CenterAt(int x, int y) => CenterAt(new Point(x, y)); /// /// Centers the given point in the image in the center of the control /// /// The X co-ordinate of the point to center. /// The Y co-ordinate of the point to center. public void CenterAt(double x, double y) => CenterAt(new Point(x, y)); /// /// Resets the viewport to show the center of the image. /// public void CenterToImage() { Offset = new Vector(HorizontalScrollBar.Maximum / 2, VerticalScrollBar.Maximum / 2); } #endregion #region Selection / ROI methods /// /// Returns the source scaled according to the current zoom level /// /// The X co-ordinate of the point to scale. /// The Y co-ordinate of the point to scale. /// A which has been scaled to match the current zoom level public Point GetScaledPoint(int x, int y) { return GetScaledPoint(new Point(x, y)); } /// /// Returns the source scaled according to the current zoom level /// /// The X co-ordinate of the point to scale. /// The Y co-ordinate of the point to scale. /// A which has been scaled to match the current zoom level public PointF GetScaledPoint(float x, float y) { return GetScaledPoint(new PointF(x, y)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public Point GetScaledPoint(Point source) { return new(source.X * ZoomFactor, source.Y * ZoomFactor); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public PointF GetScaledPoint(PointF source) { return new((float)(source.X * ZoomFactor), (float)(source.Y * ZoomFactor)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been scaled to match the current zoom level public Rect GetScaledRectangle(int x, int y, int width, int height) { return GetScaledRectangle(new Rect(x, y, width, height)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been scaled to match the current zoom level public RectangleF GetScaledRectangle(float x, float y, float width, float height) { return GetScaledRectangle(new RectangleF(x, y, width, height)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The location of the source rectangle. /// The size of the source rectangle. /// A which has been scaled to match the current zoom level public Rect GetScaledRectangle(Point location, Size size) { return GetScaledRectangle(new Rect(location, size)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The location of the source rectangle. /// The size of the source rectangle. /// A which has been scaled to match the current zoom level public RectangleF GetScaledRectangle(PointF location, SizeF size) { return GetScaledRectangle(new RectangleF(location, size)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public Rect GetScaledRectangle(Rect source) { return new(source.Left * ZoomFactor, source.Top * ZoomFactor, source.Width * ZoomFactor, source.Height * ZoomFactor); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public RectangleF GetScaledRectangle(RectangleF source) { return new((float)(source.Left * ZoomFactor), (float)(source.Top * ZoomFactor), (float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor)); } /// /// Returns the source size scaled according to the current zoom level /// /// The width of the size to scale. /// The height of the size to scale. /// A which has been resized to match the current zoom level public SizeF GetScaledSize(float width, float height) { return GetScaledSize(new SizeF(width, height)); } /// /// Returns the source size scaled according to the current zoom level /// /// The width of the size to scale. /// The height of the size to scale. /// A which has been resized to match the current zoom level public Size GetScaledSize(int width, int height) { return GetScaledSize(new Size(width, height)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been resized to match the current zoom level public SizeF GetScaledSize(SizeF source) { return new((float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been resized to match the current zoom level public Size GetScaledSize(Size source) { return new(source.Width * ZoomFactor, source.Height * ZoomFactor); } /// /// Creates a selection region which encompasses the entire image /// /// Thrown if no image is currently set public void SelectAll() { var image = Image; if (image is null) return; SelectionRegion = new Rect(0, 0, image.Size.Width, image.Size.Height); } /// /// Clears any existing selection region /// public void SelectNone() { SelectionRegion = Rect.Empty; } #endregion #region Viewport and image region methods /// /// Gets the source image region. /// /// public Rect GetSourceImageRegion() { var image = Image; if (image is null) return Rect.Empty; switch (SizeMode) { case SizeModes.Normal: var offset = Offset; var viewPort = GetImageViewPort(); var zoomFactor = ZoomFactor; double sourceLeft = (offset.X / zoomFactor); double sourceTop = (offset.Y / zoomFactor); double sourceWidth = (viewPort.Width / zoomFactor); double sourceHeight = (viewPort.Height / zoomFactor); return new(sourceLeft, sourceTop, sourceWidth, sourceHeight); } return new(0, 0, image.Size.Width, image.Size.Height); } /// /// Gets the image view port. /// /// public Rect GetImageViewPort() { var viewPortSize = ViewPortSize; if (!IsImageLoaded || (viewPortSize.Width == 0 && viewPortSize.Height == 0)) return Rect.Empty; double xOffset = 0; double yOffset = 0; double width = 0; double height = 0; switch (SizeMode) { case SizeModes.Normal: if (AutoCenter) { xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0); yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0); } width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width); height = Math.Min(ScaledImageHeight - Math.Abs(Offset.Y), viewPortSize.Height); break; case SizeModes.Stretch: width = viewPortSize.Width; height = viewPortSize.Height; break; case SizeModes.Fit: var image = Image; double scaleFactor = Math.Min(viewPortSize.Width / image!.Size.Width, viewPortSize.Height / image.Size.Height); width = Math.Floor(image.Size.Width * scaleFactor); height = Math.Floor(image.Size.Height * scaleFactor); if (AutoCenter) { xOffset = (viewPortSize.Width - width) / 2; yOffset = (viewPortSize.Height - height) / 2; } break; default: throw new ArgumentOutOfRangeException(nameof(SizeMode), SizeMode, null); } return new(xOffset, yOffset, width, height); } #endregion #region Image methods public void LoadImage(string path) { Image = new Bitmap(path); } public Bitmap? GetSelectedBitmap() { var image = ImageAsWriteableBitmap; if (image is null || !HaveSelection) return null; var selection = SelectionRegionNet; var pixelSize = SelectionPixelSize; using var frameBuffer = image.Lock(); var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul); using var newFrameBuffer = newBitmap.Lock(); int i = 0; unsafe { var inputPixels = (uint*) (void*) frameBuffer.Address; var targetPixels = (uint*) (void*) newFrameBuffer.Address; for (int y = selection.Y; y < selection.Bottom; y++) { var thisY = y * frameBuffer.Size.Width; for (int x = selection.X; x < selection.Right; x++) { targetPixels![i++] = inputPixels![thisY + x]; } } } return newBitmap; } #endregion }