macOS Keyboard Emulation with C#
By using the CoreGraphics API in macOS, a developer can emulate a key press without any physical interaction. Doing that is fairly simple in Objective-C or C++, but how can we do the same in C#?
CoreGraphics
CoreGraphics is a system framework standard in macOS. It provides access to “Quartz Event Services”, which we can use to emulate a key press.
Let’s take a quick look at how that works in C++:
1
2
3
CGEventRef e = CGEventCreateKeyboardEvent(NULL, (CGKeyCode) 33, true);
CGEventPost(kCGHIDEventTap, e);
CFRelease(e);
It’s fairly simple - create, post and release the event. This code snippet should type a little [
into whatever window
you’re focused on.
Before we start putting this into C#, let’s examine the two important functions here.
CGEventCreateKeyboardEvent
This takes a source reference, virtual key code and boolean (will the key be pressed or unpressed?) and returns a reference to the event.
Event sources are out of scope for this post, but feel free to check out the documentation for CGEventSourceCreate.
CGEventPost
This takes a CGEventTapLocation
for the first argument, with the event reference we want to post as the second
argument.
I’m not too sure about this, but I think the event tap location is where the event will be placed / processed.
We’ll just use kCGHIDEventTap
for now, which will place the event tap “where HID system events enter the window
server”1
Unmanaged Interop
Let’s get this into C#!
We’ve already established the functions we need:
- CGEventCreateKeyboardEvent
- CGEventPost
- CFRelease
CoreFoundation and CoreGraphics can both be found in /System/Library/Frameworks
. More specifically, we can find the
dynamic libraries at /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
and /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics
.
Let’s use DllImport to expose these functions.
1
2
3
4
5
6
7
8
public enum CGKeyCode : ushort {
// ...
}
// Note that these snippets use unsafe code - if you can't use that, use IntPtr instead of void*, etc.
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
public static extern unsafe void* CGEventCreateKeyboardEvent(void* source, CGKeyCode key, bool keyDown);
1
2
3
4
5
6
7
8
public enum CGEventTapLocation : ushort {
cghidEventTap = 0,
cgSessionEventTap = 1,
cgAnnotatedSessionEventTap = 2
}
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
public static extern unsafe void* CGEventPost(CGEventTapLocation tap, void* eventRef);
1
2
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern unsafe void CFRelease(void* typeRef);
Basic Usage
As a small example, let’s rewrite the C++ sample from the beginning:
1
2
3
var e = CGEventCreateKeyboardEvent((void*)0x0, (CGKeyCode)33, true);
CGEventPost(CGEventTapLocation.cghidEventTap, e);
CFRelease(e);
Footnotes
-
https://developer.apple.com/documentation/coregraphics/cgeventtaplocation ↩