0

0

RGFW 底层:原始鼠标输入和鼠标锁定

王林

王林

发布时间:2024-08-14 11:03:02

|

1245人浏览过

|

来源于dev.to

转载

rgfw 底层:原始鼠标输入和鼠标锁定

解释如何锁定光标并为 x11、winapi、cocoa 和 emscripten 启用原始鼠标输入的教程。

介绍

rgfw 是一个轻量级单头窗口库,其源代码可以在这里找到。
本教程基于其源代码。

当您创建锁定光标的应用程序时,例如带有第一人称相机的游戏,能够禁用光标非常重要。
这意味着将光标锁定在屏幕中间并获取原始输入。

此方法的唯一替代方法是在鼠标移动时将鼠标拉回到窗口的中心。然而,这是一个 hack,所以它可能有错误
并且不适用于所有操作系统。因此,使用原始输入正确锁定鼠标非常重要。

本教程解释了 rgfw 如何处理原始鼠标输入,以便您可以了解如何自己实现它。

概述

所需步骤的快速概述

  1. 锁定光标
  2. 将光标居中
  3. 启用原始输入
  4. 处理原始输入
  5. 禁用原始输入
  6. 解锁光标

当用户要求 rgfw 保持光标时,rgfw 启用一个表示光标已保持的位标志。

win->_winargs |= rgfw_hold_mouse;

第 1 步(锁定光标)

在 x11 上,可以通过 xgrabpointer 抓取光标来锁定光标

xgrabpointer(display, window, true, pointermotionmask, grabmodeasync, grabmodeasync, none, none, currenttime);

这使窗口可以完全控制指针。

在 windows 上,clipcursor 将光标锁定到屏幕上的特定矩形。
这意味着我们必须在屏幕上找到窗口矩形,然后将鼠标夹到该矩形上。

还使用:getclientrect) 和 clienttoscreen

//first get the window size (the rgfw_window struct also includes this information, but using this ensures it's correct)
rect cliprect;
getclientrect(window, &cliprect);

// clipcursor needs screen coordinates, not coordinates relative to the window
clienttoscreen(window, (point*) &cliprect.left);
clienttoscreen(window, (point*) &cliprect.right);

// now we can lock the cursor
clipcursor(&cliprect);

macos 和 emscripten 上,启用原始输入的功能也会锁定光标。所以我将在步骤 4 中了解它的功能。

步骤2(将光标置于中心)

光标锁定后,应居中于屏幕中间。
这可确保光标锁定在正确的位置,不会干扰其他任何内容。

rgfw 使用名为 rgfw_window_movemouse 的 rgfw 函数将鼠标移动到窗口中间。

在x11上,xwarppointer可用于将光标移动到窗口中心

xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);

在windows上,使用setcursorpos

setcursorpos(window_x + (window_width / 2), window_y + (window_height / 2));

在 macos 上,使用 cgwarpmousecursorposition

cgwarpmousecursorposition(window_x + (window_width / 2), window_y + (window_height / 2));

在 emscripten 上,rgfw 不移动鼠标。

步骤 3(启用原始输入)

对于 x11,xi 用于启用原始输入

// mask for xi and set mouse for raw mouse input ("rawmotion")
unsigned char mask[ximasklen(xi_rawmotion)] = { 0 };
xisetmask(mask, xi_rawmotion);

// set up x1 struct
xieventmask em;
em.deviceid = xiallmasterdevices;
em.mask_len = sizeof(mask);
em.mask = mask;

//enable raw input using the structure
xiselectevents(display, xdefaultrootwindow(display), &em, 1);

在 windows 上,您需要设置 rawinputdevice 结构并使用 registerrawinputdevices 启用它

Magician
Magician

Figma插件,AI生成图标、图片和UX文案

下载
const rawinputdevice id = { 0x01, 0x02, 0, window };
registerrawinputdevices(&id, 1, sizeof(id));

在 macos 上你只需要运行 cgassociatemouseandmousecursorposition
这还通过解除鼠标光标和鼠标移动的关联来锁定光标

cgassociatemouseandmousecursorposition(0);

在 emscripten 上你只需要请求用户锁定指针

emscripten_request_pointerlock("#canvas", 1);

步骤 4(处理原始输入事件)

这些都发生在事件循环期间。

对于x11,您必须处理普通的motionnotify,手动将输入转换为原始输入。
要检查原始鼠标输入事件,您需要使用 genericevent。

switch (e.type) {
    (...)
    case motionnotify:
        /* check if mouse hold is enabled */
        if ((win->_winargs & rgfw_hold_mouse)) {
            /* convert e.xmotion to raw input by subtracting the previous point */
            win->event.point.x = win->_lastmousepoint.x - e.xmotion.x;
            win->event.point.y = win->_lastmousepoint.y - e.xmotion.y;

            //the mouse must be moved back to the center when it moves
            xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
        }

        break;

    case genericevent: {
        /* motionnotify is used for mouse events if the mouse isn't held */                
        if (!(win->_winargs & rgfw_hold_mouse)) {
            xfreeeventdata(display, &e.xcookie);
            break;
        }

        xgeteventdata(display, &e.xcookie);
        if (e.xcookie.evtype == xi_rawmotion) {
            xirawevent *raw = (xirawevent *)e.xcookie.data;
            if (raw->valuators.mask_len == 0) {
                xfreeeventdata(display, &e.xcookie);
                break;
            }

            double deltax = 0.0f; 
            double deltay = 0.0f;

            /* check if relative motion data exists where we think it does */
            if (ximaskisset(raw->valuators.mask, 0) != 0)
                deltax += raw->raw_values[0];
            if (ximaskisset(raw->valuators.mask, 1) != 0)
                deltay += raw->raw_values[1];

            //the mouse must be moved back to the center when it moves
            xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
            win->event.point = rgfw_point((u32)-deltax, (u32)-deltay);
        }

        xfreeeventdata(display, &e.xcookie);
        break;
    }

在 windows 上,您只需要处理 wm_input 事件并检查原始运动输入

switch (msg.message) {
    (...)
    case wm_input: {
        /* check if the mouse is being held */
        if (!(win->_winargs & rgfw_hold_mouse))
            break;

        /* get raw data as an array */
        unsigned size = sizeof(rawinput);
        static rawinput raw[sizeof(rawinput)];
        getrawinputdata((hrawinput)msg.lparam, rid_input, raw, &size, sizeof(rawinputheader));

        //make sure raw data is valid 
        if (raw->header.dwtype != rim_typemouse || (raw->data.mouse.llastx == 0 && raw->data.mouse.llasty == 0) )
            break;

        //the data is flipped  
        win->event.point.x = -raw->data.mouse.llastx;
        win->event.point.y = -raw->data.mouse.llasty;
        break;
    }

在 macos 上,您可以正常检查鼠标输入,同时使用 deltax 和 deltay 获取和翻转鼠标点

switch (objc_msgsend_uint(e, sel_registername("type"))) {
    case nseventtypeleftmousedragged:
    case nseventtypeothermousedragged:
    case nseventtyperightmousedragged:
    case nseventtypemousemoved:
        if ((win->_winargs & rgfw_hold_mouse) == 0) // if the mouse is not held
                    break;

                nspoint p;
        p.x = ((cgfloat(*)(id, sel))abi_objc_msgsend_fpret)(e, sel_registername("deltax"));
        p.y = ((cgfloat(*)(id, sel))abi_objc_msgsend_fpret)(e, sel_registername("deltay"));

                //the raw input must be flipped for macos as well, and cast for rgfw's event data
        win->event.point = rgfw_point((u32) -p.x, (u32) -p.y));

在 emscripten 上,可以像平常一样检查鼠标事件,除了我们要使用和翻转 e->movementx/y

em_bool emscripten_on_mousemove(int eventtype, const emscriptenmouseevent* e, void* userdata) {
    if ((rgfw_root->_winargs & rgfw_hold_mouse) == 0) // if the mouse is not held
            return

    //the raw input must be flipped for emscripten as well
        rgfw_point p = rgfw_point(-e->movementx, -e->movementy);
}

步骤 5(禁用原始输入)

最后,rgfw 允许禁用原始输入并解锁光标以恢复正常的鼠标输入。

首先,rgfw 禁用位标志。

win->_winargs ^= rgfw_hold_mouse;

在x11中,首先,你必须创建一个带有空白掩码的结构。
这将禁用原始输入。

unsigned char mask[] = { 0 };
xieventmask em;
em.deviceid = xiallmasterdevices;

em.mask_len = sizeof(mask);
em.mask = mask;
xiselectevents(display, xdefaultrootwindow(display), &em, 1);

对于 windows,您可以使用 ridev_remove 传递原始输入设备结构来禁用原始输入。

const rawinputdevice id = { 0x01, 0x02, ridev_remove, null };
registerrawinputdevices(&id, 1, sizeof(id));

在 macos 和 emscripten 上,解锁光标也会禁用原始输入。

第6步(解锁光标)

在x11上,xungrabpoint用于解锁光标。

xungrabpointer(display, currenttime);

在 windows 上,将 null 矩形指针传递给 clipcursor 以指向光标。

clipcursor(null);

在 macos 上,关联鼠标光标和鼠标移动将禁用原始输入并解锁光标

cgassociatemouseandmousecursorposition(1);

在 emscripten 上,退出指针锁定将解锁光标并禁用原始输入。

emscripten_exit_pointerlock();

完整代码示例

x11

// this can be compiled with 
// gcc x11.c -lx11 -lxi

#include 
#include 
#include 
#include 

#include 

int main(void) {
    unsigned int window_width = 200;
    unsigned int window_height = 200;

    display* display = xopendisplay(null);  
    window window = xcreatesimplewindow(display, rootwindow(display, defaultscreen(display)), 400, 400, window_width, window_height, 1, blackpixel(display, defaultscreen(display)), whitepixel(display, defaultscreen(display)));

    xselectinput(display, window, exposuremask | keypressmask);
    xmapwindow(display, window);

    xgrabpointer(display, window, true, pointermotionmask, grabmodeasync, grabmodeasync, none, none, currenttime);

    xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);

    // mask for xi and set mouse for raw mouse input ("rawmotion")
    unsigned char mask[ximasklen(xi_rawmotion)] = { 0 };
    xisetmask(mask, xi_rawmotion);

    // set up x1 struct
    xieventmask em;
    em.deviceid = xiallmasterdevices;
    em.mask_len = sizeof(mask);
    em.mask = mask;

    // enable raw input using the structure
    xiselectevents(display, xdefaultrootwindow(display), &em, 1);

    bool rawinput = true;
    xpoint point;
    xpoint _lastmousepoint;

    xevent event;

    for (;;) {
        xnextevent(display, &event);
        switch (event.type) {
            case motionnotify:
                /* check if mouse hold is enabled */
                if (rawinput) {
                    /* convert e.xmotion to rawinput by substracting the previous point */
                    point.x = _lastmousepoint.x - event.xmotion.x;
                    point.y = _lastmousepoint.y - event.xmotion.y;
                    printf("rawinput %i %i\n", point.x, point.y);
                    xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
                }

                break;

            case genericevent: {
                /* motionnotify is used for mouse events if the mouse isn't held */                
                if (rawinput == false) {
                    xfreeeventdata(display, &event.xcookie);
                    break;
                }

                xgeteventdata(display, &event.xcookie);
                if (event.xcookie.evtype == xi_rawmotion) {
                    xirawevent *raw = (xirawevent *)event.xcookie.data;
                    if (raw->valuators.mask_len == 0) {
                        xfreeeventdata(display, &event.xcookie);
                        break;
                    }

                    double deltax = 0.0f; 
                    double deltay = 0.0f;

                    /* check if relative motion data exists where we think it does */
                    if (ximaskisset(raw->valuators.mask, 0) != 0)
                        deltax += raw->raw_values[0];
                    if (ximaskisset(raw->valuators.mask, 1) != 0)
                        deltay += raw->raw_values[1];

                    point = (xpoint){-deltax, -deltay};
                    xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);

                    printf("rawinput %i %i\n", point.x, point.y);
                }   

                xfreeeventdata(display, &event.xcookie);
                break;
            }
            case keypress:
                if (rawinput == false)
                    break;

                unsigned char mask[] = { 0 };
                xieventmask em;
                em.deviceid = xiallmasterdevices;

                em.mask_len = sizeof(mask);
                em.mask = mask;
                xiselectevents(display, xdefaultrootwindow(display), &em, 1);
                xungrabpointer(display, currenttime);

                printf("raw input disabled\n");
                break;
            default: break;
        }
    }

    xclosedisplay(display);
 }

维纳皮

// compile with gcc winapi.c

#include 

#include 
#include 
#include 

int main() {
    WNDCLASS wc = {0};
    wc.lpfnWndProc   = DefWindowProc; // Default window procedure
    wc.hInstance     = GetModuleHandle(NULL);
    wc.lpszClassName = "SampleWindowClass";

    RegisterClass(&wc);

    int window_width = 300;
    int window_height = 300;
    int window_x = 400;
    int window_y = 400;

    HWND hwnd = CreateWindowA(wc.lpszClassName, "Sample Window", 0,
            window_x, window_y, window_width, window_height,
            NULL, NULL, wc.hInstance, NULL);

    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    // first get the window size (the RGFW_window struct also includes this informaton, but using this ensures it's correct)
    RECT clipRect;
    GetClientRect(hwnd, &clipRect);

    // ClipCursor needs screen coords, not coords relative to the window
    ClientToScreen(hwnd, (POINT*) &clipRect.left);
    ClientToScreen(hwnd, (POINT*) &clipRect.right);

    // now we can lock the cursor
    ClipCursor(&clipRect);

    SetCursorPos(window_x + (window_width / 2), window_y + (window_height / 2));    
    const RAWINPUTDEVICE id = { 0x01, 0x02, 0, hwnd };
    RegisterRawInputDevices(&id, 1, sizeof(id));

    MSG msg;

    BOOL holdMouse = TRUE;

    BOOL running = TRUE;

    POINT point;

    while (running) {
        if (PeekMessageA(&msg, hwnd, 0u, 0u, PM_REMOVE)) {
            switch (msg.message) {
                case WM_CLOSE:
                case WM_QUIT:
                    running = FALSE;
                    break;
                case WM_INPUT: {
                    /* check if the mouse is being held */
                    if (holdMouse == FALSE)
                        break;

                    /* get raw data as an array */
                    unsigned size = sizeof(RAWINPUT);
                    static RAWINPUT raw[sizeof(RAWINPUT)];
                    GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));

                    // make sure raw data is valid 
                    if (raw->header.dwType != RIM_TYPEMOUSE || (raw->data.mouse.lLastX == 0 && raw->data.mouse.lLastY == 0) )
                        break;

                    // the data is flipped  
                    point.x = -raw->data.mouse.lLastX;
                    point.y = -raw->data.mouse.lLastY;
                    printf("raw input: %i %i\n", point.x, point.y);
                    break;
                }
                case WM_KEYDOWN:
                    if (holdMouse == FALSE)
                        break;

                    const RAWINPUTDEVICE id = { 0x01, 0x02, RIDEV_REMOVE, NULL };
                    RegisterRawInputDevices(&id, 1, sizeof(id));
                    ClipCursor(NULL);

                    printf("rawinput disabled\n");
                    holdMouse = FALSE;
                    break;

                default: break;
            }
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        running = IsWindow(hwnd);
    }

    DestroyWindow(hwnd);
    return 0;
}

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

231

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

435

2024.03.01

windows查看端口占用情况
windows查看端口占用情况

Windows端口可以认为是计算机与外界通讯交流的出入口。逻辑意义上的端口一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。怎么查看windows端口占用情况呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

576

2023.07.26

查看端口占用情况windows
查看端口占用情况windows

端口占用是指与端口关联的软件占用端口而使得其他应用程序无法使用这些端口,端口占用问题是计算机系统编程领域的一个常见问题,端口占用的根本原因可能是操作系统的一些错误,服务器也可能会出现端口占用问题。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1098

2023.07.27

windows照片无法显示
windows照片无法显示

当我们尝试打开一张图片时,可能会出现一个错误提示,提示说"Windows照片查看器无法显示此图片,因为计算机上的可用内存不足",本专题为大家提供windows照片无法显示相关的文章,帮助大家解决该问题。

790

2023.08.01

windows查看端口被占用的情况
windows查看端口被占用的情况

windows查看端口被占用的情况的方法:1、使用Windows自带的资源监视器;2、使用命令提示符查看端口信息;3、使用任务管理器查看占用端口的进程。本专题为大家提供windows查看端口被占用的情况的相关的文章、下载、课程内容,供大家免费下载体验。

452

2023.08.02

windows无法访问共享电脑
windows无法访问共享电脑

在现代社会中,共享电脑是办公室和家庭的重要组成部分。然而,有时我们可能会遇到Windows无法访问共享电脑的问题。这个问题可能会导致数据无法共享,影响工作和生活的正常进行。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

2347

2023.08.08

windows自动更新
windows自动更新

Windows操作系统的自动更新功能可以确保系统及时获取最新的补丁和安全更新,以提高系统的稳定性和安全性。然而,有时候我们可能希望暂时或永久地关闭Windows的自动更新功能。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

776

2023.08.10

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

36

2026.01.14

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Excel 教程
Excel 教程

共162课时 | 11.7万人学习

Laravel 8 课程精讲(台湾同胞版)
Laravel 8 课程精讲(台湾同胞版)

共22课时 | 2.3万人学习

vscode其实很简单
vscode其实很简单

共72课时 | 28.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号