所谓辉煌的人生,不过是欲望的囚徒。
——叔本华
注:自本节开始,所有的示例都以 OpenGLWidget 实现,
一、鼠标事件
重写鼠标事件,
#ifndef MYOPENGL_H
#define MYOPENGL_H
#include <QOpenGLWidget>
#include <QTimer>
#include <QMouseEvent>
class MyOpenGL : public QOpenGLWidget
{
public:
MyOpenGL(QWidget *parent = nullptr);
public slots:
void onTimerUpdate();
protected:
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
void mousePressEvent(QMouseEvent *event); // 鼠标点击事件
void mouseMoveEvent(QMouseEvent *event); // 鼠标移动事件
void mouseReleaseEvent(QMouseEvent *event); // 鼠标释放事件
private:
QTimer * fpsTimer = nullptr;
bool isPressed = false;
};
#endif // MYOPENGL_H
#include "live2d/LAppDelegate.hpp"
#include "live2d/LAppView.hpp"
#include "live2d/LAppPal.hpp"
#include "live2d/LAppLive2DManager.hpp"
#include "live2d/LAppDefine.hpp"
#include "myopengl.h"
MyOpenGL::MyOpenGL(QWidget *parent)
:QOpenGLWidget{parent} {
// 60 fps
fpsTimer = new QTimer(this);
fpsTimer->start((1.0/60.0)*1000);
connect(fpsTimer,&QTimer::timeout,this,&MyOpenGL::onTimerUpdate);
}
void MyOpenGL::onTimerUpdate()
{
update();
}
void MyOpenGL::initializeGL(){
LAppDelegate::GetInstance()->Initialize(this);
}
void MyOpenGL::resizeGL(int w, int h){
LAppDelegate::GetInstance()->resize(w,h);
}
void MyOpenGL::paintGL(){
LAppDelegate::GetInstance()->update();
}
void MyOpenGL::mouseMoveEvent(QMouseEvent*e){
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(e->x(),e->y());
update();
e->ignore();
}
void MyOpenGL::mousePressEvent(QMouseEvent*e){
if(e->button() == Qt::LeftButton)
{
isPressed = true;
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(e->x(),e->y());
}
e->ignore();
}
void MyOpenGL::mouseReleaseEvent(QMouseEvent*e){
if(e->button() == Qt::LeftButton)
{
isPressed = false;
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(e->x(), e->y());
update();
}
e->ignore();
}
效果如下,
二、实时追踪
窗口默认设置为不追踪鼠标移动,只有鼠标按键按下时才影响鼠标移动 mousePressEvent 事件,我们可以通过 setMouseTracking 方法调整为实时追踪,
setMouseTracking(true);
// ...
MyOpenGL::MyOpenGL(QWidget *parent)
:QOpenGLWidget{parent} {
// 60 fps
fpsTimer = new QTimer(this);
fpsTimer->start((1.0/60.0)*1000);
connect(fpsTimer,&QTimer::timeout,this,&MyOpenGL::onTimerUpdate);
setMouseTracking(true);
}
// ...
效果如下,
三、全屏追踪
全屏追踪的方案有两种:
- 窗口全屏(应用级)。重写 QT 的鼠标事件,是仅针对于当前窗口的鼠标捕获,一旦脱离了当前窗口,就没法捕获系统的鼠标事件。
- 使用系统钩子(系统级)。这种方法涉及到操作系统级别的编程,需要特殊权限和对应平台的 API 知识。
除了实现全屏追踪,我们还需要鼠标穿透到桌面来操作其他窗口,而应用级的鼠标穿透 Qt::WA_TransparentForMouseEvents 不会再接收系统级的鼠标事件。综合考虑,我们采用方案 2 实现全屏追踪。
1、鼠标钩子
#ifndef MYOPENGL_H
#define MYOPENGL_H
#include <QOpenGLWidget>
#include <QTimer>
#include <QMouseEvent>
#include <Windows.h>
#include <QWindow>
class MyOpenGL : public QOpenGLWidget
{
public:
MyOpenGL(QWidget *parent = nullptr);
static MyOpenGL* GetInstance();
~MyOpenGL();
void mouseMoveCallBack(float x,float y);
void mouseReleaseCallBack(float x,float y);
void mousePressCallBack(float x,float y);
public slots:
void onTimerUpdate();
protected:
void initFpsTimer();
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
void mousePressEvent(QMouseEvent *event); // 鼠标点击事件
void mouseMoveEvent(QMouseEvent *event); // 鼠标移动事件
void mouseReleaseEvent(QMouseEvent *event); // 鼠标释放事件
void releaseInstance();
private:
QTimer * fpsTimer = nullptr;
bool isPressed = false;
HHOOK mouseHook;
bool isInitializeGL = false;
};
#endif // MYOPENGL_H
#include "live2d/LAppDelegate.hpp"
#include "live2d/LAppView.hpp"
#include "live2d/LAppPal.hpp"
#include "live2d/LAppLive2DManager.hpp"
#include "live2d/LAppDefine.hpp"
#include "myopengl.h"
namespace {
MyOpenGL* s_instance = NULL;
}
DWORD lastCallbackTime = 0;
DWORD THROTTLE_INTERVAL = (1.0/30.0)*1000;
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
DWORD currentTime = GetTickCount(); // 获取当前时间戳
if (currentTime - lastCallbackTime >= THROTTLE_INTERVAL) { // 检查是否满足节流间隔
if (nCode >= 0) {
MSLLHOOKSTRUCT* pMouseStruct = (MSLLHOOKSTRUCT*)lParam;
// 处理鼠标移动事件
float x = pMouseStruct->pt.x;
float y = pMouseStruct->pt.y;
//qDebug() << "Mouse position: (" << x << ", " << y << ")";
switch (wParam)
{
case WM_LBUTTONDOWN: // 鼠标左键按下
MyOpenGL::GetInstance()->mousePressCallBack(x,y);
break;
case WM_LBUTTONUP: // 鼠标左键抬起
MyOpenGL::GetInstance()->mouseReleaseCallBack(x,y);
break;
case WM_MOUSEMOVE: // 鼠标移动
MyOpenGL::GetInstance()->mouseMoveCallBack(x,y);
break;
}
}
lastCallbackTime = currentTime; // 更新上次触发回调函数的时间戳
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
MyOpenGL::MyOpenGL(QWidget *parent)
:QOpenGLWidget{parent} {
// 60 fps
initFpsTimer();
setMouseTracking(true);
if (s_instance == NULL)
{
s_instance = this;
}
}
MyOpenGL::~MyOpenGL()
{
UnhookWindowsHookEx(mouseHook);
LAppDelegate::GetInstance()->Release();
LAppDelegate::ReleaseInstance();
releaseInstance();
}
void MyOpenGL::initFpsTimer(){
fpsTimer = new QTimer(this);
fpsTimer->start((1.0/30.0)*1000);
connect(fpsTimer,&QTimer::timeout,this,&MyOpenGL::onTimerUpdate);
}
void MyOpenGL::releaseInstance(){
if (s_instance != NULL)
{
delete s_instance;
}
s_instance = NULL;
}
MyOpenGL* MyOpenGL::GetInstance()
{
return s_instance;
}
void MyOpenGL::onTimerUpdate()
{
update();
}
void MyOpenGL::mouseReleaseCallBack(float x,float y){
if(!isInitializeGL){
return;
}
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(x,y);
update();
}
void MyOpenGL::mousePressCallBack(float x,float y){
if(!isInitializeGL){
return;
}
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(x,y);
}
void MyOpenGL::mouseMoveCallBack(float x,float y){
if(!isInitializeGL){
return;
}
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(x,y);
update();
}
void MyOpenGL::initializeGL(){
LAppDelegate::GetInstance()->Initialize(this);
HINSTANCE hInstance = GetModuleHandle(NULL);
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, hInstance, 0);
isInitializeGL = true;
}
void MyOpenGL::resizeGL(int w, int h){
LAppDelegate::GetInstance()->resize(w,h);
}
void MyOpenGL::paintGL(){
LAppDelegate::GetInstance()->update();
}
void MyOpenGL::mouseMoveEvent(QMouseEvent*e){
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(e->x(),e->y());
update();
e->ignore();
}
void MyOpenGL::mousePressEvent(QMouseEvent*e){
if(e->button() == Qt::LeftButton)
{
isPressed = true;
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(e->x(),e->y());
}
e->ignore();
}
void MyOpenGL::mouseReleaseEvent(QMouseEvent*e){
if(e->button() == Qt::LeftButton)
{
isPressed = false;
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(e->x(), e->y());
update();
}
e->ignore();
}
2、鼠标穿透
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QGuiApplication>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setGeometry(QGuiApplication::primaryScreen()->availableGeometry());
// 一定要先设置鼠标穿透,否则无法穿透,应该是属性中间有值影响
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
//setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool);
myopengl = new MyOpenGL(this);
setCentralWidget(myopengl);
}
MainWindow::~MainWindow()
{
delete ui;
}
3、运行效果
四、补充:坐标变换
当窗口大小是远小于屏幕大小的时候,需要转换鼠标坐标与窗口坐标,
void MyOpenGL::mouseMoveCallBack(float x,float y){
// 模型坐标
QRect rect = this->rect();
// window point
int renderTargetWidth = this->width();
int renderTargetHeight = this->height();
// mouse point
long posx = x;
long posy = y;
//
long myWindowWidth = GetSystemMetrics(SM_CXSCREEN);
long myWindowHeight = GetSystemMetrics(SM_CYSCREEN);
//::GetWindowRect(_hw, &rect);
long rectX = rect.left() + renderTargetWidth * 0.5;
long rectY = rect.top() + renderTargetHeight * 0.5;
if (x <= rectX)
{
posx = LONG(((float)posx / (float)rectX)*((float)renderTargetWidth * 0.5));
}
else {
posx -= rectX;
posx = LONG(((float)posx / (float)(myWindowWidth - rectX))*((float)renderTargetWidth * 0.5));
posx += LONG((float)renderTargetWidth * 0.5);
}
if (posy <= rectY)
{
posy = LONG(((float)posy / (float)rectY)*((float)renderTargetHeight * 0.5));
}
else {
posy -= rectY;
posy = LONG(((float)posy / (float)(myWindowHeight - rectY))*((float)renderTargetHeight * 0.5));
posy += LONG((float)renderTargetHeight * 0.5);
}
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(posx,posy);
update();
}
#include "live2d/LAppDelegate.hpp"
#include "live2d/LAppView.hpp"
#include "live2d/LAppPal.hpp"
#include "live2d/LAppLive2DManager.hpp"
#include "live2d/LAppDefine.hpp"
#include "myopengl.h"
namespace {
MyOpenGL* s_instance = NULL;
}
DWORD lastCallbackTime = 0;
DWORD THROTTLE_INTERVAL = (1.0/30.0)*1000;
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
DWORD currentTime = GetTickCount(); // 获取当前时间戳
if (currentTime - lastCallbackTime >= THROTTLE_INTERVAL) { // 检查是否满足节流间隔
if (nCode >= 0) {
MSLLHOOKSTRUCT* pMouseStruct = (MSLLHOOKSTRUCT*)lParam;
// 处理鼠标移动事件
float x = pMouseStruct->pt.x;
float y = pMouseStruct->pt.y;
//qDebug() << "Mouse position: (" << x << ", " << y << ")";
switch (wParam)
{
case WM_LBUTTONDOWN: // 鼠标左键按下
MyOpenGL::GetInstance()->mousePressCallBack(x,y);
break;
case WM_LBUTTONUP: // 鼠标左键抬起
MyOpenGL::GetInstance()->mouseReleaseCallBack(x,y);
break;
case WM_MOUSEMOVE: // 鼠标移动
MyOpenGL::GetInstance()->mouseMoveCallBack(x,y);
break;
}
}
lastCallbackTime = currentTime; // 更新上次触发回调函数的时间戳
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
MyOpenGL::MyOpenGL(QWidget *parent)
:QOpenGLWidget{parent} {
// 60 fps
initFpsTimer();
setMouseTracking(true);
if (s_instance == NULL)
{
s_instance = this;
}
}
MyOpenGL::~MyOpenGL()
{
UnhookWindowsHookEx(mouseHook);
LAppDelegate::GetInstance()->Release();
LAppDelegate::ReleaseInstance();
releaseInstance();
}
void MyOpenGL::initFpsTimer(){
fpsTimer = new QTimer(this);
fpsTimer->start((1.0/30.0)*1000);
connect(fpsTimer,&QTimer::timeout,this,&MyOpenGL::onTimerUpdate);
}
void MyOpenGL::releaseInstance(){
if (s_instance != NULL)
{
delete s_instance;
}
s_instance = NULL;
}
MyOpenGL* MyOpenGL::GetInstance()
{
return s_instance;
}
void MyOpenGL::onTimerUpdate()
{
update();
}
void MyOpenGL::mouseReleaseCallBack(float x,float y){
if(!isInitializeGL){
return;
}
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(x,y);
update();
}
void MyOpenGL::mousePressCallBack(float x,float y){
if(!isInitializeGL){
return;
}
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(x,y);
}
// void MyOpenGL::mouseMoveCallBack(float x,float y){
// if(!isInitializeGL){
// return;
// }
// LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(x,y);
// update();
// }
void MyOpenGL::mouseMoveCallBack(float x,float y){
// 模型坐标
QRect rect = this->rect();
// window point
int renderTargetWidth = this->width();
int renderTargetHeight = this->height();
// mouse point
long posx = x;
long posy = y;
//
long myWindowWidth = GetSystemMetrics(SM_CXSCREEN);
long myWindowHeight = GetSystemMetrics(SM_CYSCREEN);
//::GetWindowRect(_hw, &rect);
long rectX = rect.left() + renderTargetWidth * 0.5;
long rectY = rect.top() + renderTargetHeight * 0.5;
if (x <= rectX)
{
posx = LONG(((float)posx / (float)rectX)*((float)renderTargetWidth * 0.5));
}
else {
posx -= rectX;
posx = LONG(((float)posx / (float)(myWindowWidth - rectX))*((float)renderTargetWidth * 0.5));
posx += LONG((float)renderTargetWidth * 0.5);
}
if (posy <= rectY)
{
posy = LONG(((float)posy / (float)rectY)*((float)renderTargetHeight * 0.5));
}
else {
posy -= rectY;
posy = LONG(((float)posy / (float)(myWindowHeight - rectY))*((float)renderTargetHeight * 0.5));
posy += LONG((float)renderTargetHeight * 0.5);
}
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(posx,posy);
update();
}
void MyOpenGL::initializeGL(){
LAppDelegate::GetInstance()->Initialize(this);
HINSTANCE hInstance = GetModuleHandle(NULL);
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, hInstance, 0);
isInitializeGL = true;
}
void MyOpenGL::resizeGL(int w, int h){
LAppDelegate::GetInstance()->resize(w,h);
}
void MyOpenGL::paintGL(){
LAppDelegate::GetInstance()->update();
}
void MyOpenGL::mouseMoveEvent(QMouseEvent*e){
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(e->x(),e->y());
update();
e->ignore();
}
void MyOpenGL::mousePressEvent(QMouseEvent*e){
if(e->button() == Qt::LeftButton)
{
isPressed = true;
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(e->x(),e->y());
}
e->ignore();
}
void MyOpenGL::mouseReleaseEvent(QMouseEvent*e){
if(e->button() == Qt::LeftButton)
{
isPressed = false;
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(e->x(), e->y());
update();
}
e->ignore();
}
效果如下,
五、补充:屏幕自适应
当窗口大小等于屏幕大小时,窗口需要自适应屏幕分辨率与放缩。Qt 5.6 版本之前,默认情况下是不开启 DPI 支持的,需要手动设置启用 ,并且需要手动计算放缩因子。从 Qt 5.6 版本开始,默认情况下,Qt 会自动开启 DPI(像素密度)支持。Qt 会根据系统的显示设置自动进行 DPI 缩放,以适应不同的屏幕密度和分辨率。
window 缩放因子 / 100% = logicalDotsPerInch * devicePixelRatio / 96
目前使用 QT6 做屏幕自适应方案,主窗口宽高为客户区的宽高(QGuiApplication::primaryScreen()->availableGeometry().size()),逻辑如下:
1、应用首次启动时,获取客户区屏幕的分辨率,作为参数初始化窗口宽高
2、connect 屏幕物料密度信号与槽函数,当屏幕大小、分辨率发生变化时,触发槽函数
3、槽函数获取当前客户区屏幕的分辨率,resize 窗口宽高
4、OpenGLWidget 会自适应窗口分辨率 => 主窗口宽高变化,触发 OpenGLWidget resizeGL
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "myopengl.h"
#include <QScreen>
#include <QGuiApplication>
#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onPhysicalDotsPerInchChanged(qreal dpi);
private:
Ui::MainWindow *ui;
MyOpenGL *myopengl = nullptr;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QGuiApplication>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setGeometry(QGuiApplication::primaryScreen()->availableGeometry());
// 一定要先设置鼠标穿透,否则无法穿透,应该是属性中间有值影响
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
//setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool);
myopengl = new MyOpenGL(this);
setCentralWidget(myopengl);
// 切换缩放比时,EnableHighDpiScaling 会触发 physicalDotsPerInchChanged
// 此时 Qt6 不触发 logicalDotsPerInchChanged
QScreen *screen = qApp->primaryScreen();
connect(screen,&QScreen::physicalDotsPerInchChanged,this,&MainWindow::onPhysicalDotsPerInchChanged);
}
void MainWindow::onPhysicalDotsPerInchChanged(qreal dpi)
{
resize(QGuiApplication::primaryScreen()->availableGeometry().size());
}
MainWindow::~MainWindow()
{
delete ui;
}
效果如下,
当主窗口宽高完全为屏幕宽高时(QGuiApplication::primaryScreen()->size()),这会出现一个问题(在虚拟机中不会,仅在物理机中出现):在 100% 放缩因子下,主窗口透明背景为黑屏,而 OpenGLWidget 正常渲染模型。此时考虑了是主窗口与 OpenGLWidget 的宽高关系,实际上,尝试了:
1、主窗口宽高 = 屏幕宽高,OpenGLWidget 宽高 = 屏幕宽高各减一,结果 fail
2、主窗口宽高 > 屏幕宽高,OpenGLWidget 宽高 = 屏幕宽高,结果 success
3、主窗口宽高 < 屏幕宽高,OpenGLWidget 宽高 = 屏幕宽高,结果 success
最终得出这么一个方案,将主窗口宽高设为屏幕宽高 +-2,然后 (x,y) 偏移 +-1,使得主窗口居中,然后 OpenGLWidget 就比较随意设置了,都不会导致主屏幕透明背景黑屏,
void MainWindow::initWindowSettings(){
// ...
setGeometry(QRect(-1,-1,QGuiApplication::primaryScreen()->size().width() + 2, QGuiApplication::primaryScreen()->size().height() + 2));
// ...
}
void MainWindow::onPhysicalDotsPerInchChanged(qreal dpi)
{
// ...
resize(QGuiApplication::primaryScreen()->size().width() + 2, QGuiApplication::primaryScreen()->size().height() + 2);
// ...
}
void MyOpenGL::resizeGL(int w, int h){
LAppDelegate::GetInstance()->resize(QGuiApplication::primaryScreen()->geometry().width(),QGuiApplication::primaryScreen()->geometry().height());
}
效果如下,