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
102 [DllImport("user32.dll")]
103 private static extern uint GetDoubleClickTime();
104
108 [DllImport("user32.dll")]
109 private static extern short GetAsyncKeyState(int vKey);
110
111 [DllImport("user32.dll")]
112 private static extern int GetSystemMetrics(int index);
113
114 private const int SM_CXDOUBLECLK = 36;
115 private const int SM_CYDOUBLECLK = 37;
116 private const int SM_CXDRAG = 68;
117 private const int SM_CYDRAG = 69;
118
119 // when and where the last button press was put down, so a press that follows too soon and too close
120 // can be held back rather than joining it into a double click
121 private static DateTime _lastPress = DateTime.MinValue;
122 private static Point _lastPressPoint = new Point(int.MinValue, int.MinValue);
123
130 private static void RecordPress(int x, int y)
131 {
132 _lastPress = DateTime.UtcNow;
133 _lastPressPoint = new Point(x, y);
134 }
135
148 private static void SeparateFromLastPress(int x, int y)
149 {
150 if (int.MinValue != _lastPressPoint.X)
151 {
152 int window = (int)GetDoubleClickTime();
153 int elapsed = (int)(DateTime.UtcNow - _lastPress).TotalMilliseconds;
154
155 bool nearby = Math.Abs(x - _lastPressPoint.X) <= GetSystemMetrics(SM_CXDOUBLECLK) / 2
156 && Math.Abs(y - _lastPressPoint.Y) <= GetSystemMetrics(SM_CYDOUBLECLK) / 2;
157
158 if (true == nearby && elapsed < window)
159 {
160 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Holding [{window - elapsed}]ms so this press is not read as a double click with the one before it.", null, GPALObjectType.None);
161 Thread.Sleep(window - elapsed + 20);
162 }
163 }
164 }
165
174 // https://stackoverflow.com/questions/8739523/directing-mouse-events-dllimportuser32-dll-click-double-click
175 [DllImport("user32.dll")]
176 static extern void mouse_event(int dwFlags, int dx, int dy,
177 int dwData, int dwExtraInfo);
178
182 [Flags]
183 public enum MouseEventFlags
184 {
185 LEFTDOWN = 0x00000002,
186 LEFTUP = 0x00000004,
187 MIDDLEDOWN = 0x00000020,
188 MIDDLEUP = 0x00000040,
189 MOVE = 0x00000001,
190 ABSOLUTE = 0x00008000,
191 RIGHTDOWN = 0x00000008,
192 RIGHTUP = 0x00000010
193 }
194
211 internal static void ReleaseStrayModifiers(ModifierKeys wanted)
212 {
213 foreach (var held in new[]
214 {
215 (Key: ModifierKeys.Control, Left: GPAL.VK_CONTROL_LEFT, Right: GPAL.VK_CONTROL_RIGHT),
216 (Key: ModifierKeys.Shift, Left: GPAL.VK_SHIFT_LEFT, Right: GPAL.VK_SHIFT_RIGHT),
217 (Key: ModifierKeys.Alt, Left: GPAL.VK_LMENU, Right: GPAL.VK_RMENU),
218 })
219 {
220 if (0 != (held.Key & wanted))
221 continue;
222
223 if (0 != (GetAsyncKeyState(held.Left) & 0x8000) || 0 != (GetAsyncKeyState(held.Right) & 0x8000))
224 {
225 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{held.Key}] was still held and nothing asked for it, so it is being released before this action.", null, GPALObjectType.None);
226
227 ReleaseModifierKeys(held.Key, true);
228 }
229 }
230 }
231
245 internal static void DragTravel(int toX, int toY, bool simulate)
246 {
247 if (true == simulate)
248 MoveMouse(toX, toY, 4, 4, 0.5);
249 else
250 {
251 GetCursorPos(out Point from);
252
253 int stepX = from.X + Math.Sign(toX - from.X) * (GetSystemMetrics(SM_CXDRAG) + 2);
254 int stepY = from.Y + Math.Sign(toY - from.Y) * (GetSystemMetrics(SM_CYDRAG) + 2);
255
256 MoveMouse(stepX, stepY);
257 Thread.Sleep(30);
258 MoveMouse(toX, toY);
259 Thread.Sleep(30);
260 }
261 }
262
263 public static void MouseDown(int x, int y, ClickType clickType)
264 {
265 SeparateFromLastPress(x, y);
266 RecordPress(x, y);
267
268 MoveMouse(x, y);
269 if (ClickType.LeftClick == clickType)
270 mouse_event((int)(MouseEventFlags.LEFTDOWN), x, y, 0, 0);
271 else if (ClickType.MiddleClick == clickType)
272 mouse_event((int)(MouseEventFlags.MIDDLEDOWN), x, y, 0, 0);
273 else if (ClickType.RightClick == clickType)
274 mouse_event((int)(MouseEventFlags.RIGHTDOWN), x, y, 0, 0);
275 else
276 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown mouse down clicktype [{clickType}]", null, GPALObjectType.None);
277 }
284 public static void MouseUp(int x, int y, ClickType clickType)
285 {
286 MoveMouse(x, y);
287 if (ClickType.LeftClick == clickType)
288 mouse_event((int)(MouseEventFlags.LEFTUP), x, y, 0, 0);
289 else if (ClickType.MiddleClick == clickType)
290 mouse_event((int)(MouseEventFlags.MIDDLEUP), x, y, 0, 0);
291 else if (ClickType.RightClick == clickType)
292 mouse_event((int)(MouseEventFlags.RIGHTUP), x, y, 0, 0);
293 else
294 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown mouse down clicktype [{clickType}]", null, GPALObjectType.None);
295 }
296
302 public static void MiddleClick(int x, int y)
303 {
304 MoveMouse(x, y);
305 // Separate down + up with a short dwell - a zero-duration combined click is dropped
306 // intermittently by browsers (notably Firefox). Mirrors the reliable RightClick pattern.
307 mouse_event((int)(MouseEventFlags.MIDDLEDOWN), x, y, 0, 0);
308 Thread.Sleep(10);
309 mouse_event((int)(MouseEventFlags.MIDDLEUP), x, y, 0, 0);
310
311 RecordPress(x, y);
312 }
318 public static void LeftClick(int x, int y)
319 {
320 MoveMouse(x, y);
321 // Separate down + up with a short dwell - a zero-duration combined click is dropped
322 // intermittently by browsers (notably Firefox). Mirrors the reliable RightClick pattern.
323 mouse_event((int)(MouseEventFlags.LEFTDOWN), x, y, 0, 0);
324 Thread.Sleep(10);
325 mouse_event((int)(MouseEventFlags.LEFTUP), x, y, 0, 0);
326
327 RecordPress(x, y);
328 }
334 public static void LeftDoubleClick(int x, int y)
335 {
336 MoveMouse(x, y);
337 mouse_event((int)(MouseEventFlags.LEFTDOWN | MouseEventFlags.LEFTUP), x, y, 0, 0);
338 Thread.Sleep(15);
339 mouse_event((int)(MouseEventFlags.LEFTDOWN | MouseEventFlags.LEFTUP), x, y, 0, 0);
340
341 RecordPress(x, y);
342 }
348 public static void RightClick(int x, int y)
349 {
350 MoveMouse(x, y);
351 // Simulate right mouse button down
352 mouse_event((int)MouseEventFlags.RIGHTDOWN, x, y, 0, 0);
353 Thread.Sleep(150);
354 // Simulate right mouse button up
355 mouse_event((int)MouseEventFlags.RIGHTUP, x, y, 0, 0);
356
357 RecordPress(x, y);
358 }
364 public static void MoveMouse(int x, int y)
365 {
366 x = 0 <= x ? x : 0;
367 y = 0 <= y ? y : 0;
368
369 SetCursorPos(x, y);
370 }
376 internal static void PressModifierKey(ModifierKeys modifierKeys, bool dontPubolish = false)
377 {
378 if (false == dontPubolish)
379 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Pressed [{Browser.Browser.GetModifierKeys(modifierKeys)}]", modifierKeys, GPALObjectType.Other);
380
381 // the extended flag describes the key, not the direction. Right Control and Right Alt are extended
382 // keys and the left ones are not, and neither Shift is. A key pressed with one flag and released
383 // with another is a keyup Windows does not match to the keydown, and the key stays down for every
384 // window afterwards, which is what a stuck Shift is
385 if (0 != (ModifierKeys.Control & modifierKeys))
386 {
387 keybd_event(GPAL.VK_CONTROL_LEFT, 0, 0, 0);
388 keybd_event(GPAL.VK_CONTROL_RIGHT, 0, GPAL.KEYEVENTF_EXTENDEDKEY, 0);
389 }
390
391 if (0 != (ModifierKeys.Shift & modifierKeys))
392 {
393 keybd_event(GPAL.VK_SHIFT_LEFT, 0, 0, 0);
394 keybd_event(GPAL.VK_SHIFT_RIGHT, 0, 0, 0);
395 }
396
397 if (0 != (ModifierKeys.Alt & modifierKeys))
398 {
399 keybd_event(GPAL.VK_LMENU, 0, 0, 0); // Left Alt
400 keybd_event(GPAL.VK_RMENU, 0, GPAL.KEYEVENTF_EXTENDEDKEY, 0); // Right Alt
401 }
402
403 if (0 != (ModifierKeys.Windows & modifierKeys))
404 {
405 keybd_event(GPAL.VK_LWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
406 keybd_event(GPAL.VK_RWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
407 }
408
409 if (0 != (ModifierKeys.Application & modifierKeys))
410 {
411 keybd_event(GPAL.VK_APPS, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
412 }
413
414 if (0 != (ModifierKeys.ScrollLock & modifierKeys))
415 {
416 keybd_event(GPAL.VK_SCROLL, 0, 0, 0); // Press Scroll Lock, which is not an extended key
417 }
418 }
419
425 internal static void ReleaseModifierKeys(ModifierKeys modifierKeys, bool dontPublish = false)
426 {
427 if (false == dontPublish)
428 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Released [{Browser.Browser.GetModifierKeys(modifierKeys)}]", modifierKeys, GPALObjectType.Other);
429
430 // released with the same flag each was pressed with, or the keyup does not match the keydown
431 if (0 != (ModifierKeys.Alt & modifierKeys))
432 {
433 keybd_event(GPAL.VK_RMENU, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0); // Right Alt
434 Thread.Sleep(150);
435 keybd_event(GPAL.VK_LMENU, 0, GPAL.KEYEVENTF_KEYUP, 0); // Left Alt
436 }
437
438 if (0 != (ModifierKeys.Control & modifierKeys))
439 {
440 keybd_event(GPAL.VK_CONTROL_RIGHT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
441 Thread.Sleep(150);
442 keybd_event(GPAL.VK_CONTROL_LEFT, 0, GPAL.KEYEVENTF_KEYUP, 0);
443 }
444
445 if (0 != (ModifierKeys.Shift & modifierKeys))
446 {
447 keybd_event(GPAL.VK_SHIFT_RIGHT, 0, GPAL.KEYEVENTF_KEYUP, 0);
448 Thread.Sleep(150);
449 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_KEYUP, 0);
450 }
451
452 if (0 != (ModifierKeys.Windows & modifierKeys))
453 {
454 keybd_event(GPAL.VK_RWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
455 Thread.Sleep(150);
456 keybd_event(GPAL.VK_LWIN, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
457 }
458
459 if (0 != (ModifierKeys.Application & modifierKeys))
460 {
461 keybd_event(GPAL.VK_APPS, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
462 }
463
464 if (0 != (ModifierKeys.ScrollLock & modifierKeys))
465 {
466 keybd_event(GPAL.VK_SCROLL, 0, GPAL.KEYEVENTF_KEYUP, 0); // Release Scroll Lock
467 }
468 }
475 public static bool SendKey(byte keyToSend, ModifierKeys modifierKeys = ModifierKeys.NONE)
476
477 {
478 if (ModifierKeys.NONE != modifierKeys)
479 PressModifierKey(modifierKeys);
480
481 try
482 {
483 keybd_event(keyToSend, scanCode, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
484 Thread.Sleep(_random.Next(10, 30)); // dwell time between press down and release, milliseconds
485 keybd_event(keyToSend, scanCode, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
486 }
487 catch { }
488
489 if (ModifierKeys.NONE != modifierKeys)
490 ReleaseModifierKeys(modifierKeys);
491
492 return true;
493 }
500 // 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.
501 public static void SendString(string stringToSend, int delayMs = 0)
502 {
503 bool first = true;
504 foreach (char c in stringToSend)
505 {
506 if (false == first && 0 < delayMs)
507 Thread.Sleep(GetTypingDelay(delayMs));
508 SendChar(c, ModifierKeys.NONE, true);
509 first = false;
510 }
511 }
518 public static void SendChar(char charToSend, ModifierKeys modifierKeys = ModifierKeys.NONE, bool dontPublish = false)
519 {
520 short vkKeyScan = HardwareHelper.VkKeyScan(charToSend);
521 System.Windows.Forms.Keys vkCode = (System.Windows.Forms.Keys)(vkKeyScan & 0xff);
522
523 var shift = (vkKeyScan & 0x100) > 0;
524 //var ctrl = (vkKeyScan & 0x200) > 0;
525 //var alt = (vkKeyScan & 0x400) > 0;
526
527 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
528 {
529 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
530 }
531
532 if (ModifierKeys.NONE != modifierKeys)
533 PressModifierKey(modifierKeys, dontPublish);
534
535 try
536 {
537 keybd_event((byte)vkCode, (byte)vkKeyScan, GPAL.KEYEVENTF_EXTENDEDKEY | 0, 0);
538 Thread.Sleep(_random.Next(10, 30)); // dwell time between press down and release, milliseconds
539 keybd_event((byte)vkCode, (byte)vkKeyScan, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
540 }
541 catch { }
542
543 if (ModifierKeys.NONE != modifierKeys)
544 ReleaseModifierKeys(modifierKeys, dontPublish);
545
546 if (true == shift)
547 {
548 keybd_event(GPAL.VK_SHIFT_LEFT, 0, GPAL.KEYEVENTF_EXTENDEDKEY | GPAL.KEYEVENTF_KEYUP, 0);
549 }
550
551 }
552
559 public static void HardwareClick(int x, int y, ClickType clickType)
560 {
561 switch (clickType)
562 {
563 case ClickType.LeftClick:
564 LeftClick(x, y);
565 break;
566 case ClickType.LeftDoubleClick:
567 LeftDoubleClick(x, y);
568 break;
569 case ClickType.MiddleClick:
570 MiddleClick(x, y);
571 break;
572 case ClickType.RightClick:
573 RightClick(x, y);
574 break;
575 }
576
577 }
578
579 // https://stackoverflow.com/questions/913646/c-sharp-moving-the-mouse-around-realistically
580 // https://ben.land/post/2021/04/25/windmouse-human-mouse-movement/
581 #region Improved WindMouse - Less Bounce, More Natural (C# 8 Compatible)
582
591 public static void MoveMouse(int targetX, int targetY, int randomnessX = 4, int randomnessY = 4, double speed = 1.0)
592 {
593 Point current = new Point();
594 GetCursorPos(out current);
595
596 Random rnd = new Random();
597
598 // Small target randomness
599 targetX = Math.Max(0, targetX + rnd.Next(-randomnessX, randomnessX + 1));
600 targetY = Math.Max(0, targetY + rnd.Next(-randomnessY, randomnessY + 1));
601
602 double adjustedSpeed = Math.Max(0.4, speed);
603
604 ImprovedWindMouse(
605 current.X, current.Y,
606 targetX, targetY,
607 gravity: 9.0,
608 wind: 2.5,
609 minWait: 7.0 / adjustedSpeed,
610 maxWait: 16.0 / adjustedSpeed,
611 maxStep: 38.0 * adjustedSpeed,
612 targetArea: 18.0
613 );
614 }
615
629 private static void ImprovedWindMouse(double xs, double ys, double xe, double ye,
630 double gravity, double wind, double minWait, double maxWait,
631 double maxStep, double targetArea)
632 {
633 double dist, windX = 0, windY = 0, veloX = 0, veloY = 0;
634 int oldX, oldY, newX = (int)Math.Round(xs), newY = (int)Math.Round(ys);
635
636 double waitDiff = maxWait - minWait;
637 double sqrt2 = Math.Sqrt(2.0);
638 double sqrt3 = Math.Sqrt(3.0);
639 double sqrt5 = Math.Sqrt(5.0);
640
641 Random rnd = new Random();
642
643 dist = Hypot(xe - xs, ye - ys);
644
645 while (dist > 0.8)
646 {
647 wind = Math.Min(wind, dist * 0.8);
648
649 if (dist >= targetArea)
650 {
651 // Far from target
652 windX = windX / sqrt3 + (rnd.NextDouble() * wind * 2 - wind) / sqrt5;
653 windY = windY / sqrt3 + (rnd.NextDouble() * wind * 2 - wind) / sqrt5;
654 }
655 else
656 {
657 // Close to target - smooth damping
658 windX = windX / sqrt2 * 0.75;
659 windY = windY / sqrt2 * 0.75;
660
661 if (maxStep < 4)
662 maxStep = 3.0 + rnd.NextDouble() * 2;
663 else
664 maxStep = maxStep / sqrt5 * 0.85;
665 }
666
667 // Gravity pull (weaker when very close)
668 double pull = gravity * (dist > 1.0 ? 1.0 : dist);
669 veloX += windX + pull * (xe - xs) / dist;
670 veloY += windY + pull * (ye - ys) / dist;
671
672 // Limit velocity
673 double mag = Hypot(veloX, veloY);
674 if (mag > maxStep)
675 {
676 double stepSize = maxStep * 0.55 + rnd.NextDouble() * (maxStep * 0.45);
677 veloX = (veloX / mag) * stepSize;
678 veloY = (veloY / mag) * stepSize;
679 }
680
681 // Extra damping near target
682 if (dist < targetArea * 0.6)
683 {
684 veloX *= 0.88;
685 veloY *= 0.88;
686 }
687
688 oldX = (int)Math.Round(xs);
689 oldY = (int)Math.Round(ys);
690 xs += veloX;
691 ys += veloY;
692 dist = Hypot(xe - xs, ye - ys);
693
694 newX = (int)Math.Round(xs);
695 newY = (int)Math.Round(ys);
696
697 if (oldX != newX || oldY != newY)
698 SetCursorPos(newX, newY);
699
700 double step = Hypot(xs - oldX, ys - oldY);
701 int wait = (int)Math.Round(waitDiff * (step / maxStep) + minWait);
702 Thread.Sleep(Math.Max(1, wait));
703 }
704
705 // Gentle final settling (no hard snap)
706 int finalX = (int)Math.Round(xe);
707 int finalY = (int)Math.Round(ye);
708
709 if (Math.Abs(newX - finalX) > 1 || Math.Abs(newY - finalY) > 1)
710 {
711 while (Math.Abs(newX - finalX) > 0 || Math.Abs(newY - finalY) > 0)
712 {
713 int dx = Math.Sign(finalX - newX) * rnd.Next(1, 3);
714 int dy = Math.Sign(finalY - newY) * rnd.Next(1, 3);
715
716 newX = Math.Max(finalX - 3, Math.Min(finalX + 3, newX + dx));
717 newY = Math.Max(finalY - 3, Math.Min(finalY + 3, newY + dy));
718
719 SetCursorPos(newX, newY);
720 Thread.Sleep(rnd.Next(8, 18));
721 }
722 }
723
724 SetCursorPos(finalX, finalY);
725 }
726
733 private static double Hypot(double dx, double dy)
734 {
735 return Math.Sqrt(dx * dx + dy * dy);
736 }
737
738 #endregion
739 #region VKCODE to ASCII
743 internal static class KeyboardUtils
744 {
754 // P/Invoke declarations
755 [DllImport("user32.dll")]
756 private static extern int ToAscii(uint uVirtKey, uint uScanCode, byte[] lpKeyState,
757 [Out] StringBuilder lpChar, uint uFlags);
758
764 [DllImport("user32.dll")]
765 private static extern int GetKeyboardState(byte[] lpKeyState);
766
773 [DllImport("user32.dll")]
774 private static extern uint MapVirtualKey(uint uCode, uint uMapType);
775
781 internal static string VkCodeToAscii(uint vkCode)
782 {
783 // Get the scan code for the virtual key
784 uint scanCode = MapVirtualKey(vkCode, 0); // MAPVK_VK_TO_VSC
785
786 // Get current keyboard state
787 byte[] keyState = new byte[256];
788 if (GetKeyboardState(keyState) == 0)
789 return string.Empty;
790
791 // Adjust keyboard state based on your application's maintained state
792 // Example: If Shift is "held" in your state, set VK_SHIFT (0x10)
793 // keyState[0x10] = 0x80; // Uncomment to simulate Shift pressed
794 // keyState[0x14] = 0x80; // Uncomment to simulate Caps Lock toggled
795
796 // Buffer for output character
797 StringBuilder charBuffer = new StringBuilder(2);
798
799 // Call ToAscii to translate VK code to ASCII
800 int result = ToAscii(vkCode, scanCode, keyState, charBuffer, 0);
801
802 if (result == 1)
803 {
804 // One character was written to the buffer
805 return charBuffer[0].ToString();
806 }
807 else if (result == 2)
808 {
809 // Two characters (e.g., dead key combo); return first
810 return charBuffer[0].ToString();
811 }
812 else
813 {
814 // No translation (e.g., VK_SCROLL, VK_LMENU) or error
815 return string.Empty;
816 }
817 }
818 }
819 #endregion VKCODE to ASCII
820 }
821}
822