Pinch-zoom an image but starting zoomed out - c#

I'm writing Pinch/Zoom into my application, I have it working pretty good but am running into a problem or two. First, I want the image to start off, fully zoomed out. Where you see the whole image. Second, I want the application to stop zooming out when it reaches that point. So you can keep pinching/panning but it doesn't continue to calculate. Thanks in advance for you help. See below for the code.
public MainWindow()
{
InitializeComponent();
var files = System.IO.Directory.GetFiles(#"D:\", "*.jpg");
MainImage.Source = new BitmapImage(new Uri(files[0]));
}
private void Image_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void Image_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
int nrOfPoints = 0;
lock (mutex)
{
nrOfPoints = nrOfTouchPoints;
}
if (nrOfPoints >= 2)
{
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
else
{
ScrollViewerParent.ScrollToHorizontalOffset(ScrollViewerParent.HorizontalOffset - e.DeltaManipulation.Translation.X);
ScrollViewerParent.ScrollToVerticalOffset(ScrollViewerParent.VerticalOffset - e.DeltaManipulation.Translation.Y);
}
}
private void MainImage_TouchDown(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
}
}
private void MainImage_TouchUp(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints--;
}
}
<ScrollViewer Grid.Row="1"
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<Image MouseDown="MainImage_MouseDown"
x:Name="MainImage"
IsManipulationEnabled="True"
TouchDown="MainImage_TouchDown"
TouchUp="MainImage_TouchUp"
ManipulationDelta="Image_ManipulationDelta"
ManipulationStarting="Image_ManipulationStarting"/>
</ScrollViewer>

Related

Which part of the code make Drawing on bitmap slow...C# [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 6 years ago.
Improve this question
Code running slow.. the code generates multiple circles have a center can be specified by first click then change the radius by moving the pointer to desired circle size then click to finish drawing the circle.. and repeat to draw another circle..I solved the problem by very simple step see the comments down
public partial class Form1 : Form
{
public class Seat
{
private string _SeatKey;
private Rectangle _SeatRectangle;
public Seat(string seatKey, Rectangle seatRectangle)
{
_SeatKey = seatKey;
_SeatRectangle = seatRectangle;
}
public string SeatKey
{
get { return _SeatKey; }
}
public Rectangle SeatRectangle
{
get { return _SeatRectangle; }
set { _SeatRectangle = value; }
}
}
List<Seat> _Seats = new List<Seat>();
List<Seat> _center = new List<Seat>();
public Form1()
{
InitializeComponent();
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
for (int i = 0; i < 30; i = i + 1)
{
string mystring = "regctangle" + i.ToString();
_Seats.Add(new Seat(mystring, new Rectangle(50, 50, 50, 50)));
}
for (int i = 0; i < 30; i = i + 1)
{
string mystring = "center" + i.ToString();
_center.Add(new Seat(mystring, new Rectangle(50 , 50 , 3, 3)));
}
}
Bitmap background;
Graphics scG;
private Point clickCurrent = Point.Empty;
private Point clickPrev = Point.Empty;
int clikno = 1;
int xpos;
int ypos;
int clicknew = 0;
int radius=0;
int recH;
int recW;
int xcen;
int ycen;
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
clikno = clikno + 1;
clicknew = clicknew + 1;
}
private void Form1_Load(object sender, EventArgs e)
{
background = new Bitmap(Width, Height);
scG = Graphics.FromImage(background);
}
protected override void OnPaint(PaintEventArgs pe)
{
pe.Graphics.DrawImage(Draw(), 0, 0);
}
public Bitmap Draw()
{
Graphics scG = Graphics.FromImage(background);
Pen myPen = new Pen(System.Drawing.Color.Red, 1/2);
Pen mPen = new Pen(System.Drawing.Color.Black, 3);
scG.Clear(SystemColors.Control);
_Seats[clikno].SeatRectangle = new Rectangle(xpos, ypos, recH, recW);
_center[clikno].SeatRectangle = new Rectangle(xcen, ycen, 3, 3);
for (int i = 0; i < clikno+1; i = i + 1)
{
scG.DrawEllipse(myPen, _Seats[i].SeatRectangle);
scG.DrawEllipse(mPen, _center[i].SeatRectangle);
}
Refresh();//This what cause the code running slow
return background;
}
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
AutoSize = true;
clickCurrent = this.PointToClient(Cursor.Position);
clickPrev = clickCurrent;
if (clickPrev == Point.Empty) return;
Refresh();
clickCurrent = Point.Empty;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
double oradius = Math.Sqrt((Math.Pow(clickPrev.X - e.X, 2)) + (Math.Pow(clickPrev.Y - e.Y, 2)));
radius = Convert.ToInt32(oradius);
if (clicknew == 1)
{
recH = radius;
recW = radius;
xpos = clickPrev.X - recW / 2;
ypos = clickPrev.Y - recH / 2;
xcen = clickPrev.X - 3 / 2;
ycen = clickPrev.Y - 3 / 2;
Refresh();
}
if (clicknew == 2)
clicknew = 0;
Refresh();
}
}
The part that is so slow is your MouseMove.
Usually one checks for the left mousebutton to be pressed:
private void yourDrawingControl_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button.HasFlag(MouseButtons.Left) )
{
// draw stuff
}
}
If that is not a condition that applies you should check for having moved for at least more than one pixel:
Point oldLocation = Point.Empty;
private void yourDrawingControl_MouseMove(object sender, MouseEventArgs e)
{
int minStep = 3;
if ( (Math.Abs(oldLocation.X - e.X) + Math.Abs(oldLocation.Y - e.Y) > minStep) )
{
// draw stuff
}
oldLocation = e.Location;
}
Also: While the graphics is being built up, do not draw into a Bitmap which you then draw with DrawImage in your Paint event. Instead in the Paint event draw directly onto the surfce of the control from a List<yourDrawingshapeClass> !
Drawing hundreds of Rectangles is extremely fast compared to drawing even one of your Bitmaps..
Also: It looks as if you are drawing onto the Form? Better draw onto a dedicated control with just the right size; the natural choice is a PictureBox, which is made for this and is double-buffered out-of-the-box. Instead your enforce the form to refresh all it controls..
Finally: Even if you refuse to change your convoluted code to something well-proven, at least make it not call Refresh in the MouseMove unconditionally (in the last line), even if nothing at all has changed!

Moving the dynamically drawn rectangle inside the canvas using MouseMove event

I'm trying to move a dynamically drawn rectangle inside the canvas. I’m able to draw the rectangle dynamically with in the canvas and while trying to move the rectangle inside the canvas I’m facing problem
XAML :
<Grid x:Name="Gridimage1" Margin="0,0,411,100">
<Image Name="image1" HorizontalAlignment="Left" Stretch="Fill" VerticalAlignment="Top"></Image>
<Canvas x:Name="BackPanel" Margin="20,67,0,0" Height="317" Width="331">
<Rectangle x:Name="selectionRectangle" Stroke="LightBlue" Fill="#220000FF"/>
</Canvas>
</Grid>
C# :
After drawing the rectangle dynamically I'm adding the below mouse events.
selectionRectangle.MouseLeftButtonDown += new MouseButtonEventHandler(Rect1_MouseDown);
selectionRectangle.MouseMove += new MouseEventHandler(Rectangle_MouseMove_1);
selectionRectangle.MouseUp += new MouseButtonEventHandler(Rect1_MouseUp);
# region "rectangle move"
private bool drag = false;
private Point startPt;
private int wid;
private int hei;
private Point lastLoc;
private double CanvasLeft, CanvasTop;
private void Rect1_MouseDown(object sender, MouseButtonEventArgs e)
{
drag = true;
Cursor = Cursors.Hand;
startPt = e.GetPosition(BackPanel);
wid = (int)selectionRectangle.Width;
hei = (int)selectionRectangle.Height;
lastLoc = new Point(Canvas.GetLeft(selectionRectangle), Canvas.GetTop(selectionRectangle));
Mouse.Capture((IInputElement)sender);
}
private void Rectangle_MouseMove_1(object sender, MouseEventArgs e)
{
try
{
if (drag)
{
var newX = (startPt.X + (e.GetPosition(BackPanel).X - startPt.X));
var newY = (startPt.Y + (e.GetPosition(BackPanel).Y - startPt.Y));
Point offset = new Point((startPt.X - lastLoc.X), (startPt.Y - lastLoc.Y));
CanvasTop = newY - offset.Y;
CanvasLeft = newX - offset.X;
selectionRectangle.SetValue(Canvas.TopProperty, CanvasTop);
selectionRectangle.SetValue(Canvas.LeftProperty, CanvasLeft);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void Rect1_MouseUp(object sender, MouseButtonEventArgs e)
{
drag = false;
Cursor = Cursors.Arrow;
Mouse.Capture(null);
}
#endregion
Problem: I'm able to move the rectangle all over the window. I want only to move the rectangle inside the canvas margin.
I'm able to move the rectangle outside the canvas
You should be able to get the Bounds of your selectionRectangle and see if they exceed the Width and/or Height of your canvas before committing the drag operation.
selectionRectangle.MouseMove += new MouseEventHandler(Rectangle_MouseMove_1);
private bool drag = false;
private Point startPt;
private int wid;
private int hei;
private Point lastLoc;
private double CanvasLeft, CanvasTop;
private void Rectangle_MouseMove_1(object sender, MouseEventArgs e)
{
try
{
if (drag)
{
var newX = (startPt.X + (e.GetPosition(BackPanel).X - startPt.X));
var newY = (startPt.Y + (e.GetPosition(BackPanel).Y - startPt.Y));
Point offset = new Point((startPt.X - lastLoc.X), (startPt.Y - lastLoc.Y));
CanvasTop = newY - offset.Y;
CanvasLeft = newX - offset.X;
// check if the drag will pull the rectangle outside of it's host canvas before performing
// TODO: protect against lower limits too...
if ((CanvasTop + selectionRectangle.Height > BackPanel.Height) || (CanvasLeft + selectionRectangle.Width > BackPanel.Width) || CanvasTop < 0 || CanvasLeft < 0)
{
return;
}
selectionRectangle.SetValue(Canvas.TopProperty, CanvasTop);
selectionRectangle.SetValue(Canvas.LeftProperty, CanvasLeft);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
if ((CanvasTop + selectionRectangle.ActualHeight > BackPanel.ActualHeight))
{
CanvasTop = BackPanel.ActualHeight - selectionRectangle.ActualHeight;
}
if (CanvasLeft + selectionRectangle.ActualWidth > BackPanel.ActualWidth)
{
CanvasLeft = BackPanel.ActualWidth - selectionRectangle.ActualWidth;
}
if (CanvasTop < 0)
{
CanvasTop = 0;
}
if (CanvasLeft < 0)
{
CanvasLeft = 0;
}

How to arrange Thumbs with a Line for moving a line in a WPF custom Adorner

I am working on a Adorner for a Line in a drawing program using WPF. The Line is drawn in code-behind and then adorned with my custom Adorner called LineAdorner. I've managed to use a Thumb for the start and end point of the Line. It's working fine for resizing. My problem is i cant able to move (drag & drop) the line, how to do that?
public class ResizingAdorner : Adorner
{
// Resizing adorner uses Thumbs for visual elements.
// The Thumbs have built-in mouse input handling.
//Thumb topLeft, topRight, bottomLeft, bottomRight;
private Thumb startThumb;
private Thumb endThumb;
private Line selectedLine;
private Point startPoint;
private Point endPoint;
// To store and manage the adorner's visual children.
VisualCollection visualChildren;
bool IsControlModeOn = false;
// Override the VisualChildrenCount and GetVisualChild properties to interface with
// the adorner's visual collection.
protected override int VisualChildrenCount { get { return visualChildren.Count; } }
protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
// Initialize the ResizingAdorner.
public ResizingAdorner(UIElement adornedElement)
: base(adornedElement)
{
visualChildren = new VisualCollection(this);
selectedLine = AdornedElement as Line;
startThumb = new Thumb { Cursor = Cursors.Hand, Width = 8, Height = 8, Background = Brushes.Green };
endThumb = new Thumb { Cursor = Cursors.Hand, Width = 8, Height = 8, Background = Brushes.BlueViolet };
startThumb.DragDelta += StartDragDelta;
endThumb.DragDelta += EndDragDelta;
startThumb.DragCompleted += new DragCompletedEventHandler(startThumb_DragCompleted);
endThumb.DragCompleted += new DragCompletedEventHandler(endThumb_DragCompleted);
visualChildren.Add(startThumb);
visualChildren.Add(endThumb);
}
public event EndDragDeltaEvent endDragDeltaEvent;
public delegate void EndDragDeltaEvent(object obj, DragCompletedEventArgs e, bool isEnd);
void startThumb_DragCompleted(object sender, DragCompletedEventArgs e)
{
if (endDragDeltaEvent != null)
endDragDeltaEvent(selectedLine, e, false);
}
void endThumb_DragCompleted(object sender, DragCompletedEventArgs e)
{
if (endDragDeltaEvent != null)
endDragDeltaEvent(selectedLine, e, true);
}
// Arrange the Adorners.
protected override Size ArrangeOverride(Size finalSize)
{
selectedLine = AdornedElement as Line;
double left = Math.Min(selectedLine.X1, selectedLine.X2);
double top = Math.Min(selectedLine.Y1, selectedLine.Y2);
var startRect = new Rect(selectedLine.X1 - (startThumb.Width / 2), selectedLine.Y1 - (startThumb.Width / 2), startThumb.Width, startThumb.Height);
startThumb.Arrange(startRect);
var endRect = new Rect(selectedLine.X2 - (endThumb.Width / 2), selectedLine.Y2 - (endThumb.Height / 2), endThumb.Width, endThumb.Height);
endThumb.Arrange(endRect);
return finalSize;
}
private void StartDragDelta(object sender, DragDeltaEventArgs e)
{
Point position = Mouse.GetPosition(this);
selectedLine.X1 = position.X;
selectedLine.Y1 = position.Y;
}
// Event for the Thumb End Point
private void EndDragDelta(object sender, DragDeltaEventArgs e)
{
Point position = Mouse.GetPosition(this);
selectedLine.X2 = position.X;
selectedLine.Y2 = position.Y;
}
protected override void OnRender(DrawingContext drawingContext)
{
if (AdornedElement is Line)
{
selectedLine = AdornedElement as Line;
startPoint = new Point(selectedLine.X1, selectedLine.Y1);
endPoint = new Point(selectedLine.X2, selectedLine.Y2);
}
}
}
You'll need to handle the MouseDown, MouseMove and MouseUp events on the line to be able to do that:
Add handlers for those events in the constructor
selectedLine.MouseLeftButtonDown += SelectedLineOnMouseLeftButtonDown;
selectedLine.MouseMove += SelectedLineOnMouseMove;
selectedLine.MouseLeftButtonUp += SelectedLineOnMouseLeftButtonUp;
And the implementation is something like this
private Point origin;
private void SelectedLineOnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
Line line = (Line) sender;
line.CaptureMouse();
startPoint = new Point(line.X1, line.Y1);
endPoint = new Point(line.X2, line.Y2);
origin = e.GetPosition(line);
base.OnMouseLeftButtonDown(e);
}
private void SelectedLineOnMouseMove(object sender, MouseEventArgs e)
{
base.OnMouseMove(e);
Line line = (Line) sender;
if (e.MouseDevice.LeftButton == MouseButtonState.Pressed)
{
Point position = e.GetPosition(this);
e.Handled = true;
double horizontalDelta = position.X - origin.X;
double verticalDelta = position.Y - origin.Y;
line.X1 = startPoint.X + horizontalDelta;
line.X2 = endPoint.X + horizontalDelta;
line.Y1 = startPoint.Y + verticalDelta;
line.Y2 = endPoint.Y + verticalDelta;
InvalidateArrange();
}
else
{
line.ReleaseMouseCapture();
}
}
private void SelectedLineOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
Line line = (Line) sender;
line.ReleaseMouseCapture();
e.Handled = true;
base.OnMouseLeftButtonUp(e);
}
I also added some InvalidateArrange() calls in the StartDragDelta and EndDragDelta handlers to makes sure the Thumbs move when being dragged.

Shooting game Help

I'm making a shooting game like Space Invaders. Every time I launched the missile, it's always on the same position. How will I change it depending on the place where the spaceship is.
Here's my codes for now.
class GraphicsApplication
{
private Form f;
private PictureBox pb;
private PictureBox pb1;
private PictureBox pb2;
private Boolean bMove;
Timer Clock = new Timer();
Timer Missile = new Timer();
int x = 0;
public GraphicsApplication()
{
f = new Form();
pb = new PictureBox();
pb1 = new PictureBox();
pb2 = new PictureBox();
bMove = false;
}
public void Launch()
{
f.Size = new Size(600, 600);
f.StartPosition = FormStartPosition.CenterScreen;
f.KeyDown += new KeyEventHandler(f_KeyDown);
f.KeyPress += new KeyPressEventHandler(f_KeyPress);
pb.SetBounds(300, 470, 70, 70);
pb.Image = new Bitmap("spaceship.png");
pb.SizeMode = PictureBoxSizeMode.StretchImage;
f.Controls.Add(pb);
pb1.Image = Image.FromFile("spacedisc.png");
pb1.SetBounds(20, 20, 130, 80);
pb1.SizeMode = PictureBoxSizeMode.StretchImage;
f.Controls.Add(pb1);
pb2.Image = Image.FromFile("missile.png");
pb2.SetBounds(pb.Location.X, pb.Location.Y, 25, 40); //pb2 missile //pb spaceship
pb2.SizeMode = PictureBoxSizeMode.StretchImage;
Clock = new Timer();
Clock.Interval = 40;
Clock.Start();
Clock.Tick += new EventHandler(Clock_Tick);
Missile = new Timer();
Missile.Interval = 40;
Missile.Tick += new EventHandler(Missile_Tick);
f.ShowDialog();
}
private void f_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
Missile.Start();
}
}
public void Missile_Tick(object sender, EventArgs e)
{
if (bMove == true)
{
f.Controls.Add(pb2);
pb2.Top = pb2.Top -= 5;
}
}
private void f_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == 'd')
{
pb.Left = pb.Left += 5;
}
if (e.KeyChar == 'a')
{
pb.Left = pb.Left -= 5;
}
}
public void Clock_Tick(object sender, EventArgs e)
{
if(x == 400)
{
bMove = true;
}
else if (x == 30)
{
bMove = false;
}
if (bMove == false)
{
x += 5;
pb1.Location = new Point(20 + x, 20);
}
else
{
x -= 5;
pb1.Location = new Point(x - 20, 20);
}
}
}
}
You probably want something like
pb2.Location.X = pb.Location.X;
pb2.Location.Y = pb.Location.Y;
in your f_KeyDown() function, so that the missile starts in the same location as the spaceship.
You have to position your bullets, rockets...etc. relative to your space ship's gun.
Imagine a gun that is mounted on the ship. You could represent this gun with an object.
For example:
public class Gun
{
private ISpaceshipDesign _spaceshipDesign;
public Gun(ISpaceshipDesign spaceshipDesign)
{
this._spaceshipDesign = spaceshipDesign;
}
public void Fire()
{
//...
}
}
Pass in a reference to your spaceship when creating the gun, so that you know onto which spaceship the gun is mounted.
The spaceship should always know where it is in the 2D-plane (X, Y coördinates). It should also know where on the spaceship the gun is mounted.
public interface ISpaceshipDesign
{
public Point GunLocation { get; }
}
The GunLocation property must return the gun's location relative to the ship's current position. For example:
public Point GunLocation
{
get
{
double x = (double) this.GetValue(Canvas.LeftProperty) + 21;
double y = (double) this.GetValue(Canvas.TopProperty) + 17;
return new Point(x, y);
}
}
You can then access this data in the Gun's Fire() method.
For example:
public void Fire()
{
Point gunLocation = _spaceshipDesign.GunLocation;
// Position your missle using the gun's current coördinates (X, Y).
}
About a year ago I wrote a 10-series part about creating a similar game (Asteroids) in Silverlight. One article discusses how to make the gun fire. You can find it here:
https://github.com/geersch/SilverlightAsteroids/blob/master/src/part-6/README.md
You can choose to mount several guns on the ship, one that fires regular bullets, another one for missles...etc. Each gun would have a different location on the ship. You can alter the Fire() method to be triggered by different keys (A = missle, space = bullets).
Hope this helps.

Pan & Zoom Image

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?
After using samples from this question I've made complete version of pan & zoom app with proper zooming relative to mouse pointer. All pan & zoom code has been moved to separate class called 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();
}
}
}
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.
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;
}
}
}
I have some source code demonstrating this Jot the sticky note app.
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
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).
#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 :)
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);
I also tried this answer but was not entirely happy with the result. I kept googling around and finally found a Nuget Package that helped me to manage the result I wanted, anno 2021. I would like to share it with the former developers of Stack Overflow.
I used this Nuget Package Gu.WPF.Geometry found via this Github Repository. All credits for develoment should go to Johan Larsson, the owner of this package.
How I used it? I wanted to have the commands as buttons below the zoombox, as shown here in MachineLayoutControl.xaml .
<UserControl
x:Class="MyLib.MachineLayoutControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:csmachinelayoutdrawlib="clr-namespace:CSMachineLayoutDrawLib"
xmlns:effects="http://gu.se/Geometry">
<UserControl.Resources>
<ResourceDictionary Source="Resources/ResourceDictionaries/AllResourceDictionariesCombined.xaml" />
</UserControl.Resources>
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Margin="0,0"
Padding="0"
BorderThickness="1"
Style="{StaticResource Border_Head}"
Visibility="Visible">
<effects:Zoombox
x:Name="ImageBox"
IsManipulationEnabled="True"
MaxZoom="10"
MinZoom="0.1"
Visibility="{Binding Zoombox_Visibility}">
<ContentControl Content="{Binding Viewing_Canvas}" />
</effects:Zoombox>
</Border>
<StackPanel
Grid.Column="1"
Margin="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Command="effects:ZoomCommands.Increase"
CommandParameter="2.0"
CommandTarget="{Binding ElementName=ImageBox}"
Content="Zoom In"
Style="{StaticResource StyleForResizeButtons}" />
<Button
Command="effects:ZoomCommands.Decrease"
CommandParameter="2.0"
CommandTarget="{Binding ElementName=ImageBox}"
Content="Zoom Out"
Style="{StaticResource StyleForResizeButtons}" />
<Button
Command="effects:ZoomCommands.Uniform"
CommandTarget="{Binding ElementName=ImageBox}"
Content="See Full Machine"
Style="{StaticResource StyleForResizeButtons}" />
<Button
Command="effects:ZoomCommands.UniformToFill"
CommandTarget="{Binding ElementName=ImageBox}"
Content="Zoom To Machine Width"
Style="{StaticResource StyleForResizeButtons}" />
</StackPanel>
</Grid>
</UserControl>
In the underlying Viewmodel, I had the following relevant code:
public Visibility Zoombox_Visibility { get => movZoombox_Visibility; set { movZoombox_Visibility = value; OnPropertyChanged(nameof(Zoombox_Visibility)); } }
public Canvas Viewing_Canvas { get => mdvViewing_Canvas; private set => mdvViewing_Canvas = value; }
Also, I wanted that immediately on loading, the Uniform to Fill Command was executed, this is something that I managed to do in the code-behind MachineLayoutControl.xaml.cs . You see that I only set the Zoombox to visible if the command is executed, to avoid "flickering" when the usercontrol is loading.
public partial class MachineLayoutControl : UserControl
{
#region Constructors
public MachineLayoutControl()
{
InitializeComponent();
Loaded += MyWindow_Loaded;
}
#endregion Constructors
#region EventHandlers
private void MyWindow_Loaded(object sender, RoutedEventArgs e)
{
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.ApplicationIdle,
new Action(() =>
{
ZoomCommands.Uniform.Execute(null, ImageBox);
((MachineLayoutControlViewModel)DataContext).Zoombox_Visibility = Visibility.Visible;
}));
}
#endregion EventHandlers
}
# 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 :-)
Yet another version of the same kind of control. It has similar functionality as the others, but it adds:
Touch support (drag/pinch)
The image can be deleted (normally, the Image control locks the image on disk, so you cannot delete it).
An inner border child, so the panned image doesn't overlap the border. In case of borders with rounded rectangles, look for ClippedBorder classes.
Usage is simple:
<Controls:ImageViewControl ImagePath="{Binding ...}" />
And the code:
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);
}
}
}
This will zoom in and out as well as pan but keep the image within the bounds of the container. Written as a control so add the style to the App.xaml directly or through the Themes/Viewport.xaml.
For readability I've also uploaded this on gist and github
I've also packaged this up on 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>
Usage:
<viewers:Viewport>
<Image Source="{Binding}"/>
</viewers:Viewport>
Any issues, give me a shout.
Happy coding :)
One addition to the superb solution provided by #Wiesław Šoltés answer above
The existing code resets the image position using right click, but I am more accustomed to doing that with a double click. Just replace the existing child_MouseLeftButtonDown handler:
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();
}
}
With this:
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if ((e.ChangedButton == MouseButton.Left && e.ClickCount == 1))
{
if (child != null)
{
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
}
}
if ((e.ChangedButton == MouseButton.Left && e.ClickCount == 2))
{
this.Reset();
}
}

Categories