Create a Mouse Cursor Tracker using PowerShell and WPF

Sometimes when you have a little bit of free time, you decide to build things just to see if you can do it regardless of how useful it might actually be. I think this might be one of those occasions.

While working on a different side project, I somehow took a path that led me down looking at the X and Y coordinates of my mouse cursor in relation to its position on my window. Like some of the things that I start working on, I may not have a good reason to do it other than just for the simple fact of challenging myself to make it work.

Before I dive into what I did to make a “mouse tracker”, I wanted to explain my goal of actually making it. My goal is to have a small UI that can track my mouse movements in real time that would show my X and Y coordinates of the cursor based on its location on the window. It is important to know that the X coordinate is based on the position from the left of the window while the Y coordinate is based on the top of the window.

image

The process to get your mouse coordinates is actually pretty simple. We just have to look at the Position property in the System.Windows.Forms.Cursor class. Note that you will have to use Add-Type System.Windows.Forms if running from the console!

[System.Windows.Forms.Cursor]::Position

image

Note the X and Y coordinates above. This means that at the time that I ran the command, my mouse cursor was 54 pixels from the top of my screen and 362 pixels from the left of my screen. Now that we know how to get the mouse coordinates, the next step is to create our UI to display this on the screen.

I do want to add that my example code includes stuff related to runspaces because I make it a habit to put all of my UI stuff in runspaces for better responsiveness.

$Runspacehash = [hashtable]::Synchronized(@{})
$Runspacehash.host = $Host
$Runspacehash.runspace = [RunspaceFactory]::CreateRunspace()
$Runspacehash.runspace.ApartmentState = 'STA'
$Runspacehash.runspace.ThreadOptions = 'ReuseThread'
$Runspacehash.runspace.Open()
$Runspacehash.psCmd = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,System.Windows.Forms}.GetPowerShell()
$Runspacehash.runspace.SessionStateProxy.SetVariable('Runspacehash',$Runspacehash)
$Runspacehash.psCmd.Runspace = $Runspacehash.runspace
$Runspacehash.Handle = $Runspacehash.psCmd.AddScript({

Since I only care about showing the X and Y coordinates on the screen, I am going with a Grid that has 2 rows and 2 columns to show the coordinates. I also want to make this pretty small as I have no need to display a large UI just to display something like this. Being that I am using WPF as my UI, I will create the front end UI using XAML.

#Build the GUI
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window" WindowStartupLocation = "CenterScreen"
Width = "80" Height = "50" ShowInTaskbar = "False" ResizeMode = "NoResize"
Topmost = "True" WindowStyle = "None" AllowsTransparency="True" Background="Transparent">
<Border BorderBrush="{x:Null}" BorderThickness="1" Height="50" Width="80" HorizontalAlignment="Left"
CornerRadius="15" VerticalAlignment="Top" Name="Border">
<Border.Background>
<LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
<LinearGradientBrush.GradientStops> <GradientStop Color='#C4CBD8' Offset='0' /> <GradientStop Color='#E6EAF5' Offset='0.2' />
<GradientStop Color='#CFD7E2' Offset='0.9' /> <GradientStop Color='#C4CBD8' Offset='1' /> </LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>
<Grid ShowGridLines='False'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height = '*'/>
<RowDefinition Height = '*'/>
</Grid.RowDefinitions>
<Label x:Name='X_lbl' Tag = 'X' Grid.Column = '0' Grid.Row = '0' FontWeight = 'Bold'
HorizontalContentAlignment="Center">
<TextBlock TextDecorations="Underline"
Text="{Binding Path=Tag,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Label}}}"/>
</Label>
<Label x:Name='Y_lbl' Tag = 'Y' Grid.Column = '1' Grid.Row = '0' FontWeight = 'Bold'
HorizontalContentAlignment="Center">
<TextBlock TextDecorations="Underline"
Text="{Binding Path=Tag,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Label}}}"/>
</Label>
<Label x:Name='X_data_lbl' Grid.Column = '0' Grid.Row = '1' FontWeight = 'Bold'
HorizontalContentAlignment="Center" />
<Label x:Name='Y_data_lbl' Grid.Column = '1' Grid.Row = '1' FontWeight = 'Bold'
HorizontalContentAlignment="Center" />
</Grid>
</Border>
</Window>
"@

Now I can load up the XAML and connect to my controls that I will need for updating the data.

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

#region Connect to Controls
$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach {
New-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force -ErrorAction SilentlyContinue -Scope Script
}
#endregion Connect to Controls

At this point I have a UI that will work if we happened to run it.

[void]$Window.ShowDialog()
}).BeginInvoke()

That last line is just closing the loop with creating my runspace. But what we get is the following:

image

Getting there! You may have noticed that there is not a close button and if you run this, it may be difficult to move it around based on how I configured the window. I get around this by adding event handlers for the Right and Left mouse buttons to drag the window around and to close  the window.

$Window.Add_MouseRightButtonUp({
$This.close()
})
$Window.Add_MouseLeftButtonDown({
$This.DragMove()
})

We have the UI and the X and Y label already taken care of. The next piece of the puzzle is to get our coordinates to show up so we can start tracking our cursor position. This is where I had to think a little on how to ensure that the coordinates would update as I moved my mouse around. I looked at a number of events for the window but none of them really fit what I wanted to do. We had mouse enter and mouse leave events, but they only updated 1 time each time the event fired meaning that I wasn’t able to get the coordinates any other time.

In the end I decided to use a timer object attached to my window that will update every 10 milliseconds. I figure this was enough of a time that it would appear that the mouse tracker was updating the entire time while I move my mouse.

$Window.Add_SourceInitialized({
#Create Timer object
Write-Verbose 'Creating timer object'
$Script:timer = new-object System.Windows.Threading.DispatcherTimer
#Fire off every 1 minutes
Write-Verbose 'Adding 1 minute interval to timer object'
$timer.Interval = [TimeSpan]'0:0:0.01'
#Add event per tick
Write-Verbose 'Adding Tick Event to timer object'
$timer.Add_Tick({
$Mouse = [System.Windows.Forms.Cursor]::Position
$X_data_lbl.Content = $Mouse.x
$Y_data_lbl.Content = $Mouse.y
})
#Start timer
Write-Verbose 'Starting Timer'
$timer.Start()
If (-NOT $timer.IsEnabled) {
$Window.Close()
}
})

Now I have a working mouse tracker to use for whatever you would like to do with it!

MouseTracker

 

Download MouseTracker

https://gallery.technet.microsoft.com/scriptcenter/PowerShell-Mouse-Cursor-2cde303d

This entry was posted in powershell and tagged , , , . Bookmark the permalink.

4 Responses to Create a Mouse Cursor Tracker using PowerShell and WPF

  1. Hello,
    your XAML code is broken in this article all the non xml compatible signs like “<” are replaced with their xml-friendly equivalents, like “<“.

    Another thing you can improve, is loading the xaml directly using the Parse method on the parser
    [string]$xaml = “… your xaml here ”
    $Window=[Windows.Markup.XamlReader]::Parse( $xaml )
    But make sure that the $xaml is string not XML.

    Last thing is that there is an event that does exactly what you need. It notifies you about mouse move and its position. You just need to ask for it. Here is C# code for it, I am lazy to setup the whole whing in PowerShell :))

        private void MainWindow_OnMouseMove(object sender, MouseEventArgs e)
        {
            var mousePosition = e.GetPosition((IInputElement) sender);
            Title = $"x: {mousePosition.X} y: {mousePosition.Y}";
        }
    

    You could also have a look in this thread: http://stackoverflow.com/questions/1479665/easiest-way-to-draw-a-sequence-of-points-in-wpf-from-code that outlines how to draw line under the cursor in an extremely simple manner. (I would just cast the sender to IInputElement instead of using canvas directly. )

    I still love your runspace stuff though 🙂

    • Boe Prox says:

      Thanks for the heads up on the XAML code. Sometimes wordpress decides to mess things up and other times it ‘just works’.I have fixed the formatting so it should work fine now.

      I’m not sure I understand the rest of your comment in regards to drawing a line under the cursor. This UI is only used to determine the X and Y coordinates on a window while the actual UI itself is very small as to only display the coordinates and really has no need to draw any lines.

      The On_MouseMove event that is referenced only fires if the cursor is moving over the actual control that is event being referenced on, most likly the Window itself. It will not actually fire if you are outside of the control. That makes it not that useful in this particular UI given the small size of it.

      • Oh, I probably just read only the first paragraph where you talk about tracking the mouse movement on your window. So I lived under the impression that tracking movement in the Window area is enough for you. Looking at the gif, it’s not easy to see that you track the movement on the whole screen.

        So that’s why I suggested to use the mouse move event (and suggested drawing a line that follows the mouse cursor, which is more impressive than just showing the x and y coordinates.)

        Have a nice day!

        • Boe Prox says:

          Yea, I didn’t want to show the whole window because it would have been a pretty small thing to view (people could just click on it if they wanted) so that is something I could fix for the sake of better viewing.
          The addition of drawing a line behind the visor does sound cool and if I get some free time, I may give it a go (but definitely outside the scope of this article).
          Thanks!

Leave a comment