Rotate graphic towards mouse in WPF (like an analog dial) - c#

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);
}

Related

How to create a control for panning and tilt together?

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).

Scale canvas to mouse position

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;

TranslateTransform offsets element a bit too much after multiple transforms in Windows 8 store application

I have a draggable element in a StackPanel in a Windows 8 store app. My goal is simple: drag the item somewhere on the screen and immediately after the user stops dragging it the element should return to its original starting position.
I have the following code which is meant to accomplish this task:
private void grdCover_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
Grid coverControl = sender as Grid;
double xOffset = -e.Cumulative.Translation.X;
double yOffset = -e.Cumulative.Translation.Y;
if (coverControl.RenderTransform is TranslateTransform)
{
TranslateTransform existingTransform = coverControl.RenderTransform as TranslateTransform;
existingTransform.X += xOffset;
existingTransform.Y += yOffset;
}
else
{
TranslateTransform transform = new TranslateTransform();
transform.X = xOffset;
transform.Y = yOffset;
coverControl.RenderTransform = transform;
}
}
The code sort of works. The screen looks like this upon application start:
The top element, which looks like a H is well aligned with the bottom element which looks like a U. When I first drag the H element it jumps back to its well aligned position, or at least any misalignment is so little that it's hardly perceivable to the naked eye. As I keep dragging and releasing the H element it gets more and more misaligned like this:
After 15-20 dragging moves the H element gets completely misplaced:
The problem might be due to some rounding error of the double values in the Point objects when they are translated into pixels, but that's only a wild guess. Also, on a high resolution device, such as my PC it takes more moves for the misalignment to become visible than on a lower resolution one, such as the Windows Simulator in Visual Studio.
Some other code that may be related to this issue:
I perform the dragging operation with the following code:
private void Grid_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
TranslateTransform translateTransform = (sender as Grid).RenderTransform as TranslateTransform;
translateTransform.X += e.Delta.Translation.X;
translateTransform.Y += e.Delta.Translation.Y;
}
I also have the following event handler in place to stop the element from moving too much on the screen after the user is done dragging the element:
private void grdCover_ManipulationInertiaStarting(object sender, ManipulationInertiaStartingRoutedEventArgs e)
{
e.TranslationBehavior.DesiredDeceleration = 10000000000000000;
e.Handled = true;
}
In case you're wondering I wanted to set the desired deceleration to Double.Max to block any further "sliding" movement on the screen but I got an exception saying it was out of bounds. So instead I chose this arbitrary large value.
I have no extra margins or padding on the layout root or the element to be moved or its container.
Any ideas are welcome.
Thanks, Andras
OK, I got it to work. The problem was that there was a slight mismatch between the Point coordinates in the following code bits:
TranslateTransform translateTransform = (sender as Grid).RenderTransform as TranslateTransform;
translateTransform.X += e.Delta.Translation.X;
translateTransform.Y += e.Delta.Translation.Y;
and
double xOffset = -e.Cumulative.Translation.X;
double yOffset = -e.Cumulative.Translation.Y;
The solution was to retrieve the value of
e.Delta.Translation.X
and
e.Delta.Translation.Y
and assign their values to the variables xOffset and yOffset. Retrieving the values of
e.Cumulative.Translation.X
and
e.Cumulative.Translation.Y
gave slightly different coordinates so the element kept jumping back to the wrong location.

DynamicDataDisplay D3:ChartPlotter zoom proplem

I'm using a simple ChartPlotter in my C# WPF application. When I zoom in/out by mouse scrolling, both Axis are changed. How can I control the zooming by mouse scrolling so it will affect only the X-Axis?
This feature is already built into D3, if you hover your mouse over one of the axes, and do a mouse wheel scroll, the zoom is only pertained to the axis you were hovered over. If you want to replicate this in your code, you can see examples of it in the source code.
The zoom feature is implemented in "MouseNavigation.cs". The MouseWheel handler will call underneath function:
Viewport.Visible = Viewport.Visible.Zoom(zoomTo, zoomSpeed);
And fortunately, there is a ZoomX function for your needs.
Thus just remove MouseNavigation from your plotter, then re-implement your own as below:
// Remove mouse navigation
plotter.Children.Remove(plotter.MouseNavigation);
// ZoomX when wheeling mouse
private void plotter_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
if (!e.Handled)
{
Point mousePos = e.GetPosition(this);
Point zoomTo = mousePos.ScreenToViewport(plotter.Viewport.Transform);
double zoomSpeed = Math.Abs(e.Delta / Mouse.MouseWheelDeltaForOneLine);
zoomSpeed *= 1.2;
if (e.Delta < 0)
{
zoomSpeed = 1 / zoomSpeed;
}
plotter.Viewport.SetChangeType(ChangeType.Zoom);
plotter.Viewport.Visible = plotter.Viewport.Visible.ZoomX(zoomTo, zoomSpeed);
plotter.Viewport.SetChangeType();
e.Handled = true;
}
}

Rotating an image to follow mouse position - WPF C#

I am trying to make an image rotate to follow the mouse position just like the ship from asteroids but to be controled with the mouse position instead of the arrow keys... help would be apreciated!
First you need to know the position of your image. You can then find the cursor position by the MouseMove event. This event invokes every time your mouse moves. You can use the GetPosition method to find a Point to obtain your X and Y coordinates.
private void Window_MouseMove(object sender, MouseEventArgs e)
{
Point point = e.GetPosition(null);
}
Now that you've obtained the X and Y coordinate you can use Pythagorean Theorem to find the distance between your image and where your cursor is. Now to find the angle you can AngleOfImage = sin^-1(Y/total distance between your image and cursor) to find the angle.
If you want the image to move after it has already rendered you need to use the RenderTransform property. Since you need the image to rotate you can make use of the RotateTransform class to accomplish this. Since you have calculated the angle and set it equal to a property, AngleOfImage, you can bind that property to the Angle dependency property of RotateTransform So your xaml would look something like this.
<Image>
<Image.RenderTransform>
<RotateTransform Angle="{Binding AngleOfImage}"/>
</Image.RenderTransform>
</Image>
Actual working code for MouseMove event to calculate the angle
private void Window_MouseMove(object sender, MouseEventArgs e)
{
var somePoint = e.GetPosition(mainWindow);
X = somePoint.X;
Y = somePoint.Y;
var newX = Abs(X - RectangleOriginX);
var newY = Abs(Y - RectangleOriginY);
var powX = Pow(newX, 2);
var powY = Pow(newY, 2);
var distance = Sqrt(powX + powY);
var result = newX / distance;
Angle = Asin(result).ToDegrees();
}
Where ToDegrees() is just an extension method to convert the value to degrees. RectangleOriginX and RectangleOriginY are the points of my control I am moving which I got from this
private void mainWindow_Loaded(object sender, RoutedEventArgs e)
{
var origialPoint = rect.TransformToAncestor(mainWindow).Transform(new Point(0, 0));
RectangleOriginX = origialPoint.X;
RectangleOriginY = origialPoint.Y;
}
Also my working xaml
<Rectangle x:Name="rect" Width="100" Height="100" RenderTransformOrigin="0.5,0.5" Fill="Red">
<Rectangle.RenderTransform>
<RotateTransform Angle="{Binding Angle}"/>
</Rectangle.RenderTransform>
</Rectangle>
Sorry for my bad english
Try to use System.Windows.Forms.Timer, and in the Timer_tick event call some Move(MouseEventArgs e).
Something like this
private void Move(MouseEventArgs e)
{
if(mainwindow has focus)
{
your_image.Position = NextPosition(e);
}
}
instance of MouseEventArgs is field of your class, which changes in MouseMove event.
In NextPosition you can use any pathfind algorithm
If I can understand you right
you can make it by some maths
Get the center point first
Get the mouse position
Use Math.ASin() to get the angle
It will be like that Math.ASin(x / d);
x will be mouse location - image center location
d will be the distance between the center of the image and mouse location you can get it by Pythagorean theorem
then use a timer to make that happen.
I hope that my answer will help you.

Categories

Resources