Pan & Zoom Gambar

131

Saya ingin membuat penampil gambar sederhana di WPF yang akan memungkinkan pengguna untuk:

  • Pan (dengan mouse menyeret gambar).
  • Zoom (dengan slider).
  • Tampilkan overlay (misalnya pemilihan persegi panjang).
  • Tampilkan gambar asli (dengan bilah gulir jika perlu).

Bisakah Anda menjelaskan bagaimana cara melakukannya?

Saya tidak menemukan sampel yang bagus di web. Haruskah saya menggunakan ViewBox? Atau ImageBrush? Apakah saya perlu ScrollViewer?

Yuval Peled
sumber
Untuk mendapatkan Kontrol Zoom profesional untuk WPF, periksa ZoomPanel . Ini tidak gratis, tetapi sangat mudah digunakan dan memiliki banyak fitur - zoom dan panning animasi, dukungan untuk ScrollViewer, dukungan roda mouse, termasuk ZoomController (dengan gerakan, zoom in, zoom out, zoom persegi panjang, tombol reset). Itu juga datang dengan banyak contoh kode.
Andrej Benedik
Saya menulis sebuah artikel di codeproject.com tentang implementasi kontrol zoom dan pan untuk WPF. codeproject.com/KB/WPF/zoomandpancontrol.aspx
Ashley Davis
Bagus temukan. Gratis untuk dicoba, dan mereka ingin $ 69 / komputer untuk lisensi jika Anda bermaksud membangun perangkat lunak dengannya. Ini adalah DLL untuk digunakan, sehingga mereka tidak bisa menghentikan Anda, tetapi itu adalah di mana, jika Anda membangunnya secara komersial untuk klien, terutama yang membutuhkan utilitas pihak ketiga untuk dinyatakan & dilisensikan secara individual, Anda harus membayar biaya pengembangan. Namun, dalam EULA itu tidak mengatakan itu berdasarkan "per aplikasi", jadi segera setelah Anda mendaftarkan pembelian Anda, itu akan menjadi "gratis" untuk semua aplikasi yang Anda buat, dan dapat menyalin file lisensi berbayar Anda di dengan itu untuk mewakili pembelian.
vapcguy

Jawaban:

116

Cara saya memecahkan masalah ini adalah menempatkan gambar di dalam Perbatasan dengan properti ClipToBounds yang disetel ke True. RenderTransformOrigin pada gambar kemudian diatur ke 0,5,0,5 sehingga gambar akan mulai memperbesar di tengah gambar. RenderTransform juga diatur ke TransformGroup yang berisi ScaleTransform dan TranslateTransform.

Saya kemudian menangani acara MouseWheel pada gambar untuk menerapkan pembesaran

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

Untuk menangani panning, hal pertama yang saya lakukan adalah menangani event MouseLeftButtonDown pada gambar, untuk menangkap mouse dan untuk merekam lokasinya, saya juga menyimpan nilai TranslateTransform saat ini, ini yang diperbarui untuk mengimplementasikan panning.

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

Kemudian saya menangani acara MouseMove untuk memperbarui 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;
    }
}

Akhirnya jangan lupa untuk melepaskan tangkapan mouse.

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

Sedangkan untuk pemilihan pegangan untuk mengubah ukuran ini dapat dilakukan dengan menggunakan adorner, lihat artikel ini untuk informasi lebih lanjut.

Ian Oakes
sumber
9
Satu pengamatan meskipun, memanggil CaptureMouse di image_MouseLeftButtonDown akan menghasilkan panggilan ke image_MouseMove di mana asal belum diinisialisasi - dalam kode di atas, itu akan menjadi nol secara kebetulan, tetapi jika asal bukan (0,0), gambar akan mengalami lompatan singkat. Oleh karena itu, saya pikir lebih baik untuk memanggil image.CaptureMouse () di akhir image_MouseLeftButtonDown untuk memperbaiki masalah ini.
Andrei Pana
2
Dua hal. 1) Ada bug pada image_MouseWheel, Anda harus mendapatkan ScaleTransform dengan cara yang sama seperti Anda mendapatkan TranslateTransform. Yaitu, Cast ke TransformGroup lalu pilih dan masukkan Child yang sesuai. 2) Jika gerakan Anda Jittery ingat bahwa Anda tidak dapat menggunakan gambar untuk mendapatkan posisi mouse Anda (karena dinamisnya), Anda harus menggunakan sesuatu yang statis. Dalam contoh ini, perbatasan digunakan.
Dave
169

Setelah menggunakan sampel dari pertanyaan ini, saya telah membuat versi lengkap aplikasi pan & zoom dengan zoom yang tepat relatif terhadap pointer mouse. Semua kode pan & zoom telah dipindahkan ke kelas terpisah yang disebut 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
sumber
10
Sayangnya, saya tidak bisa memberi Anda lebih banyak poin. Ini bekerja sangat luar biasa.
Tobiel
6
Sebelum komentar diblokir untuk "Pekerjaan Bagus!" atau "Great Work" Saya hanya ingin mengatakan Nice Job dan Great Work. Ini adalah permata WPF. Ini meniup zoombox WPF keluar dari air.
Jesse Seger
4
Berdiri di luar. Saya mungkin bisa pulang malam ini ... +1000
Bruce Pierson
1
LUAR BIASA. Saya tidak berpikir tentang implementasi seperti itu tetapi sangat bagus! Terima kasih banyak!
Noel Widmer
3
jawaban bagus! Saya telah menambahkan sedikit koreksi pada faktor zoom, sehingga tidak memperbesar "lebih lambat"double zoomCorrected = zoom*st.ScaleX; st.ScaleX += zoomCorrected; st.ScaleY += zoomCorrected;
DELUXEnized
46

Jawabannya diposting di atas tetapi tidak lengkap. di sini adalah versi yang sudah selesai:

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>

Kode di Balik

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

Saya punya contoh proyek wpf lengkap menggunakan kode ini di situs web saya: Mencatat aplikasi catatan tempel .

Kelly
sumber
1
Ada saran tentang cara membuat ini dapat digunakan di Silverlight 3? Saya memiliki masalah dengan Vector dan mengurangi satu Point dari yang lain ... Terima kasih.
Nomor 8
@ Number8 Diposting implementasi yang bekerja di Silverlight 3 untuk Anda di bawah ini :)
Henry C
4
kelemahan kecil - gambar tumbuh dengan perbatasan, dan tidak di dalam perbatasan
itsho
bisakah kalian menyarankan sesuatu bagaimana menerapkan hal yang sama di aplikasi gaya metro windows 8 .. saya bekerja pada c #, xaml pada windows8
raj
1
Di image_MouseWheel Anda dapat menguji nilai transform.caleX dan ScaleY dan jika nilai-nilai + zoom> batas Anda, jangan menerapkan garis zoom = =.
Kelly
10

Coba Kontrol Zoom ini: http://wpfextensions.codeplex.com

penggunaan kontrol ini sangat sederhana, merujuk pada rakitan wpfextensions daripada:

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

Bilah gulir tidak didukung saat ini. (Ini akan menjadi rilis berikutnya yang akan tersedia dalam satu atau dua minggu).

Palesz
sumber
Yup, nikmati itu. Perpustakaanmu yang lain cukup sepele.
EightyOne Unite
Namun sepertinya tidak ada dukungan langsung untuk 'Tampilkan overlay (pemilihan persegi panjang)', tetapi untuk perilaku zoom / panning, ini adalah kontrol yang hebat.
jsirr13
9
  • Pan: Letakkan gambar di dalam kanvas. Implementasikan Mouse Atas, Bawah, dan Pindahkan acara untuk memindahkan properti Canvas.Top, Canvas.Left. Saat turun, Anda menandai isDraggingFlag ke true, saat Anda mengatur flag menjadi false. Saat bergerak, Anda memeriksa apakah bendera telah disetel, apakah Anda mengimbangi properti Canvas.Top dan Canvas.Left pada gambar di dalam kanvas.
  • Zoom: Ikatkan slider ke Transform Scale of the Canvas
  • Perlihatkan overlay: tambahkan kanvas tambahan tanpa latar belakang di atas kanvas yang berisi gambar.
  • perlihatkan gambar asli: kontrol gambar di dalam ViewBox
markti
sumber
4

@Anothen dan @ Number8 - Kelas Vector tidak tersedia di Silverlight, jadi untuk membuatnya bekerja kita hanya perlu menyimpan catatan posisi terakhir yang terlihat terakhir kali acara MouseMove dipanggil, dan membandingkan dua titik untuk menemukan perbedaan ; kemudian sesuaikan transformasinya.

XAML:

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

Kode di belakang:

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

Perhatikan juga bahwa Anda tidak memerlukan TransformGroup atau koleksi untuk menerapkan pan dan zoom; sebagai gantinya, CompositeTransform akan melakukan trik dengan lebih mudah.

Saya cukup yakin ini benar-benar tidak efisien dalam hal penggunaan sumber daya, tetapi setidaknya itu bekerja :)

Henry C
sumber
2

Untuk memperbesar relatif ke posisi mouse, yang Anda butuhkan adalah:

var position = e.GetPosition(image1);
image1.RenderTransformOrigin = new Point(position.X / image1.ActualWidth, position.Y / image1.ActualHeight);
Patrick
sumber
Saya menggunakan PictureBox, RenderTransformOrigin tidak ada lagi.
Beralih
@Switch RenderTransformOrigin untuk kontrol WPF.
Xam
2

@ Merk

Untuk solusi Anda, instal ekspresi lambda Anda dapat menggunakan kode berikut:

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

kode ini dapat digunakan seperti untuk .Net Frame work 3.0 atau 2.0

Semoga Ini membantu Anda :-)

nishantcop
sumber
2

Versi lain dari jenis kontrol yang sama. Ini memiliki fungsi yang mirip dengan yang lain, tetapi ia menambahkan:

  1. Dukungan sentuh (seret / cubit)
  2. Gambar dapat dihapus (biasanya, kontrol Gambar mengunci gambar pada disk, sehingga Anda tidak dapat menghapusnya).
  3. Anak perbatasan bagian dalam, sehingga gambar yang di-panning tidak tumpang tindih dengan perbatasan. Dalam hal perbatasan dengan persegi panjang bulat, cari kelas ClippedBorder.

Penggunaannya sederhana:

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

Dan kodenya:

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
sumber
1
Satu-satunya masalah yang saya temukan adalah bahwa jika jalur ke gambar ditentukan dalam XAML, ia mencoba merendernya sebelum objek gambar dibuat (yaitu sebelum OnLoaded dipanggil). Untuk memperbaikinya, saya memindahkan kode "image = new Image ...", dari metode onLoaded ke konstruktor. Terima kasih.
Mitch
Masalah lainnya adalah gambar bisa if (image.ActualWidth*(st.ScaleX + zoom) < 200 || image.ActualHeight*(st.ScaleY + zoom) < 200) //don't zoom out too small. return;diperkecil menjadi kecil sampai kita tidak bisa melakukan apa-apa dan tidak melihat apa-apa. Saya menambahkan sedikit batasan: dalam gambar. Mouse roda
huoxudong125
1

Ini akan memperbesar dan memperkecil serta menggeser tetapi menjaga gambar dalam batas wadah. Ditulis sebagai kontrol, jadi tambahkan gaya ke App.xamllangsung atau melalui Themes/Viewport.xaml.

Untuk keterbacaan, saya juga mengunggah ini di intisari dan github

Saya juga mengemas ini di 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>

Pemakaian:

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

Masalah apa pun, beri aku teriakan.

Selamat coding :)

Adam H
sumber
Hebat, saya suka versi ini. Adakah cara untuk menambahkan scrollbar ke sana?
Etienne Charland
Ngomong-ngomong Anda menggunakan properti ketergantungan salah. Untuk Zoom dan Terjemahan, Anda tidak dapat memasukkan kode di setter properti karena tidak dipanggil sama sekali saat mengikat. Anda perlu mendaftarkan Penanganan Perubahan dan Perdagangan pada properti ketergantungan itu sendiri dan melakukan pekerjaan di sana.
Etienne Charland
Saya telah mengubah jawaban ini secara besar-besaran sejak menulisnya, dan memperbaruinya dengan perbaikan untuk beberapa masalah yang saya miliki dalam menggunakannya nanti
Adam H
Solusi ini bagus, tapi saya tidak tahu mengapa fungsi gulir roda mouse tampaknya memiliki tarikan aneh ke satu arah saat memperbesar dan memperkecil gambar, alih-alih menggunakan posisi penunjuk mouse sebagai asal zoom. Apakah saya gila atau ada penjelasan logis untuk ini?
Paul Karkoska
Saya berusaha keras agar ini berfungsi secara konsisten dalam kontrol ScrollViewer. Saya memodifikasinya sedikit untuk menggunakan posisi cusor sebagai asal skala (untuk memperbesar dan memperkecil menggunakan posisi mouse), tetapi benar-benar dapat menggunakan beberapa input tentang cara membuatnya bekerja di dalam ScrollViewer. Terima kasih!
Paul Karkoska