C#でライブラリレスでJoyStick(GamePad)の入力イベントを受け取る (Windows API)


# 経緯

部屋を片付けていたら、いい感じのコントローラ(USB)が出てきた[^1]。(/・ω・)/
入力デバイスとしての可能性を感じた[^2]ので（？）、入力イベントを受け取る方法が無いか調べてみたところ、
Windows API使えばライブラリ無しで入力イベントを取得できることが分かったので、やってみました。

[^1]: 画像は「いらすとや」さんのものですので実物とは異なります。

[^2]: うまく使えば、水平の線をたくさん引いたりするような、マウスでやるとダルい特定の作業を、やりやすくできそう。（目的に合わない使いづらいアプリを使っているのが問題な気もするが・・・）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/d66f8bfc-d409-5a88-ddb6-4451618dd78a.png)


# ソースコード

```csharp

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;


class GamePadTest : Form
{
	class MyMessageFilter : IMessageFilter
	{
        const int MM_JOY1MOVE       = 0x3A0;//	Joystick JOYSTICKID1 changed position in the x- or y-direction.
        const int MM_JOY2MOVE	    = 0x3A1;//  Joystick JOYSTICKID2 changed position in the x- or y-direction
        const int MM_JOY1ZMOVE      = 0x3A2;//	Joystick JOYSTICKID1 changed position in the z-direction.
        const int MM_JOY2ZMOVE      = 0x3A3;//	Joystick JOYSTICKID2 changed position in the z-direction.
        const int MM_JOY1BUTTONDOWN = 0x3B5;//	A button on joystick JOYSTICKID1 has been pressed.
        const int MM_JOY2BUTTONDOWN	= 0x3B6;//  A button on joystick JOYSTICKID2 has been pressed.
        const int MM_JOY1BUTTONUP   = 0x3B7;//	A button on joystick JOYSTICKID1 has been released.
        const int MM_JOY2BUTTONUP   = 0x3B8;//	A button on joystick JOYSTICKID2 has been released.

        // https://docs.microsoft.com/en-us/windows/win32/multimedia/mm-joy1buttondown

		public bool PreFilterMessage(ref Message m)
		{
			if (m.Msg == MM_JOY1BUTTONDOWN
             || m.Msg == MM_JOY1BUTTONUP
             || m.Msg == MM_JOY1MOVE
             || m.Msg == MM_JOY1ZMOVE) {
                try{
                    int fwButtons = (int)m.WParam;
                    ulong tmp = (ulong)m.LParam; // 64bit環境で31bit目が1のIntPtrをuintにキャストするとOverflowExceptionが発生するっぽい???ので対策。
                    int xPos = (int)( tmp     &0xFFFF);
                    int yPos = (int)((tmp>>16)&0xFFFF);
                    Console.Write("Message:0x");
                    Console.WriteLine(m.Msg.ToString("X3"));
                    Console.Write("buttons:0x");
                    Console.WriteLine(fwButtons.ToString("X8"));
                    Console.Write("x:");
                    Console.WriteLine(xPos);
                    Console.Write("y:");
                    Console.WriteLine(yPos);
                }
                catch(OverflowException e) {
                    Console.WriteLine(e);
                }
			}
			return false;
		}
	}

    private static class NativeMethods
    {
        [DllImport("winmm.dll")]
        public static extern int joyGetNumDevs();

        [DllImport("winmm.dll")]
        public static extern int joyGetPosEx(int uJoyID, ref JOYINFOEX pji);


        // hwnd  ... Handle to the window to receive the joystick messages.
        // uJoyID ... Identifier of the joystick to be captured. Valid values for uJoyID range from zero (JOYSTICKID1) to 15.
        // uPeriod ... Polling frequency, in milliseconds.
        // fChanged ... Change position flag. Specify TRUE for this parameter to send messages only when the position changes by a value greater than the joystick movement threshold. Otherwise, messages are sent at the polling frequency specified in uPeriod.
        [DllImport("winmm.dll")]
        public static extern int joySetCapture(IntPtr hwnd, int uJoyID, int uPeriod, int fChanged);


        [DllImport("winmm.dll")]
        public static extern int joyReleaseCapture(int uJoyID);
    }

    const int JOYSTICKID1 = 0;

    const int MMSYSERR_BADDEVICEID = 2; // The specified joystick identifier is invalid.
    const int MMSYSERR_NODRIVER = 6;    // The joystick driver is not present.
    const int MMSYSERR_INVALPARAM = 11; // An invalid parameter was passed.
    const int JOYERR_PARMS = 165;       // The specified joystick identifier is invalid.
    const int JOYERR_NOCANDO = 166;     // Cannot capture joystick input because a required service (such as a Windows timer) is unavailable.
    const int JOYERR_UNPLUGGED = 167;   // The specified joystick is not connected to the system.

    const int JOY_RETURNX        = 0x001;
    const int JOY_RETURNY        = 0x002;
    const int JOY_RETURNZ        = 0x004;
    const int JOY_RETURNR        = 0x008;
    const int JOY_RETURNU        = 0x010;
    const int JOY_RETURNV        = 0x020;
    const int JOY_RETURNPOV      = 0x040;
    const int JOY_RETURNBUTTONS  = 0x080;
    const int JOY_RETURNALL      = 0x0FF;
    
    const int JOY_RETURNRAWDATA  = 0x100;
    const int JOY_RETURNPOVCTS   = 0x200;
    const int JOY_RETURNCENTERED = 0x400;

    [StructLayout(LayoutKind.Sequential)]
    private struct JOYINFOEX {
        public int dwSize;
        public int dwFlags;
        public int dwXpos;
        public int dwYpos;
        public int dwZpos;
        public int dwRpos;
        public int dwUpos;
        public int dwVpos;
        public int dwButtons;
        public int dwButtonNumber;
        public int dwPOV;
        public int dwReserved1;
        public int dwReserved2;
    }

    
    MyMessageFilter messageFilter;

    GamePadTest()
    {
        Text = "GamePadTest";

        ClientSize = new Size(300, 300);

        var btnDevNum = new Button(){
            Location = new Point(0, 0),
            Size = new Size(200, 30),
            Text = "Call joyGetNumDevs",
        };
        btnDevNum.Click += (s,e)=>{
            int n = NativeMethods.joyGetNumDevs();
            Console.Write("joyGetNumDevs:");
            Console.WriteLine(n);
        };
        Controls.Add(btnDevNum);


        var btnDevPos = new Button(){
            Location = new Point(0, 60),
            Size = new Size(200, 30),
            Text = "Call joyGetPosEx",
        };
        btnDevPos.Click += (s,e)=>{
            var joyInfo = new JOYINFOEX();
            joyInfo.dwSize = Marshal.SizeOf(joyInfo);
            joyInfo.dwFlags = JOY_RETURNALL;
            int mmresultCode = NativeMethods.joyGetPosEx(JOYSTICKID1, ref joyInfo);
            Console.Write("joyGetPosEx:");
            Console.WriteLine(mmresultCode);
            Console.Write("dwButtons:0x");
            Console.WriteLine(joyInfo.dwButtons.ToString("X8"));
        };
        Controls.Add(btnDevPos);


        Load += (s,e)=>{
            messageFilter = new MyMessageFilter();
            Application.AddMessageFilter(messageFilter);

            NativeMethods.joySetCapture(this.Handle, JOYSTICKID1, 50, 1);
        };

        Closed += (s,e)=>{
            NativeMethods.joyReleaseCapture(JOYSTICKID1);
        };
    }

    
    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new GamePadTest());
    }
}

```

# 実行結果

十字キーとかボタンを色々押してみた結果

```

Message:0x3A0
buttons:0x00000000
x:32511
y:32511
Message:0x3A0
buttons:0x00000000
x:32511
y:65535
Message:0x3A0
buttons:0x00000000
x:32511
y:32511
Message:0x3A0
buttons:0x00000000
x:65535
y:32511
Message:0x3A0
buttons:0x00000000
x:32511
y:32511
Message:0x3A0
buttons:0x00000000
x:32511
y:0
Message:0x3A0
buttons:0x00000000
x:32511
y:32511
Message:0x3A0
buttons:0x00000000
x:0
y:32511
Message:0x3A0
buttons:0x00000000
x:32511
y:32511
Message:0x3B5
buttons:0x00000101
x:32511
y:32511
Message:0x3B7
buttons:0x00000100
x:32511
y:32511
Message:0x3B5
buttons:0x00000202
x:32511
y:32511
Message:0x3B7
buttons:0x00000200
x:32511
y:32511
Message:0x3B5
buttons:0x00000808
x:32511
y:32511
Message:0x3B7
buttons:0x00000800
x:32511
y:32511
Message:0x3B5
buttons:0x00000404
x:32511
y:32511
Message:0x3B7
buttons:0x00000400
x:32511
y:32511
joyGetNumDevs:16
joyGetPosEx:0
dwButtons:0x00000000
```

# ソースコード その２ - SendInputと組み合わせてマウスを動かす

※使用するデバイス(GamePad)に依存します。GamePadを操作したときにどういう入力が入るかを確認して適宜修正してみてください。
`joySetCapture`の最後の引数を0にすることで、ポーリング式で定期的にイベントを発生させます。

```csharp

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;


class GamePadTest : Form
{
	class MyMessageFilter : IMessageFilter
	{
        const int MM_JOY1MOVE       = 0x3A0;//	Joystick JOYSTICKID1 changed position in the x- or y-direction.
        const int MM_JOY2MOVE	    = 0x3A1;//  Joystick JOYSTICKID2 changed position in the x- or y-direction
        const int MM_JOY1ZMOVE      = 0x3A2;//	Joystick JOYSTICKID1 changed position in the z-direction.
        const int MM_JOY2ZMOVE      = 0x3A3;//	Joystick JOYSTICKID2 changed position in the z-direction.
        const int MM_JOY1BUTTONDOWN = 0x3B5;//	A button on joystick JOYSTICKID1 has been pressed.
        const int MM_JOY2BUTTONDOWN	= 0x3B6;//  A button on joystick JOYSTICKID2 has been pressed.
        const int MM_JOY1BUTTONUP   = 0x3B7;//	A button on joystick JOYSTICKID1 has been released.
        const int MM_JOY2BUTTONUP   = 0x3B8;//	A button on joystick JOYSTICKID2 has been released.

        // https://docs.microsoft.com/en-us/windows/win32/multimedia/mm-joy1buttondown

        Control _parent;
        int _leftThre;
        int _rightThre;
        int _upThre;
        int _downThre;

        public MyMessageFilter(Control parent, int leftThre, int rightThre, int upThre, int downThre)
        {
            _parent = parent;
            _leftThre  = leftThre;
            _rightThre = rightThre;
            _upThre    = upThre;
            _downThre  = downThre;
        }

		public bool PreFilterMessage(ref Message m)
		{
            //m.Msg == MM_JOY1BUTTONDOWN
            // || m.Msg == MM_JOY1BUTTONUP
            


			if (m.Msg == MM_JOY1MOVE) {
                ulong tmp = (ulong)m.LParam; // 64bit環境で31bit目が1のIntPtrをuintにキャストするとOverflowExceptionが発生するっぽい???ので対策。
                int xPos = (int)( tmp     &0xFFFF);
                int yPos = (int)((tmp>>16)&0xFFFF);

                int dX = 0;
                int dY = 0;
                if (xPos <= _leftThre) {
                    dX = -5;
                }
                else if (xPos >= _rightThre) {
                    dX = 5;
                }
                if (yPos <= _upThre) {
                    dY = -5;
                }
                else if (yPos >= _downThre) {
                    dY = 5;
                }

                _parent.BeginInvoke(
                    (MethodInvoker)delegate(){
                        SendInputUtil.SendInputMouseRelativeMove(dX, dY);
                    }
                );
			}
			return false;
		}
	}

    private static class NativeMethods
    {
        [DllImport("winmm.dll")]
        public static extern int joyGetNumDevs();

        [DllImport("winmm.dll")]
        public static extern int joyGetPosEx(int uJoyID, ref JOYINFOEX pji);


        // hwnd  ... Handle to the window to receive the joystick messages.
        // uJoyID ... Identifier of the joystick to be captured. Valid values for uJoyID range from zero (JOYSTICKID1) to 15.
        // uPeriod ... Polling frequency, in milliseconds.
        // fChanged ... Change position flag. Specify TRUE for this parameter to send messages only when the position changes by a value greater than the joystick movement threshold. Otherwise, messages are sent at the polling frequency specified in uPeriod.
        [DllImport("winmm.dll")]
        public static extern int joySetCapture(IntPtr hwnd, int uJoyID, int uPeriod, int fChanged);


        [DllImport("winmm.dll")]
        public static extern int joyReleaseCapture(int uJoyID);

    }

    const int JOYSTICKID1 = 0;

    const int MMSYSERR_BADDEVICEID = 2; // The specified joystick identifier is invalid.
    const int MMSYSERR_NODRIVER = 6;    // The joystick driver is not present.
    const int MMSYSERR_INVALPARAM = 11; // An invalid parameter was passed.
    const int JOYERR_PARMS = 165;       // The specified joystick identifier is invalid.
    const int JOYERR_NOCANDO = 166;     // Cannot capture joystick input because a required service (such as a Windows timer) is unavailable.
    const int JOYERR_UNPLUGGED = 167;   // The specified joystick is not connected to the system.

    const int JOY_RETURNX        = 0x001;
    const int JOY_RETURNY        = 0x002;
    const int JOY_RETURNZ        = 0x004;
    const int JOY_RETURNR        = 0x008;
    const int JOY_RETURNU        = 0x010;
    const int JOY_RETURNV        = 0x020;
    const int JOY_RETURNPOV      = 0x040;
    const int JOY_RETURNBUTTONS  = 0x080;
    const int JOY_RETURNALL      = 0x0FF;
    
    const int JOY_RETURNRAWDATA  = 0x100;
    const int JOY_RETURNPOVCTS   = 0x200;
    const int JOY_RETURNCENTERED = 0x400;

    [StructLayout(LayoutKind.Sequential)]
    private struct JOYINFOEX {
        public int dwSize;
        public int dwFlags;
        public int dwXpos;
        public int dwYpos;
        public int dwZpos;
        public int dwRpos;
        public int dwUpos;
        public int dwVpos;
        public int dwButtons;
        public int dwButtonNumber;
        public int dwPOV;
        public int dwReserved1;
        public int dwReserved2;
    }

    



    
    MyMessageFilter messageFilter;

    GamePadTest()
    {
        Text = "GamePadTest";

        ClientSize = new Size(300, 300);

        var btnDevNum = new Button(){
            Location = new Point(0, 0),
            Size = new Size(200, 30),
            Text = "Call joyGetNumDevs",
        };
        btnDevNum.Click += (s,e)=>{
            int n = NativeMethods.joyGetNumDevs();
            Console.Write("joyGetNumDevs:");
            Console.WriteLine(n);
        };
        Controls.Add(btnDevNum);


        var btnDevPos = new Button(){
            Location = new Point(0, 60),
            Size = new Size(200, 30),
            Text = "Call joyGetPosEx",
        };
        btnDevPos.Click += (s,e)=>{
            var joyInfo = new JOYINFOEX();
            joyInfo.dwSize = Marshal.SizeOf(joyInfo);
            joyInfo.dwFlags = JOY_RETURNALL;
            int mmresultCode = NativeMethods.joyGetPosEx(JOYSTICKID1, ref joyInfo);
            Console.Write("joyGetPosEx:");
            Console.WriteLine(mmresultCode);
            Console.Write("dwButtons:0x");
            Console.WriteLine(joyInfo.dwButtons.ToString("X8"));
        };
        Controls.Add(btnDevPos);


        Load += (s,e)=>{
            messageFilter = new MyMessageFilter(this,0,65535,0,65535);
            Application.AddMessageFilter(messageFilter);

            int mmResultCode = NativeMethods.joySetCapture(this.Handle, JOYSTICKID1, 50, 0);
            Console.WriteLine(mmResultCode);
        };

        Closed += (s,e)=>{
            NativeMethods.joyReleaseCapture(JOYSTICKID1);
        };
    }

    
    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new GamePadTest());
    }
}


public static class SendInputUtil
{
    private static class NativeMethods
    {
        [DllImport("user32.dll", SetLastError = true)]
        public extern static void SendInput(int nInputs, Input[] pInputs, int cbsize);
    }


    private const int WM_MOUSEMOVE   = 0x0200;
    private const int WM_LBUTTONDOWN = 0x0201;
    private const int WM_LBUTTONUP   = 0x0202;
    private const int WM_RBUTTONDOWN = 0x0204;

    private const int MOUSEEVENTF_MOVE        = 0x0001;
    private const int MOUSEEVENTF_LEFTDOWN    = 0x0002;
    private const int MOUSEEVENTF_LEFTUP      = 0x0004;
    private const int MOUSEEVENTF_VIRTUALDESK = 0x4000;
    private const int MOUSEEVENTF_ABSOLUTE    = 0x8000;

    [StructLayout(LayoutKind.Sequential)]
    struct MouseInput
    {
        public int X;
        public int Y;
        public int Data;
        public int Flags;
        public int Time;
        public IntPtr ExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct KeyboardInput
    {
        public short VirtualKey;
        public short ScanCode;
        public int Flags;
        public int Time;
        public IntPtr ExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct HardwareInput
    {
        public int uMsg;
        public short wParamL;
        public short wParamH;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct Input
    {
        public int Type;
        public InputUnion ui;
    }

    [StructLayout(LayoutKind.Explicit)]
    struct InputUnion
    {
        [FieldOffset(0)]
        public MouseInput Mouse;
        [FieldOffset(0)]
        public KeyboardInput Keyboard;
        [FieldOffset(0)]
        public HardwareInput Hardware;
    }

    public static void SendInputMouseRelativeMove(int dX, int dY)
    {
        var t = new Input();
        t.Type = 0; // MOUSE = 0
        t.ui.Mouse.X = dX;
        t.ui.Mouse.Y = dY;
        t.ui.Mouse.Data = 0;
        t.ui.Mouse.Flags = MOUSEEVENTF_MOVE;
        t.ui.Mouse.Time = 0;
        t.ui.Mouse.ExtraInfo = IntPtr.Zero;

        Input[] inputs = new Input[]{t};
        NativeMethods.SendInput(inputs.Length, inputs, Marshal.SizeOf(inputs[0]));
    }
}

```

# 環境

Windows10 64bit.

使用したデバイス：[ELECOM JC-U2410TBK](https://www.google.co.jp/search?q=ELECOM+JC-U2410TBK)


# 補足

ボタン数が多い場合は、イベント`MM_JOY1BUTTONDOWN`が送られないケースがあります。
この場合、`joyGetPosEx`でポーリングするしかないようです。


# 参考サイト

- [ジョイスティックをVBで使おう](http://www.cs.k.tsukuba-tech.ac.jp/labo/koba/software/joystk.html)
- [JoyStick API - MSDN](https://docs.microsoft.com/en-us/windows/win32/api/joystickapi/)
- [ウィンドウメッセージをキャプチャするメッセージフィルターの利用例と利用方法](https://csharp.keicode.com/basic/message-filter.php)
- [USBゲームパッドを使えるようにしたい - C# と VB.NET の質問掲示板](http://bbs.wankuma.com/index.cgi?mode=al2&namber=87399&KLOG=150)