using System; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media.Imaging; namespace TeamAAS.Camera.UI.Controls { /// /// 图像显示控件:支持拖放图片、鼠标缩放平移、定位十字、保存图像。 /// 使用 Canvas + Image.Width/Height + Canvas.SetLeft/Top 实现缩放平移, /// 完全不使用 RenderTransform,避免 DPI/布局尺寸不匹配的裁剪问题。 /// public partial class ImageDisplayControl : UserControl { #region Dependency Properties public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(BitmapSource), typeof(ImageDisplayControl), new PropertyMetadata(null, OnSourceChanged)); public static readonly DependencyProperty ShowCrosshairProperty = DependencyProperty.Register(nameof(ShowCrosshair), typeof(bool), typeof(ImageDisplayControl), new PropertyMetadata(false, OnShowCrosshairChanged)); /// /// 显示的图像源(BitmapSource)。支持数据绑定。 /// public BitmapSource Source { get => (BitmapSource)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } /// /// 是否显示定位十字。 /// public bool ShowCrosshair { get => (bool)GetValue(ShowCrosshairProperty); set => SetValue(ShowCrosshairProperty, value); } #endregion [DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr hObject); // 缩放与平移状态(全部基于像素,不涉及 DPI 转换) private double _zoom = 1.0; private double _offsetX = 0; private double _offsetY = 0; private bool _isDragging; private System.Windows.Point _dragStart; private double _startOffsetX; private double _startOffsetY; private bool _autoFit = true; // 图像像素尺寸(来自 BitmapSource.PixelWidth/Height) private int _imgPixelWidth = 0; private int _imgPixelHeight = 0; public ImageDisplayControl() { InitializeComponent(); SizeChanged += OnSizeChanged; } #region Source Management private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ctrl = (ImageDisplayControl)d; var newSource = e.NewValue as BitmapSource; var oldSource = e.OldValue as BitmapSource; ctrl.ImageControl.Source = newSource; ctrl.EmptyText.Visibility = newSource == null ? Visibility.Visible : Visibility.Collapsed; if (newSource != null) { ctrl._imgPixelWidth = newSource.PixelWidth; ctrl._imgPixelHeight = newSource.PixelHeight; // 仅在从 null → 非 null(第一帧)时自动适应 if (oldSource == null && ctrl._autoFit) { ctrl.Dispatcher.BeginInvoke(new Action(() => { if (ctrl._autoFit) ctrl.FitToWindow(); }), System.Windows.Threading.DispatcherPriority.Render); } else { // 后续帧:保持当前缩放和偏移,仅更新尺寸 ctrl.UpdateImageLayout(); } } } private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ctrl = (ImageDisplayControl)d; var show = (bool)e.NewValue; ctrl.OverlayCanvas.Visibility = show ? Visibility.Visible : Visibility.Collapsed; ctrl.CrosshairMenuItem.IsChecked = show; ctrl.UpdateCrosshair(); } /// /// 设置 System.Drawing.Bitmap 图像。 /// public void SetImage(Bitmap bitmap) { if (bitmap == null) { Clear(); return; } var bmpSource = BitmapToBitmapSource(bitmap); SetCurrentValue(SourceProperty, bmpSource); } /// /// 设置 BitmapSource(线程安全,可从非UI线程调用)。 /// public void SetImage(BitmapSource source) { if (source == null) { Clear(); return; } if (!source.IsFrozen) source.Freeze(); Dispatcher.BeginInvoke(new Action(() => { SetCurrentValue(SourceProperty, source); })); } /// /// 清除图像。 /// public void Clear() { SetCurrentValue(SourceProperty, null); ImageControl.Source = null; EmptyText.Visibility = Visibility.Visible; } private BitmapSource BitmapToBitmapSource(Bitmap bitmap) { var hBitmap = bitmap.GetHbitmap(); try { var bmpSource = Imaging.CreateBitmapSourceFromHBitmap( hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); bmpSource.Freeze(); return bmpSource; } finally { DeleteObject(hBitmap); bitmap.Dispose(); } } #endregion #region Zoom & Pan /// /// 核心方法:根据 _zoom 和 _offset 更新 Image 的 Width/Height 和 Canvas 位置。 /// 所有尺寸计算基于像素,不涉及 DPI 转换。 /// private void UpdateImageLayout() { if (_imgPixelWidth == 0 || _imgPixelHeight == 0) return; ImageControl.Width = _imgPixelWidth * _zoom; ImageControl.Height = _imgPixelHeight * _zoom; Canvas.SetLeft(ImageControl, _offsetX); Canvas.SetTop(ImageControl, _offsetY); UpdateZoomText(); } private void OnMouseWheel(object sender, MouseWheelEventArgs e) { if (ImageControl.Source == null) return; _autoFit = false; var position = e.GetPosition(MainGrid); var oldZoom = _zoom; if (e.Delta > 0) _zoom *= 1.2; else _zoom /= 1.2; _zoom = Math.Max(0.05, Math.Min(50, _zoom)); // 以鼠标位置为中心缩放 var ratio = _zoom / oldZoom; _offsetX = position.X - (position.X - _offsetX) * ratio; _offsetY = position.Y - (position.Y - _offsetY) * ratio; UpdateImageLayout(); } private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (ImageControl.Source == null) return; // 双击适应窗口 if (e.ClickCount == 2) { FitToWindow(); return; } _isDragging = true; _dragStart = e.GetPosition(MainGrid); _startOffsetX = _offsetX; _startOffsetY = _offsetY; MainGrid.CaptureMouse(); } private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _isDragging = false; MainGrid.ReleaseMouseCapture(); } private void OnMouseMove(object sender, MouseEventArgs e) { var pos = e.GetPosition(MainGrid); // 更新坐标显示(直接像素坐标,无需 DPI 转换) if (ImageControl.Source != null && _imgPixelWidth > 0) { var imgX = (int)((pos.X - _offsetX) / _zoom); var imgY = (int)((pos.Y - _offsetY) / _zoom); if (imgX >= 0 && imgX < _imgPixelWidth && imgY >= 0 && imgY < _imgPixelHeight) { CoordText.Text = $"X: {imgX}, Y: {imgY}"; } else { CoordText.Text = ""; } } // 拖拽平移 if (_isDragging) { var delta = pos - _dragStart; _offsetX = _startOffsetX + delta.X; _offsetY = _startOffsetY + delta.Y; UpdateImageLayout(); } } private void OnMouseLeave(object sender, MouseEventArgs e) { _isDragging = false; CoordText.Text = ""; } /// /// 适应窗口大小。 /// public void FitToWindow() { if (_imgPixelWidth == 0 || _imgPixelHeight == 0) return; if (MainGrid.ActualWidth == 0 || MainGrid.ActualHeight == 0) { _autoFit = true; return; } var scaleX = MainGrid.ActualWidth / _imgPixelWidth; var scaleY = MainGrid.ActualHeight / _imgPixelHeight; _zoom = Math.Min(scaleX, scaleY); if (_zoom <= 0) _zoom = 1; // 居中 _offsetX = (MainGrid.ActualWidth - _imgPixelWidth * _zoom) / 2; _offsetY = (MainGrid.ActualHeight - _imgPixelHeight * _zoom) / 2; UpdateImageLayout(); } /// /// 实际大小(100%)。 /// public void ActualSize() { _zoom = 1.0; _offsetX = (MainGrid.ActualWidth - _imgPixelWidth) / 2; _offsetY = (MainGrid.ActualHeight - _imgPixelHeight) / 2; _autoFit = false; UpdateImageLayout(); } private void UpdateZoomText() { if (ImageControl.Source != null) { ZoomText.Text = $"{_zoom * 100:F0}% | GW:{MainGrid.ActualWidth:F0} GH:{MainGrid.ActualHeight:F0} PW:{_imgPixelWidth} PH:{_imgPixelHeight} IW:{ImageControl.Width:F0} OX:{_offsetX:F0} OY:{_offsetY:F0}"; } else { ZoomText.Text = $"{_zoom * 100:F0}%"; } } #endregion #region Crosshair private void UpdateCrosshair() { if (!ShowCrosshair) return; var cx = MainGrid.ActualWidth / 2; var cy = MainGrid.ActualHeight / 2; CrossLineH.X1 = 0; CrossLineH.Y1 = cy; CrossLineH.X2 = MainGrid.ActualWidth; CrossLineH.Y2 = cy; CrossLineV.X1 = cx; CrossLineV.Y1 = 0; CrossLineV.X2 = cx; CrossLineV.Y2 = MainGrid.ActualHeight; } private void OnSizeChanged(object sender, SizeChangedEventArgs e) { UpdateCrosshair(); if (_autoFit && ImageControl.Source != null) { FitToWindow(); } } #endregion #region Drag & Drop private void OnDragOver(object sender, DragEventArgs e) { e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None; e.Handled = true; } private void OnDrop(object sender, DragEventArgs e) { if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return; var files = (string[])e.Data.GetData(DataFormats.FileDrop); if (files == null || files.Length == 0) return; try { var bmp = new BitmapImage(); bmp.BeginInit(); bmp.UriSource = new Uri(files[0]); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.EndInit(); bmp.Freeze(); SetCurrentValue(SourceProperty, bmp); _autoFit = true; FitToWindow(); } catch (Exception ex) { MessageBox.Show($"无法加载图片: {ex.Message}", "图像显示", MessageBoxButton.OK, MessageBoxImage.Warning); } } #endregion #region Context Menu Actions private void MenuToggleCrosshair_Click(object sender, RoutedEventArgs e) { ShowCrosshair = CrosshairMenuItem.IsChecked; } private void MenuSaveImage_Click(object sender, RoutedEventArgs e) { if (ImageControl.Source == null) { MessageBox.Show("没有图像可保存", "图像显示"); return; } var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "PNG 图像|*.png|JPEG 图像|*.jpg|BMP 图像|*.bmp", DefaultExt = ".png", FileName = $"image_{DateTime.Now:yyyyMMdd_HHmmss}" }; if (dlg.ShowDialog() != true) return; try { BitmapEncoder encoder = Path.GetExtension(dlg.FileName).ToLower() switch { ".jpg" or ".jpeg" => new JpegBitmapEncoder(), ".bmp" => new BmpBitmapEncoder(), _ => new PngBitmapEncoder() }; encoder.Frames.Add(BitmapFrame.Create((BitmapSource)ImageControl.Source)); using var fs = new FileStream(dlg.FileName, FileMode.Create); encoder.Save(fs); } catch (Exception ex) { MessageBox.Show($"保存失败: {ex.Message}", "图像显示", MessageBoxButton.OK, MessageBoxImage.Error); } } private void MenuFitWindow_Click(object sender, RoutedEventArgs e) { _autoFit = true; FitToWindow(); } private void MenuActualSize_Click(object sender, RoutedEventArgs e) { ActualSize(); } #endregion } }