Receive JoyStick (GamePad) input events in C # without library (Windows API)

9 minute read

Background

When I was cleaning up the room, a nice controller (USB) came out [^ 1]. (/ ・ Ω ・) /
I felt the potential as an input device [^ 2], so I checked if there was a way to receive input events.
I found that I could get input events without a library using Windows API, so I tried it.

[^ 1]: The image is from “Irasutoya”, so it is different from the real thing.

image.png

Source code


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; //OverflowException seems to occur when casting IntPtr with 31st bit 1 to uint in 64bit environment???So measures.
                    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());
    }
}

Execution result

The result of pressing various buttons such as the cross key


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

Source code # 2-Move the mouse in combination with SendInput

  • It depends on the device (GamePad) used. Please check what kind of input is entered when operating the GamePad and correct it as appropriate.
    By setting the last argument of joySetCapture to 0, the polling expression will generate an event periodically.

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; //It seems that OverflowException occurs when IntPtr with 31st bit 1 is cast to uint in 64bit environment.???So measures.
                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]));
    }
}

environment

Windows10 64bit.

Device used: ELECOM JC-U2410TBK

Supplement

If there are many buttons, the event MM_JOY1BUTTONDOWN may not be sent.
In this case, it seems that there is no choice but to poll with joyGetPosEx.

Reference site

-Use the joystick in VB

Tags:

Updated: