老周一般很少玩游戏,在某宝上买了一堆散件,计划在过年期间自己做个机械臂耍耍。头脑中划过一道紫蓝色的闪电,想起用游戏手柄来控制机械臂。机械臂是由树莓派(大草莓)负责控制,然后客户端通过 Socket UDP 来发送信号。优先考虑在 PC 和手机上测试,就顺便折腾一下 XInput API。当然,读取手柄数据有多套 API。本文老周先介绍 XInput 方案,后面再介绍 Windows.Gaming.Input 方案。Windows.Gaming.Input 是 UWP API,也可以在.NET 项目中使用。.NET 程序适合用这套 API。

XInput 中的 X 指的就是“西瓜手柄”,哦不,是 XBox 手柄。当然了,并不局限于 XB 手柄,结构与 XB 相似的手柄也能用。老周用的是北通的阿修罗无线版,经测试是可用的。

XInput API 基本的定义都在 Xinput.h 头文件中。读手柄的数值要用到 XInputGetState 函数,它的原型如下:

DWORD WINAPI XInputGetState
(
_In_ DWORD dwUserIndex, // Index of the gamer associated with the device
_Out_ XINPUT_STATE* pState // Receives the current state
)

dwUserIndex 指的是手柄设备索引,范围为 0 - 3,也就是只能连接四个手柄(其实也够玩了)。如果只连接了一个手柄,这个参数直接用 0 就可以了。如果你想用 for 循环来访问各个手柄,还可以用到以下宏:

#define XUSER_MAX_COUNT       4

pState 参数是指针类型,指向 XINPUT_STATE 结构体,它只有两个成员:

typedef struct _XINPUT_STATE
{
DWORD dwPacketNumber;
XINPUT_GAMEPAD Gamepad;
} XINPUT_STATE, *PXINPUT_STATE;

DWORD 就是 double word,一个 word 是 16 位无符号整数,两个就是 32 位。所以 dwPacketNumber 字段是一个整数值。它表示你读取数据的序号,它的值会不断累加,在读取数据时,咱们可以用一个变量保存它的值,每次读手柄后进行比较,如果这个序号没有变化,说明用户没有操作手柄;相反,如果值不同,表明手柄的状态有改变。

Gamepad 字段是另一个结构体—— XINPUT_GAMEPAD。

typedef struct _XINPUT_GAMEPAD
{
WORD wButtons;
BYTE bLeftTrigger;
BYTE bRightTrigger;
SHORT sThumbLX;
SHORT sThumbLY;
SHORT sThumbRX;
SHORT sThumbRY;
} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

wButtons 表示手柄被按下的按键,“w”表示它的值是 word 类型。以下宏定义了这些按键:

1、方向键。

/* 下面这四个是手柄上的方向键:上、下、左、右 */
#define XINPUT_GAMEPAD_DPAD_UP 0x0001
#define XINPUT_GAMEPAD_DPAD_DOWN 0x0002
#define XINPUT_GAMEPAD_DPAD_LEFT 0x0004
#define XINPUT_GAMEPAD_DPAD_RIGHT 0x0008

就是中间那四个,如下图红圈内。

2、开始和返回。

#define XINPUT_GAMEPAD_START            0x0010
#define XINPUT_GAMEPAD_BACK 0x0020

如下图黄圈那两个键:

3、X、Y、A、B 键。

#define XINPUT_GAMEPAD_A                0x1000
#define XINPUT_GAMEPAD_B 0x2000
#define XINPUT_GAMEPAD_X 0x4000
#define XINPUT_GAMEPAD_Y 0x8000

就是右摇杆上面的四个键,如下图绿色圈内部分:

4、下面两个表示摇杆上的按钮按下时触发:

/* 左摇杆按下 */
#define XINPUT_GAMEPAD_LEFT_THUMB 0x0040 /* 右摇杆按下 */
#define XINPUT_GAMEPAD_RIGHT_THUMB 0x0080

5、下面两个是“肩膀”键,在手柄的左上角和右上角。

#define XINPUT_GAMEPAD_LEFT_SHOULDER    0x0100
#define XINPUT_GAMEPAD_RIGHT_SHOULDER 0x0200

下图中蓝色圈内就是。

好,回到 XINPUT_GAMEPAD 结构体,接着看其他字段。

typedef struct _XINPUT_GAMEPAD
{
……
BYTE bLeftTrigger;
BYTE bRightTrigger;
SHORT sThumbLX;
SHORT sThumbLY;
SHORT sThumbRX;
SHORT sThumbRY;
} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

bLeftTrigger 和 bRightTrigger 是两个扳机键,玩打鬼子游戏时用来开枪,它的范围是 0 - 255,所以类型是字节。

后机四个 sThumb-- 是两个摇杆的读数(左摇杆的X、Y值,右摇杆的X、Y值),范围是有符号的 16 位整数值,取值在 -32768 和 32767 内,摇杆位于中心位置时,读值为 0。摇杆向前(向上)推时Y为正值,向后(向下)推时Y为负值;摇杆向左推时X为负值,向右推时X为正值。

就这样了,有了上述知识,你已经可以读手柄了。下面咱们做个示例。

新建一个 C++ 控制台项目就可以了,不需要 Windows / Win32 应用。

a、包含 Xinput.h 头文件。

#include <stdio.h>
#include <Windows.h>
#include <Xinput.h>

b、光包含头文件还不行,因为项目默认没有导入相关的 .lib 文件。.lib 可不是什么静态库,只是描述动态库的符号罢了。在“解决方案”窗口右击项目,打开属性窗口。“配置”处选“所有”,免得为 Debug 和 Release 版本重复配置。

在左边导航节点中找到“链接器” -> “输入”,并选中。在窗口右边点击“附加依赖项”右边的下拉箭头,点击“编辑...”。

在弹出的对话框中加上 “Xinput.lib”。

确定保存即可。

下面是整个程序的代码:

#include <stdio.h>
#include <Windows.h>
#include <Xinput.h> /* 此变量保存读数序号 */
static unsigned long readOrder = 0; /* 入口点 */
int main(int argc, char** argv)
{
/* 开始读数 */
XINPUT_STATE xis = { 0 };
while (1)
{
if (ERROR_SUCCESS != XInputGetState(0, &xis)) {
continue; /* 这一次没读成功,下一次再读 */
}
/* 注意比较一下数据序号,相同的值不需要处理 */
if (readOrder == xis.dwPacketNumber) {
continue;
}
/* 保存新的序号 */
readOrder = xis.dwPacketNumber;
/* 分析数据 */
printf_s("左摇杆:x= %d,y= %d\t右摇杆:x= %d,y= %d\n",
xis.Gamepad.sThumbLX,
xis.Gamepad.sThumbLY,
xis.Gamepad.sThumbRX,
xis.Gamepad.sThumbRY); /* 休息一会儿 */
Sleep(60);
}
printf_s("即将退出\n");
return 0;
}

XInputGetState 函数调用成功,返回 ERROR_SUCCESS,也就是 0。注意:每次读取后,要比较一下数据序号,如果新序号没有变,说明手柄的状态未改变,不用处理;如果值不相同,说明有新的状态,要处理,并且保存最新的数据序号

把你的手柄连接好,运行程序。接着推动左右摇杆,会看到控制台打印各个坐标值。

如果需要,还可以加入对按键的判断,比如这里,我加入对 X、Y、A、B 键的判断。

    while (1)
{
………………
/* 判断按键 */
if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_A) == XINPUT_GAMEPAD_A)
{
printf_s("你按下了【A】键\t");
}
else if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_B) == XINPUT_GAMEPAD_B)
{
printf_s("你按下了【B】键\t");
}
else if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_X) == XINPUT_GAMEPAD_X)
{
printf_s("你按下了【X】键\t");
}
else if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_Y) == XINPUT_GAMEPAD_Y)
{
printf_s("你按下了【Y】键");
}
printf_s("\n"); /* 休息一会儿 */
Sleep(60);
}

各个键位的宏所定义的值都是占用一个二进制位,所以这里咱们可以通过位运算来确定哪个按钮被触发。

效果如下:

-----------------------------------------------------------------------------------------------------

好了,现在,离模拟鼠标动作不远了,下一步就是如发送消息了。发送输入模拟需要用 SendInput 函数。它的原型如下:

UINT SendInput(
UINT cInputs, // number of input in the array
LPINPUT pInputs, // array of inputs
int cbSize); // sizeof(INPUT)

为了好看,我去掉修饰参数的宏。这个函数如果返回 0,说明输入消息发送不成功;成功的时候是返回已发送的消息数。调用这个函数的核心是 INPUT 结构体。一个 INPUT 实例就代表一条指令,一个操作可能会有多条指令完成,所以, INPUT以数组的形式传递。

cInputs 参数指定数组中 INPUT 实例的个数,cbSize 是一个 INPUT 实例的大小(字节,用 sizeof 运算符)。pInputs 就是指向 INPUT 数组第一个元素的指针。

然后,咱们了解一下 INPUT 结构体。

typedef struct tagINPUT {
DWORD type; union
{
MOUSEINPUT mi;
KEYBDINPUT ki;
HARDWAREINPUT hi;
} DUMMYUNIONNAME;
} INPUT, *PINPUT, FAR* LPINPUT;

指向 INPUT 结构的指针是 LP(长指针、分配远程堆,所以有 far),这个咱们一般不用管它,这东西是16位处理器的遗留物,现在处理器都 32 位以上了。不过,咱们得关注的是:这个结构体里面,除了第一个字段 type,其他的字段是共用内存的(union)。说人话就是,mi、ki、hi 字段的偏移地址相同。不过,这个结构体是 8 字节对齐的,type 只有4字节,剩下4字节留空,下一个字段是从第9个字节开始(偏移是8)。最后就相当于第一个字段用了8字节,后面的字段用32字节,整个结构体40字节。如果 dll import 到 .NET 项目,得注意这个偏移,不然后无法调用。如果你照抄网上的 PInvoke 代码,至少在 64 位系统上是不起作用的。老周后面的水文中会告诉大伙伴们怎么 Dll Import,

mi、ki、hi 这几个货的大小并不一致,分别占用 32、24、8 字节。为了使用访问性能最优,故选择 8 字节对齐。说人话就是程序单次处理8个字节,如 8、16、24、32、64 等。

INPUT 结构体的 type 字段指明要模拟的输入类型:

#define INPUT_MOUSE     0            /* 鼠标 */
#define INPUT_KEYBOARD 1 /* 键盘 */
#define INPUT_HARDWARE 2 /* 硬件消息 */

从名字就知道是啥了,就是模拟鼠标、键盘事件,这两个是最常用的;第三个是除鼠标、键盘以外的硬件输入消息,直接用消息编号,这个极少用。这三个值对应 INPUT 结构体中的 mi、ki、hi 字段。

咱们的例子只是模拟鼠标动作,所以用到 MOUSEINPUT 结构体。

typedef struct tagMOUSEINPUT {
LONG dx;
LONG dy;
DWORD mouseData;
DWORD dwFlags;
DWORD time;
ULONG_PTR dwExtraInfo;
} MOUSEINPUT, *PMOUSEINPUT, FAR* LPMOUSEINPUT;

dwFlags 是一个整数值,可以由多个二进制组合使用,包括:

#define MOUSEEVENTF_MOVE        0x0001 /* mouse move */
#define MOUSEEVENTF_LEFTDOWN 0x0002 /* left button down */
#define MOUSEEVENTF_LEFTUP 0x0004 /* left button up */
#define MOUSEEVENTF_RIGHTDOWN 0x0008 /* right button down */
#define MOUSEEVENTF_RIGHTUP 0x0010 /* right button up */
#define MOUSEEVENTF_MIDDLEDOWN 0x0020 /* middle button down */
#define MOUSEEVENTF_MIDDLEUP 0x0040 /* middle button up */
#define MOUSEEVENTF_XDOWN 0x0080 /* x button down */
#define MOUSEEVENTF_XUP 0x0100 /* x button down */
#define MOUSEEVENTF_WHEEL 0x0800 /* wheel button rolled */
#if (_WIN32_WINNT >= 0x0600)
#define MOUSEEVENTF_HWHEEL 0x01000 /* hwheel button rolled */
#endif
#if(WINVER >= 0x0600)
#define MOUSEEVENTF_MOVE_NOCOALESCE 0x2000 /* do not coalesce mouse moves */
#endif /* WINVER >= 0x0600 */
#define MOUSEEVENTF_VIRTUALDESK 0x4000 /* map to entire virtual desktop */
#define MOUSEEVENTF_ABSOLUTE 0x8000 /* absolute move */

这里老周仅模拟移动、左键按下/弹起、右键按下/弹起,以及滚轮。如果要模拟滚轮,要指定 MOUSEEVENTF_WHEEL,滚轮的值通过 MOUSEINPUT 结构体的 mouseData 字段传递

把前面的示例程序改一下,这回咱们不输出文本了,而是从手柄读数据,然后用 SendInput 函数发送鼠标模拟。

    while (1)
{
if (ERROR_SUCCESS != XInputGetState(0, &xis)) {
continue; /* 这一次没读成功,下一次再读 */
}
/* 注意比较一下数据序号,相同的值不需要处理 */
if (readOrder == xis.dwPacketNumber) {
continue;
}
/* 保存新的序号 */
readOrder = xis.dwPacketNumber; // 转换一下
int xx = xis.Gamepad.sThumbRX / 1000;
int yy = -xis.Gamepad.sThumbRY / 1000;
// 这个是滚轮
int wheel = xis.Gamepad.sThumbLY / 500;
/* 准备发送消息 */
INPUT mouseAction = { 0 };
mouseAction.type = INPUT_MOUSE;
// 设置偏移坐标
mouseAction.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_WHEEL;
mouseAction.mi.dx = xx;
mouseAction.mi.dy = yy;
mouseAction.mi.mouseData = wheel; // 滚轮数据
SendInput(1, &mouseAction, sizeof(INPUT)); /* 模拟左键单击 */
if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_A) == XINPUT_GAMEPAD_A)
{
// 一次单击包含两条消息:按下 + 弹起
INPUT leftDown = { 0 };
leftDown.type = INPUT_MOUSE;
leftDown.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
INPUT leftUp = { 0 };
leftUp.type = INPUT_MOUSE;
leftUp.mi.dwFlags = MOUSEEVENTF_LEFTUP;
// 创建数组
INPUT cmds[] = { leftDown, leftUp };
SendInput(2, cmds, sizeof(INPUT));
} /* 模拟右键单击 */
if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_B) == XINPUT_GAMEPAD_B)
{
// 也是两条消息:右键按下 + 弹起
INPUT rightDown = { 0 };
rightDown.type = INPUT_MOUSE;
rightDown.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN;
INPUT rightUp = { 0 };
rightUp.type = INPUT_MOUSE;
rightUp.mi.dwFlags = MOUSEEVENTF_RIGHTUP;
// 创建数组
INPUT inputs[] = { rightDown, rightUp };
SendInput(2, inputs, sizeof(INPUT));
} /* 休息一会儿 */
Sleep(60);
}

MOUSEINPUT结构体的 dwFlags 字段要注意一下:

1、移动鼠标用的是 MOUSEEVENTF_MOVE, 滚轮用的是 MOUSEEVENTF_WHEEL。这里老周是把两者合起来发送:MOUSEEVENTF_MOVE | MOUSEEVENTF_WHEEL;

2、MOUSEEVENTF_MOVE 值可以与 MOUSEEVENTF_ABSOLUTE 值组合用。如果指定了 MOUSEEVENTF_ABSOLUTE,表示使用绝对定位坐标,值的范围在 0 和 65535 之间。这个范围不管你的显示器屏幕的大小,总之,左上角是 (0, 0),右下角是 (65535, 65535),鼠标指针的位置在这范围内换算。这里不用 MOUSEEVENTF_ABSOLUTE 值,dx、dy 就变成移动量,以像素为单位的。正值表示向下/向右移动;负值表示向上/向左移动。这里还是选择移动量好一些,可避免鼠标指针飘得太快难以操控。例如,-22 表示向反方向移动 22 像素,+50 表示正向移动 50 像素。

前面的演示中咱们知道摇杆的读值是 -32768 到 32767,这个值有点大,所以老周做了运算:

int xx = xis.Gamepad.sThumbRX / 1000;
int yy = -xis.Gamepad.sThumbRY / 1000;
// 这个是滚轮
int wheel = xis.Gamepad.sThumbLY / 500;

移动量除以 1000,滚轮值除以 500。这个换算不是固定的,只是老周觉得这个值比较合适,除数的值越大,活动的范围越小。

在上面演示中,老周用 A 键模拟左键单击,B 键模拟右键单击。左摇杆的 Y 方向模拟滚轮,右摇杆模拟鼠标指针的移动。当然,你可以使用任何你喜欢的键和摇杆来模拟。我这里仅作参考。

用手柄移动手柄的效果如下:

模拟鼠标点击效果如下:

滚轮模拟的效果如下:

好了,今天就说到这里。下一篇咱们用 .NET P/Invoke 来实现。

【XInput】游戏手柄模拟鼠标动作的更多相关文章

  1. C#模拟鼠标、键盘操作

    C语言 在程序中打开网页,模拟鼠标点击.键盘输入 一.简述         记--使用C语言 打开指定网页,并模拟鼠标点击.键盘输入.实现半自动填写账号密码,并登录网站(当然现在的大部分网站都有验证码 ...

  2. 可以用py库: pyautogui (自动测试模块,模拟鼠标、键盘动作)来代替pyuserinput

    PyAutoGUI 是一个人性化的跨平台 GUI 自动测试模块 pyUserInput模块安装前需要安装pywin32和pyHook模块.(想要装的看https://www.cnblogs.com/m ...

  3. py库: pyautogui (自动测试模块,模拟鼠标、键盘动作)

    PyAutoGUI 是一个人性化的跨平台 GUI 自动测试模块 pyautogui 库 2017-10-4 pip install pyautogui python pip.exe install p ...

  4. C#模拟鼠标键盘控制其他窗口(一)

    编写程序模拟鼠标和键盘操作可以方便的实现你需要的功能,而不需要对方程序为你开放接口.比如,操作飞信定时发送短信等.我之前开发过飞信耗子,用的是对飞信协议进行抓包,然后分析协议,进而模拟协议的执行,开发 ...

  5. C# 模拟鼠标移动与点击

    我们需要用到的mouse_event函数,位于user32.dll这个库文件里面,所以我们要先声明引用. [System.Runtime.InteropServices.DllImport(" ...

  6. python模拟鼠标键盘操作 GhostMouse tinytask 调用外部脚本或程序 autopy右键另存为

    0.关键实现:程序窗口前置 python 通过js控制滚动条拉取全文 通过psutil获取pid窗口句柄,通过win32gui使程序窗口前置 通过pyauto实现右键菜单和另存为操作 1.参考 aut ...

  7. selenium webdriver模拟鼠标键盘操作

    在测试使用Selenium webdriver测试WEB系统的时候,用到了模拟鼠标.键盘的一些输入操作. 1.鼠标的左键点击.双击.拖拽.右键点击等: 2.键盘的回车.回退.空格.ctrl.alt.s ...

  8. selenium模拟鼠标操作

    Selenium提供了一个类ActionChains来处理模拟鼠标事件,如单击.双击.拖动等. 基本语法: class ActionChains(object): """ ...

  9. selenium webdriver模拟鼠标键盘

    在测试使用Selenium webdriver测试WEB系统的时候,用到了模拟鼠标.键盘的一些输入操作. 1.鼠标的左键点击.双击.拖拽.右键点击等: 2.键盘的回车.回退.空格.ctrl.alt.s ...

  10. Python模拟鼠标和键盘操作实现重复性操作

    前言 由于工作需要,要利用某软件去采集数据,做重复的动作大概500多次.所以想写一个程序代替人,去点击和输入. 一开始的思路有两个:1.用Python或者windows对此软件直接操作.2.利用Pyt ...

随机推荐

  1. web - 解决 formdata 打印空对象

    获取单个值可以使用formData对象.get();而直接打印是看不到的.因为外界访问不到,你使用append方法后,对应的键值对就已经添加到表单里面了,你在控制台看到的是FormData原型,存储的 ...

  2. 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.11)

    一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...

  3. [转帖]MySQL如何进行索引重建操作?

    MySQL如何进行索引重建操作? - 潇湘隐者 - 博客园 (cnblogs.com) 在MySQL数据库中,没有类似于SQL Server数据库或Oracle数据库中索引重建的语法(ALTER IN ...

  4. [转帖]云数据库是杀猪盘么,去掉中间商赚差价,aws数据库性能提升 10 倍!价格便宜十倍。

    https://tidb.net/blog/021059f1 于是乎dba中的冯大嘴喊出了云数据库就是杀猪盘.让每个公司自建数据库. 那么有没有一种数据库又便宜又好用呢.有 哪就是tidb数据库. 之 ...

  5. [转帖]CentOS8时间同步服务

    时间同步服务 CentOS7之前的版本用的是ntpdate服务,之后用的是chrony服务 默认是安装的了 查看版本 [root@centos8 ~]#rpm -qi chrony Name : ch ...

  6. [转帖]py_innodb_page_info.py工具使用

    目录 一.Linux安装Python3 1. 解压包 2. 安装环境 3. 生成编译脚本 4. 检查python3.10的编译器 5. 建立Python3和pip3的软链 6. 添加到PATH 7.  ...

  7. Python学习之十_paramiko的简单学习

    Python学习之十_paramiko的简单学习 简介 pywinrm 是python用于连接访问windows的工具 paramiko 是python用于连接访问linux的工具 ansible等工 ...

  8. [转帖]Linux系统:page fault

    Linux进程如何访问内存 Linux下,进程并不是直接访问物理内存,而是通过内存管理单元(MMU)来访问内存资源,原因后面会讲到. 为什么需要虚拟内存地址空间 假设某个进程需要4MB的空间,内存假设 ...

  9. 文心一言 VS 讯飞星火 VS chatgpt (186)-- 算法导论14.1 3题

    三.用go语言,写出OS-SELECT的非递归版本. 文心一言: 下面是使用Go语言编写的OS-SELECT的非递归版本: func OS_SELECT(T *Node, k int) *Node { ...

  10. 买彩票能中大奖?用Java盘点常见的概率悖论 | 京东云技术团队

    引言 <双色球头奖概率与被雷劈中的概率哪个高?> <3人轮流射击,枪法最差的反而更容易活下来?> 让我们用Java来探索ta们! 悖论1:著名的三门问题 规则描述:你正在参加一 ...