297 lines
9.1 KiB
C#
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
|
|
}
|
|
}
|