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

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

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

介绍

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

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

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

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

概述

所需步骤的快速概述

锁定光标将光标居中启用原始输入处理原始输入禁用原始输入解锁光标

当用户要求 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 windowclienttoscreen(window, (point*) &cliprect.left);clienttoscreen(window, (point*) &cliprect.right);// now we can lock the cursorclipcursor(&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 structxieventmask em;em.deviceid = xiallmasterdevices;em.mask_len = sizeof(mask);em.mask = mask;//enable raw input using the structurexiselectevents(display, xdefaultrootwindow(display), &em, 1);

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

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 %in", 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 %in", 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 disabledn");                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 %in", 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 disabledn");                    holdMouse = FALSE;                    break;                default: break;            }            TranslateMessage(&msg);            DispatchMessage(&msg);        }        running = IsWindow(hwnd);    }    DestroyWindow(hwnd);    return 0;}

以上就是RGFW 底层:原始鼠标输入和鼠标锁定的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1457153.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月18日 09:26:42
下一篇 2025年12月9日 05:40:30

相关推荐

  • C++框架跨平台开发:应对不同操作系统的挑战

    跨平台应用程序开发要求应用程序能在不同操作系统间无缝运行。c++++框架,如qt和wxwidgets,通过封装操作系统差异、提供图形库和硬件抽象来解决此挑战,简化开发过程。实战展示了使用qt创建跨平台界面的示例。 C++ 框架跨平台开发:应对不同操作系统的挑战 在当今相互关联的世界中,开发跨平台应用…

    2025年12月18日
    000
  • C++ 框架如何在跨平台场景中保持可扩展性和维护性?

    在 c++++ 开发中,框架可促进跨平台可扩展性和维护性:可扩展性:使用抽象类和接口实现平台无关的功能。利用代码生成工具自动化平台特定代码编写。组织代码为可重用模块以促进共享。维护性:应用统一的代码风格。进行单元测试以确保代码正确性。使用版本控制系统来跟踪更改并支持协作。实战案例:一个跨平台 gui…

    2025年12月18日
    000
  • C++框架如何解决跨平台兼容性问题?

    c++++ 框架通过跨平台抽象层 (pal) 解决跨平台兼容性问题,例如 qt、boost 和 poco 提供了特定的 pal 实现,允许应用程序在不同平台上调用特定于操作系统底层的函数。通过使用 pal,应用程序可以轻松地针对多个平台进行开发,无需重新编写代码。 C++ 框架跨平台兼容性解决方案 …

    2025年12月18日
    000
  • 如何将C++框架与跨平台技术集成

    集成c++++框架与跨平台技术实现跨平台软件开发至关重要。步骤包括:选择跨平台技术(如qt、wxwidgets、electron)创建项目并集成框架导入c++库实例化跨平台对象使用跨平台代码编写应用逻辑构建并部署应用程序 如何将C++框架与跨平台技术集成 在现代软件开发中,将C++框架与跨平台技术集…

    2025年12月18日
    000
  • 如何将 C++ 框架与 Java 技术集成?

    可以将 c++++ 框架与 java 技术集成,步骤如下:构建 c++ 库,并包含要集成的函数;在 java 应用中加载 c++ 库;创建 java nio 通道,映射 c++ 库的内存区域;使用 mmaplookup 查找 c++ 函数地址;使用 unsafe 类调用 c++ 函数。 如何将 C+…

    2025年12月18日
    000
  • 揭秘C++框架中的交叉平台和兼容性支持

    跨平台与兼容性支持:c++++ 框架使用工具(如 cmake)支持跨平台开发,允许在不同平台(如 windows、linux)上编译和运行代码。标准库和框架的兼容性层(如 qt 的宏)确保代码在不同平台和编译器上的行为一致。例如,qt 提供跨平台宏来根据目标平台定制代码,而 boost 库包含跨平台…

    2025年12月18日
    000
  • C++框架如何提升代码可维护性?

    c++++ 框架通过以下方法提升代码可维护性:强制执行架构模式,例如 mvc 和 ddd,以组织代码。促进代码重用,减少重复和提高一致性。提供对第三方库和服务的集成,利用外部功能。 利用 C++ 框架提升代码可维护性 在现代软件开发中,代码可维护性至关重要,因为它直接影响软件的长期生命周期。C++ …

    2025年12月18日
    000
  • C++框架内置功能在跨平台开发中的作用

    在跨平台开发中,c++++ 框架的内置功能发挥着至关重要的作用,包括:跨平台兼容性:在多平台上稳定运行,简化移植。图形用户界面 (gui) 支持:提供跨平台的 gui 库,无需编写平台特定代码。数据库集成:支持多种数据库系统,实现数据访问和操作的跨平台性。网络通信:提供网络通信机制,用于分布式应用程…

    2025年12月18日
    000
  • 哪些开源C++框架可以用于商业目的?

    是的,商业应用可以使用开源 c++++ 框架,这可以带来显着的优势,包括:免费的使用、修改和分发(得益于许可证的灵活性)强大的功能和广泛的社区支持持续的开发 如何在商业项目中使用开源 C++ 框架 在商业应用中使用开源 C++ 框架可以带来显著的优势。得益于其许可证的灵活性,开发者可以免费使用、修改…

    2025年12月18日
    000
  • C++框架在计算机图形学中的地位

    c++++ 框架在计算机图形学中至关重要,为开发人员提供了以下优势:高性能,利用 c++ 的速度和效率;跨平台支持,可用于 windows、macos 和 linux 等多种操作系统;健壮性,稳定可靠地处理复杂的图形场景和实时交互;丰富的生态系统,提供广泛的图形编程支持库和工具。 C++ 框架在计算…

    2025年12月18日
    000
  • C++框架在人工智能领域如何助力模型训练和部署?

    c++++框架在人工智能领域备受青睐,为模型训练和部署提供高性能、可扩展、灵活、交叉平台支持的平台。具体而言,tensorflow lite用于移动推理,pytorch用于灵活训练,caffe2适用于计算机视觉和自然语言处理。 C++框架在人工智能领域促进模型训练和部署 在人工智能(AI)领域,C+…

    2025年12月18日
    000
  • C++ 框架与其他框架的未来展望:技术演进趋势预测

    c++++ 框架未来发展趋势:跨平台兼容性:支持多平台开发高性能计算:适合密集型任务代码重用和模块化:提升开发效率对新技术支持:集成 ai/ml社区支持:提供更新和学习资源与其他框架相比,c++ 优势在于性能、内存管理和跨平台兼容性。 C++ 框架与其他框架的未来展望:技术演进趋势预测 简介 随着软…

    2025年12月18日
    000
  • C++ 框架适用场景探究:与其他框架的适用性对比

    在选择 c++++ 框架时,首先考虑其适用场景:性能要求高的应用程序需要可扩展性和可维护性的复杂应用程序跨平台开发需要灵活性和可定制性的场景与其他框架对比:c# .net 框架更适合非性能至上的应用程序。java spring 框架适用于需要企业级功能和支持的应用程序。python django 框…

    2025年12月18日
    000
  • C++框架与其他语言框架在开发大型项目中的适用性

    在大型项目开发中,c++++ 框架因高性能和底层控制而适用,但复杂性和维护成本使其并不适用于所有项目。其他语言框架,如 java、python 和 node.js,在可扩展性、开发速度和服务器端功能方面提供了不同的优势。具体选择取决于项目的特定需求和开发者的偏好。 C++ 框架与其他语言框架在大型项…

    2025年12月18日
    000
  • C++ 框架与其他框架的竞争关系:市场份额与技术优势对比

    在竞争激烈的框架市场中,c++++ 框架以其技术优势保持竞争力,包括:高性能:c++ 编译成机器码,带来极速执行。跨平台:c++可在多种操作系统上运行,实现轻松部署。底层访问:c++ 直接访问硬件和内存,优化性能。尽管市场份额较小,c++ 框架在游戏开发、高性能计算和机器学习等领域广泛应用。 C++…

    2025年12月18日
    000
  • 用于开发桌面应用的C++框架

    使用 c++++ 框架,可以快速开发强大的桌面应用程序。本文探索了三种流行的框架:qt:跨平台框架,提供全面的 gui 组件和多媒体支持。wxwidgets:轻量级跨平台框架,具有灵活性强的 gui 组件和事件处理功能。juce:专注于音乐和音频应用程序开发的框架,提供强大的图形和音频功能。 利用 …

    2025年12月18日
    000
  • 适合企业级项目的C++框架

    企业级c++++框架的最佳选择包括boost(用于库)、qt(用于跨平台gui开发)、wxwidgets(用于跨平台gui)、ace(用于网络和分布式系统)和cereal(用于序列化)。使用boost框架构建的企业级文档管理系统实战案例展示了使用boost.filesystem、boost.rege…

    2025年12月18日
    000
  • C++ 框架优势显现:与其他框架的独到之处

    c++++ 框架优势:性能优势:编译时优化,提高代码执行效率。跨平台支持:可在多种操作系统和处理器架构运行。广泛的应用领域:适用于桌面应用程序、移动应用程序、游戏开发和嵌入式系统。模块化和可扩展性:仅包含所需组件,提高灵活性,简化维护和扩展。 C++ 框架优势显现:与其他框架的独到之处 前言 C++…

    2025年12月18日
    000
  • 流行的跨平台C++框架

    本文介绍了开发跨平台 c++++ 应用的 3 个流行框架:qt:开源、跨平台 gui 框架,提供丰富的功能和易用性。wxwidgets:开源、跨平台 gui 框架,以其轻量级和灵活性著称。juce:模块化、高性能跨平台框架,专注于音频、图形和用户界面开发。 流行的跨平台 C++ 框架 跨平台应用已成…

    2025年12月18日
    000
  • 未来C++框架的机遇与挑战

    c++++ 框架在未来将面临机遇和挑战。机遇包括云计算、人工智能和实时系统的发展,而挑战包括生态系统碎片化、并发性管理和跨平台支持。实战案例包括 boost(机器学习和云计算)和 qt(跨平台 gui)。 C++ 框架的未来:机遇与挑战 C++ 框架在软件开发中发挥着至关重要的作用,为其提供了一个结…

    2025年12月18日
    000

发表回复

登录后才能评论
关注微信