Image panoramique et zoom

Je veux créer un visualiseur d’images simple dans WPF qui permettra à l’utilisateur de:

  • Pan (en faisant glisser la souris sur l’image).
  • Zoom (avec un curseur).
  • Afficher les superpositions (sélection de rectangle par exemple).
  • Afficher l’image originale (avec des barres de défilement si nécessaire).

Pouvez-vous expliquer comment le faire?

Je n’ai pas trouvé un bon échantillon sur le web. Dois-je utiliser ViewBox? Ou ImageBrush? Ai-je besoin de ScrollViewer?

Merci!

La façon dont j’ai résolu ce problème consistait à placer l’image dans une bordure avec la propriété ClipToBounds définie sur True. Le RenderTransformOrigin sur l’image est alors défini sur 0.5.0.5 pour que l’image commence à zoomer sur le centre de l’image. RenderTransform est également défini sur un TransformGroup contenant un ScaleTransform et un TranslateTransform.

J’ai ensuite géré l’événement MouseWheel sur l’image pour implémenter le zoom

private void image_MouseWheel(object sender, MouseWheelEventArgs e) { var st = (ScaleTransform)image.RenderTransform; double zoom = e.Delta > 0 ? .2 : -.2; st.ScaleX += zoom; st.ScaleY += zoom; } 

Pour gérer le panoramique, la première chose que j’ai faite a été de gérer l’événement MouseLeftButtonDown sur l’image, de capturer la souris et d’enregistrer son emplacement. Je stocke également la valeur actuelle de TranslateTransform.

 Point start; Point origin; private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { image.CaptureMouse(); var tt = (TranslateTransform)((TransformGroup)image.RenderTransform) .Children.First(tr => tr is TranslateTransform); start = e.GetPosition(border); origin = new Point(tt.X, tt.Y); } 

Ensuite, j’ai géré l’événement MouseMove pour mettre à jour le TranslateTransform.

 private void image_MouseMove(object sender, MouseEventArgs e) { if (image.IsMouseCaptured) { var tt = (TranslateTransform)((TransformGroup)image.RenderTransform) .Children.First(tr => tr is TranslateTransform); Vector v = start - e.GetPosition(border); tt.X = origin.X - vX; tt.Y = origin.Y - vY; } } 

Enfin, n’oubliez pas de libérer la capture de la souris.

 private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { image.ReleaseMouseCapture(); } 

En ce qui concerne les poignées de sélection pour le redimensionnement, vous pouvez utiliser un adorner, consultez cet article pour plus d’informations.

Après avoir utilisé des échantillons de cette question, j’ai créé une version complète de l’application Pan & Zoom avec un zoom approprié par rapport au pointeur de la souris. Tout le code de panoramique et zoom a été déplacé dans une classe distincte appelée ZoomBorder.

ZoomBorder.cs

 using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace PanAndZoom { public class ZoomBorder : Border { private UIElement child = null; private Point origin; private Point start; private TranslateTransform GetTranslateTransform(UIElement element) { return (TranslateTransform)((TransformGroup)element.RenderTransform) .Children.First(tr => tr is TranslateTransform); } private ScaleTransform GetScaleTransform(UIElement element) { return (ScaleTransform)((TransformGroup)element.RenderTransform) .Children.First(tr => tr is ScaleTransform); } public override UIElement Child { get { return base.Child; } set { if (value != null && value != this.Child) this.Initialize(value); base.Child = value; } } public void Initialize(UIElement element) { this.child = element; if (child != null) { TransformGroup group = new TransformGroup(); ScaleTransform st = new ScaleTransform(); group.Children.Add(st); TranslateTransform tt = new TranslateTransform(); group.Children.Add(tt); child.RenderTransform = group; child.RenderTransformOrigin = new Point(0.0, 0.0); this.MouseWheel += child_MouseWheel; this.MouseLeftButtonDown += child_MouseLeftButtonDown; this.MouseLeftButtonUp += child_MouseLeftButtonUp; this.MouseMove += child_MouseMove; this.PreviewMouseRightButtonDown += new MouseButtonEventHandler( child_PreviewMouseRightButtonDown); } } public void Reset() { if (child != null) { // reset zoom var st = GetScaleTransform(child); st.ScaleX = 1.0; st.ScaleY = 1.0; // reset pan var tt = GetTranslateTransform(child); tt.X = 0.0; tt.Y = 0.0; } } #region Child Events private void child_MouseWheel(object sender, MouseWheelEventArgs e) { if (child != null) { var st = GetScaleTransform(child); var tt = GetTranslateTransform(child); double zoom = e.Delta > 0 ? .2 : -.2; if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4)) return; Point relative = e.GetPosition(child); double abosuluteX; double abosuluteY; abosuluteX = relative.X * st.ScaleX + tt.X; abosuluteY = relative.Y * st.ScaleY + tt.Y; st.ScaleX += zoom; st.ScaleY += zoom; tt.X = abosuluteX - relative.X * st.ScaleX; tt.Y = abosuluteY - relative.Y * st.ScaleY; } } private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (child != null) { var tt = GetTranslateTransform(child); start = e.GetPosition(this); origin = new Point(tt.X, tt.Y); this.Cursor = Cursors.Hand; child.CaptureMouse(); } } private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (child != null) { child.ReleaseMouseCapture(); this.Cursor = Cursors.Arrow; } } void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e) { this.Reset(); } private void child_MouseMove(object sender, MouseEventArgs e) { if (child != null) { if (child.IsMouseCaptured) { var tt = GetTranslateTransform(child); Vector v = start - e.GetPosition(this); tt.X = origin.X - vX; tt.Y = origin.Y - vY; } } } #endregion } } 

MainWindow.xaml

        

MainWindow.xaml.cs

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace PanAndZoom { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } } 

La réponse a été postée ci-dessus mais n’était pas complète. voici la version complétée:

XAML

           

Code Derrière

 using System.Linq; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace MapTest { public partial class Window1 : Window { private Point origin; private Point start; public Window1() { InitializeComponent(); TransformGroup group = new TransformGroup(); ScaleTransform xform = new ScaleTransform(); group.Children.Add(xform); TranslateTransform tt = new TranslateTransform(); group.Children.Add(tt); image.RenderTransform = group; image.MouseWheel += image_MouseWheel; image.MouseLeftButtonDown += image_MouseLeftButtonDown; image.MouseLeftButtonUp += image_MouseLeftButtonUp; image.MouseMove += image_MouseMove; } private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { image.ReleaseMouseCapture(); } private void image_MouseMove(object sender, MouseEventArgs e) { if (!image.IsMouseCaptured) return; var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); Vector v = start - e.GetPosition(border); tt.X = origin.X - vX; tt.Y = origin.Y - vY; } private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { image.CaptureMouse(); var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); start = e.GetPosition(border); origin = new Point(tt.X, tt.Y); } private void image_MouseWheel(object sender, MouseWheelEventArgs e) { TransformGroup transformGroup = (TransformGroup) image.RenderTransform; ScaleTransform transform = (ScaleTransform) transformGroup.Children[0]; double zoom = e.Delta > 0 ? .2 : -.2; transform.ScaleX += zoom; transform.ScaleY += zoom; } } } 

J’ai un exemple de projet wpf complet utilisant ce code sur mon site Web:

Essayez ce contrôle de zoom: http://wpfextensions.codeplex.com

l’utilisation du contrôle est très simple, référence à l’assembly wpfextensions que:

    

Les barres de défilement ne sont pas sockets en charge pour le moment. (Ce sera dans la prochaine version qui sera disponible dans une ou deux semaines).

  • Pan: Placez l’image à l’intérieur d’un canevas. Implémentez les événements Mouse Up, Down et Move pour déplacer les propriétés Canvas.Top, Canvas.Left. Lorsque vous êtes en bas, vous marquez isDraggingFlag à true, lorsque vous placez l’indicateur sur false. En déplacement, vous vérifiez si l’indicateur est défini, si c’est vous qui décale les propriétés Canvas.Top et Canvas.Left sur l’image dans le canevas.
  • Zoom: liez le curseur à la transformation d’échelle du canevas
  • Afficher les superpositions: ajoutez des zones de dessin supplémentaires sans arrière-plan sur la zone contenant l’image.
  • montrer l’image originale: contrôle de l’image à l’intérieur d’un ViewBox

@Anothen and @ Number8 – La classe Vector n’est pas disponible dans Silverlight. Pour que cela fonctionne, il suffit de garder une trace de la dernière position détectée la dernière fois que l’événement MouseMove a été appelé et de comparer les deux points pour trouver la différence. ; puis ajustez la transformation.

XAML:

       

Code-behind:

  public Point _mouseClickPos; public bool bMoving; public MainPage() { InitializeComponent(); viewboxMain.RenderTransform = new CompositeTransform(); } void MouseMoveHandler(object sender, MouseEventArgs e) { if (bMoving) { //get current transform CompositeTransform transform = viewboxMain.RenderTransform as CompositeTransform; Point currentPos = e.GetPosition(viewboxBackground); transform.TranslateX += (currentPos.X - _mouseClickPos.X) ; transform.TranslateY += (currentPos.Y - _mouseClickPos.Y) ; viewboxMain.RenderTransform = transform; _mouseClickPos = currentPos; } } void MouseClickHandler(object sender, MouseButtonEventArgs e) { _mouseClickPos = e.GetPosition(viewboxBackground); bMoving = true; } void MouseReleaseHandler(object sender, MouseButtonEventArgs e) { bMoving = false; } 

Notez également que vous n’avez pas besoin d’un TransformGroup ou d’une collection pour implémenter pan et zoom; Au lieu de cela, un CompositeTransform fera l’affaire avec moins de tracas.

Je suis sûr que c’est vraiment inefficace en termes d’utilisation des ressources, mais au moins ça marche 🙂

Pour zoomer par rapport à la position de la souris, il vous suffit de:

 var position = e.GetPosition(image1); image1.RenderTransformOrigin = new Point(position.X / image1.ActualWidth, position.Y / image1.ActualHeight); 

@ Merk

Pour la solution ur insted de l’expression lambda, vous pouvez utiliser le code suivant:

 //var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform); TranslateTransform tt = null; TransformGroup transformGroup = (TransformGroup)grid.RenderTransform; for (int i = 0; i < transformGroup.Children.Count; i++) { if (transformGroup.Children[i] is TranslateTransform) tt = (TranslateTransform)transformGroup.Children[i]; } 

ce code peut être utilisé tel quel pour le travail de .Net Frame 3.0 ou 2.0

J'espère que ça vous aide 🙂

Encore une autre version du même type de contrôle. Il a des fonctionnalités similaires aux autres, mais il ajoute:

  1. Support tactile (glisser / pincer)
  2. L’image peut être supprimée (normalement, le contrôle Image verrouille l’image sur le disque, vous ne pouvez donc pas le supprimer).
  3. Un enfant de bordure interne, de sorte que l’image déphasée ne chevauche pas la bordure. Dans le cas de bordures avec des rectangles arrondis, recherchez les classes ClippedBorder.

L’utilisation est simple:

  

Et le code:

 public class ImageViewControl : Border { private Point origin; private Point start; private Image image; public ImageViewControl() { ClipToBounds = true; Loaded += OnLoaded; } #region ImagePath ///  /// ImagePath Dependency Property ///  public static readonly DependencyProperty ImagePathProperty = DependencyProperty.Register("ImagePath", typeof (ssortingng), typeof (ImageViewControl), new FrameworkPropertyMetadata(ssortingng.Empty, OnImagePathChanged)); ///  /// Gets or sets the ImagePath property. This dependency property /// indicates the path to the image file. ///  public ssortingng ImagePath { get { return (ssortingng) GetValue(ImagePathProperty); } set { SetValue(ImagePathProperty, value); } } ///  /// Handles changes to the ImagePath property. ///  private static void OnImagePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var target = (ImageViewControl) d; var oldImagePath = (ssortingng) e.OldValue; var newImagePath = target.ImagePath; target.ReloadImage(newImagePath); target.OnImagePathChanged(oldImagePath, newImagePath); } ///  /// Provides derived classes an opportunity to handle changes to the ImagePath property. ///  protected virtual void OnImagePathChanged(ssortingng oldImagePath, ssortingng newImagePath) { } #endregion private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { image = new Image { //IsManipulationEnabled = true, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new TransformGroup { Children = new TransformCollection { new ScaleTransform(), new TranslateTransform() } } }; // NOTE I use a border as the first child, to which I add the image. I do this so the panned image doesn't partly obscure the control's border. // In case you are going to use rounder corner's on this control, you may to update your clipping, as in this example: // http://wpfspark.wordpress.com/2011/06/08/clipborder-a-wpf-border-that-clips/ var border = new Border { IsManipulationEnabled = true, ClipToBounds = true, Child = image }; Child = border; image.MouseWheel += (s, e) => { var zoom = e.Delta > 0 ? .2 : -.2; var position = e.GetPosition(image); image.RenderTransformOrigin = new Point(position.X / image.ActualWidth, position.Y / image.ActualHeight); var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform); st.ScaleX += zoom; st.ScaleY += zoom; e.Handled = true; }; image.MouseLeftButtonDown += (s, e) => { if (e.ClickCount == 2) ResetPanZoom(); else { image.CaptureMouse(); var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); start = e.GetPosition(this); origin = new Point(tt.X, tt.Y); } e.Handled = true; }; image.MouseMove += (s, e) => { if (!image.IsMouseCaptured) return; var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform); var v = start - e.GetPosition(this); tt.X = origin.X - vX; tt.Y = origin.Y - vY; e.Handled = true; }; image.MouseLeftButtonUp += (s, e) => image.ReleaseMouseCapture(); //NOTE I apply the manipulation to the border, and not to the image itself (which caused stability issues when translating)! border.ManipulationDelta += (o, e) => { var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform); var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform); st.ScaleX *= e.DeltaManipulation.Scale.X; st.ScaleY *= e.DeltaManipulation.Scale.X; tt.X += e.DeltaManipulation.Translation.X; tt.Y += e.DeltaManipulation.Translation.Y; e.Handled = true; }; } private void ResetPanZoom() { var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform); var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform); st.ScaleX = st.ScaleY = 1; tt.X = tt.Y = 0; image.RenderTransformOrigin = new Point(0.5, 0.5); } ///  /// Load the image (and do not keep a hold on it, so we can delete the image without problems) ///  ///  ///  private void ReloadImage(ssortingng path) { try { ResetPanZoom(); // load the image, specify CacheOption so the file is not locked var bitmapImage = new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.UriSource = new Uri(path, UriKind.RelativeOrAbsolute); bitmapImage.EndInit(); image.Source = bitmapImage; } catch (SystemException e) { Console.WriteLine(e.Message); } } } 

Pour obtenir un contrôle de zoom professionnel pour WPF, consultez ZoomPanel .

Il n’est pas gratuit, mais il est très facile à utiliser et possède de nombreuses fonctionnalités: zoom et panoramique animés, prise en charge de ScrollViewer, support de la molette de la souris, ZoomController inclus (déplacer, zoomer, dézoomer, zoomer, réinitialiser). Il est également livré avec de nombreux exemples de code.

Mon échantillon London Underground le fait, bien qu’avec une carte plutôt qu’une image statique.

Cela fera un zoom avant et arrière, mais conservera l’image dans les limites du conteneur. Écrit comme un contrôle, ajoutez le style directement à App.xaml ou via Themes/Viewport.xaml .

Pour plus de lisibilité, j’ai également téléchargé ce fichier sur gist et github

Je l’ai aussi emballé sur nuget

 PM > Install-Package Han.Wpf.ViewportControl 

./Controls/Viewport.cs:

 public class Viewport : ContentControl { private bool _capture; private FrameworkElement _content; private Masortingx _masortingx; private Point _origin; public static readonly DependencyProperty MaxZoomProperty = DependencyProperty.Register( nameof(MaxZoom), typeof(double), typeof(Viewport), new PropertyMetadata(0d)); public static readonly DependencyProperty MinZoomProperty = DependencyProperty.Register( nameof(MinZoom), typeof(double), typeof(Viewport), new PropertyMetadata(0d)); public static readonly DependencyProperty ZoomSpeedProperty = DependencyProperty.Register( nameof(ZoomSpeed), typeof(float), typeof(Viewport), new PropertyMetadata(0f)); public static readonly DependencyProperty ZoomXProperty = DependencyProperty.Register( nameof(ZoomX), typeof(double), typeof(Viewport), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty ZoomYProperty = DependencyProperty.Register( nameof(ZoomY), typeof(double), typeof(Viewport), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty OffsetXProperty = DependencyProperty.Register( nameof(OffsetX), typeof(double), typeof(Viewport), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty OffsetYProperty = DependencyProperty.Register( nameof(OffsetY), typeof(double), typeof(Viewport), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty BoundsProperty = DependencyProperty.Register( nameof(Bounds), typeof(Rect), typeof(Viewport), new FrameworkPropertyMetadata(default(Rect), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public Rect Bounds { get => (Rect) GetValue(BoundsProperty); set => SetValue(BoundsProperty, value); } public double MaxZoom { get => (double) GetValue(MaxZoomProperty); set => SetValue(MaxZoomProperty, value); } public double MinZoom { get => (double) GetValue(MinZoomProperty); set => SetValue(MinZoomProperty, value); } public double OffsetX { get => (double) GetValue(OffsetXProperty); set => SetValue(OffsetXProperty, value); } public double OffsetY { get => (double) GetValue(OffsetYProperty); set => SetValue(OffsetYProperty, value); } public float ZoomSpeed { get => (float) GetValue(ZoomSpeedProperty); set => SetValue(ZoomSpeedProperty, value); } public double ZoomX { get => (double) GetValue(ZoomXProperty); set => SetValue(ZoomXProperty, value); } public double ZoomY { get => (double) GetValue(ZoomYProperty); set => SetValue(ZoomYProperty, value); } public Viewport() { DefaultStyleKey = typeof(Viewport); Loaded += OnLoaded; Unloaded += OnUnloaded; } private void Arrange(Size desired, Size render) { _masortingx = Masortingx.Identity; var zx = desired.Width / render.Width; var zy = desired.Height / render.Height; var cx = render.Width < desired.Width ? render.Width / 2.0 : 0.0; var cy = render.Height < desired.Height ? render.Height / 2.0 : 0.0; var zoom = Math.Min(zx, zy); if (render.Width > desired.Width && render.Height > desired.Height) { cx = (desired.Width - (render.Width * zoom)) / 2.0; cy = (desired.Height - (render.Height * zoom)) / 2.0; _masortingx = new Masortingx(zoom, 0d, 0d, zoom, cx, cy); } else { _masortingx.ScaleAt(zoom, zoom, cx, cy); } } private void Attach(FrameworkElement content) { content.MouseMove += OnMouseMove; content.MouseLeave += OnMouseLeave; content.MouseWheel += OnMouseWheel; content.MouseLeftButtonDown += OnMouseLeftButtonDown; content.MouseLeftButtonUp += OnMouseLeftButtonUp; content.SizeChanged += OnSizeChanged; content.MouseRightButtonDown += OnMouseRightButtonDown; } private void ChangeContent(FrameworkElement content) { if (content != null && !Equals(content, _content)) { if (_content != null) { Detatch(); } Attach(content); _content = content; } } private double Constrain(double value, double min, double max) { if (min > max) { min = max; } if (value <= min) { return min; } if (value >= max) { return max; } return value; } private void Constrain() { var x = Constrain(_masortingx.OffsetX, _content.ActualWidth - _content.ActualWidth * _masortingx.M11, 0); var y = Constrain(_masortingx.OffsetY, _content.ActualHeight - _content.ActualHeight * _masortingx.M22, 0); _masortingx = new Masortingx(_masortingx.M11, 0d, 0d, _masortingx.M22, x, y); } private void Detatch() { _content.MouseMove -= OnMouseMove; _content.MouseLeave -= OnMouseLeave; _content.MouseWheel -= OnMouseWheel; _content.MouseLeftButtonDown -= OnMouseLeftButtonDown; _content.MouseLeftButtonUp -= OnMouseLeftButtonUp; _content.SizeChanged -= OnSizeChanged; _content.MouseRightButtonDown -= OnMouseRightButtonDown; } private void Invalidate() { if (_content != null) { Constrain(); _content.RenderTransformOrigin = new Point(0, 0); _content.RenderTransform = new MasortingxTransform(_masortingx); _content.InvalidateVisual(); ZoomX = _masortingx.M11; ZoomY = _masortingx.M22; OffsetX = _masortingx.OffsetX; OffsetY = _masortingx.OffsetY; var rect = new Rect { X = OffsetX * -1, Y = OffsetY * -1, Width = ActualWidth, Height = ActualHeight }; Bounds = rect; } } public override void OnApplyTemplate() { base.OnApplyTemplate(); _masortingx = Masortingx.Identity; } protected override void OnContentChanged(object oldContent, object newContent) { base.OnContentChanged(oldContent, newContent); if (Content is FrameworkElement element) { ChangeContent(element); } } private void OnLoaded(object sender, RoutedEventArgs e) { if (Content is FrameworkElement element) { ChangeContent(element); } SizeChanged += OnSizeChanged; Loaded -= OnLoaded; } private void OnMouseLeave(object sender, MouseEventArgs e) { if (_capture) { Released(); } } private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (IsEnabled && !_capture) { Pressed(e.GetPosition(this)); } } private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (IsEnabled && _capture) { Released(); } } private void OnMouseMove(object sender, MouseEventArgs e) { if (IsEnabled && _capture) { var position = e.GetPosition(this); var point = new Point { X = position.X - _origin.X, Y = position.Y - _origin.Y }; var delta = point; _origin = position; _masortingx.Translate(delta.X, delta.Y); Invalidate(); } } private void OnMouseRightButtonDown(object sender, MouseButtonEventArgs e) { if (IsEnabled) { Reset(); } } private void OnMouseWheel(object sender, MouseWheelEventArgs e) { if (IsEnabled) { var scale = e.Delta > 0 ? ZoomSpeed : 1 / ZoomSpeed; var position = e.GetPosition(_content); var x = Constrain(scale, MinZoom / _masortingx.M11, MaxZoom / _masortingx.M11); var y = Constrain(scale, MinZoom / _masortingx.M22, MaxZoom / _masortingx.M22); _masortingx.ScaleAtPrepend(x, y, position.X, position.Y); ZoomX = _masortingx.M11; ZoomY = _masortingx.M22; Invalidate(); } } private void OnSizeChanged(object sender, SizeChangedEventArgs e) { if (_content?.IsMeasureValid ?? false) { Arrange(_content.DesiredSize, _content.RenderSize); Invalidate(); } } private void OnUnloaded(object sender, RoutedEventArgs e) { Detatch(); SizeChanged -= OnSizeChanged; Unloaded -= OnUnloaded; } private void Pressed(Point position) { if (IsEnabled) { _content.Cursor = Cursors.Hand; _origin = position; _capture = true; } } private void Released() { if (IsEnabled) { _content.Cursor = null; _capture = false; } } private void Reset() { _masortingx = Masortingx.Identity; if (_content != null) { Arrange(_content.DesiredSize, _content.RenderSize); } Invalidate(); } } 

./Themes/Viewport.xaml:

    

./App.xaml

          

Usage:

    

N’importe quels problèmes, donnez-moi un cri.

Heureux codage 🙂