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);
}
}
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 having trouble getting the selectionBox not to center. In the image below, I tried to draw a box around the word Final Bill (accented ms paint box) by clicking and dragging but the resulting selectionBox (dashed line in red) that outputs always starts in the center. The rectangle values that are calculated and saved in the mouseUp event are all correct, suggesting perhaps a XAML display issue?
I am very knew to WPF/XAML and front end stuff in general.
EDIT: By placing just the selectionBox in a <Canvas> tag I was able to get it almost working. It no longer centers but the start point appears to be twice as far from the left and top borders as the mouse is when clicked.
XAML
<DockPanel Width="Auto" Margin="225,65,5,5">
<Border x:Name="img_Border" ClipToBounds="True" Height="Auto" Width="Auto" Margin="0,0,0,0" VerticalAlignment="Top">
<Grid>
<ScrollViewer>
<Image x:Name="img_Box" ClipToBounds="True" MouseMove="img_Box_MouseMove" MouseWheel="img_Box_MouseWheel">
</Image>
</ScrollViewer>
<Rectangle x:Name="selectionBox" Visibility="Collapsed" Stroke="Red" StrokeThickness="3" StrokeDashArray="3,1">
</Rectangle>
</Grid>
</Border>
</DockPanel>
I can get the selectionBox to work correctly if I use <Canvas> tags. but that causes the image to not fit inside the <DockPanel>.
c#
public MainWindow()
{
InitializeComponent();
img_Box.MouseLeftButtonDown += img_Box_MouseLeftButtonDown;
img_Box.MouseLeftButtonUp += img_Box_MouseLeftButtonUp;
img_Box.MouseMove += img_Box_MouseMove;
Point mouseDownPos;
}
private void img_Box_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
img_Box.CaptureMouse();
var tt = (TranslateTransform)((TransformGroup)img_Box.RenderTransform).Children.First(tr => tr is TranslateTransform);
double BoxX = (e.GetPosition(img_Box).X / img_Box.ActualWidth) * 1;
double BoxY = (e.GetPosition(img_Box).Y / img_Box.ActualHeight) * 1;
double xValue = Math.Round((BoxX * img_Box.Source.Width), 0);
double yValue = Math.Round((BoxY * img_Box.Source.Height), 0);
StartDrag = new System.Windows.Point(xValue, yValue);
mouseDownPos.X = (int)xValue;
mouseDownPos.Y = (int)yValue;
Canvas.SetLeft(selectionBox, xValue);
Canvas.SetTop(selectionBox, yValue);
selectionBox.Width = 0;
selectionBox.Height = 0;
selectionBox.Visibility = Visibility.Visible;
}
private void img_Box_MouseMove(object sender, MouseEventArgs e)
{
double x = e.GetPosition(img_Box).X;
double y = e.GetPosition(img_Box).Y;
double BoxX = (x / img_Box.ActualWidth) * 1;
double BoxY = (y / img_Box.ActualHeight) * 1;
double xValue = Math.Round((BoxX * img_Box.Source.Width), 0);
double yValue = Math.Round((BoxY * img_Box.Source.Height), 0);
if (mouseDownPos.X < xValue)
{
Canvas.SetLeft(selectionBox, mouseDownPos.X);
selectionBox.Width = xValue - mouseDownPos.X;
}
else
{
Canvas.SetLeft(selectionBox, xValue);
selectionBox.Width = mouseDownPos.X - xValue;
}
if (mouseDownPos.Y < yValue)
{
Canvas.SetTop(selectionBox, mouseDownPos.Y);
selectionBox.Height = yValue - mouseDownPos.Y;
}
else
{
Canvas.SetTop(selectionBox, yValue);
selectionBox.Height = mouseDownPos.Y - yValue;
}
}
Not sure if I fully understand what exactly you are trying to accomplish here but if you just need a border around that text and not to allow the user to move it around with the mouse, set the image to be the background of the Grid and then partition the Grid so that one of the grid cells is directly above that text you want to place the border around. Place the border within that cell and If all the bill images are exactly the same size and have the text in exactly the same location it will always be directly over that text as long as you do not allow users to resize the image... If you want the scrolling just put the entire grid within the ScrollViewer control.
Something like this:
<Grid>
<Grid.Background>
<VisualBrush TileMode="None" >
<VisualBrush.Visual>
<Image Source="{Binding ImageSource}"/>
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> //Distance from the left edge of the image to the right edge of the border around the "Final Bill" text.
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="150"/> //Distance from the top of the image to the top of the desired location of the border around the "Final Bill" text.
<RowDefinition Height="50"/> //Desired height of the border around the "Final Bill" text.
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="1" BorderThickness="1"/>
</Grid>
What I want to do overall is add an ellipse to a canvas to represent the position of a golf ball on the Circumference of a circle when it is struck at a certain angle by a golf putter from the center of that circle. To do this I am using two images one which is a circle divided up into sections to represent the angle the ball was struck at. The other image is just a putter head that is rotated to represent the angle which the club hits the ball at. This image sits on top of the circle image. The blue ellipse represents the position of the ball.See the image output got from my application in the link below:
Required application output
To do this I basically calculated the width and height of the grid using this code:
public FacePage()
{
InitializeComponent();
GridGraphs.SizeChanged += GridGraphs_SizeChanged;
}
void GridGraphs_SizeChanged(object sender, SizeChangedEventArgs e)
{
GetMeasurements();
}
private void GetMeasurements()
{
GridGraphs.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double width = GridGraphs.DesiredSize.Width;
double height = GridGraphs.DesiredSize.Height;
RotatePutter(width, height);
}
and then passed the width and height into RotatePutter() where the the width and height was divided by two to get the centre point of the grid see below:
public void RotatePutter(double width, double height)
{
double radius = width * 0.5;
double centerX = width * 0.5;
double centerY = height * 0.5;
Random ran = new Random();
double angle = ran.Next(-15, 15);
if (angle >= 0)
{
if (angle <= 5)
{
lblShotStaus.Content = "Square";
}
else
{
lblShotStaus.Content = "Open";
}
}
else
{
lblShotStaus.Content = "Closed";
}
double angleradians = (angle * (Math.PI / 180));
Point putterCentre = new Point(0.5, 0.5);
imgPutter.RenderTransformOrigin = putterCentre;
RotateTransform rt = new RotateTransform(angle, 0.5, 0.5);
imgPutter.RenderTransform = rt;
BallLocation(radius, angleradians, centerX, centerY);
}
The centre point,radius,angle and the putter Image is rotated here and the radius, angleradians,centerX and centerY are passed to BallLocation to calculate the position of the Ball like so:
public void BallLocation(double rad, double anglerad, double centerX, double centerY)
{
Ellipse ellipse = new Ellipse();
double xBallPoint = (centerX - (rad * Math.Cos(anglerad)));
double yBallPoint = (centerY - (rad * Math.Sin(anglerad)));
ellipse.Height = 10;
ellipse.Width = 10;
ellipse.Fill = new SolidColorBrush(Colors.Aqua);
Canvas.SetLeft(ellipse, xBallPoint);
Canvas.SetTop(ellipse, yBallPoint);
canvasFaceAngle.Children.Add(ellipse);
}
This works ok in full screen put once I change the size of the window the position of the ball for the angle it was hit at is all wrong.
Would anyone have any idea how to dynamically get the grid width and height as the window size changes so to calculate the correct position for the ellipse (ball). Or any other alternative ways of completing this is also welcomed. Thanks in advance.
Here is my xaml:
<UserControl x:Class="HoleMorePuttsApplication.Pages.FacePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="60*"/>
</Grid.ColumnDefinitions>
<Viewbox Grid.ColumnSpan="1">
<Label Content="Face Angle Page" FontWeight="Bold" />
</Viewbox>
<Grid Name="GridGraphs" Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Name="columnLeft" Width="50*"/>
<ColumnDefinition Name="columnRight" Width="50*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Name="rowTop" Height="70*"/>
<RowDefinition Name="rowBottom" Height="30*"/>
</Grid.RowDefinitions>
<Image x:Name="imgAngles" Source="C:\Users\BernardWork\Documents\Work\Net\PuttingApp\Images\360Angles.jpg" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image x:Name="imgPutter" Source="C:\Users\BernardWork\Documents\Work\Net\PuttingApp\Images\Putter.jpg" Opacity="0.5" Margin="130,105,70,105" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
<Label x:Name="lblShotStaus" Content="Label" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
<Canvas Name="canvasFaceAngle" Grid.ColumnSpan="2" Grid.Row="0" RenderTransformOrigin="0.543,0.511"></Canvas>
</Grid>
</Grid>
If I understand correct, I believe the problem may be with:
double width = GridGraphs.DesiredSize.Width;
double height = GridGraphs.DesiredSize.Height;
You are calling Measure with no bounds provided which means that it will tell you the size the Grid would be were there no limitations (hence, the DesiredSize). The DesiredSize and the ActualSize may be two very different values.
In the case above, the args of the SizeChanged method will provide you with what you need.
void GridGraphs_SizeChanged(object sender, SizeChangedEventArgs e)
{
RotatePutter(e.NewSize.Width, e.NewSize.Height);
}
So if the goal is just to get the grid width/height, the ActualWidth and ActualHeight properties should give you that information. One thing to be careful about though: while the Grid control changes size along with the window, the Canvas control does not. In other words, if you read the actualwidth/height of a grid, it should correspond to the side of the window, but if you read the actualwith/height of a canvas, it will probably give you incorrect information.
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.