Skip to content

Commit c6f4c4f

Browse files
committed
Various small updates
- Recognize when there are no active touches - Color code app background to make it obvious when someone is / is not touching trackpad - Register touches when app is not focused - Allow window to minimize to tray - Show touch status (green/red) in tray icon - Keep window on top of other windows - Start window in bottom right corner to keep it out of the way - Update to .NET 9.0
1 parent 38a9e68 commit c6f4c4f

9 files changed

+237
-73
lines changed

Images/Screenshot.png

-8.47 KB
Binary file not shown.

Images/screenshot-green.png

54.8 KB
Loading

Images/screenshot-red.png

25.5 KB
Loading

Images/screenshot-tray.png

132 KB
Loading

README.md

+14-4
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,23 @@ Sample to capture inputs from Precision Touchpad by [Raw Input](https://docs.mic
77

88
## Requirements
99

10-
- .NET 5.0
10+
- .NET 9.0
1111

12-
## Example
12+
Originally built on .NET 5.0, and upgraded in 2025, but no changes were needed. So, it should easily run on older versions of .NET
1313

14-
When five fingers are touching the touchpad of Surface Pro 4 Type Cover, five contacts appear with each coordinates.
14+
## Examples
1515

16-
![Screenshot](Images/Screenshot.png)
16+
When there are no fingers touching the touchpad, the window turns red
17+
18+
![Screenshot](Images/screenshot-red.png)
19+
20+
When five fingers are touching the touchpad of Surface Pro 4 Type Cover, five contacts appear with each coordinates, and the window turns green
21+
22+
![Screenshot](Images/screenshot-green.png)
23+
24+
When minimized the program shows up as a circle icon in the tray which is red when no touch is detected. When touch is detected it turns green and shows the number of touches.
25+
26+
![Screenshot](Images/screenshot-tray.png)
1727

1828
## License
1929

Source/RawInput.Touchpad/MainWindow.xaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
44
x:Name="WindowRoot"
55
Title="RawInput Touchpad"
6-
Height="260" Width="400" ResizeMode="NoResize">
6+
Height="180" Width="300" ResizeMode="CanMinimize">
77
<Grid>
88
<Grid.RowDefinitions>
99
<RowDefinition Height="Auto"/>
+218-66
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Drawing;
4+
using System.IO;
35
using System.Linq;
46
using System.Text;
7+
using System.Threading;
58
using System.Threading.Tasks;
69
using System.Windows;
710
using System.Windows.Controls;
@@ -14,69 +17,218 @@
1417

1518
namespace RawInput.Touchpad
1619
{
17-
public partial class MainWindow : Window
18-
{
19-
public bool TouchpadExists
20-
{
21-
get { return (bool)GetValue(TouchpadExistsProperty); }
22-
set { SetValue(TouchpadExistsProperty, value); }
23-
}
24-
public static readonly DependencyProperty TouchpadExistsProperty =
25-
DependencyProperty.Register("TouchpadExists", typeof(bool), typeof(MainWindow), new PropertyMetadata(false));
26-
27-
public string TouchpadContacts
28-
{
29-
get { return (string)GetValue(TouchpadContactsProperty); }
30-
set { SetValue(TouchpadContactsProperty, value); }
31-
}
32-
public static readonly DependencyProperty TouchpadContactsProperty =
33-
DependencyProperty.Register("TouchpadContacts", typeof(string), typeof(MainWindow), new PropertyMetadata(null));
34-
35-
public MainWindow()
36-
{
37-
InitializeComponent();
38-
}
39-
40-
private HwndSource _targetSource;
41-
private readonly List<string> _log = new();
42-
43-
protected override void OnSourceInitialized(EventArgs e)
44-
{
45-
base.OnSourceInitialized(e);
46-
47-
_targetSource = PresentationSource.FromVisual(this) as HwndSource;
48-
_targetSource?.AddHook(WndProc);
49-
50-
TouchpadExists = TouchpadHelper.Exists();
51-
52-
_log.Add($"Precision touchpad exists: {TouchpadExists}");
53-
54-
if (TouchpadExists)
55-
{
56-
var success = TouchpadHelper.RegisterInput(_targetSource.Handle);
57-
58-
_log.Add($"Precision touchpad registered: {success}");
59-
}
60-
}
61-
62-
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
63-
{
64-
switch (msg)
65-
{
66-
case TouchpadHelper.WM_INPUT:
67-
var contacts = TouchpadHelper.ParseInput(lParam);
68-
TouchpadContacts = string.Join(Environment.NewLine, contacts.Select(x => x.ToString()));
69-
70-
_log.Add("---");
71-
_log.Add(TouchpadContacts);
72-
break;
73-
}
74-
return IntPtr.Zero;
75-
}
76-
77-
private void Copy_Click(object sender, RoutedEventArgs e)
78-
{
79-
Clipboard.SetText(string.Join(Environment.NewLine, _log));
80-
}
81-
}
82-
}
20+
public partial class MainWindow : Window
21+
{
22+
[System.Runtime.InteropServices.DllImport("user32.dll")]
23+
static extern bool DestroyIcon(IntPtr handle);
24+
25+
public bool TouchpadExists
26+
{
27+
get { return (bool)GetValue(TouchpadExistsProperty); }
28+
set { SetValue(TouchpadExistsProperty, value); }
29+
}
30+
31+
public static readonly DependencyProperty TouchpadExistsProperty =
32+
DependencyProperty.Register(
33+
"TouchpadExists",
34+
typeof(bool),
35+
typeof(MainWindow),
36+
new PropertyMetadata(false)
37+
);
38+
39+
public string TouchpadContacts
40+
{
41+
get { return (string)GetValue(TouchpadContactsProperty); }
42+
set { SetValue(TouchpadContactsProperty, value); }
43+
}
44+
45+
public static readonly DependencyProperty TouchpadContactsProperty =
46+
DependencyProperty.Register(
47+
"TouchpadContacts",
48+
typeof(string),
49+
typeof(MainWindow),
50+
new PropertyMetadata(null)
51+
);
52+
53+
// reference to tray icon
54+
private System.Windows.Forms.NotifyIcon _notifyIcon;
55+
56+
// tracks if touch is showing, used to avoid updating the icon if it is already correct
57+
private int _touchesShowing = 0;
58+
59+
// when was the last touch event received
60+
private long _lastTouchTime = 0;
61+
62+
// timer to expire last touch event to detect when no one is touching the track pad anymore
63+
private Timer _touchTimer;
64+
65+
// list of all received touch events
66+
private readonly List<string> _log = new();
67+
68+
public MainWindow()
69+
{
70+
InitializeComponent();
71+
72+
// add tray icon
73+
_notifyIcon = new System.Windows.Forms.NotifyIcon
74+
{
75+
Text = "Touchpad Monitor",
76+
Visible = true,
77+
};
78+
SetIconState(System.Drawing.Color.Red, ' ');
79+
// restore window on tray icon click
80+
_notifyIcon.Click += (s, e) => RestoreWindow();
81+
// keep window on top when showing
82+
Topmost = true;
83+
// start window in bottom right corner
84+
var workingArea = SystemParameters.WorkArea;
85+
Left = workingArea.Width - Width;
86+
Top = workingArea.Height - Height;
87+
// start timer to track expired touches
88+
_touchTimer = new Timer(TouchTimerCallback, null, 0, 50);
89+
}
90+
91+
protected override void OnClosed(EventArgs e)
92+
{
93+
base.OnClosed(e);
94+
95+
_notifyIcon.Dispose();
96+
}
97+
98+
protected override void OnStateChanged(EventArgs e)
99+
{
100+
base.OnStateChanged(e);
101+
102+
if (WindowState == WindowState.Minimized)
103+
{
104+
Hide();
105+
_notifyIcon.Visible = true;
106+
}
107+
}
108+
109+
protected override void OnSourceInitialized(EventArgs e)
110+
{
111+
base.OnSourceInitialized(e);
112+
113+
var targetSource = PresentationSource.FromVisual(this) as HwndSource;
114+
targetSource?.AddHook(WndProc);
115+
116+
TouchpadExists = TouchpadHelper.Exists();
117+
118+
_log.Add($"Precision touchpad exists: {TouchpadExists}");
119+
120+
if (TouchpadExists)
121+
{
122+
var success = TouchpadHelper.RegisterInput(targetSource.Handle);
123+
124+
_log.Add($"Precision touchpad registered: {success}");
125+
}
126+
127+
Visibility = Visibility.Hidden;
128+
}
129+
130+
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
131+
{
132+
switch (msg)
133+
{
134+
case TouchpadHelper.WM_INPUT:
135+
var contacts = TouchpadHelper.ParseInput(lParam);
136+
_lastTouchTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
137+
TouchpadContacts = string.Join(
138+
Environment.NewLine,
139+
contacts.Select(x => x.ToString())
140+
);
141+
142+
var touchCount = contacts.Length;
143+
144+
if (touchCount != _touchesShowing)
145+
{
146+
_touchesShowing = touchCount;
147+
Background = new SolidColorBrush(
148+
System.Windows.Media.Color.FromRgb(144, 238, 144)
149+
);
150+
SetIconState(System.Drawing.Color.Green, touchCount.ToString().ToCharArray()[0]);
151+
}
152+
153+
_log.Add("---");
154+
_log.Add(TouchpadContacts);
155+
break;
156+
}
157+
return IntPtr.Zero;
158+
}
159+
160+
private void Copy_Click(object sender, RoutedEventArgs e)
161+
{
162+
Clipboard.SetText(string.Join(Environment.NewLine, _log));
163+
}
164+
165+
private void TouchTimerCallback(object state)
166+
{
167+
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
168+
if (now - _lastTouchTime > 100 && _touchesShowing > 0)
169+
{
170+
Dispatcher.Invoke(() =>
171+
{
172+
_touchesShowing = 0;
173+
TouchpadContacts = "";
174+
Background = new SolidColorBrush(
175+
System.Windows.Media.Color.FromRgb(238, 144, 144)
176+
);
177+
SetIconState(System.Drawing.Color.Red, ' ');
178+
});
179+
}
180+
}
181+
182+
private void RestoreWindow()
183+
{
184+
Show();
185+
WindowState = WindowState.Normal;
186+
Activate();
187+
_notifyIcon.Visible = false;
188+
}
189+
190+
private void SetIconState(System.Drawing.Color color, char label)
191+
{
192+
using (var icon = CreateCircleIcon(color, label))
193+
{
194+
_notifyIcon.Icon = icon;
195+
DestroyIcon(icon.Handle);
196+
}
197+
}
198+
199+
private static Icon CreateCircleIcon(System.Drawing.Color color, char character)
200+
{
201+
int size = 32;
202+
using (var bitmap = new Bitmap(size, size))
203+
{
204+
using (var graphics = Graphics.FromImage(bitmap))
205+
{
206+
graphics.Clear(System.Drawing.Color.Transparent);
207+
208+
// Draw the circle with the specified color
209+
using (var brush = new SolidBrush(color))
210+
{
211+
graphics.FillEllipse(brush, 0, 0, size, size);
212+
}
213+
214+
// Set up the font and brush for the character
215+
using var font = new Font(System.Drawing.FontFamily.GenericSansSerif, 6, System.Drawing.FontStyle.Bold);
216+
using var textBrush = new SolidBrush(System.Drawing.Color.White);
217+
// Measure the size of the character to center it
218+
SizeF textSize = graphics.MeasureString(character.ToString(), font);
219+
220+
// Calculate the position to center the character
221+
float x = (size - textSize.Width) / 2;
222+
float y = (size - textSize.Height) / 2;
223+
224+
// Draw the character centered on the circle
225+
graphics.DrawString(character.ToString(), font, textBrush, x, y);
226+
}
227+
228+
IntPtr hIcon = bitmap.GetHicon();
229+
return System.Drawing.Icon.FromHandle(hIcon);
230+
}
231+
}
232+
233+
}
234+
}

Source/RawInput.Touchpad/RawInput.Touchpad.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
<PropertyGroup>
44
<OutputType>WinExe</OutputType>
5-
<TargetFramework>net5.0-windows</TargetFramework>
5+
<TargetFramework>net9.0-windows</TargetFramework>
66
<UseWPF>true</UseWPF>
7+
<UseWindowsForms>true</UseWindowsForms>
78
<Authors>emoacht</Authors>
89
<Copyright>Copyright © 2021 emoacht</Copyright>
910
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Source/RawInput.Touchpad/TouchpadHelper.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,8 @@ public static bool RegisterInput(IntPtr windowHandle)
297297
{
298298
usUsagePage = 0x000D,
299299
usUsage = 0x0005,
300-
dwFlags = 0, // WM_INPUT messages come only when the window is in the foreground.
300+
dwFlags = 0x00000100, // WM_INPUT messages come regardless of what window is in focus
301+
// dwFlags = 0, // WM_INPUT messages come only when the window is in the foreground.
301302
hwndTarget = windowHandle
302303
};
303304

0 commit comments

Comments
 (0)