팬 & 줌 이미지


131

사용자가 다음을 수행 할 수 있도록 WPF에서 간단한 이미지 뷰어를 만들고 싶습니다.

  • 팬 (이미지를 마우스로 드래그하여).
  • 확대 / 축소 (슬라이더 사용)
  • 오버레이 표시 (예 : 사각형 선택).
  • 원본 이미지를 표시합니다 (필요한 경우 스크롤 막대 포함).

어떻게하는지 설명해 주실 수 있습니까?

웹에서 좋은 샘플을 찾지 못했습니다. ViewBox를 사용해야합니까? 아니면 ImageBrush? ScrollViewer가 필요합니까?


WPF에 대한 전문적인 줌 컨트롤을 얻으려면 ZoomPanel을 확인하십시오 . 무료는 아니지만 사용하기 쉽고 애니메이션 확대 / 축소 및 이동, ScrollViewer 지원, 마우스 휠 지원, ZoomController (이동, 확대, 축소, 사각형 확대 / 축소 버튼 포함)와 같은 많은 기능이 있습니다. 또한 많은 코드 샘플과 함께 제공됩니다.
Andrej Benedik

codeproject.com에 WPF의 확대 / 축소 및 이동 제어 구현에 대한 기사를 썼습니다. codeproject.com/KB/WPF/zoomandpancontrol.aspx
Ashley Davis

잘 찾았어요 무료로 사용해 볼 수 있으며 소프트웨어를 구축하려는 경우 라이선스 당 $ 69 / 컴퓨터를 원합니다. DLL은 사용할 수 있으므로 고객을 막을 수는 없지만 클라이언트를 위해 상업적으로 빌드하는 경우, 특히 타사 유틸리티를 선언하고 개별적으로 라이센스를 부여 해야하는 경우 지불해야합니다. 개발 비용. EULA에서는 "응용 프로그램 별"이라고 말하지 않았지만 구매를 등록하자마자 생성 한 모든 응용 프로그램에 대해 "무료"가되며 유료 라이센스 파일을 복사 할 수 있습니다. 그것으로 구매를 나타냅니다.
vapcguy

답변:


116

이 문제를 해결하는 방법은 ClipToBounds 속성이 True로 설정된 이미지를 Border 내에 배치하는 것입니다. 그런 다음 이미지의 RenderTransformOrigin이 0.5,0.5로 설정되어 이미지가 이미지 중앙에서 확대되기 시작합니다. RenderTransform도 ScaleTransform과 TranslateTransform을 포함하는 TransformGroup으로 설정됩니다.

그런 다음 이미지에서 MouseWheel 이벤트를 처리하여 확대 / 축소를 구현했습니다.

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

패닝을 처리하기 위해 이미지에서 MouseLeftButtonDown 이벤트를 처리하고 마우스를 캡처하고 위치를 기록하는 것이 가장 중요했습니다. 또한 TranslateTransform의 현재 값도 저장합니다.

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

그런 다음 MouseMove 이벤트를 처리하여 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;
    }
}

마지막으로 마우스 캡처를 해제하는 것을 잊지 마십시오.

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

크기 조정을위한 선택 핸들은 adorner를 사용하여 수행 할 수 있습니다 . 자세한 내용은 이 기사 를 확인하십시오.


9
그러나 image_MouseLeftButtonDown에서 CaptureMouse를 호출하면 원점이 아직 초기화되지 않은 image_MouseMove가 호출됩니다. 위의 코드에서는 순전히 0이지만 원점이 (0,0)이 아닌 경우 이미지 짧은 점프를 경험할 것입니다. 따라서이 문제를 해결하기 위해 image_MouseLeftButtonDown의 끝에 image.CaptureMouse ()를 호출하는 것이 좋습니다.
Andrei Pana

2
두가지. 1) image_MouseWheel에 버그가 있으므로 TranslateTransform과 비슷한 방식으로 ScaleTransform을 가져와야합니다. 즉, TransformGroup으로 캐스트 한 다음 적절한 Child를 선택하여 캐스트하십시오. 2) 움직임이 불안한 경우 이미지를 사용하여 마우스 위치를 얻을 수 없다는 것을 기억하십시오 (동적이므로). 정적 인 것을 사용해야합니다. 이 예에서는 테두리가 사용됩니다.
Dave

169

이 질문의 샘플을 사용한 후 마우스 포인터를 기준으로 적절한 확대 / 축소를 사용하여 전체 버전의 팬 및 확대 / 축소 앱을 만들었습니다. 모든 이동 및 확대 / 축소 코드가 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
슬프게도, 나는 당신에게 더 많은 지적을 줄 수 없습니다. 이것은 정말 훌륭합니다.
Tobiel

6
"Nice Job!"에 대한 의견이 차단되기 전에 또는 "훌륭한 일"나는 단지 좋은 일과 위대한 일을 말하고 싶습니다. 이것은 WPF 보석입니다. 그것은 물에서 wpf ext zoombox를 날려 버립니다.
Jesse Seger

4
훌륭해. 나는 오늘 밤 집에 갈 수 있을지도 모른다 ... +1000
Bruce Pierson

1
대박. 나는 그런 구현에 대해하지 않았지만 정말 좋습니다! 정말 고맙습니다!
Noel Widmer

3
좋은 답변입니다! 나는 그래서 "느린"확대하지 않고, 줌 배율에 약간의 보정을 추가 한double zoomCorrected = zoom*st.ScaleX; st.ScaleX += zoomCorrected; st.ScaleY += zoomCorrected;
DELUXEnized

46

답변이 위에 게시되었지만 완료되지 않았습니다. 완성 된 버전은 다음과 같습니다.

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>

뒤에 코드

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

내 웹 사이트 에서이 코드를 사용하는 전체 wpf 프로젝트의 예가 있습니다 : Jot the sticky note app .


1
Silverlight 3에서이 기능을 사용하는 방법에 대한 제안 사항이 있습니까? Vector에 문제가 있고 한 포인트를 다른 포인트에서 빼는 데 ... 고마워요.
번호 8

@ Number8 아래 Silverlight 3에서 작동하는 구현을 게시했습니다. :)
Henry C

4
작은 단점은 - 이미지는 성장 경계, 그리고 내부 경계
itsho

Windows 8 메트로 스타일 앱에서 동일한 것을 구현하는 방법을 제안 할 수 있습니까? windows #에서 c #, xaml로 작업하는 im.
raj

1
image_MouseWheel에서 transform.ScaleX 및 ScaleY 값을 테스트 할 수 있으며 해당 값이 + zoom> 한계 인 경우 + = 줌 선을 적용하지 마십시오.
켈리

10

이 줌 컨트롤을 사용해보십시오 : http://wpfextensions.codeplex.com

컨트롤의 사용법은 매우 간단합니다. wpfextensions 어셈블리를 참조하는 것보다

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

현재 스크롤바는 지원되지 않습니다. (다음 릴리스에서는 1 ~ 2 주 후에 제공 될 예정입니다).


그래, 즐기고있어 그래도 나머지 도서관은 매우 사소합니다.
EightyOne Unite

'오버레이 표시 (예 : 사각형 선택)'를 직접 지원하는 것 같지는 않지만 줌 / 패닝 동작의 경우 훌륭한 컨트롤입니다.
jsirr13

9
  • 팬 : 이미지를 캔버스 안에 넣습니다. Mouse Up, Down 및 Move 이벤트를 구현하여 Canvas.Top, Canvas.Left 속성을 이동합니다. 아래로 내려 가면 isDraggingFlag를 true로, 위로 올리면 플래그를 false로 설정합니다. 이동하면 플래그가 설정되어 있는지, 캔버스 내 이미지에서 Canvas.Top 및 Canvas.Left 속성이 오프셋되어 있는지 확인합니다.
  • 줌 : 슬라이더를 캔버스의 스케일 변환에 바인딩
  • 오버레이 표시 : 이미지가 포함 된 캔버스 위에 배경이없는 추가 캔버스를 추가합니다.
  • 원본 이미지 표시 : ViewBox 내부의 이미지 제어

4

@Anothen 및 @ Number8-Vector 클래스는 Silverlight에서 사용할 수 없으므로 제대로 작동하려면 MouseMove 이벤트가 마지막으로 호출 된 마지막 위치를 기록한 다음 두 지점을 비교하여 차이점을 찾아야합니다. ; 그런 다음 변환을 조정하십시오.

XAML :

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

코드 숨김 :

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

또한 이동 및 확대 / 축소를 구현하기 위해 TransformGroup 또는 컬렉션이 필요하지 않습니다. 대신 CompositeTransform 은 번거 로움없이 트릭을 수행합니다.

나는 이것이 리소스 사용 측면에서 실제로 비효율적이라고 확신하지만 적어도 작동합니다 :)


2

마우스 위치를 기준으로 확대 / 축소하려면 필요한 것은 다음과 같습니다.

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

PictureBox를 사용하고 있는데 RenderTransformOrigin은 더 이상 존재하지 않습니다.
전환

@Switch RenderTransformOrigin은 WPF 컨트롤을위한 것입니다.
Xam

2

@ 머크

람다 식을 넣은 ur 솔루션의 경우 다음 코드를 사용할 수 있습니다.

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

이 코드는 .Net Frame work 3.0 또는 2.0에서 그대로 사용할 수 있습니다.

희망 :-)


2

같은 종류의 제어의 또 다른 버전. 다른 기능과 비슷한 기능을 가지고 있지만 다음을 추가합니다.

  1. 터치 지원 (드래그 / 핀치)
  2. 이미지를 삭제할 수 있습니다 (일반적으로 이미지 컨트롤은 디스크의 이미지를 잠그므로 삭제할 수 없습니다).
  3. 안쪽 테두리 자식이므로 패닝 된 이미지가 테두리와 겹치지 않습니다. 사각형이 둥근 테두리의 경우 ClippedBorder 클래스를 찾으십시오.

사용법은 간단합니다.

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

그리고 코드 :

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
내가 찾은 문제는 XAML에 이미지 경로가 지정된 경우 이미지 객체가 생성되기 전에 (즉, OnLoaded가 호출되기 전에) 렌더링하려고 시도한다는 것입니다. 이 문제를 해결하기 위해 "image = new Image ..."코드를 onLoaded 메소드에서 생성자로 옮겼습니다. 감사.
Mitch

: 다른 문제는 우리가 아무것도하지 않고 약간의 제한을 추가 아무 것도 아니야 볼 수있을 때까지 이미지가 작은로 축소 될 수있다 if (image.ActualWidth*(st.ScaleX + zoom) < 200 || image.ActualHeight*(st.ScaleY + zoom) < 200) //don't zoom out too small. return;image.MouseWheel에
huoxudong125

1

이렇게하면 확대 / 축소 및 축소되지만 이미지는 컨테이너 경계 안에 유지됩니다. 컨트롤로 작성되었으므로 스타일을 App.xaml직접 또는를 통해 추가하십시오 Themes/Viewport.xaml.

가독성을 위해 이것을 gistgithub 에 업로드했습니다.

나는 또한 너겟이것을 포장했다.

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>

용법:

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

어떤 문제라도, 나에게 소리 지르십시오.

행복한 코딩 :)


좋아, 나는이 버전을 좋아한다. 스크롤바를 추가 할 수있는 방법이 있습니까?
Etienne Charland 2016 년

그런데 종속성 속성을 잘못 사용하고 있습니다. 확대 / 축소 및 번역의 경우 바인딩 할 때 코드가 전혀 호출되지 않으므로 속성 설정기에 코드를 넣을 수 없습니다. 종속성 및 등록 정보 자체에 Change 및 Coerce 핸들러를 등록하고 거기에서 작업을 수행해야합니다.
Etienne Charland

이 답변을 작성한 후이 답변을 크게 변경했습니다. 나중에 프로덕션에서 사용했던 일부 문제에 대한 수정 사항으로 업데이트했습니다.
Adam H

이 솔루션은 훌륭하지만 마우스 포인터 위치를 줌 원점으로 사용하는 대신 마우스 휠 스크롤 기능이 이미지를 확대 / 축소 할 때 한 방향으로 이상한 당기는 이유를 알 수 없습니다. 내가 미쳤거나 이것에 대한 논리적 설명이 있습니까?
Paul Karkoska

이것이 ScrollViewer 컨트롤 내에서 일관되게 작동하도록 노력하고 있습니다. 마우스 위치를 사용하여 확대 및 축소하기 위해 커서 위치를 스케일 원점으로 사용하도록 약간 수정했지만 실제로 ScrollViewer 내에서 작동하도록하는 방법에 대한 입력을 사용할 수 있습니다. 감사!
Paul Karkoska
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.