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
}
}