Skip to content

Instantly share code, notes, and snippets.

@obviliontsk
Forked from DrustZ/Simulation.cs
Last active May 9, 2023 13:04
Show Gist options
  • Save obviliontsk/90403a0fea8c24258570f3a577704864 to your computer and use it in GitHub Desktop.
Save obviliontsk/90403a0fea8c24258570f3a577704864 to your computer and use it in GitHub Desktop.
Simulate keyboard and mouse input in WPF (similar to sendkeys in winform)
using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Input;
namespace RoslynScriptContext;
/// <summary>
/// Native methods
/// </summary>
public static partial class NativeMethods
{
//User32 wrappers cover API's used for Mouse input
#region User32
// Two special bitMasks we define to be able to grab
// shift and character information out of a VKey.
internal const int VKeyShiftMask = 0x0100;
internal const int VKeyCharMask = 0x00FF;
// Various Win32 constants
internal const int KeyeventfExtendedkey = 0x0001;
internal const int KeyeventfKeyup = 0x0002;
internal const int KeyeventfUnicode = 0x0004;
internal const int KeyeventfScancode = 0x0008;
internal const int MouseeventfVirtualdesk = 0x4000;
internal const int SMXvirtualscreen = 76;
internal const int SMYvirtualscreen = 77;
internal const int SMCxvirtualscreen = 78;
internal const int SMCyvirtualscreen = 79;
internal const int XButton1 = 0x0001;
internal const int XButton2 = 0x0002;
internal const int WheelDelta = 120;
internal const int InputMouse = 0;
internal const int InputKeyboard = 1;
// Various Win32 data structures
[StructLayout(LayoutKind.Sequential)]
internal struct INPUT
{
internal int type;
internal INPUTUNION union;
};
[StructLayout(LayoutKind.Explicit)]
internal struct INPUTUNION
{
[FieldOffset(0)]
internal MOUSEINPUT mouseInput;
[FieldOffset(0)]
internal KEYBDINPUT keyboardInput;
};
[StructLayout(LayoutKind.Sequential)]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal int dwFlags;
internal int time;
internal IntPtr dwExtraInfo;
};
[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
internal short wVk;
internal short wScan;
internal int dwFlags;
internal int time;
internal IntPtr dwExtraInfo;
};
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
public readonly int X;
public readonly int Y;
}
[Flags]
internal enum SendMouseInputFlags
{
Move = 0x0001,
LeftDown = 0x0002,
LeftUp = 0x0004,
RightDown = 0x0008,
RightUp = 0x0010,
MiddleDown = 0x0020,
MiddleUp = 0x0040,
XDown = 0x0080,
XUp = 0x0100,
Wheel = 0x0800,
Absolute = 0x8000,
};
// Importing various Win32 APIs that we need for input
[LibraryImport("user32.dll")]
internal static partial int GetSystemMetrics(int nIndex);
// DllImport CharSet = CharSet.Auto -> Unicode / Utf.16 for windows, so 'W'
[LibraryImport("user32.dll", EntryPoint = "MapVirtualKeyW")]
internal static partial int MapVirtualKey(int nVirtKey, int nMapType);
[LibraryImport("user32.dll", SetLastError = true)]
internal static partial int SendInput(int nInputs, ref INPUT mi, int cbSize);
[LibraryImport("user32.dll", EntryPoint = "BlockInput")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool BlockInput([MarshalAs(UnmanagedType.Bool)] bool fBlockIt);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetCursorPos(out POINT lpPoint);
#endregion
}
/// <summary>
/// Exposes a simple interface to common mouse operations, allowing the user to simulate mouse input.
/// </summary>
/// <example>The following code moves to screen coordinate 100,100 and left clicks.
/// <code>
/**
Mouse.MoveTo(new Point(100, 100));
Mouse.Click(MouseButton.Left);
*/
/// </code>
/// </example>
public static class Mouse
{
/// <summary>
/// Clicks a mouse button at specific coordinates.
/// </summary>
/// <param name="x">X coordinate</param>
/// <param name="y">Y coordinate</param>
/// <param name="mouseButton">The mouse button to click.</param>
public static void ClickAt(int x, int y, MouseButton mouseButton)
{
NativeMethods.GetCursorPos(out var startPoint);
MoveTo(new Point(x, y));
Click(mouseButton);
// return to start position
MoveTo(new Point(startPoint.X, startPoint.Y));
}
/// <summary>
/// Clicks a mouse button.
/// </summary>
/// <param name="mouseButton">The mouse button to click.</param>
public static void Click(MouseButton mouseButton)
{
Down(mouseButton);
Up(mouseButton);
}
/// <summary>
/// Double-clicks a mouse button.
/// </summary>
/// <param name="mouseButton">The mouse button to click.</param>
public static void DoubleClick(MouseButton mouseButton)
{
Click(mouseButton);
Click(mouseButton);
}
/// <summary>
/// Clicks a mouse button N times.
/// </summary>
/// <param name="mouseButton">The mouse button to click.</param>
/// <param name="clicksAmount">Clicks amount, default = 1</param>
/// <param name="clickDelayMs">Delay between clicks in milliseconds, default = 0</param>
/// <exception cref="ArgumentOutOfRangeException">clicksAmount less than 1 or clickDelayMs less than 0</exception>
public static void NClick(MouseButton mouseButton, int clicksAmount = 1, int clickDelayMs = 0)
{
if (clicksAmount < 1) throw new ArgumentOutOfRangeException(nameof(clicksAmount), clicksAmount, "Less than one");
if (clickDelayMs < 0) throw new ArgumentOutOfRangeException(nameof(clickDelayMs), clickDelayMs, "Less than zero");
for (int i = 0; i < clicksAmount; i++)
{
Click(mouseButton);
if (clickDelayMs > 0) Thread.Sleep(clickDelayMs);
}
}
/// <summary>
/// Performs a mouse-down operation for a specified mouse button.
/// </summary>
/// <param name="mouseButton">The mouse button to use.</param>
public static void Down(MouseButton mouseButton)
{
switch (mouseButton)
{
case MouseButton.Left:
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.LeftDown);
break;
case MouseButton.Right:
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.RightDown);
break;
case MouseButton.Middle:
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.MiddleDown);
break;
case MouseButton.XButton1:
SendMouseInput(0, 0, NativeMethods.XButton1, NativeMethods.SendMouseInputFlags.XDown);
break;
case MouseButton.XButton2:
SendMouseInput(0, 0, NativeMethods.XButton2, NativeMethods.SendMouseInputFlags.XDown);
break;
default:
throw new InvalidOperationException("Unsupported MouseButton input.");
}
}
/// <summary>
/// Moves the mouse pointer to the specified screen coordinates.
/// </summary>
/// <param name="point">The screen coordinates to move to.</param>
public static void MoveTo(Point point)
{
SendMouseInput(point.X, point.Y, 0, NativeMethods.SendMouseInputFlags.Move | NativeMethods.SendMouseInputFlags.Absolute);
}
/// <summary>
/// Moves the mouse pointer to the specified screen coordinates.
/// </summary>
/// <param name="x">X coordinate</param>
/// <param name="y">Y coordinate</param>
public static void MoveTo(int x, int y)
{
Point toPoint = new Point(x, y);
MoveTo(toPoint);
}
/// <summary>
/// Resets the system mouse to a clean state.
/// </summary>
public static void Reset()
{
// Maybe better to reset cursor position explicitly, only when needed?
//MoveTo(new Point(0, 0));
if (System.Windows.Input.Mouse.LeftButton == MouseButtonState.Pressed)
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.LeftUp);
if (System.Windows.Input.Mouse.MiddleButton == MouseButtonState.Pressed)
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.MiddleUp);
if (System.Windows.Input.Mouse.RightButton == MouseButtonState.Pressed)
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.RightUp);
if (System.Windows.Input.Mouse.XButton1 == MouseButtonState.Pressed)
SendMouseInput(0, 0, NativeMethods.XButton1, NativeMethods.SendMouseInputFlags.XUp);
if (System.Windows.Input.Mouse.XButton2 == MouseButtonState.Pressed)
SendMouseInput(0, 0, NativeMethods.XButton2, NativeMethods.SendMouseInputFlags.XUp);
}
/// <summary>
/// Resets the mouse position to 0,0.
/// </summary>
public static void ResetPosition() => MoveTo(0, 0);
/// <summary>
/// Simulates scrolling of the mouse wheel up or down.
/// </summary>
/// <param name="lines">The number of lines to scroll. Use positive numbers to scroll up and negative numbers to scroll down.</param>
public static void Scroll(double lines)
{
int amount = (int)(NativeMethods.WheelDelta * lines);
SendMouseInput(0, 0, amount, NativeMethods.SendMouseInputFlags.Wheel);
}
/// <summary>
/// Performs a mouse-up operation for a specified mouse button.
/// </summary>
/// <param name="mouseButton">The mouse button to use.</param>
public static void Up(MouseButton mouseButton)
{
switch (mouseButton)
{
case MouseButton.Left:
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.LeftUp);
break;
case MouseButton.Right:
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.RightUp);
break;
case MouseButton.Middle:
SendMouseInput(0, 0, 0, NativeMethods.SendMouseInputFlags.MiddleUp);
break;
case MouseButton.XButton1:
SendMouseInput(0, 0, NativeMethods.XButton1, NativeMethods.SendMouseInputFlags.XUp);
break;
case MouseButton.XButton2:
SendMouseInput(0, 0, NativeMethods.XButton2, NativeMethods.SendMouseInputFlags.XUp);
break;
default:
throw new InvalidOperationException("Unsupported MouseButton input.");
}
}
/// <summary>
/// Sends mouse input.
/// </summary>
/// <param name="x">x coordinate</param>
/// <param name="y">y coordinate</param>
/// <param name="data">scroll wheel amount</param>
/// <param name="flags">SendMouseInputFlags flags</param>
private static void SendMouseInput(int x, int y, int data, NativeMethods.SendMouseInputFlags flags)
{
int intFlags = (int)flags;
if ((intFlags & (int)NativeMethods.SendMouseInputFlags.Absolute) != 0)
{
// Absolute position requires normalized coordinates.
NormalizeCoordinates(ref x, ref y);
intFlags |= NativeMethods.MouseeventfVirtualdesk;
}
var mi = new NativeMethods.INPUT
{
type = NativeMethods.InputMouse
};
mi.union.mouseInput.dx = x;
mi.union.mouseInput.dy = y;
mi.union.mouseInput.mouseData = data;
mi.union.mouseInput.dwFlags = intFlags;
mi.union.mouseInput.time = 0;
mi.union.mouseInput.dwExtraInfo = new IntPtr(0);
if (NativeMethods.SendInput(1, ref mi, Marshal.SizeOf(mi)) == 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
private static void NormalizeCoordinates(ref int x, ref int y)
{
int vScreenWidth = NativeMethods.GetSystemMetrics(NativeMethods.SMCxvirtualscreen);
int vScreenHeight = NativeMethods.GetSystemMetrics(NativeMethods.SMCyvirtualscreen);
int vScreenLeft = NativeMethods.GetSystemMetrics(NativeMethods.SMXvirtualscreen);
int vScreenTop = NativeMethods.GetSystemMetrics(NativeMethods.SMYvirtualscreen);
// Absolute input requires that input is in 'normalized' coords - with the entire
// desktop being (0,0)...(65536,65536). Need to convert input x,y coords to this
// first.
//
// In this normalized world, any pixel on the screen corresponds to a block of values
// of normalized coords - eg. on a 1024x768 screen,
// y pixel 0 corresponds to range 0 to 85.333,
// y pixel 1 corresponds to range 85.333 to 170.666,
// y pixel 2 corresponds to range 170.666 to 256 - and so on.
// Doing basic scaling math - (x-top)*65536/Width - gets us the start of the range.
// However, because int math is used, this can end up being rounded into the wrong
// pixel. For example, if we wanted pixel 1, we'd get 85.333, but that comes out as
// 85 as an int, which falls into pixel 0's range - and that's where the pointer goes.
// To avoid this, we add on half-a-"screen pixel"'s worth of normalized coords - to
// push us into the middle of any given pixel's range - that's the 65536/(Width*2)
// part of the formula. So now pixel 1 maps to 85+42 = 127 - which is comfortably
// in the middle of that pixel's block.
// The key ting here is that unlike points in coordinate geometry, pixels take up
// space, so are often better treated like rectangles - and if you want to target
// a particular pixel, target its rectangle's midpoint, not its edge.
x = ((x - vScreenLeft) * 65536) / vScreenWidth + 65536 / (vScreenWidth * 2);
y = ((y - vScreenTop) * 65536) / vScreenHeight + 65536 / (vScreenHeight * 2);
}
}
/// <summary>
/// Exposes a simple interface to common keyboard operations, allowing the user to simulate keyboard input.
/// </summary>
/// <example>
/// The following code types "Hello world" with the specified casing,
/// and then types "hello, capitalized world" which will be in all caps because
/// the left shift key is being held down.
/// To send input with hotkeys use BlockInput method (app will need admin rights) to block all users inputs
/// and reset inputs state, so they won't interfere
/// <code>
/**
NativeMethods.BlockInput(true);
Keyboard.Reset();
Keyboard.Type("Hello world");
Keyboard.Press(Key.LeftShift);
Keyboard.Type("hello, capitalized world");
Keyboard.Release(Key.LeftShift);
NativeMethods.BlockInput(false);
*/
/// </code>
/// </example>
public static class Keyboard
{
#region Public Members
/// <summary>
/// Presses down a key.
/// </summary>
/// <param name="key">The key to press.</param>
public static void Press(Key key)
{
SendKeyboardInput(key, true);
}
/// <summary>
/// Releases a key.
/// </summary>
/// <param name="key">The key to release.</param>
public static void Release(Key key)
{
SendKeyboardInput(key, false);
}
/// <summary>
/// Presses down a key.
/// </summary>
/// <param name="input">The key to press.</param>
public static void Press(char input)
{
SendKeyboardInput(input, true);
}
/// <summary>
/// Releases a key.
/// </summary>
/// <param name="input">The key to release.</param>
public static void Release(char input)
{
SendKeyboardInput(input, false);
}
/// <summary>
/// Resets the system keyboard to a clean state.
/// </summary>
public static void Reset()
{
foreach (Key key in Enum.GetValues(typeof(Key)))
{
if (key != Key.None && (System.Windows.Input.Keyboard.GetKeyStates(key) & KeyStates.Down) > 0)
{
Release(key);
}
}
}
/// <summary>
/// Performs a press-and-release operation for the specified key, which is effectively equivalent to typing.
/// </summary>
/// <param name="key">The key to press.</param>
/// <param name="releaseDelayMs">Delay before key release</param>
/// <exception cref="ArgumentOutOfRangeException">releaseDelayMs less than 0</exception>
public static void Type(Key key, int releaseDelayMs = 0)
{
if (releaseDelayMs < 0) throw new ArgumentOutOfRangeException(nameof(releaseDelayMs), releaseDelayMs, "Less than zero");
Press(key);
if (releaseDelayMs > 0)
Thread.Sleep(releaseDelayMs);
Release(key);
}
/// <summary>
/// Performs a press-and-release operation for the specified key specific amount of times.
/// </summary>
/// <param name="key">The key to press.</param>
/// <param name="amountToType"></param>
/// <param name="inputDelayMs">Delay after releasing key</param>
/// <param name="releaseDelayMs">Delay before key release</param>
/// <exception cref="ArgumentOutOfRangeException">releaseDelayMs less than 0 or amountToType less than 1 or inputDelayMs less than 0</exception>
public static void TypeKeyNTimes(Key key, int amountToType = 1, int inputDelayMs = 0, int releaseDelayMs = 0)
{
if (amountToType < 1) throw new ArgumentOutOfRangeException(nameof(amountToType), amountToType, "Less than one");
if (inputDelayMs < 0) throw new ArgumentOutOfRangeException(nameof(inputDelayMs), releaseDelayMs, "Less than zero");
for (int i = 0; i < amountToType; i++)
{
Type(key, releaseDelayMs);
if (inputDelayMs > 0)
Thread.Sleep(releaseDelayMs);
}
}
/// <summary>
/// Types the specified text.
/// </summary>
/// <param name="text">The text to type.</param>
/// <param name="inputDelayMs">Delay between typing each key</param>
/// <param name="releaseDelayMs">Delay before key release</param>
/// <exception cref="ArgumentOutOfRangeException">releaseDelayMs less than 0 or inputDelayMs less than 0</exception>
public static void Type(string text, int inputDelayMs = 0, int releaseDelayMs = 0)
{
int delay = 0;
if (text.Length > 1)
delay = inputDelayMs;
foreach (char c in text)
{
Type(c, releaseDelayMs);
if (delay > 0) Thread.Sleep(delay);
}
}
/// <summary>
/// Types the specified char.
/// </summary>
/// <param name="input">The char to type.</param>
/// <param name="releaseDelayMs">Delay before key release</param>
/// <exception cref="ArgumentOutOfRangeException">releaseDelayMs less than 0 or inputDelayMs less than 0</exception>
public static void Type(char input, int releaseDelayMs = 0)
{
if (releaseDelayMs < 0) throw new ArgumentOutOfRangeException(nameof(releaseDelayMs), releaseDelayMs, "Less than zero");
Press(input);
if (releaseDelayMs > 0) Thread.Sleep(releaseDelayMs);
Release(input);
}
/// <summary>
/// Types a key while a set of modifier keys are being pressed. Modifer keys
/// are pressed in the order specified and released in reverse order.
/// </summary>
/// <param name="key">Key to type.</param>
/// <param name="modifierKeys">Set of keys to hold down with key is typed.</param>
/// <param name="releaseDelayMs">Delay before key release</param>
public static void Type(Key key, Key[] modifierKeys, int releaseDelayMs = 0)
{
foreach (Key modifierKey in modifierKeys)
{
if (modifierKey == Key.None)
continue;
Press(modifierKey);
}
Type(key, releaseDelayMs);
foreach (Key modifierKey in modifierKeys.Reverse())
{
if (modifierKey == Key.None)
continue;
Release(modifierKey);
}
}
/// <summary>
/// Types a key while a set of modifier keys are being pressed. Modifer keys
/// are pressed in the order Ctrl->Shift->Alt->Win.
/// </summary>
/// <param name="key">Key to type.</param>
/// <param name="modifierKeys">Set of ModifierKeys enum flags to hold down with key is typed. </param>
/// <param name="releaseDelayMs">Delay before key release</param>
public static void Type(Key key, ModifierKeys modifierKeys, int releaseDelayMs = 0)
{
var modifierKeysArray = ModifierKeysToArrayConverter(modifierKeys);
Type(key, modifierKeysArray, releaseDelayMs);
}
#endregion
#region Private Members
/// <summary>
/// Helper method to convert ModifierKeys enum flags to Key array
/// </summary>
/// <param name="modifierKeys">ModifierKeys enum flags</param>
private static Key[] ModifierKeysToArrayConverter(ModifierKeys modifierKeys)
{
Key[] keys = new Key[4];
if (modifierKeys.HasFlag(ModifierKeys.Control))
keys.SetValue(Key.LeftCtrl, 0);
if (modifierKeys.HasFlag(ModifierKeys.Shift))
keys.SetValue(Key.LeftShift, 1);
if (modifierKeys.HasFlag(ModifierKeys.Alt))
keys.SetValue(Key.LeftAlt, 2);
if (modifierKeys.HasFlag(ModifierKeys.Windows))
keys.SetValue(Key.LWin, 3);
return keys;
}
/// <summary>
/// Injects keyboard input into the system.
/// </summary>
/// <param name="key">Indicates the key pressed or released. Can be one of the constants defined in the Key enum.</param>
/// <param name="press">True to inject a key press, false to inject a key release.</param>
private static void SendKeyboardInput(Key key, bool press)
{
NativeMethods.INPUT ki = new NativeMethods.INPUT
{
type = NativeMethods.InputKeyboard
};
ki.union.keyboardInput.wVk = (short)KeyInterop.VirtualKeyFromKey(key);
ki.union.keyboardInput.wScan = (short)NativeMethods.MapVirtualKey(ki.union.keyboardInput.wVk, 0);
int dwFlags = 0;
if (ki.union.keyboardInput.wScan > 0)
{
dwFlags |= NativeMethods.KeyeventfScancode;
}
if (!press)
{
dwFlags |= NativeMethods.KeyeventfKeyup;
}
ki.union.keyboardInput.dwFlags = dwFlags;
if (ExtendedKeys.Contains(key))
{
ki.union.keyboardInput.dwFlags |= NativeMethods.KeyeventfExtendedkey;
}
ki.union.keyboardInput.time = 0;
ki.union.keyboardInput.dwExtraInfo = new IntPtr(0);
if (NativeMethods.SendInput(1, ref ki, Marshal.SizeOf(ki)) == 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
/// <summary>
/// Injects keyboard input into the system.
/// </summary>
/// <param name="input">Indicates the character pressed or released..</param>
/// <param name="press">True to inject a key press, false to inject a key release.</param>
private static void SendKeyboardInput(char input, bool press)
{
NativeMethods.INPUT ki = new NativeMethods.INPUT
{
type = NativeMethods.InputKeyboard
};
ki.union.keyboardInput.wVk = 0;
ki.union.keyboardInput.wScan = (short) input;
int dwFlags = 0;
dwFlags |= NativeMethods.KeyeventfUnicode;
if (!press)
{
dwFlags |= NativeMethods.KeyeventfKeyup;
}
ki.union.keyboardInput.dwFlags = dwFlags;
ki.union.keyboardInput.time = 0;
ki.union.keyboardInput.dwExtraInfo = new IntPtr(0);
if (NativeMethods.SendInput(1, ref ki, Marshal.SizeOf(ki)) == 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
// From the SDK:
// The extended-key flag indicates whether the keystroke message originated from one of
// the additional keys on the enhanced keyboard. The extended keys consist of the ALT and
// CTRL keys on the right-hand side of the keyboard; the INS, DEL, HOME, END, PAGE UP,
// PAGE DOWN, and arrow keys in the clusters to the left of the numeric keypad; the NUM LOCK
// key; the BREAK (CTRL+PAUSE) key; the PRINT SCRN key; and the divide (/) and ENTER keys in
// the numeric keypad. The extended-key flag is set if the key is an extended key.
//
// - docs appear to be incorrect. Use of Spy++ indicates that break is not an extended key.
// Also, menu key and windows keys also appear to be extended.
private static readonly Key[] ExtendedKeys = {
Key.RightAlt,
Key.RightCtrl,
Key.NumLock,
Key.Insert,
Key.Delete,
Key.Home,
Key.End,
Key.Prior,
Key.Next,
Key.Up,
Key.Down,
Key.Left,
Key.Right,
Key.Apps,
Key.RWin,
Key.LWin
};
// Note that there are no distinct values for the following keys:
// numpad divide
// numpad enter
#endregion
}
@obviliontsk
Copy link
Author

VkKeyScan method removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment