I'd like to create a control in my WPF app that allows the user to drag a dot inside a box/circle. This will be used to drive the pan and tilt values for a camera.
I am not sure how to create a control like that. The picture below is an example of the type of control that I want to develop.
Here's a very quick solution to get you started.
For the XAML, I've used an Ellipse control for the "dot". The Ellipse is placed inside a Canvas control (which allows the dot to be moved around):-
<Grid Background="White"
MouseUp="ParentOnMouseUp">
<Canvas x:Name="canvas"
Background="Green"
Width="200"
Height="200"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MouseMove="CanvasOnMouseMove">
<!-- Implement your blue circle b/g as an Image control here ... />
<Ellipse x:Name="dot"
Width="20"
Height="20"
Fill="Blue"
Loaded="DotOnLoaded"
MouseDown="DotOnMouseDown"/>
</Canvas>
</Grid>
First I handle the Ellipse's MouseDown event:
private void DotOnMouseDown(object sender, MouseButtonEventArgs e)
{
_isDraggingDot = true;
}
All I do here is set a flag to indicate that I'm starting to drag the dot.
Next, I handle the Canvas MouseMove event, which is where I move the dot around. It includes logic to ensure the dot doesn't stray outside the canvas:
private void CanvasOnMouseMove(object sender, MouseEventArgs e)
{
if (_isDraggingDot)
{
var mousePos = e.GetPosition(canvas);
var x = mousePos.X;
if (x < 0)
{
x = 0;
}
if (x > canvas.Width)
{
x = canvas.Width;
}
var y = mousePos.Y;
if (y < 0)
{
y = 0;
}
if (y > canvas.Height)
{
y = canvas.Height;
}
dot.SetValue(Canvas.LeftProperty, x - (dot.Width / 2.0)); // offset ensures dot is centred on mouse pointer
dot.SetValue(Canvas.TopProperty, y - (dot.Height / 2.0));
}
}
This is also where you would calculate the dot's vertical and horizontal offset from centre, and use these values to update the pan and tilt.
Finally, I implement the MouseUp event on the outer control (the Grid in my example):
private void ParentOnMouseUp(object sender, MouseButtonEventArgs e)
{
_isDraggingDot = false;
CentreDot();
}
private void CentreDot()
{
dot.SetValue(Canvas.LeftProperty, (canvas.Width / 2.0) - (dot.Width / 2.0));
dot.SetValue(Canvas.TopProperty, (canvas.Height / 2.0) - (dot.Height / 2.0));
}
The reason for handling the event on the outer control is to ensure that the dot is returned to the centre if the user releases the mouse button outside the Canvas.
(Note that I also set a b/g colour on the Grid, otherwise it defaults to transparent and won't detect mouse events!)
Lastly I wire up the Ellipse's Loaded event to initially centre the dot when the UI loads:
private void DotOnLoaded(object sender, RoutedEventArgs e)
{
CentreDot();
}
As I mentioned, this is just a quick solution where the dot simply follows the mouse. One If you don't like this, you could calculate how far the mouse is (vertically and horizontally) from the canvas centre, then use a small percentage of these values to position the dot away from the centre, effectively requiring more mouse movement to move the dot, which might feel more "natural".
Another idea may be to "snap" the dot's position to the nearest of the four arrow buttons (N,S,E,W), or even include the points in between (NE,SE,SW,NW).
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'm using the ViewportControl to scroll around and zoom in and out of my Map. In this map I've got a green ellipse which I wish to move around. Previously I used a ScrollViewer where I set the manipulationMode of the ScrollViewer to control, and thus making it capable of moving my ellipse around. However I can't find a similar way for the ViewportControl. So is there a way to move my ellipse around?
The code I've got so far is:
The xaml part where I have my ViewportControl around my map
<ViewportControl x:Name="ViewPortTestTest" Bounds="0,0,1271,1381.5" Height="480" Width="800" Canvas.ZIndex="1" Grid.Row="1">
<ViewportControl.RenderTransform>
<CompositeTransform x:Name="myTransformTest"/>
</ViewportControl.RenderTransform>
<View:Map x:Name="ZoomableContent" >
<View:Map.RenderTransform>
<CompositeTransform x:Name="myTransform" />
<!-- ScaleX="{Binding Map.imageScale}" ScaleY="{Binding Map.imageScale}"/>-->
</View:Map.RenderTransform>
</View:Map>
</ViewportControl>
It is in the map where I add the ellipse. The viewModel where I manipulate my ellipse
public void ManStart(ManipulationStartedEventArgs e)
{
e.Handled = true;
ViewportControl VP = FindParentOfType<ViewportControl>(ChampViewModelSel);
}
}
public void ManDelta(ManipulationDeltaEventArgs e)
{
e.Handled = true;
Point fingerPosition = e.DeltaManipulation.Translation;
Temp.x = fingerPosition.X;
Temp.y = fingerPosition.Y;
}
}
Where Temp.x and Temp.y is the new position of the ellipse.
I think you could try to use TouchPanel from XNA and Touch.FrameReported event for this purpose. Probably Map and VieportControl handle the manipulation event so it won't fire with your code.
In my proposal Touch_FrameReported will be fired every time you touch the screen, so we have to check only for situations we need - here comes IsGestureAvailable and TouchAction. Simple code can look like this:
public MainPage()
{
InitializeComponent();
TouchPanel.EnabledGestures = GestureType.FreeDrag;
Touch.FrameReported += Touch_FrameReported;
}
private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
if (TouchPanel.IsGestureAvailable) // check only dragging
{
// get point relative to Viewport
TouchPoint mainTouch = e.GetPrimaryTouchPoint(ViewPortTestTest);
// check if drag has completed (key up)
if (mainTouch.Action == TouchAction.Up)
{
Temp.x = mainTouch.Position.X;
Temp.y = mainTouch.Position.Y;
}
}
}
You can read more about Gestures here at MSDN and on this blog.
You can also check other GestureTypes and for example check if TouchAction.Down and user clicked on your Ellipse.
These methods give you many possibilities to read TouchPoints and their Actions so maybe it will help.
It took me a while to find a way how to disable Vieport. I've found a little 'hack' for that, it will be easier if your VieportControl was only horizontal or vertical (there is a lock property, but doesn't work for both). Here is this little 'hack' which should disable both horizontal and vertical scrolling:
Rect originalBounds = new Rect();
private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPoint myTouchPoint = e.GetPrimaryTouchPoint(ViewPortTestTest);
// disable scrolling
if (myTouchPoint.Action == TouchAction.Down) // here probably some other statement like if user hit ellipse
{
originalBounds = ViewPortTestTest.Bounds;
ViewPortTestTest.Bounds = ViewPortTestTest.Viewport; // set current view as Bounds
}
// more logic
// enable once again
if (myTouchPoint.Action == TouchAction.Up)
ViewPortTestTest.Bounds = originalBounds;
}
When I want to disable scrolling - I set bounds of the VieportControl to the current view (and I remember original ones). When I want to enable scrolling - I bring back original bounds.
As I've tested it - it is working - of course it needs a little more logic in locking if statement, so that it won't lock every time you touch the screen.
I am writing a silverlight application, where there is a requirement, that I need to draw rectangle over image and move it along with the mouse move. I can move the rectangle by holding left mouse click, but now I need to move without clicking or holding the mouse left click.
I have seen many examples but they all implements the moving shapes and rectangle on left mouse click, which definitely is not my requirement.
I tried many ways but couldn't get it right. Below is the code what I do currently. Any suggestions would be welcomed.
XAML
<Canvas x:Name="draw" Grid.Column="0" Background="Transparent"
Margin="0,0,0,150" Grid.RowSpan="2">
<Rectangle x:Name="SquareBlue" Width="100" Height="100" Canvas.Top="155" Canvas.Left="268" Fill="Transparent" Stroke="Black" StrokeThickness="2" />
</Canvas>
<Image x:Name="myImage" Height="100"/>
<TextBox x:Name="X" Margin="0,0,110,0"></TextBox>
<TextBox x:Name="Y" Margin="0,0,110,0"/>
<Image x:Name="pictureBox1" Height="100"/>
Code Behind
Boolean isMouseCaptured;
Double mouseX;
Double mouseY;
Int32 zIndex = 0;
private void Shape_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
Shape s = sender as Shape;
isMouseCaptured = false;
s.ReleaseMouseCapture();
mouseY = -1;
mouseX = -1;
}
private void Shape_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Shape s = sender as Shape;
mouseY = e.GetPosition(null).Y;
mouseX = e.GetPosition(null).X;
isMouseCaptured = true;
s.CaptureMouse();
s.SetValue(Canvas.ZIndexProperty, zIndex);
zIndex++;
}
private void Shape_MouseMove(object sender, MouseEventArgs e)
{
if (isMouseCaptured)
{
Shape s = sender as Shape;
double deltaY = e.GetPosition(null).Y - mouseY;
double deltaX = e.GetPosition(null).X - mouseX;
double newTop = deltaY + (double)s.GetValue(Canvas.TopProperty);
double newLeft = deltaX + (double)s.GetValue(Canvas.LeftProperty);
s.SetValue(Canvas.TopProperty, newTop);
s.SetValue(Canvas.LeftProperty, newLeft);
mouseY = e.GetPosition(null).Y;
mouseX = e.GetPosition(null).X;
X.Text = mouseX.ToString();
Y.Text = mouseY.ToString();
}
You probably want to consider the shape captured whenever the Shape_MouseMove is fired.
And then, every mouse move on the canvas/window moves the shape to be centerd around the mouse cursor.
You then have to decide when to release it maybe:
by detecting when the mouse leaves the canvas/window.
using the buttons for releasing explicitly.
checking the time between mouseMove events. if 1 second passed, then be in a logical state where the shape is not and cannot be captured in mouseMove. only when mouse is out of the shape, return to normal state. so user will lose the move and has to cursor out and in again to capture again.
additionaly, when entering the state described in 3, launch a timer and then if after a second the cursor is on the shape, re-capture.
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);
}
In WPF/C# how would I rotate a "graphic" to face the current mouse position?
Basically what I want is a "wheel" UI Control (like an analog volume dial). I want to be able to click and drag the dial and it will rotate to follow the mouse. Then when I release the mouse it will stop following (obviously!).
How would I create one of these? does one already exist somewhere?
I haven't seen any controls like this around (though it's been a while since I looked at all of the controls that WPF control vendors were offering), but it's relatively straightforward to create one.
All you'd have to do is create a custom control containing an Image (or XAML drawing) that you can rotate to follow the mouse. Then, bind a RotateTransform to an 'Angle' DependencyProperty on your custom control so that when 'angle' is updated, the image/drawing rotates to match:
<UserControl x:Class="VolumeControlLibrary.VolumeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VolumeControlLibrary"
Height="60" Width="60">
<Image Source="/VolumeControl;component/knob.png" RenderTransformOrigin="0.5,0.5" >
<Image.RenderTransform>
<RotateTransform Angle="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:VolumeControl}}, Path=Angle}"/>
</Image.RenderTransform>
</Image>
</UserControl>
Setting RenderTransformOrigin to "0.5, 0.5" ensures that the control rotates around its center, rather than rotating around the top left corner; we'll have to compensate for this in the angle calculation too.
In the code behind file for your control, add handlers for the mouse and the Angle DependencyProperty:
public partial class VolumeControl : UserControl
{
// Using a DependencyProperty backing store for Angle.
public static readonly DependencyProperty AngleProperty =
DependencyProperty.Register("Angle", typeof(double), typeof(VolumeControl), new UIPropertyMetadata(0.0));
public double Angle
{
get { return (double)GetValue(AngleProperty); }
set { SetValue(AngleProperty, value); }
}
public VolumeControl()
{
InitializeComponent();
this.MouseLeftButtonDown += new MouseButtonEventHandler(OnMouseLeftButtonDown);
this.MouseUp += new MouseButtonEventHandler(OnMouseUp);
this.MouseMove += new MouseEventHandler(OnMouseMove);
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Mouse.Capture(this);
}
private void OnMouseUp(object sender, MouseButtonEventArgs e)
{
Mouse.Capture(null);
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (Mouse.Captured == this)
{
// Get the current mouse position relative to the volume control
Point currentLocation = Mouse.GetPosition(this);
// We want to rotate around the center of the knob, not the top corner
Point knobCenter = new Point(this.ActualHeight / 2, this.ActualWidth / 2);
// Calculate an angle
double radians = Math.Atan((currentLocation.Y - knobCenter.Y) /
(currentLocation.X - knobCenter.X));
this.Angle = radians * 180 / Math.PI;
// Apply a 180 degree shift when X is negative so that we can rotate
// all of the way around
if (currentLocation.X - knobCenter.X < 0)
{
this.Angle += 180;
}
}
}
}
Capturing the mouse ensures that your control will continue to get mouse updates even when the user mouses off of the control (until they let go of the click), and by getting the position of the mouse relative to the current element (the control), your calculation should always be the same regardless of where the control actually renders on screen.
In this example, when the mouse moves we calculate the angle between it and the center of the control, and then set this angle to the Angle DependencyProperty we created. Since the image we're displaying is bound to this angle property, WPF automatically applies the new value, which results in the knob rotating in combination with the mouse moving.
Using the control in your solution is easy; just add:
<local:VolumeControl />
You would bind to the Angle property on VolumeControl if you wanted to bind the value of the knob to something in your application; that value is currently in degrees, but could add an additional property to convert between an angle in degrees and a value that makes sense to you (say, a value from 0 - 10).
To add to that post, the angle between the mouse point and the object point is calculated like:
dot = currentLocation.X * objectPosition.X + currentLocation.Y * objectPosition.Y;
angle = Math.Acos(dot);
In my case i have dynamically created shapes which shall rotated toward mouse direction. To solve this I used a lightweight function. All I need is following:
The centerpoint of the current selected shape
The point from the last mouseover step
And the point from the current mouseover step
It is not necessary to use methods from the Math library. I calculate the angle which depends on the difference of the current mouse over point and the previous mouse over point and the position in relation o the center point. Finally I add the angle on the exisitng angle of the current object.
private void HandleLeftMouseDown(MouseButtonEventArgs eventargs)
{
//Calculate the center point of selected object
//...
//assuming Point1 is the top left point
var xCenter = (_selectedObject.Point2.X - _selectedObject.Point1.X) / 2 + _selectedObject.Point1.X
var yCenter = (_selectedObject.Point2.Y - _selectedObject.Point1.Y) / 2 + _selectedObject.Point1.Y
_selectedObjectCenterPoint = new Point((double) xCenter, (double) yCenter);
//init set of last mouse over step with the mouse click point
var clickPoint = eventargs.GetPosition(source);
_lastMouseOverPoint = new Point(clickPoint.X,clickPoint.Y);
}
private void HandleMouseMove(MouseEventArgs eventArgs)
{
Point pointMouseOver = eventArgs.GetPosition(_source);
//Get the difference to the last mouse over point
var xd = pointMouseOver.X - _lastMouseOverPoint.X;
var yd = pointMouseOver.Y - _lastMouseOverPoint.Y;
// the direction depends on the current mouse over position in relation to the center point of the shape
if (pointMouseOver.X < _selectedObjectCenterPoint.X)
yd *= -1;
if (pointMouseOver.Y > _selectedObjectCenterPoint.Y)
xd *= -1;
//add to the existing Angle
//not necessary to calculate the degree measure
_selectedObject.Angle += (xd + yd);
//save mouse over point
_lastMouseOverPoint = new Point(pointMouseOver.X, pointMouseOver.Y);
}