tags:

views:

13280

answers:

13

I want to create a simple image viewer in WPF that will enable the user to:

  • Pan (by mouse dragging the image).
  • Zoom (with a slider).
  • Show overlays (rectangle selection for example).
  • Show original image (with scroll bars if needed).

Can you explain how to do it?

I didn't find a good sample on the web. Should I use ViewBox? Or ImageBrush? Do I need ScrollViewer?

Thanks!

A: 

My London Underground sample does this, although with a map rather than a static image.

HTH, Kent

Kent Boogaart
+5  A: 
  • Pan: Put the image inside of a Canvas. Implement Mouse Up, Down, and Move events to move the Canvas.Top, Canvas.Left properties. When down, you mark a isDraggingFlag to true, when up you set the flag to false. On move, you check if the flag is set, if it is you offset the Canvas.Top and Canvas.Left properties on the image within the canvas.
  • Zoom: Bind the slider to the Scale Transform of the Canvas
  • Show overlays: add additional canvas's with no background ontop the canvas containing the image.
  • show original image: image control inside of a ViewBox
markti
+11  A: 

The way I solved this problem was to place the image within a Border with it's ClipToBounds property set to True. The RenderTransformOrigin on the image is then set to 0.5,0.5 so the image will start zooming on the center of the image. The RenderTransform is also set to a TransformGroup containing a ScaleTransform and a TranslateTransform.

I then handled the MouseWheel event on the image to implement zooming

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

To handle the panning the first thing I did was to handle the MouseLeftButtonDown event on the image, to capture the mouse and to record it's location, I also store the current value of the TranslateTransform, this what is updated to implement 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);
}

Then I handled the MouseMove event to update the 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;
    }
}

Finally don't forget to release the mouse capture.

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

As for the selection handles for resizing this can be accomplished using an adorner, check out this article for more information.

Ian Oakes
+1 for thoroughness.
Stimul8d
+1  A: 

A little improvement needed, I think. Does anyone know how to make proper zooming relative to mouse pointer? So that when you zoom-in the relative position of mouse pointer inside the control boundaries stays the same.

Setting RenderTransformOrigin brings terrible behavior, because when you change mouse position the whole control jumps to center at that point.

arconaut
+4  A: 

Try this Zoom Control: http://wpfextensions.codeplex.com

usage of the control is very simple, reference to the wpfextensions assembly than:

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

Scrollbars not supported at this moment. (It will be in the next release which will be available in one or two week).

Palesz
very very cool
Christian Hagelid
Yup, enjoying that. The rest of thee library is pretty trivial though.
Stimul8d
+5  A: 

The answer was posted above but wasn't complete. here is the completed version:

XAML

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MapTest.Window1"
x:Name="Window"
Title="Window1"
Width="1950" Height="1546" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Controls="clr-namespace:WPFExtensions.Controls;assembly=WPFExtensions" mc:Ignorable="d" Background="#FF000000">

<Grid x:Name="LayoutRoot">
 <Grid.RowDefinitions>
  <RowDefinition Height="52.92"/>
  <RowDefinition Height="*"/>
 </Grid.RowDefinitions>

    <Border Grid.Row="1" Name="border">
        <Image Name="image" Source="map3-2.png" Opacity="1" RenderTransformOrigin="0.5,0.5"  />
    </Border>

</Grid>

Code Behind

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;
        }
    }
}
Kelly
Any suggestions on how to make this usable in Silverlight 3? I have problems with Vector and subtracting one Point from another... Thanks.
Number8
+1 Nice work Kelly. That worked perfectly. Thanks.
David HAust
@Number8 Posted an implementation that works in Silverlight 3 for you below :)
Blakomen
A: 

To get a professional Zoom Control for WPF check out the ZoomPanel.

It is not free, but is very easy to use and has many features - animated zooming and panning, support for ScrollViewer, mouse wheel support, included ZoomController (with move, zoom in, zoom out, rectangle zoom, reset buttons). It also comes with many code samples.

Andrej Benedik
A: 

To zoom relative to the mouse position, all you need is:

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

i second number8's question. Is there any way you could make the MouseMove method work in Silverlight 3?

anothen
A: 

I wrote an article on codeproject.com on the implementation of a zoom and pan control for WPF.

http://www.codeproject.com/KB/WPF/zoomandpancontrol.aspx

Ashley Davis
+1  A: 

@Anothen and @Number8 - The Vector class is not available in Silverlight, so to make it work we just need to keep a record of the last position sighted the last time the MouseMove event was called, and compare the two points to find the difference; then adjust the transform.

XAML:

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

Code-behind:

    public Point _mouseClickPos;
    public bool bMoving;


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

    void MouseMoveHandler(object sender, MouseEventArgs e)
    {

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

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

            viewboxMain.RenderTransform = transform;

            _mouseClickPos = currentPos;
        }            
    }

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

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

Also note that you don't need a TransformGroup or collection to implement pan and zoom; instead, a CompositeTransform will do the trick with less hassle.

I'm pretty sure this is really inefficient in terms of resource usage, but at least it works :)

Blakomen
A: 

Can someone tell me the Linq rip version of above given sample by Kelly since I am not using .Net 3.0

Thanks and Regards Merk

Merk
Sorry it was not linq it was lambda Expression for following lines "var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);"
Merk
A: 

@ Merk

For ur solution insted of lambda expression you can use following code:

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

this code can be use as is for .Net Frame work 3.0 or 2.0

Hope It helps you :-)

nishantcop