先日の記事で、Razerデバイスを制御するソフトウェアをC++で作りました。

実を言うと、私の専門はjava/C#/VB/PHPでC++は正直そこまでなんですよね。
(RubyやらPythonやらも触れはしてるけど専門とは言えないレベル)
なので、今後C++でメンテしていくのは結構面倒臭い(ポインタ制御とか…)ので、
このプログラムをC#に移植したいと思います。

とはいえ、使用しているライブラリ自体はC++/CLIですので、
まずはこのライブラリを呼び出す部分を作らなければなりません。
ライブラリのヘッダファイル(Interception.h)を元に、C#でDllImportしていきます。

Interception.cs


using System;
using System.Text;
using System.Runtime.InteropServices;

using InterceptionContext = System.IntPtr;
using InterceptionDevice = System.Int32;
using InterceptionPrecedence = System.Int32;
using InterceptionFilter = System.UInt16;

namespace MyDeviceController.Library
{
    internal static class Interception
    {
        public const int INTERCEPTION_MAX_KEYBOARD = 10;

        public const int INTERCEPTION_MAX_MOUSE = 10;

        public const int INTERCEPTION_MAX_DEVICE = ((INTERCEPTION_MAX_KEYBOARD) + (INTERCEPTION_MAX_MOUSE));

        public static int GetKeyBoardNum(int index) => (index + 1);

        public static int GetMouseNum(int index) => ((INTERCEPTION_MAX_KEYBOARD) + (index) + 1);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        public delegate int InterceptionPredicate(InterceptionDevice device);

        [DllImport("Interception", EntryPoint = "interception_create_context", CallingConvention = CallingConvention.Cdecl)]
        public static extern InterceptionContext Interception_create_context();

        [DllImport("Interception", EntryPoint = "interception_destroy_context", CallingConvention = CallingConvention.Cdecl)]
        public static extern void Interception_destroy_context(InterceptionContext context);

        [DllImport("Interception", EntryPoint = "interception_get_precedence", CallingConvention = CallingConvention.Cdecl)]
        public static extern InterceptionPrecedence Interception_get_precedence(InterceptionContext context, InterceptionDevice device);

        [DllImport("Interception", EntryPoint = "interception_set_precedence", CallingConvention = CallingConvention.Cdecl)]
        public static extern void Interception_set_precedence(InterceptionContext context, InterceptionDevice device, InterceptionPrecedence precedence);

        [DllImport("Interception", EntryPoint = "interception_get_filter", CallingConvention = CallingConvention.Cdecl)]
        public static extern InterceptionFilter Interception_get_filter(InterceptionContext context, InterceptionDevice device);

        [DllImport("Interception", EntryPoint = "interception_set_filter", CallingConvention = CallingConvention.Cdecl)]
        public static extern void Interception_set_filter(InterceptionContext context, InterceptionPredicate predicate, InterceptionFilter filter);

        [DllImport("Interception", EntryPoint = "interception_wait", CallingConvention = CallingConvention.Cdecl)]
        public static extern InterceptionDevice Interception_wait(InterceptionContext context);

        [DllImport("Interception", EntryPoint = "interception_wait_with_timeout", CallingConvention = CallingConvention.Cdecl)]
        public static extern InterceptionDevice Interception_wait_with_timeout(InterceptionContext context, ulong milliseconds);

        [DllImport("Interception", EntryPoint = "interception_send", CallingConvention = CallingConvention.Cdecl)]
        public static extern int Interception_send(InterceptionContext context, InterceptionDevice device, ref InterceptionStroke stroke, uint nstroke);

        [DllImport("Interception", EntryPoint = "interception_receive", CallingConvention = CallingConvention.Cdecl)]
        public static extern int Interception_receive(InterceptionContext context, InterceptionDevice device, ref InterceptionStroke stroke, uint nstroke);

        [DllImport("Interception", EntryPoint = "interception_get_hardware_id", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        public static extern uint Interception_get_hardware_id(InterceptionContext context, InterceptionDevice device, StringBuilder hardware_id_buffer, uint buffer_size);

        [DllImport("Interception", EntryPoint = "interception_is_invalid", CallingConvention = CallingConvention.Cdecl)]
        public static extern int Interception_is_invalid(InterceptionDevice device);

        [DllImport("Interception", EntryPoint = "interception_is_keyboard", CallingConvention = CallingConvention.Cdecl)]
        public static extern int Interception_is_keyboard(InterceptionDevice device);

        [DllImport("Interception", EntryPoint = "interception_is_mouse", CallingConvention = CallingConvention.Cdecl)]
        public static extern int Interception_is_mouse(InterceptionDevice device);
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InterceptionStroke{
        [FieldOffset(0)] public InterceptionMouseStroke Mouse;

        [FieldOffset(0)] public InterceptionKeyStroke Key;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct InterceptionMouseStroke
    {
        public ushort State;
        public ushort Flags;
        public short Rolling;
        public int X;
        public int Y;
        public uint Information;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct InterceptionKeyStroke
    {
        public ushort Code;
        public ushort State;
        public uint Information;
    }

    [Flags]
    public enum KeyState : ushort
    {
        KeyDown = 0x00,
        KeyUp = 0x01,
        KeyE0 = 0x02,
        KeyE1 = 0x04,
        KeySetLED = 0x08,
        KeyShadow = 0x10,
        KeyVKPacket = 0x20
    };

    [Flags]
    public enum KeyStateFilter : ushort
    {
        None = 0x0000,
        All = 0xFFFF,
        KeyDown = KeyState.KeyUp,
        KeyUp = KeyState.KeyUp << 1,
        KeyE0 = KeyState.KeyE0 << 1,
        KeyE1 = KeyState.KeyE1 << 1,
        KeySetLED = KeyState.KeySetLED << 1,
        KeyShadow = KeyState.KeyShadow << 1,
        KeyVKPacket = KeyState.KeyVKPacket << 1
    };

    [Flags]
    public enum MouseState : ushort
    {
        LeftButtonDown = 0x001,
        LeftButtonUp = 0x002,
        RightButtonDown = 0x004,
        RightButtonUp = 0x008,
        MiddleButtonDown = 0x010,
        MiddleButtonUp = 0x020,

        Button1Down = LeftButtonDown,
        Button1Up = LeftButtonUp,
        Button2Down = RightButtonDown,
        Button2Up = RightButtonUp,
        Button3Down = MiddleButtonDown,
        Button3Up = MiddleButtonUp,

        Button4Down = 0x040,
        Button4Up = 0x080,
        Button5Down = 0x100,
        Button5Up = 0x200,

        MouseWheel = 0x400,
        MouseHWheel = 0x800
    };

    [Flags]
    public enum MouseStateFilter : ushort
    {
        None = 0x0000,
        All = 0xFFFF,

        LeftButtonDown = MouseState.LeftButtonDown,
        LeftButtonUp = MouseState.LeftButtonUp,
        RightButtonDown = MouseState.RightButtonDown,
        RightButtonUp = MouseState.RightButtonUp,
        MiddleButtonDown = MouseState.MiddleButtonDown,
        MiddleButtonUp = MouseState.MiddleButtonUp,

        Button1Down = MouseState.Button1Down,
        Button1Up = MouseState.Button1Up,
        Button2Down = MouseState.Button2Down,
        Button2Up = MouseState.Button2Up,
        Button3Down = MouseState.Button3Down,
        Button3Up = MouseState.Button3Up,

        Button4Down = MouseState.Button4Down,
        Button4Up = MouseState.Button4Up,
        Button5Down = MouseState.Button5Down,
        Button5Up = MouseState.Button5Up,

        MouseWheel = MouseState.MouseWheel,
        MouseHWheel = MouseState.MouseHWheel,

        MouseMove = 0x1000,
    };

    [Flags]
    public enum MouseFlags : ushort
    {
        MoveRelative = 0x000,
        MoveAbsolute = 0x001,
        VirtualDesktop = 0x002,
        AttributesChanged = 0x004,
        MoveNoCoalesce = 0x008,
        TermsvrSrcShadow = 0x100
    };
}

これで、DllImport部分が完成したので、あとは呼出元もC++からC#へ移植


using System.Text;
using MyDeviceController.Library;

using InterceptionDevice = System.Int32;
using InterceptionPrecedence = System.Int32;
using InterceptionFilter = System.UInt32;
using InterceptionContext = System.IntPtr;

using static MyDeviceController.Library.Interception;

namespace MyDeviceController
{
    class RazerObserver
    {
        public bool IsStop { get; set; } = false;

        private const string KEYBD_HID = "HID\\VID_1532&PID_0244&REV_0200&MI_01&Col01";
        private const string KEYBD_PAD = "HID\\VID_1532&PID_0244&REV_0200&MI_00";
        private const string MOUSE_HID = "HID\\VID_1532&PID_0244&REV_0200&MI_02";

        public void MonitorInput()
        {

            var context = Interception_create_context();
            if (context == null) return;

            try
            {

                Interception_set_filter(context, Interception_is_keyboard,
                    (ushort)(KeyStateFilter.KeyDown |
                             KeyStateFilter.KeyUp |
                             KeyStateFilter.KeyE0 |
                             KeyStateFilter.KeyE1));
                Interception_set_filter(context, Interception_is_mouse,
                    (ushort)(MouseStateFilter.MouseWheel |
                             MouseStateFilter.MiddleButtonDown |
                             MouseStateFilter.MouseWheel));

                InterceptionDevice keyboard = INTERCEPTION_MAX_DEVICE, pad = INTERCEPTION_MAX_DEVICE, mouse = INTERCEPTION_MAX_DEVICE;

                /* Search KeyBoard Device */
                for (int i = 0; i < INTERCEPTION_MAX_KEYBOARD; i++)
                {
                    InterceptionDevice d = GetKeyBoardNum(i);
                    StringBuilder sb = new StringBuilder(500);

                    if (Interception_get_hardware_id(context, d, sb, (uint)sb.Capacity) > 0)
                    {
                        switch(sb.ToString())
                        {
                            case KEYBD_HID:
                                keyboard = d;
                                break;

                            case KEYBD_PAD:
                                pad = d;
                                break;

                            default:
                                break;
                        }
                    }
                }

                /* Search Mouse Device */
                for (int i = 0; i < INTERCEPTION_MAX_MOUSE; i++)
                {
                    InterceptionDevice d = GetMouseNum(i);
                    StringBuilder sb = new StringBuilder(500);

                    if (Interception_get_hardware_id(context, d, sb, (uint)sb.Capacity) > 0 && (sb.ToString() == MOUSE_HID))
                    {
                        mouse = d;
                        break;
                    }
                }

                if (keyboard == INTERCEPTION_MAX_DEVICE
                    || pad == INTERCEPTION_MAX_DEVICE
                    || mouse == INTERCEPTION_MAX_DEVICE) return;

                InterceptionDevice device;
                InterceptionStroke stroke = new InterceptionStroke();
                while (!IsStop && Interception_receive(context, device = Interception_wait(context), ref stroke, 1) > 0)
                {
                    if (device == keyboard)
                    {
                        InterceptionKeyStroke s = stroke.Key;

                        bool isKeySend = true;
                        switch (s.Code)
                        {
                            case 2:
                                s.Code = 0x01;       //Esc
                                break;

                            case 3:
                                s.Code = 0x08;      //7
                                break;

                            case 4:
                                s.Code = 0x09;      //8
                                break;

                            case 5:
                                s.Code = 0x02;      //1
                                break;

                            case 6:
                                s.Code = 0x03;      //2
                                break;

                            case 15:
                                break;

                            case 16:
                                break;

                            case 17:
                                break;

                            case 18:
                                s.Code = 0x04;      //3
                                break;

                            case 19:
                                s.Code = 0x05;      //4
                                break;

                            case 58:
                                isKeySend = false;
                                s.Code = 0x2A; this.SendKey(context, device, stroke, s, 1);      //Shift
                                s.Code = 0x21; this.SendKey(context, device, stroke, s, 1);      //F
                                break;

                            case 30:
                                break;

                            case 31:
                                break;

                            case 32:
                                break;

                            case 33:
                                s.Code = 0x06;      //5
                                break;

                            case 42:
                                s.Code = 0x2C;      //Z
                                break;

                            case 44:
                                isKeySend = false; //無効化
                                break;

                            case 45:
                                isKeySend = false; //無効化
                                break;

                            case 46:
                                s.Code = 0x2D;
                                break;

                            case 57:
                                break;

                            case 56:
                                s.Code = 0x07;      //6
                                break;

                            case 75:
                                break;

                            case 77:
                                break;

                            case 72:
                                break;

                            case 80:
                                break;

                            default:
                                break;
                        }
                        if (isKeySend)
                            this.SendKey(context, device, stroke, s, 1);
                    }
                    else this.SendKey(context, device, stroke, 1);
                }
            }
            finally
            {
                Interception_destroy_context(context);
            }
        }

        public void SendKey(InterceptionContext context, InterceptionDevice device, InterceptionStroke stroke, InterceptionKeyStroke keyStroke, InterceptionFilter nstroke)
        {
            stroke.Key = keyStroke;
            Interception_send(context, device, ref stroke, nstroke);
        }
        public void SendKey(InterceptionContext context, InterceptionDevice device, InterceptionStroke stroke, InterceptionMouseStroke mouseStroke, InterceptionFilter nstroke)
        {
            stroke.Mouse = mouseStroke;
            Interception_send(context, device, ref stroke, nstroke);
        }
        public void SendKey(InterceptionContext context, InterceptionDevice device, InterceptionStroke stroke, InterceptionFilter nstroke)
        {
            Interception_send(context, device, ref stroke, nstroke);
        }
    }
}

あとは、これをマルチメソッドで呼び出してあげればOK.
今回は.NET Coreで作成したので、自作のNotifyIconコントロールを表示するように設定。

App.xaml.cs


namespace MyDeviceController
{
    /// 
    /// Interaction logic for App.xaml
    /// 
    public partial class App : Application
    {
        private NotifyIcon notifyIcon = new NotifyIcon();
        private RazerObserver razerObserver;

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            ShutdownMode = ShutdownMode.OnExplicitShutdown;

            razerObserver = new RazerObserver();

            var task = Task.Run(() => razerObserver.MonitorInput());
        }

        protected override void OnExit(ExitEventArgs e)
        {
            razerObserver.IsStop = true;
            notifyIcon.Dispose();
            base.OnExit(e);
        }
    }
}

これで問題なく動作。
メモリ管理は.NET側で管理してくれるので、今後汎用的にしていく上でC#の方が楽ですね。

今回は単純な移植ですが、今後は設定画面等追加して、
もう少し使い勝手を向上させていこうと思います。

最早FF14のブログではなく技術者ブログになりつつある件。