Hình ảnh Pan & Zoom


130

Tôi muốn tạo trình xem ảnh đơn giản trong WPF cho phép người dùng:

  • Pan (bằng cách kéo chuột hình ảnh).
  • Thu phóng (có thanh trượt).
  • Hiển thị lớp phủ (ví dụ lựa chọn hình chữ nhật).
  • Hiển thị hình ảnh gốc (với các thanh cuộn nếu cần).

Bạn có thể giải thích làm thế nào để làm điều đó?

Tôi đã không tìm thấy một mẫu tốt trên web. Tôi có nên sử dụng ViewBox không? Hoặc ImageBrush? Tôi có cần ScrollViewer không?


Để có được Điều khiển thu phóng chuyên nghiệp cho WPF, hãy kiểm tra ZoomPanel . Nó không miễn phí, nhưng rất dễ sử dụng và có nhiều tính năng - phóng to và xoay hoạt hình, hỗ trợ ScrollViewer, hỗ trợ bánh xe chuột, bao gồm ZoomContoder (có di chuyển, phóng to, thu nhỏ, thu phóng hình chữ nhật, nút đặt lại). Nó cũng đi kèm với nhiều mẫu mã.
Andrej Benedik

Tôi đã viết một bài viết trên codeproject.com về việc triển khai điều khiển phóng to và thu nhỏ cho WPF. codeproject.com/KB/WPF/zoomandpancontrol.aspx
Ashley Davis

Tìm tốt Dùng thử miễn phí và họ muốn có $ 69 / máy tính để xin giấy phép nếu bạn có ý định xây dựng phần mềm với nó. Đó là một DLL để sử dụng, vì vậy họ không thể ngăn bạn, nhưng đó là nơi, nếu bạn xây dựng nó cho khách hàng, đặc biệt là một yêu cầu bất kỳ tiện ích của bên thứ ba nào phải được khai báo & cấp phép riêng lẻ, bạn sẽ phải trả tiền phí phát triển. Tuy nhiên, trong EULA, họ không nói rằng đó là trên cơ sở "mỗi ứng dụng", vì vậy, ngay sau khi bạn đăng ký mua hàng, nó sẽ "miễn phí" cho tất cả các ứng dụng bạn đã tạo và có thể sao chép tệp giấy phép phải trả tiền của bạn trong với nó để đại diện cho việc mua hàng.
vapcguy

Câu trả lời:


116

Cách tôi giải quyết vấn đề này là đặt hình ảnh trong Đường viền với thuộc tính ClipToBound được đặt thành True. RenderTransformOrigin trên hình ảnh sau đó được đặt thành 0,5,0,5 để hình ảnh sẽ bắt đầu phóng to ở giữa hình ảnh. RenderTransform cũng được đặt thành Transformgroup chứa ScaleTransform và TranslateTransform.

Sau đó, tôi đã xử lý sự kiện MouseWheel trên hình ảnh để thực hiện thu phóng

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

Để xử lý panning, điều đầu tiên tôi làm là xử lý sự kiện MouseLeftButtonDown trên hình ảnh, để bắt chuột và ghi lại vị trí của nó, tôi cũng lưu trữ giá trị hiện tại của TranslateTransform, đây là những gì được cập nhật để thực hiện 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);
}

Sau đó, tôi đã xử lý sự kiện MouseMove để cập nhật 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;
    }
}

Cuối cùng đừng quên nhả chuột.

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

Đối với việc xử lý lựa chọn để thay đổi kích thước có thể được thực hiện bằng cách sử dụng một góc, hãy xem bài viết này để biết thêm thông tin.


9
Mặc dù vậy, một quan sát, gọi CaptureMouse trong image_MouseLeftButtonDown sẽ dẫn đến một cuộc gọi đến image_MouseMove khi nguồn gốc chưa được khởi tạo - trong mã trên, nó sẽ bằng 0, nhưng nếu nguồn gốc khác với (0,0), thì hình ảnh sẽ trải qua một bước nhảy ngắn. Do đó, tôi nghĩ tốt hơn là gọi image.CaptureMouse () ở cuối image_MouseLeftButtonDown để khắc phục vấn đề này.
Andrei Pana

2
Hai điều. 1) Có một lỗi với image_MouseWheel, bạn phải lấy ScaleTransform theo cách tương tự như bạn nhận được TranslateTransform. Đó là, Truyền nó tới Transformgroup sau đó chọn và truyền Con thích hợp. 2) Nếu chuyển động của bạn là Jittery hãy nhớ rằng bạn không thể sử dụng hình ảnh để có được vị trí chuột của bạn (vì nó là động), bạn phải sử dụng một cái gì đó tĩnh. Trong ví dụ này, một đường viền được sử dụng.
Dave

169

Sau khi sử dụng các mẫu từ câu hỏi này, tôi đã tạo phiên bản hoàn chỉnh của ứng dụng pan & zoom với thu phóng phù hợp so với con trỏ chuột. Tất cả mã pan & zoom đã được chuyển sang lớp riêng gọi là 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();
        }
    }
}

10
Đáng buồn thay, tôi không thể cho bạn thêm điểm. Điều này làm việc thực sự tuyệt vời.
Tobiel

6
Trước khi bình luận bị chặn cho "Công việc tốt đẹp!" hoặc "Công việc tuyệt vời" Tôi chỉ muốn nói Công việc tốt và Công việc tuyệt vời. Đây là một viên ngọc WPF. Nó thổi ống zoom wpf ext ra khỏi nước.
Jesse Seger

4
Nổi bật. Tôi có thể về nhà tối nay ... +1000
Bruce Pierson

1
TUYỆT VỜI. Tôi đã không mặc dù về việc thực hiện như vậy nhưng nó thực sự tốt đẹp! Cảm ơn bạn rất nhiều!
Noel Widmer

3
câu trả lời chính xác! Tôi đã thêm một hiệu chỉnh nhỏ cho hệ số thu phóng, để nó không thu phóng "chậm hơn"double zoomCorrected = zoom*st.ScaleX; st.ScaleX += zoomCorrected; st.ScaleY += zoomCorrected;
DELUXEnized

46

Câu trả lời đã được đăng ở trên nhưng chưa hoàn thành. đây là phiên bản hoàn chỉnh:

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>

Mã ẩn

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

Tôi có một ví dụ về một dự án wpf đầy đủ sử dụng mã này trên trang web của tôi: Ghi lại ứng dụng ghi chú dán .


1
Có gợi ý nào về cách làm cho nó có thể sử dụng được trong Silverlight 3 không? Tôi có vấn đề với Vector và trừ điểm này từ điểm khác ... Cảm ơn.
Số8

@ Number8 Đã đăng một triển khai hoạt động trong Silverlight 3 cho bạn bên dưới :)
Henry C

4
một nhược điểm nhỏ - hình ảnh phát triển cùng với đường viền chứ không phải bên trong đường viền
đó là

Các bạn có thể đề xuất một cái gì đó để thực hiện điều tương tự trong ứng dụng phong cách metro của windows 8. tôi đang làm việc trên c #, xaml trên windows8
raj

1
Trong image_MouseWheel, bạn có thể kiểm tra các giá trị Transform.ScaleX và ScaleY và nếu các giá trị đó + zoom> giới hạn của bạn, không áp dụng các dòng + = zoom.
Kelly

10

Hãy thử Điều khiển thu phóng này: http://wpfextensions.codeplex.com

Việc sử dụng điều khiển rất đơn giản, tham khảo cụm wpfextensions hơn:

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

Thanh cuộn không được hỗ trợ tại thời điểm này. (Nó sẽ có trong phiên bản tiếp theo sẽ có trong một hoặc hai tuần).


Yup, tận hưởng điều đó. Phần còn lại của thư viện là khá tầm thường.
EightyOne Đoàn kết

Dường như không có hỗ trợ trực tiếp cho 'Hiển thị lớp phủ (ví dụ: lựa chọn hình chữ nhật)', nhưng đối với hành vi phóng to / thu phóng, đó là một điều khiển tuyệt vời.
jsirr13

9
  • Pan: Đặt hình ảnh bên trong Canvas. Triển khai các sự kiện Chuột lên, Xuống và Di chuyển để di chuyển các thuộc tính Canvas.Top, Canvas.Left. Khi xuống, bạn đánh dấu isDraggingFlag thành true, khi lên, bạn đặt cờ thành false. Khi di chuyển, bạn kiểm tra xem cờ đã được đặt chưa, nếu đó là bạn bù các thuộc tính Canvas.Top và Canvas.Left trên hình ảnh trong khung vẽ.
  • Thu phóng: Liên kết thanh trượt với Biến đổi tỷ lệ của Canvas
  • Hiển thị lớp phủ: thêm các khung vẽ bổ sung không có nền ontop khung vẽ có chứa hình ảnh.
  • hiển thị hình ảnh gốc: kiểm soát hình ảnh bên trong ViewBox

4

@Anothen và @ Number8 - Lớp Vector không có sẵn trong Silverlight, vì vậy để làm cho nó hoạt động, chúng ta chỉ cần ghi lại vị trí cuối cùng được nhìn thấy lần cuối cùng khi sự kiện MouseMove được gọi và so sánh hai điểm để tìm sự khác biệt ; sau đó điều chỉnh biến đổi.

XAML:

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

Mã ẩn:

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

Cũng lưu ý rằng bạn không cần Transformgroup hoặc bộ sưu tập để triển khai pan và phóng to; thay vào đó, một CompositeTransform sẽ thực hiện thủ thuật với ít rắc rối hơn.

Tôi khá chắc chắn rằng điều này thực sự không hiệu quả về mặt sử dụng tài nguyên, nhưng ít nhất nó cũng hoạt động :)


2

Để thu phóng tương đối với vị trí chuột, tất cả những gì bạn cần là:

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

Tôi đang sử dụng PictureBox, RenderTransformOrigin không còn tồn tại nữa.
Chuyển

@Switch RenderTransformOrigin dành cho các điều khiển WPF.
Xam

2

@ Merk

Đối với giải pháp ur bắt nguồn từ biểu thức lambda, bạn có thể sử dụng mã sau:

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

mã này có thể được sử dụng như đối với .Net Frame làm việc 3.0 hoặc 2.0

Hy vọng nó sẽ giúp bạn :-)


2

Một phiên bản khác của cùng loại điều khiển. Nó có chức năng tương tự như những người khác, nhưng nó bổ sung:

  1. Hỗ trợ cảm ứng (kéo / véo)
  2. Hình ảnh có thể bị xóa (thông thường, điều khiển hình ảnh khóa hình ảnh trên đĩa, do đó bạn không thể xóa nó).
  3. Một đứa trẻ đường viền bên trong, vì vậy hình ảnh được dán không chồng lên đường viền. Trong trường hợp đường viền có hình chữ nhật tròn, hãy tìm các lớp ClippedBorder.

Cách sử dụng rất đơn giản:

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

Và mã:

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

1
Vấn đề duy nhất tôi tìm thấy là nếu một đường dẫn đến hình ảnh được chỉ định trong XAML, nó sẽ cố kết xuất nó trước khi đối tượng hình ảnh được xây dựng (tức là trước khi OnLoaded được gọi). Để khắc phục, tôi đã di chuyển mã "image = new Image ...", từ phương thức onLoaded sang hàm tạo. Cảm ơn.
Mitch

Vấn đề khác là hình ảnh có thể thu nhỏ thành nhỏ cho đến khi chúng tôi không thể làm gì và không thấy gì. Tôi thêm một chút giới hạn: if (image.ActualWidth*(st.ScaleX + zoom) < 200 || image.ActualHeight*(st.ScaleY + zoom) < 200) //don't zoom out too small. return;trong hình ảnh.MouseWheel
huoxudong125

1

Điều này sẽ phóng to và thu nhỏ cũng như xoay nhưng giữ hình ảnh trong giới hạn của container. Viết như một điều khiển để thêm phong cách App.xamltrực tiếp hoặc thông qua Themes/Viewport.xaml.

Để dễ đọc, tôi cũng đã tải nó lên trên gistgithub

Tôi cũng đã đóng gói cái này trên 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();
    }
}

./Theme/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>

Sử dụng:

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

Bất kỳ vấn đề, cho tôi một tiếng hét.

Chúc mừng mã hóa :)


Tuyệt vời, tôi yêu phiên bản này. Bất kỳ cách nào để thêm thanh cuộn vào nó?
Etienne Charland

Bằng cách bạn đang sử dụng thuộc tính phụ thuộc sai. Đối với Thu phóng và Dịch, bạn không thể đặt mã trong trình thiết lập thuộc tính vì nó hoàn toàn không được gọi khi ràng buộc. Bạn cần đăng ký các trình xử lý Thay đổi và Ép buộc trên chính thuộc tính phụ thuộc và thực hiện công việc trong đó.
Etienne Charland

Tôi đã thay đổi ồ ạt câu trả lời này kể từ khi viết nó, cập nhật nó với các bản sửa lỗi cho một số vấn đề tôi đã sử dụng nó trong sản xuất sau này
Adam H

Giải pháp này rất hay, nhưng tôi không thể hiểu tại sao chức năng cuộn của bánh xe chuột dường như có một lực kéo lạ theo một hướng khi phóng to và thu nhỏ hình ảnh, thay vì sử dụng vị trí con trỏ chuột làm gốc tọa độ thu phóng. Tôi điên hay có một số lời giải thích hợp lý cho điều này?
Paul Karkoska

Tôi đang cố gắng để làm cho điều này hoạt động ổn định trong một điều khiển ScrollViewer. Tôi đã sửa đổi một chút để sử dụng vị trí cusor làm gốc tọa độ (để phóng to và thu nhỏ bằng vị trí chuột), nhưng thực sự có thể sử dụng một số đầu vào về cách làm cho nó hoạt động bên trong ScrollViewer. Cảm ơn!
Paul Karkoska
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.