| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- 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
- {
- /// <summary>
- /// 图像显示控件:支持拖放图片、鼠标缩放平移、定位十字、保存图像。
- /// 使用 Canvas + Image.Width/Height + Canvas.SetLeft/Top 实现缩放平移,
- /// 完全不使用 RenderTransform,避免 DPI/布局尺寸不匹配的裁剪问题。
- /// </summary>
- 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));
- /// <summary>
- /// 显示的图像源(BitmapSource)。支持数据绑定。
- /// </summary>
- public BitmapSource Source
- {
- get => (BitmapSource)GetValue(SourceProperty);
- set => SetValue(SourceProperty, value);
- }
- /// <summary>
- /// 是否显示定位十字。
- /// </summary>
- 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();
- }
- /// <summary>
- /// 设置 System.Drawing.Bitmap 图像。
- /// </summary>
- public void SetImage(Bitmap bitmap)
- {
- if (bitmap == null)
- {
- Clear();
- return;
- }
- var bmpSource = BitmapToBitmapSource(bitmap);
- SetCurrentValue(SourceProperty, bmpSource);
- }
- /// <summary>
- /// 设置 BitmapSource(线程安全,可从非UI线程调用)。
- /// </summary>
- public void SetImage(BitmapSource source)
- {
- if (source == null)
- {
- Clear();
- return;
- }
- if (!source.IsFrozen)
- source.Freeze();
- Dispatcher.BeginInvoke(new Action(() =>
- {
- SetCurrentValue(SourceProperty, source);
- }));
- }
- /// <summary>
- /// 清除图像。
- /// </summary>
- 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
- /// <summary>
- /// 核心方法:根据 _zoom 和 _offset 更新 Image 的 Width/Height 和 Canvas 位置。
- /// 所有尺寸计算基于像素,不涉及 DPI 转换。
- /// </summary>
- 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 = "";
- }
- /// <summary>
- /// 适应窗口大小。
- /// </summary>
- 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();
- }
- /// <summary>
- /// 实际大小(100%)。
- /// </summary>
- 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
- }
- }
|