Dispenser/DispenserUI/Views/Controls/ImageViewer.axaml.cs

677 lines
20 KiB
C#

using System;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using DispenserCommon.Aop;
using DispenserCommon.Utils;
using DispenserHal.Camera.Factory;
using DispenserUI.Utils;
using Serilog;
using Bitmap = System.Drawing.Bitmap;
using Brushes = Avalonia.Media.Brushes;
using Image = Avalonia.Controls.Image;
using PixelFormat = Avalonia.Platform.PixelFormat;
using Point = Avalonia.Point;
using Rectangle = System.Drawing.Rectangle;
namespace DispenserUI.Views.Controls;
#pragma warning disable CA1416
[GlobalTry]
public partial class ImageViewer : UserControl
{
private readonly SlidingWindow _slidingWindow = new(100);
public static readonly StyledProperty<Bitmap> SourceProperty =
AvaloniaProperty.Register<ImageViewer, Bitmap>(nameof(Source));
public static readonly StyledProperty<bool> ShowToolBarProperty =
AvaloniaProperty.Register<ImageViewer, bool>(nameof(ShowToolBar), true);
public static readonly StyledProperty<bool> ShowingCrosshairProperty =
AvaloniaProperty.Register<ImageViewer, bool>(nameof(ShowingCrosshair));
public static readonly StyledProperty<double> TuningStepProperty =
AvaloniaProperty.Register<ImageViewer, double>(nameof(TuningStep), 1.0);
public static readonly StyledProperty<bool> ScaleRelativeCenterProperty =
AvaloniaProperty.Register<ImageViewer, bool>(nameof(ScaleRelativeCenter));
public static readonly StyledProperty<double> ScaleRatioProperty =
AvaloniaProperty.Register<ImageViewer, double>(nameof(ScaleRatio), 1);
public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<ImageViewer, Stretch>(nameof(Stretch));
public static readonly StyledProperty<object> SlotContentProperty =
AvaloniaProperty.Register<ImageViewer, object>(nameof(SlotContent));
private bool _isDragging;
private bool _isDraggingToolBar;
private bool _isPressingSpace;
private Point _origin = new(0, 0);
private Point _startPoint;
private Point _crosshairCenter;
private Point _crosshairCenterPoint;
private Point _crosshairPixelCenter;
private bool _isDraggingCanvas;
public event EventHandler<Point> PixelChanged;
public event EventHandler<Point> CenterPixelChanged;
public event EventHandler<double> ScaleRatioChanged;
private readonly string _id;
private readonly WriteableBitmap _image;
public ImageViewer()
{
Focusable = true;
_id = Guid.NewGuid().ToString();
_image = new WriteableBitmap(new PixelSize(4096, 3000), new Vector(96, 96),
PixelFormat.Bgra8888, AlphaFormat.Premul);
InitializeComponent();
Viewer = this.FindControl<Image>("Viewer");
ImageContainer = this.FindControl<Grid>("ImageContainer");
ToolBar = this.FindControl<ShadowBox>("ToolBar");
ImageCanvas = this.FindControl<Canvas>("ImageCanvas");
SlotViewer = this.FindControl<ContentControl>("SlotViewer");
PixelX = this.FindControl<TextBlock>("PixelX");
PixelY = this.FindControl<TextBlock>("PixelY");
ImageWidth = this.FindControl<TextBlock>("ImageWidth");
ImageHeight = this.FindControl<TextBlock>("ImageHeight");
ScaleRatioText = this.FindControl<TextBlock>("ScaleRatioText");
Viewer!.PointerWheelChanged += OnPointerWheelChanged;
Viewer.PointerPressed += OnPointerPressed;
Viewer.PointerReleased += OnPointerReleased;
Viewer.PointerMoved += OnPointerMoved;
Viewer.DoubleTapped += OnDoubleTapped;
ToolBar!.PointerPressed += OnToolBarPointerPressed;
ToolBar.PointerReleased += OnToolBarPointerReleased;
ToolBar.PointerMoved += OnToolBarPointerMoved;
ImageCanvas!.PointerWheelChanged += (sender, args) => { args.Handled = false; };
AddHandler(KeyDownEvent, OnKeyDownEvent, RoutingStrategies.Tunnel);
AddHandler(KeyUpEvent, OnKeyUpEvent, RoutingStrategies.Tunnel);
Dispatcher.UIThread.Post(() =>
{
this.GetObservable(ShowToolBarProperty).Subscribe(val =>
{
if (!val)
{
ToolBar!.IsVisible = false;
}
});
this.GetObservable(ShowingCrosshairProperty).Subscribe(ToggleCrosshair);
this.GetObservable(SourceProperty).Subscribe(source =>
{
if (source != null)
{
// 处理源图片变化
HandleSourceChanged(source).ConfigureAwait(false);
Viewer.Source = _image;
}
else
{
// 设置默认值
var blank = ImageHelper.LoadBitmap("Assets/UI/blank.png");
Viewer.Source = blank;
}
});
#pragma warning restore CA1416
this.GetObservable(ScaleRatioProperty)
.Subscribe(val => { ScaleRatioText!.Text = val.ToString("0.00"); });
InvalidateVisual();
},
DispatcherPriority.Loaded);
}
public Bitmap Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public bool ShowToolBar
{
get => GetValue(ShowToolBarProperty);
set => SetValue(ShowToolBarProperty, value);
}
public bool ShowingCrosshair
{
get => GetValue(ShowingCrosshairProperty);
set => SetValue(ShowingCrosshairProperty, value);
}
public double TuningStep
{
get => GetValue(TuningStepProperty);
set => SetValue(TuningStepProperty, value);
}
public object SlotContent
{
get => GetValue(SlotContentProperty);
set => SetValue(SlotContentProperty, value);
}
public bool ScaleRelativeCenter
{
get => GetValue(ScaleRelativeCenterProperty);
set => SetValue(ScaleRelativeCenterProperty, value);
}
public double ScaleRatio
{
get => GetValue(ScaleRatioProperty);
set => SetValue(ScaleRatioProperty, value);
}
public Stretch Stretch
{
get => GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
/// <summary>
/// 处理源图片变化事件
/// </summary>
/// <param name="source"></param>
private async Task HandleSourceChanged(Bitmap source)
{
var width = source.Width;
var height = source.Height;
await Task.Run(() =>
{
try
{
var bitmapData = source.LockBits(new Rectangle(0, 0, width, height),
ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var frameBuffer = _image.Lock())
{
var sourcePtr = bitmapData.Scan0;
var destPtr = frameBuffer.Address;
var count = (uint)(bitmapData.Stride * source.Height);
CopyMemory(destPtr, sourcePtr, count);
}
source.UnlockBits(bitmapData);
Dispatcher.UIThread.InvokeAsync(() =>
{
ImageWidth.Text = width.ToString();
ImageHeight.Text = height.ToString();
Viewer.InvalidateVisual();
});
}
catch (Exception e)
{
Log.Error(e, "图像转换异常");
}
});
}
private void OnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
// 根据鼠标滚轮的Delta值来计算缩放比例的变化
var point = e.GetPosition(Viewer);
Scaling(point, e.Delta.Y > 0);
}
private void OnKeyUpEvent(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
_isPressingSpace = false;
}
// 将光标改为拖动光标
Viewer.Cursor = new Cursor(StandardCursorType.Hand);
}
private void OnKeyDownEvent(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
_isPressingSpace = true;
// 将光标改为拖动光标
Viewer.Cursor = new Cursor(StandardCursorType.DragMove);
}
else if (e.Key == Key.LeftAlt)
{
// 将光标改为十字光标
Viewer.Cursor = new Cursor(StandardCursorType.Cross);
}
}
/// <summary>
/// 去缩放图片
/// </summary>
/// <param name="center"></param>
/// <param name="enlarge"></param>
/// <returns></returns>
private void Scaling(Point center, bool enlarge)
{
if (enlarge)
{
ScaleRatio *= 1.1;
}
else
{
ScaleRatio /= 1.1;
}
ScaleRatio = Math.Clamp(ScaleRatio, 0.5, 100);
if (ScaleRelativeCenter)
{
center = new Point(Viewer.Bounds.Width / 2, Viewer.Bounds.Height / 2);
}
if (ShowingCrosshair)
{
center = _crosshairCenter;
}
Viewer.RenderTransformOrigin = new RelativePoint(center, RelativeUnit.Absolute);
// 设置新的缩放比例
Viewer.RenderTransform = new ScaleTransform(ScaleRatio, ScaleRatio);
SlotViewer.RenderTransformOrigin = new RelativePoint(center, RelativeUnit.Absolute);
SlotViewer.RenderTransform = new ScaleTransform(ScaleRatio, ScaleRatio);
ScaleRatioChanged?.Invoke(this, ScaleRatio);
}
/// <summary>
/// 鼠标按下事件
/// </summary>
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(this).Properties;
if (properties.IsLeftButtonPressed)
{
_isDragging = true;
// 计算拖动起始点
_startPoint = e.GetPosition(this);
}
}
/// <summary>
/// 鼠标释放事件
/// </summary>
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
_isDragging = false;
Viewer.Cursor = new Cursor(StandardCursorType.Hand);
}
/// <summary>
/// 鼠标移动事件
/// </summary>
private void OnPointerMoved(object? sender, PointerEventArgs e)
{
GetRealPixelCoordinate(sender as Image, e.GetPosition(Viewer));
if (!_isDragging || !_slidingWindow.AllowValue(_id) || !_isPressingSpace) return;
var currentPoint = e.GetPosition(this);
// 计算两个点之间的差值
var deltaX = currentPoint.X - _startPoint.X;
var deltaY = currentPoint.Y - _startPoint.Y;
_origin = new Point(_origin.X + deltaX, _origin.Y + deltaY);
// 这里的移动需要包含了保持原有的缩放比例,然后再进行移动,否则移动之后图像会进行缩放
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(ScaleRatio, ScaleRatio));
transformGroup.Children.Add(new TranslateTransform(_origin.X, _origin.Y));
Viewer.RenderTransform = transformGroup;
SlotViewer.RenderTransform = transformGroup;
ImageCanvas.RenderTransform = new TranslateTransform(_origin.X, _origin.Y);
_startPoint = currentPoint;
}
/// <summary>
/// 通过双击图片是还原图像大小和位置
/// </summary>
private void OnDoubleTapped(object? sender, TappedEventArgs e)
{
ResetSize();
}
/// <summary>
/// 重置大小
/// </summary>
public void ResetSize()
{
ScaleRatioChanged?.Invoke(this, 1);
Dispatcher.UIThread.InvokeAsync(() =>
{
Viewer.Stretch = Stretch.Uniform;
ScaleRatio = 1;
Viewer.RenderTransform = new ScaleTransform(ScaleRatio, ScaleRatio);
SlotViewer.RenderTransform = new ScaleTransform(ScaleRatio, ScaleRatio);
if (!ShowingCrosshair) return;
ImageCanvas.RenderTransform = new TranslateTransform(0, 0);
_crosshairCenter = new Point(ImageCanvas.Bounds.Width / 2, ImageCanvas.Bounds.Height / 2);
_crosshairPixelCenter = new Point(Viewer.Bounds.Width / 2, Viewer.Bounds.Height / 2);
// 渲染十字线
RenderCrosshair();
});
}
/// <summary>
/// 图像缩小
/// </summary>
public void ImageShrink()
{
Dispatcher.UIThread.InvokeAsync(() =>
{
var center = new Point(Viewer.Bounds.Width / 2, Viewer.Bounds.Height / 2);
Scaling(center, false);
});
}
public Control GetViewer()
{
return Viewer;
}
public Control GetContainer()
{
return ImageContainer;
}
/// <summary>
/// 图像放大
/// </summary>
public void ImageEnlarge()
{
Dispatcher.UIThread.InvokeAsync(() =>
{
var center = new Point(Viewer.Bounds.Width / 2, Viewer.Bounds.Height / 2);
Scaling(center, true);
});
}
private void OnShowCrosshairChanged(object? sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch ts)
{
var @checked = ts.IsChecked.GetValueOrDefault();
ShowingCrosshair = @checked;
ToggleCrosshair(@checked);
}
}
private void OnScaleRelativeCenterChanged(object? sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch ts)
{
var @checked = ts.IsChecked.GetValueOrDefault();
ScaleRelativeCenter = @checked;
}
}
private void ToggleCrosshair(bool show)
{
if (show)
{
ImageCanvas.PointerPressed += OnCrosshairPointerPressed;
ImageCanvas.PointerReleased += OnCrosshairPointerReleased;
ImageCanvas.PointerMoved += OnCrosshairPointerMoved;
_crosshairCenter = new Point(Viewer.Bounds.Width / 2, Viewer.Bounds.Height / 2);
_crosshairPixelCenter = new Point(Viewer.Bounds.Width / 2, Viewer.Bounds.Height / 2);
_crosshairCenterPoint = _crosshairCenter;
// 渲染十字线
RenderCrosshair();
}
else
{
ImageCanvas.Children.Clear();
ImageCanvas.PointerPressed -= OnCrosshairPointerPressed;
ImageCanvas.PointerReleased -= OnCrosshairPointerReleased;
ImageCanvas.PointerMoved -= OnCrosshairPointerMoved;
}
}
/// <summary>
/// 步长变更
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnStepChanged(object? sender, NumericUpDownValueChangedEventArgs e)
{
if (sender is NumericUpDown step)
{
TuningStep = (double)step.Value.GetValueOrDefault();
}
}
/// <summary>
/// 拖拽十字线
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCrosshairPointerMoved(object? sender, PointerEventArgs e)
{
if (_isDraggingCanvas)
{
_crosshairCenter = e.GetPosition(ImageCanvas);
_crosshairCenterPoint = e.GetPosition(Viewer);
// 移动十字线的时候触发事件
CenterPixelChanged?.Invoke(this, _crosshairCenterPoint);
GetRealPixelCoordinate(Viewer, _crosshairCenterPoint);
RenderCrosshair();
}
}
/// <summary>
/// 拖拽十字线事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCrosshairPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ImageCanvas.Cursor = new Cursor(StandardCursorType.Arrow);
_isDraggingCanvas = false;
}
/// <summary>
/// 拖拽十字线事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCrosshairPointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(ImageCanvas).Properties;
if (properties.IsLeftButtonPressed)
{
ImageCanvas.Cursor = new Cursor(StandardCursorType.Hand);
_isDraggingCanvas = true;
}
}
/// <summary>
/// 渲染十字线
/// </summary>
private void RenderCrosshair()
{
ImageCanvas.Children.Clear();
var width = 4096;
var height = 3000;
var centerX = _crosshairCenter.X;
var centerY = _crosshairCenter.Y;
var line1 = new Line
{
StartPoint = new Point(0, centerY),
EndPoint = new Point(width, centerY),
Stroke = Brushes.Orange,
StrokeThickness = 5
};
var line2 = new Line
{
StartPoint = new Point(centerX, 0),
EndPoint = new Point(centerX, height),
Stroke = Brushes.Orange,
StrokeThickness = 5
};
var pixelText = new TextBlock
{
Text = $"({_crosshairPixelCenter.X:0.###},{_crosshairPixelCenter.Y:0.###})",
Foreground = Brushes.Orange,
FontSize = 60,
Margin = new Thickness(centerX + 40, centerY + 40, 0, 0)
};
// 中间画一个空白的圆,用来实现扩大拖拽鼠标点击区域
var circle = new Ellipse
{
Width = 100,
Height = 100,
Fill = Brushes.Transparent,
Stroke = Brushes.Transparent,
StrokeThickness = 0,
Cursor = new Cursor(StandardCursorType.Hand),
Margin = new Thickness(centerX - 50, centerY - 50, 0, 0)
};
ImageCanvas.Children.Add(circle);
ImageCanvas.Children.Add(line1);
ImageCanvas.Children.Add(line2);
ImageCanvas.Children.Add(pixelText);
}
/// <summary>
/// 获取图像的真实像素坐标
/// </summary>
/// <param name="image"></param>
/// <param name="point"></param>
private void GetRealPixelCoordinate(Image? image, Point point)
{
if (image!.Source is not Avalonia.Media.Imaging.Bitmap) return;
PixelX.Text = point.X.ToString("F0");
PixelY.Text = point.Y.ToString("F0");
_crosshairPixelCenter = point;
PixelChanged?.Invoke(this, point);
}
private void OnToolBarPointerMoved(object? sender, PointerEventArgs e)
{
if (_isDraggingToolBar)
{
var currentPoint = e.GetPosition(this);
// 计算两个点之间的差值
var deltaX = currentPoint.X - _startPoint.X;
var deltaY = currentPoint.Y - _startPoint.Y;
_origin = new Point(_origin.X + deltaX, _origin.Y + deltaY);
ToolBar.RenderTransform = new TranslateTransform(_origin.X, _origin.Y);
_startPoint = currentPoint;
}
}
private void OnToolBarPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ToolBar.Cursor = new Cursor(StandardCursorType.Arrow);
_isDraggingToolBar = false;
}
private void OnToolBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(this).Properties;
if (properties.IsLeftButtonPressed)
{
ToolBar.Cursor = new Cursor(StandardCursorType.Hand);
_isDraggingToolBar = true;
// 计算拖动起始点
_startPoint = e.GetPosition(this);
}
}
private void TriggerTakePhoto()
{
var camera = CameraManager.GetCamera()!;
camera.TokePhoto();
if (camera.CapturingVideo)
{
camera.StartCaptureVideo();
}
}
}