Bild schwenken und zoomen

130

Ich möchte einen einfachen Bildbetrachter in WPF erstellen, der es dem Benutzer ermöglicht:

  • Schwenken (durch Ziehen der Maus mit der Maus).
  • Zoom (mit einem Schieberegler).
  • Überlagerungen anzeigen (z. B. Auswahl eines Rechtecks).
  • Originalbild anzeigen (bei Bedarf mit Bildlaufleisten).

Können Sie erklären, wie es geht?

Ich habe im Web keine gute Probe gefunden. Soll ich ViewBox verwenden? Oder ImageBrush? Benötige ich ScrollViewer?

Yuval Peled
quelle
Um eine professionelle Zoomsteuerung für WPF zu erhalten, schauen Sie sich das ZoomPanel an . Es ist nicht kostenlos, aber sehr einfach zu bedienen und verfügt über viele Funktionen - animiertes Zoomen und Schwenken, Unterstützung für ScrollViewer, Mausradunterstützung, inklusive ZoomController (mit Verschieben, Vergrößern, Verkleinern, Rechteckzoom, Zurücksetzen-Schaltflächen). Es kommt auch mit vielen Codebeispielen.
Andrej Benedik
Ich habe auf codeproject.com einen Artikel über die Implementierung eines Zoom- und Schwenksteuerelements für WPF geschrieben. codeproject.com/KB/WPF/zoomandpancontrol.aspx
Ashley Davis
Guter Fund. Kostenlos zu versuchen, und sie wollen 69 $ / Computer für eine Lizenz, wenn Sie beabsichtigen, Software damit zu erstellen. Es ist eine DLL, die verwendet werden muss, sodass sie Sie nicht aufhalten können. Wenn Sie sie jedoch kommerziell für einen Client erstellen, insbesondere für einen, bei dem ein Drittanbieter-Dienstprogramm deklariert und individuell lizenziert werden muss, müssen Sie dafür bezahlen die Entwicklungsgebühr. In der EULA wurde jedoch nicht angegeben, dass es sich um eine "pro Anwendung" handelt. Sobald Sie Ihren Kauf registriert haben, ist diese für alle von Ihnen erstellten Anwendungen "kostenlos" und kann Ihre kostenpflichtige Lizenzdatei kopieren damit den Kauf darzustellen.
Vapcguy

Antworten:

116

Ich habe dieses Problem gelöst, indem ich das Bild innerhalb eines Rahmens platziert habe, dessen ClipToBounds-Eigenschaft auf True gesetzt war. Das RenderTransformOrigin für das Bild wird dann auf 0,5,0,5 festgelegt, sodass das Bild in der Bildmitte zu zoomen beginnt. Die RenderTransform wird auch auf eine TransformGroup festgelegt, die eine ScaleTransform und eine TranslateTransform enthält.

Ich habe dann das MouseWheel-Ereignis auf dem Bild behandelt, um das Zoomen zu implementieren

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

Um das Schwenken zu handhaben, habe ich zuerst das MouseLeftButtonDown-Ereignis auf dem Bild behandelt, die Maus erfasst und ihren Speicherort aufgezeichnet. Außerdem speichere ich den aktuellen Wert von TranslateTransform. Dieser Wert wird aktualisiert, um das Schwenken zu implementieren.

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

Dann habe ich das MouseMove-Ereignis behandelt, um die TranslateTransform zu aktualisieren.

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

Vergessen Sie nicht, die Mauserfassung freizugeben.

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

Weitere Informationen zu den Auswahlhandles für die Größenänderung, die mit einem Adorner ausgeführt werden können, finden Sie in diesem Artikel .

Ian Oakes
quelle
9
Eine Beobachtung beim Aufrufen von CaptureMouse in image_MouseLeftButtonDown führt jedoch zu einem Aufruf von image_MouseMove, bei dem der Ursprung noch nicht initialisiert ist. Im obigen Code ist er zufällig Null, aber wenn der Ursprung nicht (0,0) ist, ist das Bild wird einen kurzen Sprung erleben. Daher denke ich, dass es besser ist, image.CaptureMouse () am Ende von image_MouseLeftButtonDown aufzurufen, um dieses Problem zu beheben.
Andrei Pana
2
Zwei Dinge. 1) Es gibt einen Fehler mit image_MouseWheel. Sie müssen die ScaleTransform auf ähnliche Weise wie TranslateTransform abrufen. Das heißt, in eine Transformationsgruppe umwandeln und dann das entsprechende untergeordnete Element auswählen und umwandeln. 2) Wenn Ihre Bewegung nervös ist, denken Sie daran, dass Sie das Bild nicht verwenden können, um Ihre Mausposition zu ermitteln (da es dynamisch ist), müssen Sie etwas Statisches verwenden. In diesem Beispiel wird ein Rahmen verwendet.
Dave
169

Nachdem ich Beispiele aus dieser Frage verwendet habe, habe ich eine vollständige Version der Pan & Zoom-App mit dem richtigen Zoomen relativ zum Mauszeiger erstellt. Der gesamte Schwenk- und Zoomcode wurde in eine separate Klasse namens ZoomBorder verschoben.

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
quelle
10
Leider kann ich Ihnen keinen Punkt mehr geben. Das funktioniert wirklich super.
Tobiel
6
Bevor Kommentare für "Nice Job!" oder "Großartige Arbeit" Ich möchte nur "Gute Arbeit" und "Großartige Arbeit" sagen. Dies ist ein WPF-Juwel. Es bläst die wpf ext Zoombox aus dem Wasser.
Jesse Seger
4
Hervorragend. Ich könnte heute Abend vielleicht noch nach Hause gehen ... +1000
Bruce Pierson
1
GENIAL. Ich habe nicht über eine solche Implementierung nachgedacht, aber es ist wirklich schön! Ich danke dir sehr!
Noel Widmer
3
gute Antwort! Ich habe den Zoomfaktor geringfügig korrigiert, damit er nicht "langsamer" double zoomCorrected = zoom*st.ScaleX; st.ScaleX += zoomCorrected; st.ScaleY += zoomCorrected;
zoomt
46

Die Antwort wurde oben gepostet, war aber nicht vollständig. Hier ist die fertige Version:

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 dahinter

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

Ich habe ein Beispiel für ein vollständiges wpf-Projekt, das diesen Code auf meiner Website verwendet: Notieren Sie sich die Haftnotiz-App .

Kelly
quelle
1
Irgendwelche Vorschläge, wie Sie dies in Silverlight 3 verwenden können? Ich habe Probleme mit Vector und subtrahiere einen Punkt von einem anderen ... Danke.
Nummer 8
@ Number8 Posted eine Implementierung, die in Silverlight 3 für Sie unten funktioniert :)
Henry C
4
Ein kleiner Nachteil - das Bild wächst mit der Grenze und nicht innerhalb der Grenze
itsho
Könnt ihr etwas vorschlagen, wie man dasselbe in der Windows 8 Metro App implementiert? Ich arbeite an c #, xaml unter Windows 8
raj
1
In image_MouseWheel können Sie die transform.ScaleX- und ScaleY-Werte testen. Wenn diese Werte + zoom> Ihr Limit sind, wenden Sie die + = Zoomlinien nicht an.
Kelly
10

Versuchen Sie diese Zoomsteuerung: http://wpfextensions.codeplex.com

Die Verwendung des Steuerelements ist sehr einfach. Verweisen Sie auf die wpfextensions-Assembly als:

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

Bildlaufleisten werden derzeit nicht unterstützt. (Es wird in der nächsten Version sein, die in ein oder zwei Wochen verfügbar sein wird).

Palesz
quelle
Ja, das zu genießen. Der Rest deiner Bibliothek ist allerdings ziemlich trivial.
EightyOne Unite
Es scheint zwar keine direkte Unterstützung für "Überlagerungen anzeigen (z. B. Auswahl von Rechtecken)" zu geben, aber für das Zoom- / Schwenkverhalten ist dies eine hervorragende Steuerung.
jsirr13
9
  • Schwenken: Legen Sie das Bild in eine Leinwand. Implementieren Sie Mouse Up-, Down- und Move-Ereignisse, um die Eigenschaften Canvas.Top, Canvas.Left zu verschieben. Wenn Sie unten sind, markieren Sie einen isDraggingFlag als wahr, wenn Sie oben sind, setzen Sie das Flag auf false. Beim Verschieben überprüfen Sie, ob das Flag gesetzt ist, und versetzen die Eigenschaften Canvas.Top und Canvas.Left für das Bild im Canvas-Bereich.
  • Zoom: Binden Sie den Schieberegler an die Skalentransformation der Leinwand
  • Überlagerungen anzeigen: Fügen Sie zusätzliche Zeichenflächen ohne Hintergrund auf der Zeichenfläche hinzu, die das Bild enthält.
  • Originalbild anzeigen: Bildsteuerung in einer ViewBox
markti
quelle
4

@Anothen und @ Number8 - Die Vector-Klasse ist in Silverlight nicht verfügbar. Damit dies funktioniert, müssen Sie nur die letzte Position aufzeichnen, die beim letzten Aufruf des MouseMove-Ereignisses gesichtet wurde, und die beiden Punkte vergleichen, um den Unterschied zu ermitteln ;; Passen Sie dann die Transformation an.

XAML:

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

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

Beachten Sie auch, dass Sie keine TransformGroup oder Sammlung benötigen, um Schwenken und Zoomen zu implementieren. stattdessen eine CompositeTransform den Trick mit weniger Aufwand.

Ich bin mir ziemlich sicher, dass dies in Bezug auf die Ressourcennutzung wirklich ineffizient ist, aber zumindest funktioniert es :)

Henry C.
quelle
2

Um relativ zur Mausposition zu zoomen, benötigen Sie lediglich:

var position = e.GetPosition(image1);
image1.RenderTransformOrigin = new Point(position.X / image1.ActualWidth, position.Y / image1.ActualHeight);
Patrick
quelle
Ich benutze PictureBox, RenderTransformOrigin existiert nicht mehr.
Wechseln Sie
@Switch RenderTransformOrigin ist für WPF-Steuerelemente.
Xam
2

@ Merk

Für Ihre Lösung mit Lambda-Ausdruck können Sie folgenden Code verwenden:

//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];
        }

Dieser Code kann unverändert für .Net Frame Work 3.0 oder 2.0 verwendet werden

Hoffe es hilft dir :-)

Nishantcop
quelle
2

Noch eine Version derselben Art von Kontrolle. Es hat ähnliche Funktionen wie die anderen, fügt jedoch hinzu:

  1. Touch-Unterstützung (Ziehen / Kneifen)
  2. Das Bild kann gelöscht werden (normalerweise sperrt das Bildsteuerelement das Bild auf der Festplatte, sodass Sie es nicht löschen können).
  3. Ein untergeordnetes untergeordnetes Randelement, sodass das Schwenkbild den Rand nicht überlappt. Suchen Sie bei Rahmen mit abgerundeten Rechtecken nach ClippedBorder-Klassen.

Die Verwendung ist einfach:

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

Und der 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
quelle
1
Das einzige Problem, das ich fand, war, dass wenn ein Pfad zu einem Bild in der XAML angegeben ist, versucht wird, ihn zu rendern, bevor das Bildobjekt erstellt wird (dh bevor OnLoaded aufgerufen wird). Um dies zu beheben, habe ich den Code "image = new Image ..." von der onLoaded-Methode in den Konstruktor verschoben. Vielen Dank.
Mitch
Ein anderes Problem ist, dass das Bild zu klein herausgezoomt werden kann, bis wir nichts mehr tun und nichts mehr sehen können. Ich füge eine kleine Einschränkung hinzu: if (image.ActualWidth*(st.ScaleX + zoom) < 200 || image.ActualHeight*(st.ScaleY + zoom) < 200) //don't zoom out too small. return;in image.MouseWheel
huoxudong125
1

Dadurch wird das Bild vergrößert und verkleinert sowie geschwenkt, das Bild bleibt jedoch innerhalb der Grenzen des Containers. Als Steuerelement geschrieben, fügen Sie den Stil App.xamldirekt oder über das hinzuThemes/Viewport.xaml .

Zur besseren Lesbarkeit habe ich dies auch auf Gist und Github hochgeladen

Ich habe das auch auf Nuget verpackt

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>

Verwendung:

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

Bei Problemen rufen Sie mich an.

Viel Spaß beim Codieren :)

Adam H.
quelle
Großartig, ich liebe diese Version. Gibt es eine Möglichkeit, Bildlaufleisten hinzuzufügen?
Etienne Charland
Übrigens verwenden Sie falsche Abhängigkeitseigenschaften. Für Zoom und Übersetzen können Sie den Code nicht in den Eigenschaftssetter einfügen, da er beim Binden überhaupt nicht aufgerufen wird. Sie müssen Change- und Coerce-Handler für die Abhängigkeitseigenschaft selbst registrieren und die Arbeit dort ausführen.
Etienne Charland
Ich habe diese Antwort seit dem Schreiben massiv geändert. Ich werde sie mit Korrekturen für einige der Probleme aktualisieren, die ich später in der Produktion hatte
Adam H
Diese Lösung ist großartig, aber ich kann nicht genau herausfinden, warum die Mausrad-Bildlauffunktion beim Vergrößern und Verkleinern eines Bildes einen seltsamen Zug in eine Richtung zu haben scheint, anstatt die Mauszeigerposition als Zoomursprung zu verwenden. Bin ich verrückt oder gibt es eine logische Erklärung dafür?
Paul Karkoska
Ich habe Probleme damit, dass dies innerhalb eines ScrollViewer-Steuerelements konsistent funktioniert. Ich habe es ein wenig modifiziert, um die Cusor-Position als Skalenursprung zu verwenden (zum Vergrößern und Verkleinern mit der Mausposition), könnte aber wirklich einige Eingaben verwenden, wie es in einem ScrollViewer funktioniert. Vielen Dank!
Paul Karkoska