I have a canvas (OuterCanvas) within a canvas (InnerCanvas) and am handling zooming in an out as follows:
private ScaleTransform ScaleTransform = new ScaleTransform();
private void OuterCanvas_MouseWheel(Object sender, MouseWheelEventArgs e)
{
if (e.Delta >= 1)
{
ScaleTransform.ScaleX += 0.03;
ScaleTransform.ScaleY += 0.03;
}
else
{
ScaleTransform.ScaleX -= 0.03;
ScaleTransform.ScaleY -= 0.03;
}
ScaleTransform.CenterX = ActualWidth / 2;
ScaleTransform.CenterY = ActualHeight / 2;
InnerCanvas.RenderTransform = TransformGroup;
}
This zooms in and out quite okay at the centre of the canvas. I'd like to zoom in and out around the mouse pointer, so change the CenterX and CenterY lines as follows:
ScaleTransform.CenterX = e.GetPosition(this).X;
ScaleTransform.CenterY = e.GetPosition(this).Y;
Again this works satisfactory, however if I attempt to move the mouse during two zoom events (mouse wheel events), the whole canvas jumps rather drastically as the scale factor increases or decreases.
Demonstration of problem
This is the same issue as when the CenterX and CenterY properties are set to the ActualWidth/2 and ActualHeight/2 and the window size is changed.
What should the correct handling be of the CenterX and CenterY properties be to avoid this issue?
Here is a more or less complete solution, including the possibility to pan the inner Canvas:
<Canvas x:Name="outerCanvas"
MouseWheel="OnMouseWheel"
MouseMove="OnMouseMove"
MouseLeftButtonDown="OnMouseLeftButtonDown"
MouseLeftButtonUp="OnMouseLeftButtonUp">
<Canvas x:Name="innerCanvas"
Width="400" Height="400" Background="AliceBlue">
<Canvas.RenderTransform>
<MatrixTransform />
</Canvas.RenderTransform>
</Canvas>
</Canvas>
with these event handlers:
private double zoomScaleFactor = 1.1;
private Point? mousePos;
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var pos = e.GetPosition(outerCanvas);
var scale = e.Delta > 0 ? zoomScaleFactor : 1 / zoomScaleFactor;
var transform = (MatrixTransform)innerCanvas.RenderTransform;
var matrix = transform.Matrix;
matrix.ScaleAt(scale, scale, pos.X, pos.Y);
transform.Matrix = matrix;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (mousePos.HasValue)
{
var pos = e.GetPosition(outerCanvas);
var delta = pos - mousePos.Value;
var transform = (MatrixTransform)innerCanvas.RenderTransform;
var matrix = transform.Matrix;
matrix.Translate(delta.X, delta.Y);
transform.Matrix = matrix;
mousePos = pos;
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (outerCanvas.CaptureMouse())
{
mousePos = e.GetPosition(outerCanvas);
}
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
outerCanvas.ReleaseMouseCapture();
mousePos = null;
}
Related
I'm trying to create a custom Canvas by inherit from Canvas with the so famous feature Zoom and Pan.
I succeeded to Pan and Zoom using the mouse events (move and wheel), BUT : all the canvas (in my case black background) moved and not only my few shapes.
it's about the global Pan and Zoom and not drag&Drop one shape
so I added a custom Shape inherited from shape like this :
public class DataShape : Shape, INotifyPropertyChanged
{
public int ID;
public Geometry DataShapeGeometry;
public ScaleTransform ShapeScaleTransfrom;
public TranslateTransform ShapeTranslateTransform;
public TransformGroup ShapeTransformGroup;
protected override Geometry DefiningGeometry { get { return DataShapeGeometry; } }
public DataShape()
{
DataShapeGeometry = new LineGeometry(); // just to not having Null object that prevent creation of " DataShapeGeometry.Transform "
ShapeTransformGroup = new TransformGroup();
ShapeTranslateTransform = new TranslateTransform();
ShapeScaleTransfrom = new ScaleTransform();
ShapeTransformGroup.Children.Add(ShapeTranslateTransform);
ShapeTransformGroup.Children.Add(ShapeScaleTransfrom);
DataShapeGeometry.Transform = ShapeTransformGroup;
}
for the Custom Canvas I have this (part of the code)
first I used the _pan TranslateTransform as a global Transform, as I said it worked but all my Canvas moved, then i wonted to use the shapeTranslateTransform and the shapeScalTransform , no reaction but i confirmed that the values incremente properly (the zoom geting bigger and bigger , same for the offset, i even used a Textbox to show that :))
public class ZoomPanCanvas : Canvas
{
public static int Global_ID = 0;
public MainWindow w1;
public List<DataShape> selectedDataShapes;
public DataShape selectedShape;
private Point? dragStartPoint = null;
private Matrix transform = Matrix.Identity;
private TranslateTransform _pan = new TranslateTransform();
public ZoomPanCanvas()
{
this.RenderTransform = _pan;
this.MouseWheel += ZoomPanCanvas_MouseWheel;
this.MouseLeftButtonDown += ZoomPanCanvas_MouseLeftButtonDown;
this.MouseMove += ZoomPanCanvas_MouseMove;
this.MouseLeftButtonUp += ZoomPanCanvas_MouseLeftButtonUp;
}
public void RedrawAll()
{
this.InvalidateArrange();
}
#region EVENTS_METHOD
private void ZoomPanCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.Cursor = Cursors.Hand;
dragStartPoint = e.GetPosition(this);
}
bool b = false;int j = 0;
private void ZoomPanCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
Point point = e.GetPosition(this);
/* this was working <--------------------------------------
_pan.X += point.X - dragStartPoint.Value.X;
_pan.Y += point.Y - dragStartPoint.Value.Y;*/
foreach (UIElement datashape in this.Children)
{
// it works we need to pan each shape
selectedShape = datashape as DataShape;
((TranslateTransform) (selectedShape.ShapeTransformGroup.Children[0])).X += point.X - dragStartPoint.Value.X;
((TranslateTransform)(selectedShape.ShapeTransformGroup.Children[0])).Y += point.Y - dragStartPoint.Value.Y;
//((selectedShape.DataShapeGeometry.Transform)as TranslateTransform).X += point.X - dragStartPoint.Value.X;
// ((selectedShape.DataShapeGeometry.Transform) as TranslateTransform).Y += point.Y - dragStartPoint.Value.Y;
}
dragStartPoint = point;
}
}
private void ZoomPanCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.Cursor = Cursors.Arrow;
dragStartPoint = null;
}
private void ZoomPanCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
Point position = e.GetPosition(this);
// scale get only two values, it does'nt increment !
double scale = e.Delta > 0 ? 1.1 : (1.0 / 1.1);
//this.transform.ScaleAt(scale, scale, position.X, position.Y);
//this.RenderTransform = new MatrixTransform(this.transform);
foreach (UIElement datashape in this.Children)
{
// it works we need to pan each shape
selectedShape = datashape as DataShape;
//selectedShape.DataShapeGeometry.Transform.Value.Scale(scale, scale);
((ScaleTransform)(selectedShape.ShapeTransformGroup.Children[1])).ScaleX = ((ScaleTransform)(selectedShape.ShapeTransformGroup.Children[1])).ScaleX * scale;
((ScaleTransform)(selectedShape.ShapeTransformGroup.Children[1])).ScaleY = ((ScaleTransform)(selectedShape.ShapeTransformGroup.Children[1])).ScaleY * scale;
w1.TXT1.Text = ((ScaleTransform)(selectedShape.ShapeTransformGroup.Children[1])).ScaleX.ToString();
}
}
#endregion
}
I trayed to apply the GroupTransform to initialize all types of transform (Translate for Pan andScale for Zoom) in the Datashape Constructor.
I reached "selectedShape.ShapeTransformGroup.Children[]" from inside my ZoomPanCanvas i confirmed that values change but no impact in the interface.
I dare have a great expectation to create a sort of intelligent Cad Canvas (or like visio ).
I will not use ItemsControl, it's useful but when dealing with Algorithmic Geometry I prefer a straightforward simple canvas.
Many thanks
I would like to rotate the border with inertia. Where do I leave something out?
MainPage.xaml:
<Grid>
<Border x:Name="ManipulationBorder" Width="200" Height="200" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Red"/>
</Grid>
MainPage.xaml.cs:
private TransformGroup transforms;
private MatrixTransform previousTransform;
private CompositeTransform deltaTransform;
public MainPage()
{
this.InitializeComponent();
InitManipulationTransforms();
ManipulationBorder.ManipulationDelta += new ManipulationDeltaEventHandler(ManipulateMe_ManipulationDelta);
ManipulationBorder.ManipulationMode =
ManipulationModes.TranslateX |
ManipulationModes.TranslateY |
ManipulationModes.Rotate |
ManipulationModes.TranslateInertia |
ManipulationModes.RotateInertia;
}
private void InitManipulationTransforms()
{
transforms = new TransformGroup();
previousTransform = new MatrixTransform() { Matrix = Matrix.Identity };
deltaTransform = new CompositeTransform();
transforms.Children.Add(previousTransform);
transforms.Children.Add(deltaTransform);
ManipulationBorder.RenderTransform = transforms;
}
private void ManipulateMe_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
previousTransform.Matrix = transforms.Value;
// Center point for rotation
Point center = previousTransform.TransformPoint(new Point(e.Position.X, e.Position.Y));
deltaTransform.CenterX = center.X;
deltaTransform.CenterY = center.Y;
// Rotation
deltaTransform.Rotation = e.Delta.Rotation;
}
When I actually go to apply delta rotation in ManipulationDeltaRoutedEventArgs it does not work. Where am I wrong?
Thanks in advance.
When I actually go to apply delta rotation in ManipulationDeltaRoutedEventArgs it does not work. Where am I wrong?
The standard touch-based rotation requires two touch points or more:
For single touch rotation you need fix the center of deltaTransform firstly.
You need recalculate the Angle has been changed with single touch.
you will know the value of Angle = a1 - a2.
private void Right_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
var x = this.RightRotateTransform.CenterX - e.Position.X;
var y = this.RightRotateTransform.CenterY - e.Position.Y;
double a1 = Math.Atan(y / x);
double a2 = Math.Atan((e.Delta.Translation.Y - y) / (x - e.Delta.Translation.X));
this.RightRotateTransform.Angle += a1 - a2;
}
I want to rotate and afterwards move an image in c#. The image is in a Canvas . My problem is if you rotate the image with the following
private void Schiff_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
Image _schiff = (Image)sender;
if (!_schiff.IsMouseCaptured) return;
Matrix _mat = _schiff.RenderTransform.Value;
Point _mouse = e.GetPosition(_schiff);
if (e.Delta > 0)
{
_mat.RotateAtPrepend(22.5, _mouse.X, _mouse.Y);
}
else
{
_mat.RotateAtPrepend(-22.5, _mouse.X, _mouse.Y);
}
MatrixTransform _mtf = new MatrixTransform(_mat);
_schiff.RenderTransform = _mtf;
}
or RotateTransform
double _angle = 0.0;
_angle += 22.5;
if (_angle == 360.0) _angle = 0.0;
RotateTransform _rotate = new RotateTransform(_angle, _schiff.Width / 2, _schiff.Height / 2);
_schiff.RenderTransform = _rotate;A
you just rotate the "picture", but not it's base. So if you want to move the image with Canvas.GetLeft/GetTop, it behaves like it is still not rotated. So if you set the Top/Left-corner, the actual corner of the rotated image isn't placed where I wanted it to be.
At https://wpf.2000things.com/2013/03/08/772-use-rendertransformorigin-to-change-center-point-for-rotation-transforms/, in the picture you can see what I mean. How can I possibly rotate the "base" with the actual image? I saw it is possible in WinForms, but (how) does it work in WPF?
Thanks in advance, if anything is unclear/wrong I will edit my question.
edit:
https://i.stack.imgur.com/tqhKw.png
You can see the two arrows. They are my images. I rotated them at the center with my above MouseWheelEvent. On the right site there is my movement tab. You can change the speed (the checkbox after "Geschwindigkeit") and then you can either turn left in one section (where - is 0°, | is 22.5° and || is 45° and a section is 69 points in the Canvas) or right and end up in a new location.
You should use coordinates relative to the Canvas for transforming the Image element.
With this Image in a Canvas
<Canvas x:Name="canvas">
<Image Width="100"
Source="C:\Users\Public\Pictures\Sample Pictures\Koala.jpg"
MouseLeftButtonDown="OnMouseLeftButtonDown"
MouseLeftButtonUp="OnMouseLeftButtonUp"
MouseMove="OnMouseMove"
MouseWheel="OnMouseWheel">
<Image.RenderTransform>
<MatrixTransform />
</Image.RenderTransform>
</Image>
</Canvas>
the code would look like shown below.
The important part is to use e.GetPosition(canvas) and not to set the child element's Canvas.Left, Canvas.Top and RenderTransformOrigin properties. All transformations are done with a single Matrix in its RenderTransform.
private Point? mousePos;
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
((IInputElement)sender).CaptureMouse();
mousePos = e.GetPosition(canvas);
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
((IInputElement)sender).ReleaseMouseCapture();
mousePos = null;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (mousePos.HasValue)
{
var element = (UIElement)sender;
var transform = (MatrixTransform)element.RenderTransform;
var matrix = transform.Matrix;
var pos = e.GetPosition(canvas);
matrix.Translate(pos.X - mousePos.Value.X, pos.Y - mousePos.Value.Y);
transform.Matrix = matrix;
mousePos = pos;
}
}
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var element = (UIElement)sender;
var transform = (MatrixTransform)element.RenderTransform;
var matrix = transform.Matrix;
var pos = e.GetPosition(canvas);
matrix.RotateAt(e.Delta > 0 ? 22.5 : -22.5, pos.X, pos.Y);
transform.Matrix = matrix;
}
I have these three functions to trigger the events. I already have a static version of my needs, but I need a dynamically version of it.
bool captured = false;
double x_shape, x_canvas, y_shape, y_canvas;
UIElement source = null;
private void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
source = (UIElement)sender;
Mouse.Capture(source);
captured = true;
x_shape = Canvas.GetLeft(source);
x_canvas = e.GetPosition(canvasPreview).X;
y_shape = Canvas.GetTop(source);
y_canvas = e.GetPosition(canvasPreview).Y;
}
private void MouseMove(object sender, MouseEventArgs e)
{
//MessageBox.Show("test");
if (captured)
{
double x = e.GetPosition(canvasPreview).X;
double y = e.GetPosition(canvasPreview).Y;
x_shape += x - x_canvas;
Canvas.SetLeft(source, x_shape);
x_canvas = x;
y_shape += y - y_canvas;
Canvas.SetTop(source, y_shape);
y_canvas = y;
}
}
private void MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
Mouse.Capture(null);
captured = false;
}
I have made a canvas in WPF called 'canvasPreview', I want to add the rectangle (currently in the static version I am using an ellipse) to the canvas, it must be draggable with above functions. It is already working, but it have to be dynamically.
I hope you can help me, thank you in advance!
I'm sure this sample code will help you.
XAML:
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button x:Name="addRectangleButton" Content="Add Rectngle" Click="addRectangleButton_Click"/>
</StackPanel>
<Canvas Grid.Row="1" x:Name="canvas" Margin="0,12,0,0">
<Rectangle x:Name="rectangle" Width="100" Height="50" Fill="RoyalBlue" MouseDown="rectangle_MouseDown" MouseMove="rectangle_MouseMove" MouseUp="rectangle_MouseUp" Canvas.Left="0" Canvas.Top="0"/>
</Canvas>
</Grid>
C#:
bool drag = false;
Point startPoint;
public MainWindow()
{
InitializeComponent();
}
// this creates and adds rectangles dynamically
private void addRectangleButton_Click(object sender, RoutedEventArgs e)
{
// create new Rectangle
Rectangle rectangle = new Rectangle();
// assign properties
rectangle.Width = 100;
rectangle.Height = 50;
rectangle.Fill = new SolidColorBrush(Colors.RoyalBlue);
// assign handlers
rectangle.MouseDown += rectangle_MouseDown;
rectangle.MouseMove += rectangle_MouseMove;
rectangle.MouseUp += rectangle_MouseUp;
// set default position
Canvas.SetLeft(rectangle, 0);
Canvas.SetTop(rectangle, 0);
// add it to canvas
canvas.Children.Add(rectangle);
}
private void rectangle_MouseDown(object sender, MouseButtonEventArgs e)
{
// start dragging
drag = true;
// save start point of dragging
startPoint = Mouse.GetPosition(canvas);
}
private void rectangle_MouseMove(object sender, MouseEventArgs e)
{
// if dragging, then adjust rectangle position based on mouse movement
if (drag)
{
Rectangle draggedRectangle = sender as Rectangle;
Point newPoint = Mouse.GetPosition(canvas);
double left = Canvas.GetLeft(draggedRectangle);
double top = Canvas.GetTop(draggedRectangle);
Canvas.SetLeft(draggedRectangle, left + (newPoint.X - startPoint.X));
Canvas.SetTop(draggedRectangle, top + (newPoint.Y - startPoint.Y));
startPoint = newPoint;
}
}
private void rectangle_MouseUp(object sender, MouseButtonEventArgs e)
{
// stop dragging
drag = false;
}
I created a draggaable user control in silverlight, but having issues in moving the control inside page. Actually the issue is, dragged control moves out of page when dragging which i don't want. I want the control should drag inside the parent control only.
Any help/suggestion would be appreciated.
Below is the code used to perform operation:
XAML:
<Grid x:Name="LayoutRoot" >
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform x:Name="LocalTranslateTransform"/>
</TransformGroup>
</Grid.RenderTransform>
C#:
private void OnToolbarClicked(object sender, MouseButtonEventArgs e)
{
mouseDownInToolbar = true;
DragOffset = e.GetPosition(LayoutRoot);
toolbarBorder.CaptureMouse();
}
private void OnToolbarMoving(object sender, MouseEventArgs e)
{
if (mouseDownInToolbar)
{
// we want to move it based on the position of the mouse
moveUserControl(e);
}
}
private void moveUserControl (MouseEventArgs e)
{
Point mousePos = e.GetPosition(LayoutRoot);
Double newX = LocalTranslateTransform.X + (mousePos.X - DragOffset.X);
Double newY = LocalTranslateTransform.Y + (mousePos.Y - DragOffset.Y);
LocalTranslateTransform.X = newX;
LocalTranslateTransform.Y = newY;
}
private void OnToolbarReleased(object sender, MouseButtonEventArgs e)
{
mouseDownInToolbar = false;
toolbarBorder.ReleaseMouseCapture();
}
When you use TranslateTransform, you're sending instructions to the compositor thread to display the element at X/Y offsets. It does not obey the rules of your panel and any visual tree member. So, you have to tell the compositor thread about the boundaries. Use Clip like this:
<Grid Width="500" Height="500">
<Grid.Clip>
<RectangleGeometry Rect="0,0,500,500" />
</Grid.Clip>
<Grid x:Name="LayoutRoot">
<!-- This is the element being dragged by the mouse -->
</Grid>
</Grid>
The RectangleGeometry will create a boundary where child elements are allowed to appear. Anything outside, will be clipped.
After doing the desired changes this is how it looked.
You need to create a constraint.
Calculate if you're transforming the actual control outside the bounds of the parent.
private void moveUserControl (MouseEventArgs e)
{
Point mousePos = e.GetPosition(LayoutRoot);
Double newX = LocalTranslateTransform.X + (mousePos.X - DragOffset.X);
Double newY = LocalTranslateTransform.Y + (mousePos.Y - DragOffset.Y);
var minX = 0;
var maxX = 500 - ActualWidth; // 500 is parent width
var minY = 0;
var maxY = 500 - ActualHeight; // 500 is parent height
if (newX < minX)
{
newX = minX;
}
else if (newX > maxX)
{
newX = maxX;
}
if (newY < minY)
{
newY = minY;
}
else if (newY > maxY)
{
newY = maxY;
}
LocalTranslateTransform.X = newX;
LocalTranslateTransform.Y = newY;
}