A bit late to the party, but in case anyone else stumbles across this, here is a C# program flipping the Caps Lock LED. It is based on @ivanzoid's answer (and a lot more random Googling).
The basic procedure is
- obtain a handle for the keyboard device
- read the current LED status
- flip the bit corresponding to the Caps Lock LED <-- this is the place where you could do other random things
- write the status back to the keyboard
- clean up
// to compile: powershell> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc /out:capsblink.exe /target:exe capsblink.cs
//
// Following:
// - original motivation: https://stackoverflow.com/questions/72679665/is-it-possible-to-control-capslock-light-without-actual-capslocking/72679984#72679984
// - partial answer using C-API: https://stackoverflow.com/questions/2248358/way-to-turn-on-keyboards-caps-lock-light-without-actually-turning-on-caps-lock
// - full version of the partial answer from above (in C, 1999): https://www.codeguru.com/windows/manipulating-the-keyboard-lights-in-windows-nt/
// - calling C-API from C# via p/invoke: https://learn.microsoft.com/en-us/archive/msdn-magazine/2003/july/net-column-calling-win32-dlls-in-csharp-with-p-invoke
// - Win32 API docs: https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol (and related)
using System.Runtime.InteropServices;
using System.ComponentModel;
using System;
class CapsLockLight
{
[DllImport("kernel32.dll", SetLastError=true)]
public static extern Boolean DefineDosDevice(UInt32 flags, String deviceName, String targetPath);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr CreateFile(String fileName,
UInt32 desiredAccess, UInt32 shareMode, IntPtr securityAttributes,
UInt32 creationDisposition, UInt32 flagsAndAttributes, IntPtr templateFile
);
[StructLayout(LayoutKind.Sequential)]
public struct KEYBOARD_INDICATOR_PARAMETERS
{
public UInt16 unitID;
public UInt16 LEDflags;
}
[DllImport("kernel32.dll", SetLastError=true)]
public static extern Boolean DeviceIoControl(IntPtr device, UInt32 ioControlCode,
ref KEYBOARD_INDICATOR_PARAMETERS KIPin, UInt32 inBufferSize,
ref KEYBOARD_INDICATOR_PARAMETERS KIPout, UInt32 outBufferSize,
ref UInt32 bytesReturned, IntPtr overlapped
);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern Boolean DeviceIoControl(IntPtr device, UInt32 ioControlCode,
IntPtr KIPin, UInt32 inBufferSize,
ref KEYBOARD_INDICATOR_PARAMETERS KIPout, UInt32 outBufferSize,
ref UInt32 bytesReturned, IntPtr overlapped
);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern Boolean DeviceIoControl(IntPtr device, UInt32 ioControlCode,
ref KEYBOARD_INDICATOR_PARAMETERS KIPin, UInt32 inBufferSize,
IntPtr KIPout, UInt32 outBufferSize,
ref UInt32 bytesReturned, IntPtr overlapped
);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern Boolean CloseHandle(IntPtr handle);
static void Main(string[] args)
{
UInt32 bytesReturned = 0;
IntPtr device;
KEYBOARD_INDICATOR_PARAMETERS KIPbuf = new KEYBOARD_INDICATOR_PARAMETERS { unitID = 0, LEDflags = 0 };
if(!DefineDosDevice(Flags.DDD_RAW_TARGET_PATH, "myKBD", "\\Device\\KeyboardClass0"))
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
// Console.WriteLine("Created device");
device = CreateFile("\\\\.\\myKBD", Flags.GENERIC_WRITE, 0, IntPtr.Zero, Flags.OPEN_EXISTING, 0, IntPtr.Zero);
if(device == Flags.INVALID_HANDLE_VALUE)
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
// Console.WriteLine("Opened device");
if(!DeviceIoControl(device, Flags.IOCTL_KEYBOARD_QUERY_INDICATORS, IntPtr.Zero, 0, ref KIPbuf, (UInt32)Marshal.SizeOf(KIPbuf), ref bytesReturned, IntPtr.Zero))
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
// Console.WriteLine(String.Format("Read LED status: {0:x}", KIPbuf.LEDflags));
KIPbuf.LEDflags = (UInt16)(KIPbuf.LEDflags ^ Flags.KEYBOARD_CAPS_LOCK_ON);
// Console.WriteLine(String.Format("Changed LED status to: {0:x}", KIPbuf.LEDflags));
if(!DeviceIoControl(device, Flags.IOCTL_KEYBOARD_SET_INDICATORS, ref KIPbuf, (UInt32)Marshal.SizeOf(KIPbuf), IntPtr.Zero, 0, ref bytesReturned, IntPtr.Zero))
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
// Console.WriteLine("Set new LED status");
if(!CloseHandle(device))
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
// Console.WriteLine("Closed device handle");
if(!DefineDosDevice(Flags.DDD_REMOVE_DEFINITION, "myKBD", null))
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
// Console.WriteLine("Removed device definition");
}
};
class Flags
{
public static IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1);
public const UInt32 IOCTL_KEYBOARD_SET_INDICATORS = (0x0000000b << 16) | (0 << 14) | (0x0002 << 2) | 0; // from ntddkbd.h, ntddk.h
public const UInt32 IOCTL_KEYBOARD_QUERY_INDICATORS = (0x0000000b << 16) | (0 << 14) | (0x0010 << 2) | 0; // from ntddkbd.h, ntddk.h
public const UInt32 DDD_RAW_TARGET_PATH = 0x00000001;
public const UInt32 DDD_REMOVE_DEFINITION = 0x00000002;
public const UInt32 DDD_EXACT_MATCH_ON_REMOVE = 0x00000004;
public const UInt32 DDD_NO_BROADCAST_SYSTEM = 0x00000008;
public const UInt32 GENERIC_ALL = 0x10000000;
public const UInt32 GENERIC_EXECUTE = 0x20000000;
public const UInt32 GENERIC_WRITE = 0x40000000;
public const UInt32 GENERIC_READ = 0x80000000;
public const UInt32 CREATE_NEW = 1;
public const UInt32 CREATE_ALWAYS = 2;
public const UInt32 OPEN_EXISTING = 3;
public const UInt32 OPEN_ALWAYS = 4;
public const UInt32 TRUNCATE_EXISTING = 5;
public const UInt16 KEYBOARD_SCROLL_LOCK_ON = 1;
public const UInt16 KEYBOARD_NUM_LOCK_ON = 2;
public const UInt16 KEYBOARD_CAPS_LOCK_ON = 4;
public const UInt16 KEYBOARD_SHADOW = 0x4000;
public const UInt16 KEYBOARD_LED_INJECTED = 0x8000;
};