Image panoramique et zoom

131

Je souhaite créer une visionneuse d'images simple dans WPF qui permettra à l'utilisateur de:

  • Panoramique (en faisant glisser l'image avec la souris).
  • 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 faire?

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

Yuval Peled
la source
Pour obtenir un contrôle de zoom professionnel pour WPF, consultez ZoomPanel . Ce 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, prise en charge de la molette de la souris, ZoomController inclus (avec déplacement, zoom avant, zoom arrière, zoom rectangle, boutons de réinitialisation). Il est également livré avec de nombreux exemples de code.
Andrej Benedik
J'ai écrit un article sur codeproject.com sur l'implémentation d'un contrôle de zoom et de panoramique pour WPF. codeproject.com/KB/WPF/zoomandpancontrol.aspx
Ashley Davis
Bonne trouvaille. Gratuit pour essayer, et ils veulent 69 $ / ordinateur pour une licence si vous avez l'intention de créer un logiciel avec. C'est une DLL à utiliser, donc ils ne pouvaient pas vous arrêter, mais c'est là que, si vous la construisez commercialement pour un client, en particulier si vous devez déclarer un utilitaire tiers et obtenir une licence individuelle, vous devrez payer les frais de développement. Dans le CLUF, il ne disait pas que c'était sur une base "par application", donc dès que vous enregistrez votre achat, il serait alors "gratuit" pour toutes les applications que vous avez créées et pourrait copier votre fichier de licence payante dans avec elle pour représenter l'achat.
vapcguy

Réponses:

116

La façon dont j'ai résolu ce problème était de placer l'image dans une bordure avec sa propriété ClipToBounds définie sur True. Le RenderTransformOrigin sur l'image est alors défini sur 0,5,0,5 afin que l'image commence à zoomer sur le centre de l'image. Le 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, ce qui est mis à jour pour implémenter le panoramique.

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 - v.X;
        tt.Y = origin.Y - v.Y;
    }
}

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, cela peut être accompli à l'aide d'un adorner, consultez cet article pour plus d'informations.

Ian Oakes
la source
9
Une observation cependant, appeler CaptureMouse dans image_MouseLeftButtonDown entraînera un appel à image_MouseMove où l'origine n'est pas encore initialisée - dans le code ci-dessus, ce sera zéro par pur hasard, mais si l'origine est autre que (0,0), l'image fera l'expérience d'un saut court. Par conséquent, je pense qu'il est préférable d'appeler image.CaptureMouse () à la fin de l'image_MouseLeftButtonDown pour résoudre ce problème.
Andrei Pana
2
Deux choses. 1) Il y a un bogue avec image_MouseWheel, vous devez obtenir le ScaleTransform de la même manière que vous obtenez TranslateTransform. Autrement dit, lancez-le dans un TransformGroup, puis sélectionnez et castez l'enfant approprié. 2) Si votre mouvement est nerveux, rappelez-vous que vous ne pouvez pas utiliser l'image pour obtenir la position de votre souris (depuis sa dynamique), vous devez utiliser quelque chose de statique. Dans cet exemple, une bordure est utilisée.
Dave
169

Après avoir utilisé des exemples de cette question, j'ai créé une version complète de l'application panoramique et zoom avec un zoom approprié par rapport au pointeur de la souris. Tout le code de panoramique et de zoom a été déplacé vers 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 absoluteX;
                double absoluteY;

                absoluteX = relative.X * st.ScaleX + tt.X;
                absoluteY = relative.Y * st.ScaleY + tt.Y;

                st.ScaleX += zoom;
                st.ScaleY += zoom;

                tt.X = absoluteX - relative.X * st.ScaleX;
                tt.Y = absoluteY - 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 - v.X;
                    tt.Y = origin.Y - v.Y;
                }
            }
        }

        #endregion
    }
}

MainWindow.xaml

<Window x:Class="PanAndZoom.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:PanAndZoom"
        Title="PanAndZoom" Height="600" Width="900" WindowStartupLocation="CenterScreen">
    <Grid>
        <local:ZoomBorder x:Name="border" ClipToBounds="True" Background="Gray">
            <Image Source="image.jpg"/>
        </local:ZoomBorder>
    </Grid>
</Window>

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();
        }
    }
}
Wiesław Šoltés
la source
10
Malheureusement, je ne peux pas vous donner plus de points. Cela fonctionne vraiment génial.
Tobiel
6
Avant que les commentaires ne soient bloqués pour "Nice Job!" ou "Excellent travail" Je veux juste dire Beau travail et excellent travail. Ceci est un joyau WPF. Il souffle la boîte de zoom wpf ext hors de l'eau.
Jesse Seger
4
Exceptionnel. Je pourrais peut-être encore rentrer chez moi ce soir ... +1000
Bruce Pierson
1
IMPRESSIONNANT. Je n'ai pas pensé à une telle implémentation mais c'est vraiment sympa! Merci beaucoup!
Noel Widmer
3
très bonne réponse! J'ai ajouté une légère correction au facteur de zoom, donc il ne double zoomCorrected = zoom*st.ScaleX; st.ScaleX += zoomCorrected; st.ScaleY += zoomCorrected;
zoomera
46

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

XAML

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MapTest.Window1"
x:Name="Window"
Title="Window1"
Width="1950" Height="1546" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Controls="clr-namespace:WPFExtensions.Controls;assembly=WPFExtensions" mc:Ignorable="d" Background="#FF000000">

<Grid x:Name="LayoutRoot">
    <Grid.RowDefinitions>
        <RowDefinition Height="52.92"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Border Grid.Row="1" Name="border">
        <Image Name="image" Source="map3-2.png" Opacity="1" RenderTransformOrigin="0.5,0.5"  />
    </Border>

</Grid>

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 - v.X;
            tt.Y = origin.Y - v.Y;
        }

        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: Notez l'application pense-bête .

Kelly
la source
1
Des suggestions sur la façon de rendre cela utilisable dans Silverlight 3? J'ai des problèmes avec Vector et soustraire un point d'un autre ... Merci.
Number8
@ Number8 Posté une implémentation qui fonctionne dans Silverlight 3 pour vous ci-dessous :)
Henry C
4
un petit inconvénient - l'image grandit avec la bordure, et non à l' intérieur de la frontière
itsho
pouvez-vous suggérer quelque chose comment implémenter la même chose dans l'application de style métro de Windows 8 ... je travaille sur c #, xaml sur windows8
raj
1
Dans image_MouseWheel, vous pouvez tester les valeurs transform.ScaleX et ScaleY et si ces valeurs + zoom> votre limite, n'appliquez pas les lignes de zoom + =.
Kelly
10

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:

<wpfext:ZoomControl>
    <Image Source="..."/>
</wpfext:ZoomControl>

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

Palesz
la source
Ouais, j'apprécie ça. Le reste de ta bibliothèque est cependant assez trivial.
EightyOne Unite
Il ne semble pas y avoir de support direct pour «Afficher les superpositions (sélection de rectangle par exemple)», mais pour le comportement de zoom / panoramique, c'est un excellent contrôle.
jsirr13
9
  • Panoramique: place 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. Lorsqu'il est désactivé, vous marquez un isDraggingFlag sur true, lorsque vous définissez l'indicateur sur false. Lors du déplacement, vous vérifiez si l'indicateur est défini, si c'est le cas, vous décalez 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 canevas supplémentaires sans arrière-plan sur le canevas contenant l'image.
  • afficher l'image d'origine: contrôle d'image à l'intérieur d'un ViewBox
markti
la source
4

@Anothen et @ Number8 - La classe Vector n'est pas disponible dans Silverlight, donc pour le faire fonctionner, nous avons juste besoin de garder un enregistrement de la dernière position aperçue 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:

    <Border Name="viewboxBackground" Background="Black">
            <Viewbox Name="viewboxMain">
                <!--contents go here-->
            </Viewbox>
    </Border>  

Code derrière:

    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 le panoramique et le zoom; à la place, un CompositeTransform fera l'affaire avec moins de tracas.

Je suis presque sûr que c'est vraiment inefficace en termes d'utilisation des ressources, mais au moins cela fonctionne :)

Henry C
la source
2

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);
Patrick
la source
J'utilise PictureBox, RenderTransformOrigin n'existe plus.
Changez
@Switch RenderTransformOrigin est destiné aux contrôles WPF.
Xam
2

@ Merk

Pour votre solution 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 .Net Frame work 3.0 ou 2.0

J'espère que cela vous aide :-)

nishantcop
la source
2

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, la commande Image verrouille l'image sur le disque, vous ne pouvez donc pas la supprimer).
  3. Un enfant de bordure intérieure, de sorte que l'image panoramique ne chevauche pas la bordure. Dans le cas de bordures avec des rectangles arrondis, recherchez les classes ClippedBorder.

L'utilisation est simple:

<Controls:ImageViewControl ImagePath="{Binding ...}" />

Et le code:

public class ImageViewControl : Border
{
    private Point origin;
    private Point start;
    private Image image;

    public ImageViewControl()
    {
        ClipToBounds = true;
        Loaded += OnLoaded;
    }

    #region ImagePath

    /// <summary>
    ///     ImagePath Dependency Property
    /// </summary>
    public static readonly DependencyProperty ImagePathProperty = DependencyProperty.Register("ImagePath", typeof (string), typeof (ImageViewControl), new FrameworkPropertyMetadata(string.Empty, OnImagePathChanged));

    /// <summary>
    ///     Gets or sets the ImagePath property. This dependency property 
    ///     indicates the path to the image file.
    /// </summary>
    public string ImagePath
    {
        get { return (string) GetValue(ImagePathProperty); }
        set { SetValue(ImagePathProperty, value); }
    }

    /// <summary>
    ///     Handles changes to the ImagePath property.
    /// </summary>
    private static void OnImagePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (ImageViewControl) d;
        var oldImagePath = (string) e.OldValue;
        var newImagePath = target.ImagePath;
        target.ReloadImage(newImagePath);
        target.OnImagePathChanged(oldImagePath, newImagePath);
    }

    /// <summary>
    ///     Provides derived classes an opportunity to handle changes to the ImagePath property.
    /// </summary>
    protected virtual void OnImagePathChanged(string oldImagePath, string 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 - v.X;
                                   tt.Y = origin.Y - v.Y;
                                   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);
    }

    /// <summary>
    /// Load the image (and do not keep a hold on it, so we can delete the image without problems)
    /// </summary>
    /// <see cref="http://blogs.vertigo.com/personal/ralph/Blog/Lists/Posts/Post.aspx?ID=18"/>
    /// <param name="path"></param>
    private void ReloadImage(string 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);
        }
    }
}
Erik Vullings
la source
1
Le seul problème que j'ai trouvé était que si un chemin vers une image est spécifié dans le XAML, il essaie de le rendre avant que l'objet image ne soit construit (c'est-à-dire avant que OnLoaded soit appelé). Pour corriger, j'ai déplacé le code "image = new Image ...", de la méthode onLoaded vers le constructeur. Merci.
Mitch
Un autre problème est que l'image peut être if (image.ActualWidth*(st.ScaleX + zoom) < 200 || image.ActualHeight*(st.ScaleY + zoom) < 200) //don't zoom out too small. return;zoomée en arrière jusqu'à ce que nous ne puissions rien faire et ne rien voir.J'ajoute un peu de limitation: dans l'image.MouseWheel
huoxudong125
1

Cela effectuera un zoom avant et arrière ainsi qu'un panoramique, mais gardera l'image dans les limites du conteneur. Écrit en tant que contrôle, ajoutez le style App.xamldirectement ou via le Themes/Viewport.xaml.

Pour plus de lisibilité, j'ai également téléchargé ceci sur gist et github

J'ai aussi emballé ceci sur nuget

PM > Install-Package Han.Wpf.ViewportControl

./Controls/Viewport.cs:

public class Viewport : ContentControl
{
    private bool _capture;
    private FrameworkElement _content;
    private Matrix _matrix;
    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)
    {
        _matrix = Matrix.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;

            _matrix = new Matrix(zoom, 0d, 0d, zoom, cx, cy);
        }
        else
        {
            _matrix.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(_matrix.OffsetX, _content.ActualWidth - _content.ActualWidth * _matrix.M11, 0);
        var y = Constrain(_matrix.OffsetY, _content.ActualHeight - _content.ActualHeight * _matrix.M22, 0);

        _matrix = new Matrix(_matrix.M11, 0d, 0d, _matrix.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 MatrixTransform(_matrix);
            _content.InvalidateVisual();

            ZoomX = _matrix.M11;
            ZoomY = _matrix.M22;

            OffsetX = _matrix.OffsetX;
            OffsetY = _matrix.OffsetY;

            var rect = new Rect
            {
                X = OffsetX * -1,
                Y = OffsetY * -1,
                Width = ActualWidth,
                Height = ActualHeight
            };

            Bounds = rect;
        }
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _matrix = Matrix.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;

            _matrix.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 / _matrix.M11, MaxZoom / _matrix.M11);
            var y = Constrain(scale, MinZoom / _matrix.M22, MaxZoom / _matrix.M22);

            _matrix.ScaleAtPrepend(x, y, position.X, position.Y);

            ZoomX = _matrix.M11;
            ZoomY = _matrix.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()
    {
        _matrix = Matrix.Identity;

        if (_content != null)
        {
            Arrange(_content.DesiredSize, _content.RenderSize);
        }

        Invalidate();
    }
}

./Themes/Viewport.xaml:

<ResourceDictionary ... >

    <Style TargetType="{x:Type controls:Viewport}"
           BasedOn="{StaticResource {x:Type ContentControl}}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:Viewport}">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}">
                        <Grid ClipToBounds="True"
                              Width="{TemplateBinding Width}"
                              Height="{TemplateBinding Height}">
                            <Grid x:Name="PART_Container">
                                <ContentPresenter x:Name="PART_Presenter" />
                            </Grid>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

./App.xaml

<Application ... >
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>

                <ResourceDictionary Source="./Themes/Viewport.xaml"/>

            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Usage:

<viewers:Viewport>
    <Image Source="{Binding}"/>
</viewers:Viewport>

Tous les problèmes, donnez-moi un cri.

Bon codage :)

Adam H
la source
Super, j'adore cette version. Un moyen d'y ajouter des barres de défilement?
Etienne Charland
Au fait, vous utilisez mal les propriétés de dépendance. Pour Zoom et Translate, vous ne pouvez pas placer le code dans le setter de propriétés car il n'est pas du tout appelé lors de la liaison. Vous devez enregistrer les gestionnaires de modification et de contrainte sur la propriété de dépendance elle-même et y effectuer le travail.
Etienne Charland
J'ai massivement changé cette réponse depuis que je l'ai écrite, je la mets à jour avec des correctifs pour certains des problèmes que j'ai rencontrés en l'utilisant plus tard en production
Adam H
Cette solution est excellente, mais je n'arrive pas à comprendre pourquoi la fonction de défilement de la molette de la souris semble avoir une étrange traction dans une direction lors du zoom avant et arrière d'une image, au lieu d'utiliser la position du pointeur de la souris comme origine du zoom. Suis-je fou ou y a-t-il une explication logique à cela?
Paul Karkoska
J'ai du mal à faire en sorte que cela fonctionne de manière cohérente dans un contrôle ScrollViewer. Je l'ai un peu modifié pour utiliser la position du cusor comme origine de l'échelle (pour effectuer un zoom avant et arrière en utilisant la position de la souris), mais je pourrais vraiment utiliser une entrée sur la façon de le faire fonctionner à l'intérieur d'un ScrollViewer. Merci!
Paul Karkoska