Win-Live2D-C++:一种解决模型窗口透明部分鼠标事件穿透的下层窗口的办法

Win-Live2D-C++:一种解决模型窗口透明部分鼠标事件穿透的下层窗口的办法

来自AI助手的总结
通过修改代码和使用Windows API,实现了在Live2D透明窗口中鼠标事件的穿透功能。

引入:

在Live2D官方的Demo中,我们将窗口设置为透明后,即使窗口透明,我们鼠标的事件仍然会被透明窗口处理,如何才能让鼠标的事件穿透到下层窗口呢?利用WindowsAPI,我们可以实现鼠标事件的穿透。

测试环境:VS2022-C++17-Debug-x64(需在VS2022中安装Qt扩展)

测试系统:Windows11

初期参考视频:見崎音羽(看完前4集即可)

参考资料:log159

代码:

修改:

在GLCore.h中,在GLCore类中,添加成员:

QPoint m_lastMousePos;  // 添加鼠标位置缓存

在GLCore类中,添加成员函数:

void checkTransparencyMousePos_V4(const QPoint& screenPos);
bool underMouse_WinAPI();

两个函数的实现如下:

checkTransparencyMousePos_V4函数:

void GLCore::checkTransparencyMousePos_V4(const QPoint& screenPos)
{
    // 将屏幕坐标转换为窗口客户区坐标
    QPoint windowPos = this->mapFromGlobal(screenPos);

    // 检查坐标是否在窗口内
    if (!this->rect().contains(windowPos)) 
    {
        return;
    }

    // 考虑设备像素比并转换为OpenGL物理坐标
    makeCurrent(); // 确保激活了OpenGL上下文
    qreal dpr = this->devicePixelRatioF();
    int physX = windowPos.x() * dpr;
    int physY = windowPos.y() * dpr;

    // 转换为OpenGL坐标系(Y轴翻转)
    int physHeight = this->height() * dpr;
    int glY = physHeight - physY - 1;

    // 读取鼠标位置像素的RGBA值
    unsigned char rgba[4];
    glReadPixels(physX, glY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba);

    HWND hwnd = (HWND)winId();
    LONG ex = GetWindowLong(hwnd, GWL_EXSTYLE);

    // Alpha值大于0 不透明
    if (rgba[3] > 0) 
    {
        //qDebug() << "不透明区域 - Alpha:" << rgba[3];
        ex &= ~WS_EX_TRANSPARENT; // 禁用穿透
    }
    else
    {
        //qDebug() << "透明区域";
        ex |= WS_EX_TRANSPARENT; // 启用穿透
    }

    SetWindowLong(hwnd, GWL_EXSTYLE, ex);
    doneCurrent();
}

bool underMouse_WinAPI函数:

bool GLCore::underMouse_WinAPI()
{
    // 窗口不可见或最小化
    if (!isVisible() || window()->isMinimized())
    {
        return false;
    }

    HWND hwnd = reinterpret_cast<HWND>(winId());
    RECT winRect;
    if (!GetWindowRect(hwnd, &winRect))
    {
        return false;
    }

    POINT cursorPos;
    if (!GetCursorPos(&cursorPos))
    {
        return false;
    }

    // 鼠标是否在窗口矩形内
    const bool inWindow = (cursorPos.x >= winRect.left &&
        cursorPos.x < winRect.right &&
        cursorPos.y >= winRect.top &&
        cursorPos.y < winRect.bottom);

    // 不在窗口矩形内
    if (!inWindow)
    {
        return false;
    }

    // 获取窗口样式
    LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
    bool isTransparent = (exStyle & WS_EX_TRANSPARENT);

    // 透明穿透窗口
    if (isTransparent)
    {
        return true;
    }

    // 非穿透窗口,确保窗口未被其他窗口遮挡
    HWND hUnderMouse = WindowFromPoint(cursorPos);
    return (hUnderMouse == hwnd) || IsChild(hwnd, hUnderMouse);
}

在控制模型刷新率的连接到的槽函数中,如:

connect(this->timer_flush_window, &QTimer::timeout, this, &GLCore::updateWindow);

在槽函数中,做出以下修改(完整的槽函数代码):

void GLCore::updateWindow()
{
    this->update();
    this->b_IsUnderMouse = this->underMouse_WinAPI();
    if (b_IsUnderMouse)
    {
    }
    else
    {
        qDebug() << "NoUnderMouse";
    }
    // 替换为安全的位置检测
    if (b_IsUnderMouse) 
    {
        QPoint currentPos = QCursor::pos();
        qDebug() << "Pos : " << currentPos;
        // 仅当鼠标位置变化时检测
        if (currentPos != m_lastMousePos)
        {
            checkTransparencyMousePos_V4(currentPos);
            m_lastMousePos = currentPos;
        }
    }
}

修改好后运行即可。

原理:

1. 函数GLCore::underMouse_WinAPI分析:

该函数用于检查鼠标指针当前是否位于自身的窗口范围内,并动态根据窗口的可见性、透明性和层叠关系判断。

函数功能

  1. 判断窗口可见性: 判断窗口是否可见或处于最小化状态;
  2. 获取窗口的矩形边界: 确定窗口在屏幕上的位置和大小;
  3. 获取当前鼠标坐标: 确定鼠标指针当前的屏幕坐标;
  4. 检查鼠标是否在窗口内: 通过比较鼠标坐标和窗口边界确定鼠标是否位于窗口内;
  5. 处理透明性: 如果窗口是透明的,则返回 true,表示鼠标在窗口内;
  6. 检查遮挡关系: 通过 WindowFromPoint 来判断鼠标指针下方的窗口。如果指针位于该窗口上,则返回 true;如果该窗口是自身的子窗口,也返回 true

2. 函数 GLCore::checkTransparencyMousePos_V4 分析

该函数用于确定鼠标指针的位置,并在需要时根据当前位置的 Alpha 值调整窗口的透明效果。这一过程包括读取当前鼠标的位置,进行 OpenGL 调用以获取像素的 RGBA 值,并根据 Alpha 值判断该像素是透明还是不透明,从而设置窗口的透明样式。

函数功能

  1. 坐标转换: 将给定的屏幕坐标转换为窗口客户区坐标。
  2. 窗口内位置检查: 确定鼠标位置是否位于窗口内部。
  3. 激活 OpenGL 上下文: 进行 OpenGL 的状态设置,以确保可以处理 OpenGL 函数调用。
  4. 转换为 OpenGL 坐标系: 将窗口坐标转换为物理坐标并进行 Y 轴翻转,以适应 OpenGL 的坐标系统。
  5. 读取 RGBA 值: 调用 glReadPixels 读取当前位置像素的 RGBA 值。
  6. 设置窗口样式: 根据 Alpha 值判断是否禁用或启用鼠标穿透。

代码分析与详细说明

void GLCore::checkTransparencyMousePos_V4(const QPoint& screenPos)
{
    // 将屏幕坐标转换为窗口客户区坐标
    QPoint windowPos = this->mapFromGlobal(screenPos);
  • mapFromGlobal(screenPos)将全局屏幕坐标 screenPos 转换为当前窗口的客户区坐标,以便后续计算。
// 检查坐标是否在窗口内
    if (!this->rect().contains(windowPos)) 
    {
        return;
    }
  • 使用 this->rect().contains(windowPos) 检查转换后的坐标 windowPos 是否在窗口的客户区域内。
  • 如果不在,函数提前返回,不进行后续操作。
// 考虑设备像素比并转换为OpenGL物理坐标
    makeCurrent(); // 确保激活了OpenGL上下文
    qreal dpr = this->devicePixelRatioF();
    int physX = windowPos.x() * dpr;
    int physY = windowPos.y() * dpr;
  • makeCurrent() 确保 OpenGL 上下文是可用的,以使后续 OpenGL 函数可以正常工作。
  • devicePixelRatioF() 获取设备的像素比(DPI),并根据此比率将窗口坐标转换为物理像素坐标 physX 和 physY
// 转换为OpenGL坐标系(Y轴翻转)
    int physHeight = this->height() * dpr;
    int glY = physHeight - physY - 1;
  • 计算 OpenGL 使用的 Y 坐标。OpenGL 的坐标系统与普通 Windows 窗口坐标系统相反,Y 轴在屏幕上向下方向延伸。
// 读取鼠标位置像素的RGBA值
    unsigned char rgba[4];
    glReadPixels(physX, glY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba);
  • 使用 glReadPixels 从当前 OpenGL 上下文读取指定位置的像素的 RGBA 值,存储在 rgba 数组中。
HWND hwnd = (HWND)winId();
    LONG ex = GetWindowLong(hwnd, GWL_EXSTYLE);
 
  • 获取窗口句柄 hwnd 并读取当前窗口的扩展样式设置。
// Alpha值大于0 不透明
    if (rgba[3] > 0) 
    {
        ex &= ~WS_EX_TRANSPARENT; // 禁用穿透
    }
    else
    {
        ex |= WS_EX_TRANSPARENT; // 启用穿透
    }
  • 检查 Alpha 值(rgba[3]):
    • 如果 Alpha 值大于零,表示该区域不透明,禁用穿透效果(清除 WS_EX_TRANSPARENT 标志)。
    • 如果 Alpha 值等于零,表示该区域透明,启用穿透效果(添加 WS_EX_TRANSPARENT 标志)。
SetWindowLong(hwnd, GWL_EXSTYLE, ex);
    doneCurrent();
}
  • 将更新后的窗口样式写回去,应用所做的更改。
  • 最后调用 doneCurrent(),确保 OpenGL 上下文的状态被清理或恢复。

参考资料:

  1. SetWindowLongA 函数 (winuser.h) – Win32 apps | Microsoft Learn
  2. GetWindowLongA 函数 (winuser.h) – Win32 apps | Microsoft Learn
  3. 扩展窗口样式 (Winuser.h) – Win32 apps | Microsoft Learn
  4. GetWindowRect 函数 (winuser.h) – Win32 apps | Microsoft Learn

运行结果:

20250809140246196-image

视频展示:

 
© 版权声明
THE END
喜欢就支持一下吧
点赞13赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容