GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
HardwareHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Drawing;
20using System.Linq;
21using System.Runtime.InteropServices;
22using System.Text;
23using System.Threading;
24using System.Threading.Tasks;
25using System.Windows.Input;
26using static GenerallyPositive.Enums;
27using ModifierKeys = GenerallyPositive.Enums.ModifierKeys;
28
29namespace GenerallyPositive
30{
34 internal class HardwareHelper
35 {
36 // virtual keycodes: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes?redirectedfrom=MSDN
37 // scan codes: https://en.wikipedia.org/wiki/Scancode
38 static byte scanCode = 0;
39
40 // shared instance: a fresh Random(seed) per call was deterministic (same seed every time)
41 private static readonly Random _random = new Random();
42
50 internal static int GetTypingDelay(int ceilingMs)
51 {
52 if (0 >= ceilingMs)
53 return 0;
54
55 int floorMs = Math.Min(25, ceilingMs);
56 return _random.Next(floorMs, ceilingMs + 1);
57 }
58
64 // https://stackoverflow.com/questions/26845532/c-sharp-ascii-to-keycode
65 [DllImport("user32.dll")]
66 public static extern short VkKeyScan(char ch);
67
75 // http://tksinghal.blogspot.com/2011/04/how-to-press-and-hold-keyboard-key.html
76 [DllImport("user32.dll")]
77 public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, uint dwExtraInfo);
78
85 // https://docs.microsoft.com/en-us/dotnet/desktop/winforms/input-mouse/how-to-simulate-events?view=netdesktop-6.0
86 [DllImport("user32.dll", EntryPoint = "SetCursorPos")]
87 [return: MarshalAs(UnmanagedType.Bool)]
88 private static extern bool SetCursorPos(int x, int y);
89
95 [DllImport("user32.dll")]
96 public static extern bool GetCursorPos(out Point p);
97
106 // https://stackoverflow.com/questions/8739523/directing-mouse-events-dllimportuser32-dll-click-double-click
107 [DllImport("user32.dll")]
108 static extern void mouse_event(int dwFlags, int dx, int dy,
109 int dwData, int dwExtraInfo);
110
114 [Flags]
115 public enum MouseEventFlags
116 {
117 LEFTDOWN = 0x00000002,
118 LEFTUP = 0x00000004,
119 MIDDLEDOWN = 0x00000020,
120 MIDDLEUP = 0x00000040,
121 MOVE = 0x00000001,
122 ABSOLUTE = 0x00008000,
123 RIGHTDOWN = 0x00000008,
124 RIGHTUP = 0x00000010
125 }
126
133 public static void MouseDown(int x, int y, ClickType clickType)
134 {
135 MoveMouse(x, y);
136 if (ClickType.LeftClick == clickType)
137 mouse_event((int)(MouseEventFlags.LEFTDOWN), x, y, 0, 0);
138 else if (ClickType.MiddleClick == clickType)
139 mouse_event((int)(MouseEventFlags.MIDDLEDOWN), x, y, 0, 0);
140 else if (ClickType.RightClick == clickType)
141 mouse_event((int)(MouseEventFlags.RIGHTDOWN), x, y, 0, 0);
142 else
143 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown mouse down clicktype [{clickType}]", null, GPALObjectType.None);
144 }
151 public static void MouseUp(int x, int y, ClickType clickType)
152 {
153 MoveMouse(x, y);
154 if (ClickType.LeftClick == clickType)
155 mouse_event((int)(MouseEventFlags.LEFTUP), x, y, 0, 0);
156 else if (ClickType.MiddleClick == clickType)
157 mouse_event((int)(MouseEventFlags.MIDDLEUP), x, y, 0, 0);
158 else if (ClickType.RightClick == clickType)
159 mouse_event((int)(MouseEventFlags.RIGHTUP), x, y, 0, 0);
160 else
161 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown mouse down clicktype [{clickType}]", null, GPALObjectType.None);
162 }
163
169 public static void MiddleClick(int x, int y)
170 {
171 MoveMouse(x, y);
172 // Separate down + up with a short dwell - a zero-duration combined click is dropped
173 // intermittently by browsers (notably Firefox). Mirrors the reliable RightClick pattern.
174 mouse_event((int)(MouseEventFlags.MIDDLEDOWN), x, y, 0, 0);
175 Thread.Sleep(10);
176 mouse_event((int)(MouseEventFlags.MIDDLEUP), x, y, 0, 0);
177 }
183 public static void LeftClick(int x, int y)
184 {
185 MoveMouse(x, y);
186 // Separate down + up with a short dwell - a zero-duration combined click is dropped
187 // intermittently by browsers (notably Firefox). Mirrors the reliable RightClick pattern.
188 mouse_event((int)(MouseEventFlags.LEFTDOWN), x, y, 0, 0);
189 Thread.Sleep(10);
190 mouse_event((int)(MouseEventFlags.LEFTUP), x, y, 0, 0);
191 }
197 public static void LeftDoubleClick(int x, int y)
198 {
199 MoveMouse(x, y);
200 mouse_event((int)(MouseEventFlags.LEFTDOWN | MouseEventFlags.LEFTUP), x, y, 0, 0);
201 Thread.Sleep(15);
202 mouse_event((int)(MouseEventFlags.LEFTDOWN | MouseEventFlags.LEFTUP), x, y, 0, 0);
203 }
209 public static void RightClick(int x, int y)
210 {
211 MoveMouse(x, y);
212 // Simulate right mouse button down
213 mouse_event((int)MouseEventFlags.RIGHTDOWN, x, y, 0, 0);
214 Thread.Sleep(150);
215 // Simulate right mouse button up
216 mouse_event((int)MouseEventFlags.RIGHTUP, x, y, 0, 0);
217
218 }
224 public static void MoveMouse(int x, int y)
225 {
226 x = 0 <= x ? x : 0;
227 y = 0 <= y ? y : 0;
228
229 SetCursorPos(x, y);
230 }
236 internal static void PressModifierKey(ModifierKeys modifierKeys, bool dontPubolish = false)
237 {
238 if (false == dontPubolish)
239 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Pressed [{Browser.Browser.GetModifierKeys(modifierKeys)}]", modifierKeys, GPALObjectType.Other);
240
241 if (0 != (ModifierKeys.Control & modifierKeys))
242 {
243 keybd_event(GPAL.VK_CONTROL_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
244 keybd_event(GPAL.VK_CONTROL_RIGHT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
245 }
246
247 if (0 != (ModifierKeys.Shift & modifierKeys))
248 {
249 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
250 keybd_event(GPAL.VK_SHIFT_RIGHT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
251 }
252
253 if (0 != (ModifierKeys.Alt & modifierKeys))
254 {
255 keybd_event(GPAL.VK_LMENU, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0); // Left Alt
256 keybd_event(GPAL.VK_RMENU, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0); // Right Alt
257 }
258
259 if (0 != (ModifierKeys.Windows & modifierKeys))
260 {
261 keybd_event(GPAL.VK_LWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
262 keybd_event(GPAL.VK_RWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
263 }
264
265 if (0 != (ModifierKeys.Application & modifierKeys))
266 {
267 keybd_event(GPAL.VK_APPS, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
268 }
269
270 if (0 != (ModifierKeys.ScrollLock & modifierKeys))
271 {
272 keybd_event(GPAL.VK_SCROLL, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0); // Press Scroll Lock
273 }
274 }
275
281 internal static void ReleaseModifierKeys(ModifierKeys modifierKeys, bool dontPublish = false)
282 {
283 if (false == dontPublish)
284 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Released [{Browser.Browser.GetModifierKeys(modifierKeys)}]", modifierKeys, GPALObjectType.Other);
285
286 if (0 != (ModifierKeys.Alt & modifierKeys))
287 {
288 keybd_event(GPAL.VK_RMENU, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0); // Right Alt
289 Thread.Sleep(150);
290 keybd_event(GPAL.VK_LMENU, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0); // Left Alt
291 }
292
293 if (0 != (ModifierKeys.Control & modifierKeys))
294 {
295 keybd_event(GPAL.VK_CONTROL_RIGHT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
296 Thread.Sleep(150);
297 keybd_event(GPAL.VK_CONTROL_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
298 }
299
300 if (0 != (ModifierKeys.Shift & modifierKeys))
301 {
302 keybd_event(GPAL.VK_SHIFT_RIGHT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
303 Thread.Sleep(150);
304 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
305 }
306
307 if (0 != (ModifierKeys.Windows & modifierKeys))
308 {
309 keybd_event(GPAL.VK_RWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
310 Thread.Sleep(150);
311 keybd_event(GPAL.VK_LWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
312 }
313
314 if (0 != (ModifierKeys.Application & modifierKeys))
315 {
316 keybd_event(GPAL.VK_APPS, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
317 }
318
319 if (0 != (ModifierKeys.ScrollLock & modifierKeys))
320 {
321 keybd_event(GPAL.VK_SCROLL, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0); // Release Scroll Lock
322 }
323 }
330 public static bool SendKey(byte keyToSend, ModifierKeys modifierKeys = ModifierKeys.NONE)
331
332 {
333 if (ModifierKeys.NONE != modifierKeys)
334 PressModifierKey(modifierKeys);
335
336 try
337 {
338 keybd_event(keyToSend, scanCode, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
339 Thread.Sleep(_random.Next(10, 30)); // dwell time between press down and release, milliseconds
340 keybd_event(keyToSend, scanCode, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
341 }
342 catch { }
343
344 if (ModifierKeys.NONE != modifierKeys)
345 ReleaseModifierKeys(modifierKeys);
346
347 return true;
348 }
355 // CAVEAT: if SHIFT modifier is used with a mixed case string, sendchar will unshift and the whole string will not be sent in all caps.
356 public static void SendString(string stringToSend, int delayMs = 0)
357 {
358 bool first = true;
359 foreach (char c in stringToSend)
360 {
361 if (false == first && 0 < delayMs)
362 Thread.Sleep(GetTypingDelay(delayMs));
363 SendChar(c, ModifierKeys.NONE, true);
364 first = false;
365 }
366 }
373 public static void SendChar(char charToSend, ModifierKeys modifierKeys = ModifierKeys.NONE, bool dontPublish = false)
374 {
375 short vkKeyScan = HardwareHelper.VkKeyScan(charToSend);
376 System.Windows.Forms.Keys vkCode = (System.Windows.Forms.Keys)(vkKeyScan & 0xff);
377
378 var shift = (vkKeyScan & 0x100) > 0;
379 //var ctrl = (vkKeyScan & 0x200) > 0;
380 //var alt = (vkKeyScan & 0x400) > 0;
381
382 if (true == shift) // uppercase char - keybd event has no concept of uppercase 'S', that is shift + 's' even if ascii has uppercase, keyboards do not
383 {
384 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
385 }
386
387 if (ModifierKeys.NONE != modifierKeys)
388 PressModifierKey(modifierKeys, dontPublish);
389
390 try
391 {
392 keybd_event((byte)vkCode, (byte)vkKeyScan, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
393 Thread.Sleep(_random.Next(10, 30)); // dwell time between press down and release, milliseconds
394 keybd_event((byte)vkCode, (byte)vkKeyScan, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
395 }
396 catch { }
397
398 if (ModifierKeys.NONE != modifierKeys)
399 ReleaseModifierKeys(modifierKeys, dontPublish);
400
401 if (true == shift)
402 {
403 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
404 }
405
406 }
407
414 public static void HardwareClick(int x, int y, ClickType clickType)
415 {
416 switch (clickType)
417 {
418 case ClickType.LeftClick:
419 LeftClick(x, y);
420 break;
421 case ClickType.LeftDoubleClick:
422 LeftDoubleClick(x, y);
423 break;
424 case ClickType.MiddleClick:
425 MiddleClick(x, y);
426 break;
427 case ClickType.RightClick:
428 RightClick(x, y);
429 break;
430 }
431
432 }
433
434 // https://stackoverflow.com/questions/913646/c-sharp-moving-the-mouse-around-realistically
435 // https://ben.land/post/2021/04/25/windmouse-human-mouse-movement/
436 #region Improved WindMouse - Less Bounce, More Natural (C# 8 Compatible)
437
446 public static void MoveMouse(int targetX, int targetY, int randomnessX = 4, int randomnessY = 4, double speed = 1.0)
447 {
448 Point current = new Point();
449 GetCursorPos(out current);
450
451 Random rnd = new Random();
452
453 // Small target randomness
454 targetX = Math.Max(0, targetX + rnd.Next(-randomnessX, randomnessX + 1));
455 targetY = Math.Max(0, targetY + rnd.Next(-randomnessY, randomnessY + 1));
456
457 double adjustedSpeed = Math.Max(0.4, speed);
458
459 ImprovedWindMouse(
460 current.X, current.Y,
461 targetX, targetY,
462 gravity: 9.0,
463 wind: 2.5,
464 minWait: 7.0 / adjustedSpeed,
465 maxWait: 16.0 / adjustedSpeed,
466 maxStep: 38.0 * adjustedSpeed,
467 targetArea: 18.0
468 );
469 }
470
484 private static void ImprovedWindMouse(double xs, double ys, double xe, double ye,
485 double gravity, double wind, double minWait, double maxWait,
486 double maxStep, double targetArea)
487 {
488 double dist, windX = 0, windY = 0, veloX = 0, veloY = 0;
489 int oldX, oldY, newX = (int)Math.Round(xs), newY = (int)Math.Round(ys);
490
491 double waitDiff = maxWait - minWait;
492 double sqrt2 = Math.Sqrt(2.0);
493 double sqrt3 = Math.Sqrt(3.0);
494 double sqrt5 = Math.Sqrt(5.0);
495
496 Random rnd = new Random();
497
498 dist = Hypot(xe - xs, ye - ys);
499
500 while (dist > 0.8)
501 {
502 wind = Math.Min(wind, dist * 0.8);
503
504 if (dist >= targetArea)
505 {
506 // Far from target
507 windX = windX / sqrt3 + (rnd.NextDouble() * wind * 2 - wind) / sqrt5;
508 windY = windY / sqrt3 + (rnd.NextDouble() * wind * 2 - wind) / sqrt5;
509 }
510 else
511 {
512 // Close to target - smooth damping
513 windX = windX / sqrt2 * 0.75;
514 windY = windY / sqrt2 * 0.75;
515
516 if (maxStep < 4)
517 maxStep = 3.0 + rnd.NextDouble() * 2;
518 else
519 maxStep = maxStep / sqrt5 * 0.85;
520 }
521
522 // Gravity pull (weaker when very close)
523 double pull = gravity * (dist > 1.0 ? 1.0 : dist);
524 veloX += windX + pull * (xe - xs) / dist;
525 veloY += windY + pull * (ye - ys) / dist;
526
527 // Limit velocity
528 double mag = Hypot(veloX, veloY);
529 if (mag > maxStep)
530 {
531 double stepSize = maxStep * 0.55 + rnd.NextDouble() * (maxStep * 0.45);
532 veloX = (veloX / mag) * stepSize;
533 veloY = (veloY / mag) * stepSize;
534 }
535
536 // Extra damping near target
537 if (dist < targetArea * 0.6)
538 {
539 veloX *= 0.88;
540 veloY *= 0.88;
541 }
542
543 oldX = (int)Math.Round(xs);
544 oldY = (int)Math.Round(ys);
545 xs += veloX;
546 ys += veloY;
547 dist = Hypot(xe - xs, ye - ys);
548
549 newX = (int)Math.Round(xs);
550 newY = (int)Math.Round(ys);
551
552 if (oldX != newX || oldY != newY)
553 SetCursorPos(newX, newY);
554
555 double step = Hypot(xs - oldX, ys - oldY);
556 int wait = (int)Math.Round(waitDiff * (step / maxStep) + minWait);
557 Thread.Sleep(Math.Max(1, wait));
558 }
559
560 // Gentle final settling (no hard snap)
561 int finalX = (int)Math.Round(xe);
562 int finalY = (int)Math.Round(ye);
563
564 if (Math.Abs(newX - finalX) > 1 || Math.Abs(newY - finalY) > 1)
565 {
566 while (Math.Abs(newX - finalX) > 0 || Math.Abs(newY - finalY) > 0)
567 {
568 int dx = Math.Sign(finalX - newX) * rnd.Next(1, 3);
569 int dy = Math.Sign(finalY - newY) * rnd.Next(1, 3);
570
571 newX = Math.Max(finalX - 3, Math.Min(finalX + 3, newX + dx));
572 newY = Math.Max(finalY - 3, Math.Min(finalY + 3, newY + dy));
573
574 SetCursorPos(newX, newY);
575 Thread.Sleep(rnd.Next(8, 18));
576 }
577 }
578
579 SetCursorPos(finalX, finalY);
580 }
581
588 private static double Hypot(double dx, double dy)
589 {
590 return Math.Sqrt(dx * dx + dy * dy);
591 }
592
593 #endregion
594 #region VKCODE to ASCII
598 internal static class KeyboardUtils
599 {
609 // P/Invoke declarations
610 [DllImport("user32.dll")]
611 private static extern int ToAscii(uint uVirtKey, uint uScanCode, byte[] lpKeyState,
612 [Out] StringBuilder lpChar, uint uFlags);
613
619 [DllImport("user32.dll")]
620 private static extern int GetKeyboardState(byte[] lpKeyState);
621
628 [DllImport("user32.dll")]
629 private static extern uint MapVirtualKey(uint uCode, uint uMapType);
630
636 internal static string VkCodeToAscii(uint vkCode)
637 {
638 // Get the scan code for the virtual key
639 uint scanCode = MapVirtualKey(vkCode, 0); // MAPVK_VK_TO_VSC
640
641 // Get current keyboard state
642 byte[] keyState = new byte[256];
643 if (GetKeyboardState(keyState) == 0)
644 return string.Empty;
645
646 // Adjust keyboard state based on your application's maintained state
647 // Example: If Shift is "held" in your state, set VK_SHIFT (0x10)
648 // keyState[0x10] = 0x80; // Uncomment to simulate Shift pressed
649 // keyState[0x14] = 0x80; // Uncomment to simulate Caps Lock toggled
650
651 // Buffer for output character
652 StringBuilder charBuffer = new StringBuilder(2);
653
654 // Call ToAscii to translate VK code to ASCII
655 int result = ToAscii(vkCode, scanCode, keyState, charBuffer, 0);
656
657 if (result == 1)
658 {
659 // One character was written to the buffer
660 return charBuffer[0].ToString();
661 }
662 else if (result == 2)
663 {
664 // Two characters (e.g., dead key combo); return first
665 return charBuffer[0].ToString();
666 }
667 else
668 {
669 // No translation (e.g., VK_SCROLL, VK_LMENU) or error
670 return string.Empty;
671 }
672 }
673 }
674 #endregion VKCODE to ASCII
675 }
676}
677