single.php

C# WinFormsプロジェクトでキーイベントが発生したデバイスを取得したい場合の対処法

C#のWinFormsプロジェクトで、キーが押されたキーボードのデバイス名まで検出したい場合の対処法について備忘録的に投稿します。

WindowHookで検出

アプリでキーボードのキーが押されたイベントを検出するには、WindowsHookを利用して検出が可能です。

とはいえ、C#のみのアセンブリでは難しいのでWin32 APIを使います。

private static Win32.HookProc? _keyboardproc;
private readonly IntPtr _keyboardHookId = IntPtr.Zero;

[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool UnhookWindowsHookEx(IntPtr hhk);

private static IntPtr SetHook(LowLevelMouseProc proc)
{
    
    using (Process curProcess = Process.GetCurrentProcess())
    using (ProcessModule? curModule = curProcess.MainModule)
    {
        if(curModule != null)
        {
            return SetWindowsHookEx(WH_MOUSE_LL, proc,
                GetModuleHandle(curModule.ModuleName), 0);
        }
        else
        {
            return IntPtr.Zero;
        }
    }
}

IntPtr KeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0)
    {
      //キーが押された後の処理
    }
    return Win32.CallNextHookEx(_keyboardHookId, nCode, wParam, lParam);
}

public void Dispose()
{
    Win32.UnhookWindowsHookEx(_keyboardHookId);
}

押されたキーのコードまでは検出されますが、押されたデバイスの種類までは特定できません。

RawInput で取得可能

RawInput は、Windows が提供する低レベルの入力用のAPIです。

WindowsHookと異なる部分は、複数の入力デバイス(キーボード・マウスなど)を個別に識別して扱えるため「どのデバイスからの入力か」まで判定が可能です。

メッセージの受信用に、ウィンドウを持つクラスが必要になりますが、[NativeWindow]クラスを使えば、フォームを持たないクラスでも[Action]イベントを使って、キー入力やマウスのクリックなどが検出できます。

コードにすると、こんな感じです。

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;

namespace KeyLayerView
{
    internal class RawInput : NativeWindow, IDisposable
    {
        const ushort RI_KEY_BREAK = 0x0001; // KeyUp
        const int RIDEV_INPUTSINK = 0x00000100;
        public RawInput()
        {
            CreateHandle(new CreateParams());
            //GetRawInputDevices();
            RegisterRawInput();
        }

        public void Dispose()
        {
            if (this.Handle != IntPtr.Zero)
            {
                this.DestroyHandle();
            }
        }

        // =========================
        // Raw Input 登録
        // =========================
        public void RegisterRawInput()
        {
            RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[]
            {
                // キーボード
                new RAWINPUTDEVICE
                {
                    usUsagePage = 0x01,
                    usUsage = 0x06,
                    dwFlags = RIDEV_INPUTSINK,
                    hwndTarget = this.Handle
                },

                // ゲームパッド系
                new RAWINPUTDEVICE
                {
                    usUsagePage = 0x01,
                    usUsage = 0x05, // Game Pad
                    dwFlags = RIDEV_INPUTSINK,
                    hwndTarget = this.Handle
                }
                // マウスなども必要であれば、この後に追加できる   

            };

            if (!RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))))
            {
                throw new Exception("Raw Input 登録失敗");
            }
        }

        // =========================
        // メッセージ受信
        // =========================
        protected override void WndProc(ref Message m)
        {
            const int WM_INPUT = 0x00FF;

            if (m.Msg == WM_INPUT)
            {
                ProcessRawInput(m.LParam);
            }

            base.WndProc(ref m);
        }

        void ProcessRawInput(IntPtr hRawInput)
        {
            uint dwSize = 0;

            GetRawInputData(hRawInput, RID_INPUT, IntPtr.Zero, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER)));
            IntPtr buffer = Marshal.AllocHGlobal((int)dwSize);

            try
            {
                if (GetRawInputData(hRawInput, RID_INPUT, buffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))) == dwSize)
                {
                    RAWINPUT raw = Marshal.PtrToStructure<RAWINPUT>(buffer);

                    if (raw.header.dwType == RIM_TYPEKEYBOARD)
                    {
                        var kb = raw.keyboard;

                        string? deviceName = GetDeviceName(raw.header.hDevice);
                        string? deviceFrendlyName = "";
                        if (deviceName != null)
                        {
                            deviceFrendlyName = GetDeviceFriendlyName(deviceName);
                        }

                        bool isKeyUp = (kb.Flags & RI_KEY_BREAK) != 0;
                        if (isKeyUp)
                        {
                            Debug.WriteLine($"KeyUp: {kb.VKey}, Device: {deviceFrendlyName}");
                        }
                        else
                        {
                            Debug.WriteLine($"KeyDown: {kb.VKey}, Device: {deviceFrendlyName}");
                        }

                        Debug.WriteLine("");
                    }
                }
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }

        // =========================
        // デバイス名取得
        // =========================
        string? GetDeviceName(IntPtr device)
        {
            uint size = 0;
            GetRawInputDeviceInfo(device, RIDI_DEVICENAME, IntPtr.Zero, ref size);

            IntPtr ptr = Marshal.AllocHGlobal((int)size);

            try
            {
                GetRawInputDeviceInfo(device, RIDI_DEVICENAME, ptr, ref size);
                return Marshal.PtrToStringAnsi(ptr);
            }
            finally
            {
                Marshal.FreeHGlobal(ptr);
            }
        }

        // =========================
        // デバイス名の取得(VID / PID -> FrendlyName)
        // =========================
        public static string? GetDeviceFriendlyName(string devicePath)
        {
            //Debug.WriteLine("devicePath: " + devicePath);

            IntPtr deviceInfoSet = SetupDiGetClassDevs(
                IntPtr.Zero,
                null,
                IntPtr.Zero,
                DIGCF_ALLCLASSES | DIGCF_PRESENT);

            SP_DEVINFO_DATA devInfo = new SP_DEVINFO_DATA();
            devInfo.cbSize = Marshal.SizeOf(devInfo);

            for (int i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, ref devInfo); i++)
            {
                string? instanceId = GetDeviceInstanceId(deviceInfoSet, devInfo);
                //Debug.WriteLine("instanceId: " + instanceId);

                string? normalizedPath = NormalizeDevicePath(devicePath);

                if (instanceId != null && normalizedPath == instanceId)
                {
                    string? name = GetProperty(deviceInfoSet, devInfo, SPDRP_FRIENDLYNAME);

                    if (string.IsNullOrEmpty(name))
                        name = GetProperty(deviceInfoSet, devInfo, SPDRP_DEVICEDESC);

                    return name;
                }
            }

            return null;
        }

        static string? GetDeviceInstanceId(IntPtr infoSet, SP_DEVINFO_DATA devInfo)
        {
            StringBuilder sb = new StringBuilder(256);
            if (SetupDiGetDeviceInstanceId(infoSet, ref devInfo, sb, sb.Capacity, out _))
                return sb.ToString();

            return null;
        }

        static string? GetProperty(IntPtr infoSet, SP_DEVINFO_DATA devInfo, int prop)
        {
            byte[] buffer = new byte[512];

            if (SetupDiGetDeviceRegistryProperty(
                infoSet,
                ref devInfo,
                prop,
                out _,
                buffer,
                buffer.Length,
                out _))
            {
                return Encoding.Unicode.GetString(buffer).TrimEnd('\0');
            }

            return null;
        }

        static string? NormalizeDevicePath(string devicePath)
        {
            if (string.IsNullOrEmpty(devicePath))
                return null;

            string s = devicePath;

            // \\?\ を削除
            if (s.StartsWith(@"\\?\"))
                s = s.Substring(4);

            // GUID部分を削除
            int guidIndex = s.IndexOf("#{");
            if (guidIndex >= 0)
                s = s.Substring(0, guidIndex);

            // # → \ に変換
            s = s.Replace('#', '\\');

            // 大文字統一
            s = s.ToUpperInvariant();

            return s;
        }

        public void GetRawInputDevices()
        {
            uint deviceCount = 0;
            uint dwSize = (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICELIST));

            //デバイス数を取得
            GetRawInputDeviceList(IntPtr.Zero, ref deviceCount, dwSize);

            if (deviceCount == 0) return;

            //デバイスリスト用のメモリを確保
            IntPtr pRawInputDeviceList = Marshal.AllocHGlobal((int)(dwSize * deviceCount));

            try
            {
                //デバイスリストを列挙
                uint result = GetRawInputDeviceList(pRawInputDeviceList, ref deviceCount, dwSize);

                if (result == uint.MaxValue)
                {
                    // エラー処理
                    return;
                }

                for (int i = 0; i < deviceCount; i++)
                {

                    RAWINPUTDEVICELIST rid = Marshal.PtrToStructure<RAWINPUTDEVICELIST>(pRawInputDeviceList + (int)(i * dwSize));

                    // デバイスタイプ、ハンドルなどを利用する
                    string? deviceName = GetDeviceName(rid.hDevice);
                    string deviceFrendlyName = "";
                    if (deviceName != null)
                    {
                        GetDeviceFriendlyName(deviceName);
                    }

                    Debug.WriteLine($"Device Name: {deviceName}");
                    Debug.WriteLine("");
                    Debug.WriteLine($"Device FrendlyName: {deviceFrendlyName}");
                    Debug.WriteLine("");
                    Debug.WriteLine($"Device Handle: {rid.hDevice}");
                    Debug.WriteLine("");
                    Debug.WriteLine($"Type: {rid.dwType}");
                    Debug.WriteLine("");
                }
            }
            finally
            {
                // 4. メモリ解放
                Marshal.FreeHGlobal(pRawInputDeviceList);
            }
        }

        // =========================
        // Win32定義
        // =========================

        // AllocConsoleのインポート
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool AllocConsole();

        const int RID_INPUT = 0x10000003;
        const int RIM_TYPEMOUSE = 0;
        const int RIM_TYPEKEYBOARD = 1;
        const int RIM_TYPEHID = 2;
        const int RIDI_DEVICENAME = 0x20000007;

        [StructLayout(LayoutKind.Sequential)]
        public struct RAWINPUTDEVICELIST
        {
            public IntPtr hDevice;
            [MarshalAs(UnmanagedType.U4)]
            public uint dwType;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWINPUTDEVICE
        {
            public ushort usUsagePage;
            public ushort usUsage;
            public int dwFlags;
            public IntPtr hwndTarget;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWINPUTHEADER
        {
            public int dwType;
            public int dwSize;
            public IntPtr hDevice;
            public IntPtr wParam;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWKEYBOARD
        {
            public ushort MakeCode;
            public ushort Flags;
            public ushort Reserved;
            public ushort VKey;
            public uint Message;
            public uint ExtraInformation;
        }

        [StructLayout(LayoutKind.Explicit)]
        struct RAWINPUT
        {
            [FieldOffset(0)]
            public RAWINPUTHEADER header;

            [FieldOffset(24)] // 64bit環境では16が基本
            public RAWMOUSE mouse;

            [FieldOffset(24)]
            public RAWKEYBOARD keyboard;

            [FieldOffset(24)]
            public RAWHID hid;
        }
       
        [StructLayout(LayoutKind.Sequential)]
        public struct RAWMOUSE
        {
            public ushort usFlags;

            public uint ulButtons;

            public ushort usButtonFlags;
            public ushort usButtonData;

            public uint ulRawButtons;

            public int lLastX;
            public int lLastY;

            public uint ulExtraInformation;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWHID
        {
            public int dwSizeHid;
            public int dwCount;

            // 実データはこの後ろに続く(可変長)
            public byte bRawData;
        }

        // GetRawInputDeviceList APIの定義
        [DllImport("user32.dll", SetLastError = true)]
        public static extern uint GetRawInputDeviceList(
            IntPtr pRawInputDeviceList,
            ref uint puiNumDevices,
            uint cbSize);

        // デバイス情報を取得するAPI
        [DllImport("user32.dll", SetLastError = true)]
        public static extern uint GetRawInputDeviceInfo(
            IntPtr hDevice,
            uint uiCommand,
            IntPtr pData,
            ref uint pcbSize);

        [DllImport("user32.dll")]
        static extern bool RegisterRawInputDevices(
            RAWINPUTDEVICE[] pRawInputDevices,
            uint uiNumDevices,
            uint cbSize);

        [DllImport("user32.dll")]
        static extern uint GetRawInputData(
            IntPtr hRawInput,
            int uiCommand,
            IntPtr pData,
            ref uint pcbSize,
            uint cbSizeHeader);

        [DllImport("user32.dll")]
        static extern uint GetRawInputDeviceInfo(
            IntPtr hDevice,
            int uiCommand,
            IntPtr pData,
            ref uint pcbSize);

        const int DIGCF_PRESENT = 0x2;
        const int DIGCF_ALLCLASSES = 0x4;

        const int SPDRP_FRIENDLYNAME = 0x0000000C;
        const int SPDRP_DEVICEDESC = 0x00000000;

        [StructLayout(LayoutKind.Sequential)]
        struct SP_DEVINFO_DATA
        {
            public int cbSize;
            public Guid ClassGuid;
            public int DevInst;
            public IntPtr Reserved;
        }

        [DllImport("setupapi.dll")]
        static extern IntPtr SetupDiGetClassDevs(
            IntPtr ClassGuid,
            string? Enumerator,
            IntPtr hwndParent,
            int Flags);

        [DllImport("setupapi.dll")]
        static extern bool SetupDiEnumDeviceInfo(
            IntPtr DeviceInfoSet,
            int MemberIndex,
            ref SP_DEVINFO_DATA DeviceInfoData);

        [DllImport("setupapi.dll", CharSet = CharSet.Auto)]
        static extern bool SetupDiGetDeviceInstanceId(
            IntPtr DeviceInfoSet,
            ref SP_DEVINFO_DATA DeviceInfoData,
            StringBuilder DeviceInstanceId,
            int DeviceInstanceIdSize,
            out int RequiredSize);

        [DllImport("setupapi.dll", CharSet = CharSet.Auto)]
        static extern bool SetupDiGetDeviceRegistryProperty(
            IntPtr DeviceInfoSet,
            ref SP_DEVINFO_DATA DeviceInfoData,
            int Property,
            out int PropertyRegDataType,
            byte[] PropertyBuffer,
            int PropertyBufferSize,
            out int RequiredSize);


    }
}

使う場合は、呼び出し側のクラスで、RawInput クラスをインスタンス化します。

private readonly RawInput? _rawInput;

public RawImputSample()
{
  _rawInput = new RawInput();
}

実行して、適当なキーを押すとキーコードとデバイスの名前が出力されます。

まとめ

今回は短い記事ですが、C#のWinFormsプロジェクトで、キーが押されたキーボードのデバイス名まで検出したい場合の対処法について書きました。

WindowsHookでは、キーイベントが発生した際のデバイス名までは取得ができませんが、RawInput を利用してキーが押されたデバイス名までを検出できます。

WinFormsプロジェクトでキーイベントを発生させたデバイスまで検出したい人の参考になれば幸いです。

スポンサーリンク

最後までご覧いただき、ありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です