แพนและซูมภาพ


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

วิธีที่ฉันแก้ปัญหานี้คือวางภาพไว้ใน Border โดยตั้งค่าคุณสมบัติ ClipToBounds เป็น True จากนั้น RenderTransformOrigin บนภาพจะถูกตั้งค่าเป็น 0.5,0.5 ดังนั้นภาพจะเริ่มซูมตรงกลางภาพ RenderTransform ยังถูกตั้งค่าเป็น TransformGroup ที่มี ScaleTransform และ TranslateTransform

จากนั้นฉันจัดการเหตุการณ์ 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();
}

สำหรับมือจับการเลือกสำหรับการปรับขนาดสามารถทำได้โดยใช้เครื่องประดับโปรดอ่านบทความนี้สำหรับข้อมูลเพิ่มเติม


9
อย่างไรก็ตามข้อสังเกตประการหนึ่งการเรียก CaptureMouse ใน image_MouseLeftButtonDown จะส่งผลให้มีการเรียกใช้ image_MouseMove โดยที่จุดเริ่มต้นยังไม่ได้เริ่มต้น - ในรหัสด้านบนจะเป็นศูนย์โดยบังเอิญ แต่ถ้าจุดเริ่มต้นเป็นอื่นที่ไม่ใช่ (0,0) ภาพ จะได้สัมผัสกับการกระโดดระยะสั้น ดังนั้นฉันคิดว่าควรเรียก image.CaptureMouse () ที่ส่วนท้ายของ image_MouseLeftButtonDown จะดีกว่าเพื่อแก้ไขปัญหานี้
Andrei Pana

2
สองสิ่ง. 1) มีข้อผิดพลาดเกี่ยวกับ image_MouseWheel คุณต้องได้รับ ScaleTransform ในลักษณะเดียวกับที่คุณได้รับ TranslateTransform นั่นคือส่งไปยัง TransformGroup จากนั้นเลือกและส่ง Child ที่เหมาะสม 2) หากการเคลื่อนไหวของคุณกระวนกระวายใจโปรดจำไว้ว่าคุณไม่สามารถใช้ภาพเพื่อรับตำแหน่งเมาส์ของคุณได้ (เนื่องจากเป็นไดนามิก) คุณต้องใช้สิ่งที่คงที่ ในตัวอย่างนี้จะใช้เส้นขอบ
Dave

170

หลังจากใช้ตัวอย่างจากคำถามนี้ฉันได้สร้างแอพแพนและซูมเวอร์ชันสมบูรณ์พร้อมการซูมที่เหมาะสมเมื่อเทียบกับตัวชี้เมาส์ รหัสการแพนและการซูมทั้งหมดถูกย้ายไปยังคลาสแยกที่เรียกว่า 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!" หรือ "Great Work" ฉันแค่อยากจะบอกว่า Nice Job and Great Work นี่คืออัญมณี 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 แอปโน้ต


1
มีข้อเสนอแนะเกี่ยวกับวิธีทำให้ใช้งานได้ใน Silverlight 3 หรือไม่? ฉันมีปัญหากับ Vector และลบจุดหนึ่งออกจากอีกจุดหนึ่ง ... ขอบคุณ
Number8

@ Number8 โพสต์การใช้งานที่ใช้งานได้ใน Silverlight 3 สำหรับคุณด้านล่าง :)
Henry C

4
ข้อเสียเปรียบเล็กน้อย - ภาพโตขึ้นพร้อมกับเส้นขอบและไม่อยู่ในขอบ
โช

พวกคุณช่วยแนะนำวิธีใช้สิ่งเดียวกันในแอพสไตล์ windows 8 metro ได้ไหม .. ทำงานบน c #, xaml บน windows8
raj

1
ใน image_MouseWheel คุณสามารถทดสอบค่า transform.ScaleX และ ScaleY และถ้าค่าเหล่านั้น + ซูม> ขีด จำกัด ของคุณอย่าใช้เส้น + = ซูม
Kelly

10

ลองใช้การควบคุมการซูม: http://wpfextensions.codeplex.com

การใช้การควบคุมนั้นง่ายมากโดยอ้างอิงถึงแอสเซมบลี wpfextensions มากกว่า:

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

ไม่รองรับแถบเลื่อนในขณะนี้ (จะอยู่ในรุ่นถัดไปซึ่งจะวางจำหน่ายในหนึ่งหรือสองสัปดาห์)


ใช่สนุกกับมัน ส่วนที่เหลือของห้องสมุดของคุณค่อนข้างเล็กน้อยแม้ว่า
EightyOne Unite

ดูเหมือนว่าจะไม่มีการสนับสนุนโดยตรงสำหรับ "แสดงภาพซ้อนทับ (ตัวอย่างเช่นการเลือกสี่เหลี่ยมผืนผ้า)" แต่สำหรับพฤติกรรมการซูม / การแพนมันเป็นการควบคุมที่ยอดเยี่ยม
jsirr13

9
  • แพน: ใส่รูปภาพไว้ใน Canvas ใช้เมาส์ขึ้นลงและย้ายเหตุการณ์เพื่อย้ายคุณสมบัติ Canvas.Top, Canvas.Left เมื่อลงคุณจะทำเครื่องหมาย isDraggingFlag เป็น true เมื่อคุณตั้งค่าแฟล็กเป็นเท็จ ในการย้ายคุณจะตรวจสอบว่าแฟล็กถูกตั้งค่าไว้หรือไม่ถ้าคุณออฟเซ็ต Canvas.Top และ Canvas คุณสมบัติด้านซ้ายบนรูปภาพภายในแคนวาส
  • ซูม: ผูกแถบเลื่อนเข้ากับ Scale Transform of the Canvas
  • แสดงภาพซ้อนทับ: เพิ่มผืนผ้าใบเพิ่มเติมโดยไม่มีพื้นหลังบนผืนผ้าใบที่มีรูปภาพ
  • แสดงภาพต้นฉบับ: การควบคุมรูปภาพภายใน ViewBox

4

@Anothen และ @ Number8 - คลาส Vector ไม่มีให้ใช้งานใน Silverlight ดังนั้นเพื่อให้ใช้งานได้เราเพียงแค่ต้องเก็บบันทึกตำแหน่งสุดท้ายที่เห็นในครั้งสุดท้ายที่มีการเรียกเหตุการณ์ MouseMove และเปรียบเทียบทั้งสองจุดเพื่อหาความแตกต่าง ; จากนั้นปรับการแปลง

XAML:

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

behind รหัส:

    public Point _mouseClickPos;
    public bool bMoving;


    public MainPage()
    {
        InitializeComponent();
        viewboxMain.RenderTransform = new CompositeTransform();
    }

    void MouseMoveHandler(object sender, MouseEventArgs e)
    {

        if (bMoving)
        {
            //get current transform
            CompositeTransform transform = viewboxMain.RenderTransform as CompositeTransform;

            Point currentPos = e.GetPosition(viewboxBackground);
            transform.TranslateX += (currentPos.X - _mouseClickPos.X) ;
            transform.TranslateY += (currentPos.Y - _mouseClickPos.Y) ;

            viewboxMain.RenderTransform = transform;

            _mouseClickPos = currentPos;
        }            
    }

    void MouseClickHandler(object sender, MouseButtonEventArgs e)
    {
        _mouseClickPos = e.GetPosition(viewboxBackground);
        bMoving = true;
    }

    void MouseReleaseHandler(object sender, MouseButtonEventArgs e)
    {
        bMoving = false;
    }

โปรดทราบว่าคุณไม่จำเป็นต้องมี 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 ที่ติดตั้ง lambda expression คุณสามารถใช้รหัสต่อไปนี้:

//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 3.0 หรือ 2.0 ได้

หวังว่าจะช่วยคุณได้ :-)


2

ยังเป็นอีกเวอร์ชันหนึ่งของการควบคุมประเภทเดียวกัน มีฟังก์ชั่นการทำงานที่คล้ายกัน แต่เพิ่ม:

  1. การสนับสนุนแบบสัมผัส (ลาก / หยิก)
  2. ภาพสามารถลบได้ (โดยปกติ Image control จะล็อกภาพบนดิสก์ดังนั้นคุณจึงไม่สามารถลบได้)
  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;ในภาพ
MouseWheel

1

การดำเนินการนี้จะซูมเข้าและออกรวมทั้งแพน แต่ให้รูปภาพอยู่ในขอบเขตของคอนเทนเนอร์ เขียนเป็นตัวควบคุมดังนั้นให้เพิ่มสไตล์ลงในApp.xamlไฟล์Themes/Viewport.xaml.

เพื่อความสามารถในการอ่านฉันได้อัปโหลดสิ่งนี้บนgistและgithub

ฉันยังบรรจุสิ่งนี้ไว้ใน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>

การใช้งาน:

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

มีปัญหาใด ๆ ให้ฉันตะโกน

มีความสุขในการเขียนโค้ด :)


เยี่ยมมากฉันรักเวอร์ชันนี้ มีวิธีใดในการเพิ่มแถบเลื่อนหรือไม่?
Etienne Charland

โดยวิธีที่คุณใช้คุณสมบัติการพึ่งพาไม่ถูกต้อง สำหรับการซูมและแปลคุณไม่สามารถใส่รหัสในตัวตั้งค่าคุณสมบัติได้เนื่องจากไม่มีการเรียกเลยเมื่อผูก คุณต้องลงทะเบียนตัวจัดการ Change and Coerce บนคุณสมบัติการพึ่งพาตัวเองและทำงานในนั้น
Etienne Charland

ฉันเปลี่ยนคำตอบนี้อย่างมากตั้งแต่เขียนมันอัปเดตด้วยการแก้ไขสำหรับปัญหาบางอย่างที่ฉันเคยใช้ในการผลิตในภายหลัง
Adam H

วิธีนี้ดีมาก แต่ฉันไม่สามารถเข้าใจได้ว่าทำไมฟังก์ชั่นการเลื่อนล้อเลื่อนของเมาส์ดูเหมือนจะมีการดึงที่แปลกไปในทิศทางเดียวเมื่อซูมเข้าและออกจากภาพแทนที่จะใช้ตำแหน่งตัวชี้เมาส์เป็นจุดเริ่มต้นการซูม ฉันบ้าหรือมีคำอธิบายที่สมเหตุสมผลสำหรับสิ่งนี้หรือไม่?
Paul Karkoska

ฉันพยายามดิ้นรนเพื่อให้สิ่งนี้ทำงานได้อย่างสม่ำเสมอภายในการควบคุม ScrollViewer ฉันแก้ไขเล็กน้อยเพื่อใช้ตำแหน่ง cusor เป็นจุดเริ่มต้นของมาตราส่วน (เพื่อซูมเข้าและออกโดยใช้ตำแหน่งเมาส์) แต่สามารถใช้อินพุตบางอย่างในการทำให้มันทำงานภายใน ScrollViewer ได้ ขอบคุณ!
Paul Karkoska
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.