Files
XplorePlane/XplorePlane/Controls/ZoomableImageViewer.cs
T
2026-05-12 20:09:13 +08:00

297 lines
9.1 KiB
C#

// Feature: cnc-inspection-report-viewer
// Task 6: Implement ZoomableImageViewer custom control
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Prism.Commands;
namespace XplorePlane.Controls
{
/// <summary>
/// 可缩放、平移的图像查看器控件。
/// 支持鼠标滚轮缩放(25%-400%)、拖拽平移、双击全屏。
/// </summary>
public class ZoomableImageViewer : Control
{
private const double MinScale = 0.25;
private const double MaxScale = 4.0;
private const double ScaleStep = 0.1;
private ScaleTransform _scaleTransform;
private TranslateTransform _translateTransform;
private Point _lastMousePosition;
private bool _isDragging;
private Image _imageElement;
private Border _containerBorder;
static ZoomableImageViewer()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(ZoomableImageViewer),
new FrameworkPropertyMetadata(typeof(ZoomableImageViewer)));
}
public ZoomableImageViewer()
{
_scaleTransform = new ScaleTransform(1.0, 1.0);
_translateTransform = new TranslateTransform(0, 0);
FitToViewCommand = new DelegateCommand(ExecuteFitToView);
Loaded += OnLoaded;
}
#region Dependency Properties
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(
nameof(Source),
typeof(BitmapSource),
typeof(ZoomableImageViewer),
new PropertyMetadata(null, OnSourceChanged));
public BitmapSource Source
{
get => (BitmapSource)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public static readonly DependencyProperty FallbackTextProperty =
DependencyProperty.Register(
nameof(FallbackText),
typeof(string),
typeof(ZoomableImageViewer),
new PropertyMetadata("图像不可用"));
public string FallbackText
{
get => (string)GetValue(FallbackTextProperty);
set => SetValue(FallbackTextProperty, value);
}
#endregion
#region Routed Events
public static readonly RoutedEvent FullScreenRequestedEvent =
EventManager.RegisterRoutedEvent(
nameof(FullScreenRequested),
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ZoomableImageViewer));
public event RoutedEventHandler FullScreenRequested
{
add => AddHandler(FullScreenRequestedEvent, value);
remove => RemoveHandler(FullScreenRequestedEvent, value);
}
#endregion
#region Commands
public ICommand FitToViewCommand { get; }
#endregion
#region Overrides
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_imageElement = GetTemplateChild("PART_Image") as Image;
_containerBorder = GetTemplateChild("PART_Container") as Border;
if (_imageElement != null)
{
var transformGroup = new TransformGroup();
transformGroup.Children.Add(_scaleTransform);
transformGroup.Children.Add(_translateTransform);
_imageElement.RenderTransform = transformGroup;
_imageElement.RenderTransformOrigin = new Point(0.5, 0.5);
}
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
base.OnMouseWheel(e);
if (_imageElement == null || Source == null)
return;
// Calculate new scale
double delta = e.Delta > 0 ? ScaleStep : -ScaleStep;
double newScale = _scaleTransform.ScaleX + delta;
// Clamp to boundaries
if (newScale < MinScale || newScale > MaxScale)
{
// At boundary, no effect
e.Handled = true;
return;
}
newScale = Math.Max(MinScale, Math.Min(MaxScale, newScale));
// Get mouse position relative to image
Point mousePos = e.GetPosition(_imageElement);
// Calculate zoom center offset
double offsetX = mousePos.X * (_scaleTransform.ScaleX - newScale);
double offsetY = mousePos.Y * (_scaleTransform.ScaleY - newScale);
// Apply new scale
_scaleTransform.ScaleX = newScale;
_scaleTransform.ScaleY = newScale;
// Adjust translation to zoom at mouse position
_translateTransform.X += offsetX;
_translateTransform.Y += offsetY;
// Clamp translation
ClampTranslation();
e.Handled = true;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (_scaleTransform.ScaleX > 1.0 && Source != null)
{
_lastMousePosition = e.GetPosition(this);
_isDragging = true;
CaptureMouse();
e.Handled = true;
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_isDragging && e.LeftButton == MouseButtonState.Pressed)
{
Point currentPosition = e.GetPosition(this);
Vector delta = currentPosition - _lastMousePosition;
_translateTransform.X += delta.X;
_translateTransform.Y += delta.Y;
ClampTranslation();
_lastMousePosition = currentPosition;
e.Handled = true;
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (_isDragging)
{
_isDragging = false;
ReleaseMouseCapture();
e.Handled = true;
}
}
protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
{
base.OnMouseDoubleClick(e);
if (Source != null)
{
RaiseEvent(new RoutedEventArgs(FullScreenRequestedEvent, this));
e.Handled = true;
}
}
#endregion
#region Private Methods
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (Source != null)
{
ExecuteFitToView();
}
}
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var viewer = (ZoomableImageViewer)d;
if (viewer.IsLoaded && e.NewValue != null)
{
viewer.ExecuteFitToView();
}
}
private void ExecuteFitToView()
{
if (_imageElement == null || _containerBorder == null || Source == null)
return;
double containerWidth = _containerBorder.ActualWidth;
double containerHeight = _containerBorder.ActualHeight;
if (containerWidth <= 0 || containerHeight <= 0)
return;
double imageWidth = Source.PixelWidth;
double imageHeight = Source.PixelHeight;
if (imageWidth <= 0 || imageHeight <= 0)
return;
// Calculate scale to fit
double scaleX = containerWidth / imageWidth;
double scaleY = containerHeight / imageHeight;
double scale = Math.Min(scaleX, scaleY);
// Clamp to min/max
scale = Math.Max(MinScale, Math.Min(MaxScale, scale));
_scaleTransform.ScaleX = scale;
_scaleTransform.ScaleY = scale;
// Center the image
_translateTransform.X = 0;
_translateTransform.Y = 0;
}
private void ClampTranslation()
{
if (_imageElement == null || _containerBorder == null || Source == null)
return;
double containerWidth = _containerBorder.ActualWidth;
double containerHeight = _containerBorder.ActualHeight;
if (containerWidth <= 0 || containerHeight <= 0)
return;
double imageWidth = Source.PixelWidth * _scaleTransform.ScaleX;
double imageHeight = Source.PixelHeight * _scaleTransform.ScaleY;
// Calculate bounds
double maxOffsetX = Math.Max(0, (imageWidth - containerWidth) / 2);
double maxOffsetY = Math.Max(0, (imageHeight - containerHeight) / 2);
// Clamp translation so image edges don't exceed control bounds
_translateTransform.X = Math.Max(-maxOffsetX, Math.Min(maxOffsetX, _translateTransform.X));
_translateTransform.Y = Math.Max(-maxOffsetY, Math.Min(maxOffsetY, _translateTransform.Y));
}
#endregion
}
}