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 SourceProperty = AvaloniaProperty.Register(nameof(Source)); public static readonly StyledProperty ShowToolBarProperty = AvaloniaProperty.Register(nameof(ShowToolBar), true); public static readonly StyledProperty ShowingCrosshairProperty = AvaloniaProperty.Register(nameof(ShowingCrosshair)); public static readonly StyledProperty TuningStepProperty = AvaloniaProperty.Register(nameof(TuningStep), 1.0); public static readonly StyledProperty ScaleRelativeCenterProperty = AvaloniaProperty.Register(nameof(ScaleRelativeCenter)); public static readonly StyledProperty ScaleRatioProperty = AvaloniaProperty.Register(nameof(ScaleRatio), 1); public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch)); public static readonly StyledProperty SlotContentProperty = AvaloniaProperty.Register(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 PixelChanged; public event EventHandler CenterPixelChanged; public event EventHandler 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("Viewer"); ImageContainer = this.FindControl("ImageContainer"); ToolBar = this.FindControl("ToolBar"); ImageCanvas = this.FindControl("ImageCanvas"); SlotViewer = this.FindControl("SlotViewer"); PixelX = this.FindControl("PixelX"); PixelY = this.FindControl("PixelY"); ImageWidth = this.FindControl("ImageWidth"); ImageHeight = this.FindControl("ImageHeight"); ScaleRatioText = this.FindControl("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); /// /// 处理源图片变化事件 /// /// 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); } } /// /// 去缩放图片 /// /// /// /// 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); } /// /// 鼠标按下事件 /// private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { var properties = e.GetCurrentPoint(this).Properties; if (properties.IsLeftButtonPressed) { _isDragging = true; // 计算拖动起始点 _startPoint = e.GetPosition(this); } } /// /// 鼠标释放事件 /// private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { _isDragging = false; Viewer.Cursor = new Cursor(StandardCursorType.Hand); } /// /// 鼠标移动事件 /// 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; } /// /// 通过双击图片是还原图像大小和位置 /// private void OnDoubleTapped(object? sender, TappedEventArgs e) { ResetSize(); } /// /// 重置大小 /// 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(); }); } /// /// 图像缩小 /// 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; } /// /// 图像放大 /// 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; } } /// /// 步长变更 /// /// /// private void OnStepChanged(object? sender, NumericUpDownValueChangedEventArgs e) { if (sender is NumericUpDown step) { TuningStep = (double)step.Value.GetValueOrDefault(); } } /// /// 拖拽十字线 /// /// /// 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(); } } /// /// 拖拽十字线事件 /// /// /// private void OnCrosshairPointerReleased(object? sender, PointerReleasedEventArgs e) { ImageCanvas.Cursor = new Cursor(StandardCursorType.Arrow); _isDraggingCanvas = false; } /// /// 拖拽十字线事件 /// /// /// private void OnCrosshairPointerPressed(object? sender, PointerPressedEventArgs e) { var properties = e.GetCurrentPoint(ImageCanvas).Properties; if (properties.IsLeftButtonPressed) { ImageCanvas.Cursor = new Cursor(StandardCursorType.Hand); _isDraggingCanvas = true; } } /// /// 渲染十字线 /// 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); } /// /// 获取图像的真实像素坐标 /// /// /// 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(); } } }