WPF: Tips - Moving UI To Cursor
This article explains an approach which moves the entire UI towards a cursor.
Introduction
This code was written for a question on the MSDN forum here where the OP wanted to move the UI as the cursor moved. The description made reference to the UI for a game called "Destiny". The effect may have been used in that game in order to make it look "cool" or maybe to make it easier to use on some devices.
This was quite an interesting puzzle to solve.
The approach could well be useful on some industrial machines which have a joystick like toggle rather than a mouse or maybe even for laptops where the cursor is controlled using something rather more limiting than a mouse.
The potential to allow scrolling round an area bigger than the viewport using one input mechanism independent of sliders is also quite interesting.
You can download the sample for this article from here.
What Does Destiny Do
Looking at the Destiny video it was obvious that as the user moves the cursor in a direction, the ui controls move in the opposite direction. As you move the cursor up, the controls move down. As you move the cursor left, the controls move right.
The event which fires as you move the mouse is ( surprise ) MouseMove and, somehow, we want to handle that and move the UI from there.
It's also pretty obvious that this movement is not 1:1 you don't move 1cm and find the controls move 1cm in response - it's some sort of proportion of the distance and speed.
What's It Look Like?
The UI in the sample is very simple since it's a proof of concept rather than a replacement for a flashy game. The window looks like this:
( Bet you're amazed at the slick UI design, huh ?)
There are 2 rectangles which start off in the top left and bottom right corners of the window.
In the picture, the cursor has been moved towards the top left. As you do so, the contents of the window move towards the bottom right and off the window which contains them.
How Can You Move UI At All?
For most layout, controls are stuck inside their parent container. There are issues if you try and position something way outside it's container. A window is a ContentControl so you might think that's it we're going to need a rocket scientist to get this to work. Luckily, there's a trick which mere mortals can use.
A Canvas ignores some of the usual rules for it's children and that means you can position something outside of it without any awkward problems. It also means its children can be bigger than the Canvas itself which is kind of weird but something for a later article.
We want all our UI in some convenient panel so let's just make that a Grid for now and we'll position this using the attached properties Canvas.Left and Canvas.Top.
As we change these then the Grid will move.
Put that together and we get:
Title="MainWindow" Height="350" Width="525">
<Canvas Name="ParentCanvas">
<Grid Name="Container"
Canvas.Left="0"
Canvas.Top="0" >
</Grid>
</Canvas>
</Window>
We need some sort of a prototype UI in order to see this working, so let's put a few rows and columns in there and a couple of rectangles.
This is just to prove the concept that we can get things moving around.
That gives us.
Title="MainWindow" Height="350" Width="525">
<Canvas Name="ParentCanvas">
<Grid Name="Container"
Canvas.Left="0"
Canvas.Top="0"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Fill="Red"/>
<Rectangle Grid.Row="2" Grid.Column="2" Fill="Blue"/>
</Grid>
</Canvas>
</Window>
The only problem with that is one look at the designer window shows we have no rectangles. There's nothing there. What's up?
As it turns out one of those plusses which made the Canvas attractive causes this problem. The Grid doesn't automatically fill the canvas like you might expect.
That means we need to set an absolute number as the height and width of our Grid. Except of course this is WPF so we want to somehow bind those.
What could we bind to? The Window is not a great choice since Window height and width include the window chrome - the border and the heading area with title and control boxes. We could go for Window.Content but there again we have a Canvas which is filling that so let's use the canvas.
Final Markup
Naming the Canvas we can conveniently use ElementName binding to the Actual Height and Width. This will also cope with any re-sizing of the window automatically.
<Window x:Class="wpf_ContentMoves.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:wpf_ContentMoves"
mc:Ignorable="d"
Title="MainWindow" Height="550" Width="800"
MouseMove="Window_MouseMove"
MouseEnter="Window_MouseEnter"
>
<Canvas Name="ParentCanvas">
<Grid Name="Container"
Canvas.Left="0"
Canvas.Top="0"
Height="{Binding ActualHeight, ElementName=ParentCanvas}"
Width="{Binding ActualWidth, ElementName=ParentCanvas}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Fill="Red"/>
<Rectangle Grid.Row="2" Grid.Column="2" Fill="Blue"/>
</Grid>
</Canvas>
</Window>
The Grid is also named because we're going to move the thing about and want an easy way to refer to that in code. You will also notice that there are two eventhandlers there. We want the mousemove to handle the actual moving.
Should you move the cursor off the window then the UI could be all over the place so MouseEnter will be used to reset back to the original position. MouseEnter is more reliable than Leave which occasionally will not fire. MouseEnter may not fire for a while after the cursor goes over the UI but it'll eventually kick in and so if that's a bit unreliable it's less of a problem. All that will happen is the user manages to move their cursor a centimetre or so before the UI is centralised back. That's no big deal.
Code
Mousemove will fire numerous times as you move a cursor. A summary of the algorithm adopted is:
- Find the difference between the X and Y co-ordinates since the last MouseMove
- Divide by Ten
- Apply the result in the opposite direction
- Set the Canvas.Left and Canvas.Right attached properties on the Grid ( Container ).
Which ends up like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private Point LastPosition; // Points are structs and these will be 0,0
private Point CurrentPosition;
private double X;
private double Y;
private void Window_MouseMove(object sender, MouseEventArgs e)
{
CurrentPosition = Mouse.GetPosition(Container);
if (LastPosition.X == 0 && LastPosition.Y == 0)
{
LastPosition = CurrentPosition;
return;
}
double xdiff = (CurrentPosition.X - LastPosition.X) / 10;
double ydiff = (CurrentPosition.Y - LastPosition.Y) / 10;
X -= xdiff;
Y -= ydiff;
PositionContainer(X, Y);
}
private void PositionContainer(double x, double y)
{
Canvas.SetLeft(Container, X);
Canvas.SetTop(Container, Y);
LastPosition = CurrentPosition;
}
private void Window_MouseEnter(object sender, MouseEventArgs e)
{
CurrentPosition = new Point(0, 0);
X = 0;
Y = 0;
PositionContainer(X, Y);
}
}
The variables xdiff and ydiff are not really necessary, they're to make the critical calculation easier to read.
The actual moving of the Canvas is in a separate method because it will also be used from the MouseEnter event handler.
In case you're unfamiliar with the syntax, attached properties are set in a bit of a strange way. You use the object Set property method of the Type the attached property is associated with. Hence Canvas.Left uses the SetLeft method of the Canvas object.
See Also
This article is part of the WPF Tips series, if your interest is WPF then you will probably find other articles of interest there.
WPF Resources on the Technet WIki