I have an image in a scrollviewer.The image has Pinch in and out feature implemented on it.
But while scrolling the zoomed image,the aspect ratio changes and images becomes distorted.
Following the xaml:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Name="scroller" >
<Image Name="image_new" Visibility="Visible" CacheMode="BitmapCache" >
<Image.RenderTransform >
<CompositeTransform x:Name="transform"/>
</Image.RenderTransform >
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick="OnFlick" PinchStarted="OnPinchStarted" PinchDelta="OnPinchDelta" DoubleTap="Onimage_doubletap" Tap="Onimage_singletap" />
</toolkit:GestureService.GestureListener>
</Image>
</ScrollViewer>
And in the .cs file the methods are :
private void OnPinchStarted(object sender, PinchStartedGestureEventArgs e)
{
Point point0 = e.GetPosition(image_new, 0);
Point point1 = e.GetPosition(image_new, 1);
Point midpoint = new Point((point0.X + point1.X) / 2, (point0.Y + point1.Y) / 2);
image_new.RenderTransformOrigin = new Point(midpoint.X / image_new.ActualWidth, midpoint.Y / image_new.ActualHeight);
initialScale = transform.ScaleX;
}
private void OnPinchDelta(object sender, PinchGestureEventArgs e)
{
transform.ScaleX = Math.Max(Math.Min(initialScale * e.DistanceRatio, 3.0), 0.5);
transform.ScaleY = Math.Max(Math.Min(initialScale * e.DistanceRatio, 3.0), 0.5);
}
I think the problem here is that you are changing the RenderTransformOrigin for each pinch gesture, which is resulting in the distortion. I would try leaving the RenderTransformOrigin fixed at 0.5,0.5 to ensure that you get an even scale.
I assume you were moving the origin to try to zoom into/out of the part of the image that the user had started the gesture on. To achieve this, I think you will need to enable the user to pan around the image once zoomed in.
One other point, the scale factor is always the same, so you shoudl just calculate it once, and then assign it to both ScaleX and ScaleY.
Related
Many Windows applications support two fingered touchpad gestures. You can scroll by moving two fingers horizontally or vertically, and you can zoom by changing the distance between the fingers.
I'm trying replicate this behaviour in a WinUI 3 canvas.
According to the Microsoft documentation "The touchpad does not raise manipulation events. Instead, pointer events will be raised for touchpad input."
Listening for the PointerWheelChanged event handler, I'm able to detect two finger scrolls with the following code
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(myCanvas);
var isHorizontalScroll = pointer.Properties.IsHorizontalMouseWheel;
var scrollDelta = pointer.Properties.MouseWheelDelta;
// Perform the scrolling
e.Handled = true;
}
But, I'm unable to get the fingers position so I can calculate the zoom delta. Is there some lower API I can use that will give me access to the individual fingers? Or some other way to capture the zoom gesture?
I have created a little sample for you, which handles zoom using the trackpad:
MainWindow.xaml:
<Canvas x:Name="myCanvas" PointerWheelChanged="Canvas_PointerWheelChanged" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Green">
<Rectangle Width="40" Height="40" Fill="Red" Canvas.Left="0" Canvas.Top="0" Canvas.ZIndex="0" />
</Canvas>
MainWindow.xaml.cs:
private void Canvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
var ctrl = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control
if (ctrl.HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down))
{
var delta = e.GetCurrentPoint(myCanvas).Properties.MouseWheelDelta;
//Here you can handle your zooming
//My sample just resizes the rectangle
foreach (UIElement children in myCanvas.Children)
{
if (children is Rectangle rect)
{
double newSize = rect.Height + delta;
if (newSize < 0)
newSize = 0;
rect.Height = rect.Width = newSize;
}
}
}
}
The trick is, to check for the control-key press in the PointerWheelChanged-event, because the touchpad behaves the same like when you zoom using control + Mousewheel
I have a series of rectangles in which the user can add images to, by dragging the images in.
The image is then scaled down in proportion and the rectangle is then filled with an ImageBrush.
I need for the user to be able to manipulate the image within the rectangle to fit their needs. Like any photo collage app does.
My question is: How can I show the full, unmasked image on top of the rectangle so that the user can manipulate it to their needs? I don't know where to start with this one.
private async void Mask_Drop(object sender, DragEventArgs e)
{
Rectangle maskSq = e.OriginalSource as Rectangle;
var maskW = maskSq.Width.ToSingle();
var maskH = maskSq.Height.ToSingle();
double maskX = Canvas.GetLeft(maskSq);
double maskY = Canvas.GetTop(maskSq);
// Image sizes for bounding to mask
float boundH = Convert.ToSingle(size.Height);
float boundW = Convert.ToSingle(size.Width);
maskSq.Fill = new ImageBrush
{
ImageSource = new BitmapImage(new Uri("ms-appdata:///local/" + SelectedImage.Name, UriKind.Absolute)),
Stretch = Stretch.UniformToFill
};
}
private void Tap_Collage(object sender, TappedRoutedEventArgs e)
{
//Gets the full image from ImageBrush
ImageBrush brush = (ImageBrush)(((Rectangle)sender).Fill);
Rectangle rect = sender as Rectangle;
//Mask sure rectangle does not drag, just the image brush
rect.CanDrag = false;
rect.StrokeThickness = 6;
//Drag Image Functionality
rect.ManipulationDelta += ImageManipulation.Resize_ImageEdit;
ImageManipulation.ImageEdit_Drag = new TranslateTransform();
brush.Transform = ImageManipulation.ImageEdit_Drag;
//Zoom Image Functionality
ImageManipulation.ImageEdit_Zoom = new ScaleTransform();
brush.RelativeTransform = ImageManipulation.ImageEdit_Zoom;
}
Class
public static class ImageManipulation
{
public static TranslateTransform ImageEdit_Drag;
public static ScaleTransform ImageEdit_Zoom;
public static RotateTransform ImageEdit_Rotate;
public static void Resize_ImageEdit(object sender, ManipulationDeltaRoutedEventArgs e)
{
ImageEdit_Drag.X += e.Delta.Translation.X;
ImageEdit_Drag.Y += e.Delta.Translation.Y;
ImageEdit_Zoom.ScaleX *= e.Delta.Scale;
ImageEdit_Zoom.ScaleY *= e.Delta.Scale;
if (ImageEdit_Zoom.ScaleX < 1.0)
{
ImageEdit_Zoom.ScaleX = 1.0;
ImageEdit_Zoom.ScaleY = 1.0;
}
ImageEdit_Rotate.Angle += e.Delta.Rotation;
}
}
XAML
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:CollageGrid">
<Rectangle Width="{Binding CollageW}" Height="{Binding CollageH}" AllowDrop="True" CanDrag="True" Fill="Transparent"
Drop="Mask_Drop"
DragOver="Mask_DragOver"
ManipulationMode="TranslateX, TranslateY" Stroke="Black" StrokeThickness="2" DragEnter="Mask_DragEnter" DragLeave="Mask_DragLeave" Tapped="Tap_Collage">
<Rectangle.RenderTransform>
<TranslateTransform X="{Binding CollageX}" Y="{Binding CollageY}"/>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
Example of what I'm looking to acheive:
How it looks currently:
You can try to replace the Rectangle with the Polyline to draw the Rectangle so that you can access the Image which is on the bottom of the Rectangle.
<Grid>
<Image Source="Assets/image1.jpg" Width="800"
Height="400" Tapped="Image_Tapped" />
<Polyline Stroke="Black" HorizontalAlignment="Center"
VerticalAlignment="Center"
StrokeThickness="4" Tapped="Polyline_Tapped"
Points="0,0,0,200,200,200,200,0,0,0" />
</Grid>
---Update---
UniformToFill will cause the image is resized to fill the destination dimensions while it preserves its native aspect ratio. If the aspect ratio of the destination rectangle differs from the source, the source content is clipped to fit in the destination dimensions. So it is not suitable for your requirement. You should manually scale the Image to make image fit one of your Rectangle's border and keep other part out of the Rectangle.
It seems you put the image as the Rectangle's background image brush, there are no other place to display the image out of the Rectangle. So I think, we may take a considerition for a new scenario.
As my pervious reply, using Image control to display the image and Polylines to draw a Rectangle above the Image so that we can operate the Image which is below the Rectangle using the manipulation events to fit the rectangle, meanwhile we can also use the community toolkit BackdropBlurBrush on the xaml layout to Blur the outer image.
I'm currently working on a tool that allows to open multiple images in different windows. The main objective is to be able to move/resize the form and the image separately.
This is a very simple form containing a border, containing an image.
I can pan & zoom the image using a transform group and everything works fine.
My problem is that if I resize the window to a smaller size than the image, and then move the image inside the window, the part of the image that was not visible after the resizing is cropped, and can only be retrieved when resizing the window itself.
It the same manner, if I reduce my image size then reduce the window size, the image will be cropped.
So I'm wondering what element cause that behavior, and is there a way to get rid of it ? Either by asking the form not to crop the image in the first place, or to redraw it when it is translated ?
XAML
<Window x:Class="Toolboxproto.imgWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="imgWindow" Height="300" Width="300"
ShowInTaskbar="False"
WindowStyle="None"
MouseLeftButtonDown="Window_MouseLeftButtonDown"
Activated="Window_Activated"
SizeChanged="Window_SizeChanged"
ResizeMode="CanResizeWithGrip" MouseWheel="Window_MouseWheel"
>
<Border x:Name="border" ClipToBounds="True" Background="Gray" >
<Image x:Name="image"
HorizontalAlignment="Center"
Height="290"
VerticalAlignment="Center"
Width="290"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseWheel="image_MouseWheel"
MouseMove="image_MouseMove"
MouseLeftButtonUp="image_MouseLeftButtonUp"
/>
Edit :
Here is the code for the translation and scale of the image, if relevant :
private void image_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (Keyboard.IsKeyDown(Key.LeftCtrl))
{
double zoom = e.Delta > 0 ? 0.1 : -0.1;
if ((e.Delta < 0) && (scaleT.ScaleX < 0.2 || scaleT.ScaleY < 0.2))
{ return; }
Point relative = e.GetPosition(image);
//Point absolute = new Point(0,0);
double absoluteX = relative.X * scaleT.ScaleX + translateT.X;
double absoluteY = relative.Y * scaleT.ScaleY + translateT.Y;
scaleT.ScaleX += zoom;
scaleT.ScaleY += zoom;
translateT.X = absoluteX - relative.X * scaleT.ScaleX;
translateT.Y = absoluteY - relative.Y * scaleT.ScaleY;
}
}
private void image_MouseMove(object sender, MouseEventArgs e)
{
if (image.IsMouseCaptured)
{
translateT.X = origin.X - (start.X - e.GetPosition(border).X);
translateT.Y = origin.Y - (start.Y - e.GetPosition(border).Y);
}
}
I am trying to implement a zoom-functionality for a canvas using the mouse wheel.
Currently I am just Zooming to the center position of the canvas using CenterX="0.5" and CenterY="0.5".
I would like to change the behavior so that the zooming happens at the mouse position and I would like to know if this is possible with a ScaleTransform.
Currently I use the following code:
<Canvas Width="500" Height="500">
<Canvas.LayoutTransform>
<ScaleTransform CenterX="0.5" CenterY="0.5"
ScaleX="{Binding Zoom}"
ScaleY="{Binding Zoom}" />
</Canvas.LayoutTransform>
</Canvas>
A very basic approach to zoom a Canvas (or any other UIElement) at a specific position would be to use a MatrixTransform for the RenderTransform property
<Canvas Width="500" Height="500" MouseWheel="Canvas_MouseWheel">
<Canvas.RenderTransform>
<MatrixTransform/>
</Canvas.RenderTransform>
</Canvas>
and update the Matrix property of the transform like in this MouseWheel handler:
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
var element = (UIElement)sender;
var position = e.GetPosition(element);
var transform = (MatrixTransform)element.RenderTransform;
var matrix = transform.Matrix;
var scale = e.Delta >= 0 ? 1.1 : (1.0 / 1.1); // choose appropriate scaling factor
matrix.ScaleAtPrepend(scale, scale, position.X, position.Y);
transform.Matrix = matrix;
}
I spent the past two days agonizing over this issue and I figured it out. This will get you smooth zooming in toward the mouse and smooth zooming out. I'm posting my solution here for anyone who might search and stumble back here.
// Class constructor
public YourClass(Canvas theCanvas) //You may not need the Canvas as an argument depending on your scope
{
panTransform = new TranslateTransform();
zoomTransform = new ScaleTransform();
bothTransforms = new TransformGroup();
bothTransforms.Children.Add(panTransform);
bothTransforms.Children.Add(zoomTransform);
theCanvas.RenderTransform = bothTransforms;
//Handler
theCanvas.MouseWheel += wheelEvent;
//You also need your own handlers for panning, which I'm not showing here.
}
private void returnCalculatedScale()
{
double d;
//Do some math to get a new scale. I keep track of an integer, and run it through the formula y^(x/3) where X is the integer.
return d;
}
// Mouse wheel handler, where the magic happens
private void wheelEvent(object sender, MouseWheelEventArgs e)
{
Point position = e.GetPosition(mainCanvas);
zoomTransform.CenterX = position.X;
zoomTransform.CenterY = position.Y;
zoomTransform.ScaleX = returnCalculatedScale();
zoomTransform.ScaleY = returnCalculatedScale();
Point cursorpos = Mouse.GetPosition(mainCanvas); //This was the secret, as the mouse position gets out of whack when the transform occurs, but Mouse.GetPosition lets us get the point accurate to the transformed canvas.
double discrepancyX = cursorpos.X - position.X;
double discrepancyY = cursorpos.Y - position.Y;
//If your canvas is already panned an arbitrary amount, this aggregates the discrepancy to the TranslateTransform.
panTransform.X += discrepancyX;
panTransform.Y += discrepancyY;
I'm trying to learn WPF, so here's a simple question, I hope:
I have a window that contains an Image element bound to a separate data object with user-configurable Stretch property
<Image Name="imageCtrl" Source="{Binding MyImage}" Stretch="{Binding ImageStretch}" />
When the user moves the mouse over the image, I would like to determine the coordinates of the mouse with respect to the original image (before stretching/cropping that occurs when it is displayed in the control), and then do something with those coordinates (update the image).
I know I can add an event-handler to the MouseMove event over the Image control, but I'm not sure how best to transform the coordinates:
void imageCtrl_MouseMove(object sender, MouseEventArgs e)
{
Point locationInControl = e.GetPosition(imageCtrl);
Point locationInImage = ???
updateImage(locationInImage);
}
Now I know I could compare the size of Source to the ActualSize of the control, and then switch on imageCtrl.Stretch to compute the scalars and offsets on X and Y, and do the transform myself. But WPF has all the information already, and this seems like functionality that might be built-in to the WPF libraries somewhere. So I'm wondering: is there a short and sweet solution? Or do I need to write this myself?
EDIT I'm appending my current, not-so-short-and-sweet solution. Its not that bad, but I'd be somewhat suprised if WPF didn't provide this functionality automatically:
Point ImgControlCoordsToPixelCoords(Point locInCtrl,
double imgCtrlActualWidth, double imgCtrlActualHeight)
{
if (ImageStretch == Stretch.None)
return locInCtrl;
Size renderSize = new Size(imgCtrlActualWidth, imgCtrlActualHeight);
Size sourceSize = bitmap.Size;
double xZoom = renderSize.Width / sourceSize.Width;
double yZoom = renderSize.Height / sourceSize.Height;
if (ImageStretch == Stretch.Fill)
return new Point(locInCtrl.X / xZoom, locInCtrl.Y / yZoom);
double zoom;
if (ImageStretch == Stretch.Uniform)
zoom = Math.Min(xZoom, yZoom);
else // (imageCtrl.Stretch == Stretch.UniformToFill)
zoom = Math.Max(xZoom, yZoom);
return new Point(locInCtrl.X / zoom, locInCtrl.Y / zoom);
}
It would probably be easier if you used a ViewBox. For example:
<Viewbox Stretch="{Binding ImageStretch}">
<Image Name="imageCtrl" Source="{Binding MyImage}" Stretch="None"/>
</Viewbox>
Then when you go and call GetPosition(..) WPF will automatically account for the scaling.
void imageCtrl_MouseMove(object sender, MouseEventArgs e)
{
Point locationInControl = e.GetPosition(imageCtrl);
}