当前位置: 首页 > ds >正文

QT示例 基于Subdiv2D的Voronoi图实现鼠标点击屏幕碎裂掉落特效

目录导读

    • 前言
    • 涉及内容
    • 具体实现
      • 通过WInApi获取点击屏幕坐标
      • 根据鼠标点位置,生成周围的随机离散点
      • 通过OPencv的Subdiv2D类生成Voronoi图
      • 根据Voronoi图点集合,生成所有碎片,并平铺到界面上
      • 重写GraphicsItem图元控件实现了碎片的不规则形状和绘图。
      • 使用QPropertyAnimation 实现动画沿鼠标点向外延伸和掉落动画
    • 效果展示
    • 总结
      • QOpenGLWidget 的使用优化

前言

在以前玩过一个破解版的扫雷小游戏,当扫雷失败的时候,炸弹会弹出来把屏幕界面炸的粉碎,那界面动画相当出色,记忆尤新,每当我玩了一把win10版的扫雷后,我开始想这个电脑屏幕的碎片效果,用Qt来实现应该怎么实现,
于是偶尔抽点时间尝试了一下,实现了一个阉割版的示例。
之所以说是阉割版的,就是这碎片的效果不达标,完全没有屏幕玻璃碎片的感觉,但是还是实现了鼠标点击屏幕,屏幕碎开,然后掉落的一系列效果。
就像这样:
请添加图片描述

鼠标点击屏幕任意位置,屏幕直接碎开,由鼠标点击位置像外蔓延碎片并且掉落…
每块碎片只是简单的加了层阴影,显得不是太立体。

涉及内容

整个示例的效果主要是通过:

  1. 使用WIN API通过鼠标钩子获取到鼠标点击的屏幕位置 。
    在一开始软件启动时,是隐藏窗体的,直到第一次点击桌面后,获取到点击的坐标,然后软件界面覆盖整个电脑屏幕。

  2. 使用OpencvSubdiv2D类根据鼠标点周围辐射点生成Voronoi图,实现碎片的网状蔓延效果。
    这个功能主要参考OpenCV Subdiv2D 平面细分这篇文章中的说明,
    再使用cv::intersectConvexConvex方法对所有Voronoi图点集进行一个凸集求交集,移除屏幕外的碎片。

  3. 通过QGraphicsView控件QGraphicsItem图元,实现一块块的绘图碎片。
    一开始我是用QFrame控件setMask方法实现碎片的绘图效果,但是后面发现碎片数量过多时,再加上动画效果后,界面就开始卡顿了,还是改成了QGraphicsView控件使用QOpenGLWidget控件优化了界面刷新。

  4. 再使用QPropertyAnimation实现碎片的平移和掉落效果。
    使用QPropertyAnimation类实现setPos()方法的动画效果,QGraphicsItem图元不是QObject对象,在实现的时候还得优先继承QObject对象

开发环境: Windows系统 Qt Creator 5.15.2 MSCV2019 X64
整个示例涉及的技术大体就这些内容,拆开来并不复杂。

具体实现

  • 通过WInApi获取点击屏幕坐标

使用SetWindowsHookExUnhookWindowsHookEx 函数熟悉鼠标钩子,在点击屏幕后显示整个软件遮住整个电脑屏幕;

HHOOK g_mouseHook=nullptr;
//! 鼠标按钮回调函数
static std::function<void(QPoint)> MouseCallBack=nullptr;
//! 鼠标事件
static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) {if (nCode >= 0) {if (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN) {MSLLHOOKSTRUCT* pMouseStruct = (MSLLHOOKSTRUCT*)lParam;qDebug() << "Mouse clicked at:" << pMouseStruct->pt.x << pMouseStruct->pt.y;if(MouseCallBack!=nullptr)MouseCallBack(QPoint(pMouseStruct->pt.x,pMouseStruct->pt.y));}}return CallNextHookEx(g_mouseHook, nCode, wParam, lParam);
}// 安装鼠标钩子
static void installMouseHook() {qDebug()<<"注册鼠标钩子!";g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, GetModuleHandle(nullptr), 0);if (!g_mouseHook) {qDebug() << "Failed to install mouse hook";}
}// 卸载鼠标钩子
static void uninstallMouseHook() {qDebug()<<"卸载鼠标钩子!";if (g_mouseHook) {UnhookWindowsHookEx(g_mouseHook);g_mouseHook = nullptr;}
}

MouseCallBack回调函数 就是点击鼠标后就直接显示软件,这种方式就不用考虑WinApi函数的静态方法回调的问题了,
只需要在实例化界面时给MouseCallBack回调函数赋值。

  • 根据鼠标点位置,生成周围的随机离散点

根据鼠标点,最大外接半径,生成点个数,以及屏幕的外接矩形,生成一系列的离散点,用于生成Voronoi图点集

static std::vector<cv::Point2f> generateRandomRadialPoints(QPoint center, int maxRadius, int n,QRect rectScope) {std::vector<cv::Point2f> points;std::random_device rd;std::mt19937 gen(rd());std::uniform_real_distribution<float> radiusDist(0, maxRadius);std::uniform_real_distribution<float> angleDist(0, 2 * CV_PI);for (int i = 0; i < n; ++i) {int radius = radiusDist(gen);int angle = angleDist(gen);int x = center.x() + radius * std::cos(angle);int y = center.y() + radius * std::sin(angle);//! 不再范围内 重新生成随机点if(!rectScope.contains(QPoint(x,y))){i--;continue;}points.emplace_back(x, y);}return points;
}
  • 通过OPencv的Subdiv2D类生成Voronoi图

初始化Subdiv2D类,生成Voronoi图的点集集合,
这个详细建议看上面的参考文章,介绍的很详细了,
std::vector<cv::Point2f> centers; //中心点
std::vector<std::vector<cv::Point2f>> facets; //Voronoi 图点集合

try{qDebug()<<"CreateVoronoiFacet -->";using namespace cv;        //!初始化Subdiv2D类Rect rect(0, 0, screenGeometry.width(), screenGeometry.height());// 2. 构建Voronoi图Subdiv2D subdiv(rect);std::vector<cv::Point2f> points;points=generateRandomRadialPoints(MouseChecked,qMax(screenGeometry.width()/2,screenGeometry.height()/2),centernumber,screenGeometry);//! 插入端点for (size_t i = 0; i < points.size(); i++){//! 排除不在范围内的点if(rect.contains(points[i])){subdiv.insert(points[i]);}}facets.clear();centers.clear();subdiv.getVoronoiFacetList(std::vector<int>(), facets, centers);qDebug()<<"getVoronoiFacetList over -->";}catch(...){qDebug()<<"未知错误";// 显示一个带有"是/否"选择的对话框QMessageBox::StandardButton reply;reply = QMessageBox::question(q_ptr, "提示", "Subdiv2D类未知错误 请重启尝试!",QMessageBox::Yes | QMessageBox::No);qApp->quit(); //!}
  • 根据Voronoi图点集合,生成所有碎片,并平铺到界面上

void QGraphicsViewResetPrivate::CreateQGraphicsItem()
{if(backDropImg.isNull())return;//!屏幕边框 用于裁剪界面内容std::vector<cv::Point2f> polyrect;polyrect.push_back(cv::Point2f(0,0));polyrect.push_back(cv::Point2f(screenGeometry.width(),0));polyrect.push_back(cv::Point2f(screenGeometry.width(),screenGeometry.height()));polyrect.push_back(cv::Point2f(0,screenGeometry.height()));//! 遍历切片for(int i=0;i<facets.size();i++){std::vector<cv::Point2f> points=facets[i];//! 判断是否需要求交集bool isneed=false;for(int j=0;j<points.size();j++){if(!screenGeometry.contains(QPoint(points[j].x,points[j].y))){isneed=true;break;}}//!求交集if(isneed)convexIntersection(points,polyrect);//! 转换坐标单位类QPolygon polygon;for(int j=0;j<points.size();j++){polygon.append(QPoint(points[j].x,points[j].y));}QRectF boundingRectf = polygon.boundingRect();QRect boundingRect(boundingRectf.x(),boundingRectf.y(),boundingRectf.width(),boundingRectf.height());//! 切片QPixmap cropimage=backDropImg.copy(boundingRect);//! 计算相对坐标,用于图元内部图型定型QPolygon relativepolygon;for(int j=0;j<polygon.size();j++){relativepolygon.append(polygon[j]-boundingRect.topLeft());}QGraphicsItemReset* item=new QGraphicsItemReset();item->SetData(relativepolygon,cropimage);item->setPos(boundingRect.topLeft());item->setZValue(1);q_ptr->scene()->addItem(item);double dx=qAbs(distanceBetweenPoints(QPoint(centers[i].x,centers[i].y),MouseChecked));int xm=dx*1;int t=dx*10;QTimer::singleShot(xm,[item](){//! 添加阴影 玻璃碎片感觉QGraphicsDropShadowEffect *shadow = new QGraphicsDropShadowEffect();shadow->setBlurRadius(10);      // 阴影模糊半径shadow->setColor(QColor(0, 0, 0,255)); // 阴影颜色shadow->setOffset(0);        // 阴影偏移量item->setGraphicsEffect(shadow);});moveFrameBetweenPoints(item,MouseChecked,QPoint(centers[i].x,centers[i].y),t+xm);dropout(item,t+xm+2000);}
}

在一开始不用给碎片阴影效果,而根据 QTimer::singleShot(碎片到鼠标点的距离时间) 开始添加阴影,这样就有了一个碎片向外蔓延的效果。

  • 重写GraphicsItem图元控件实现了碎片的不规则形状和绘图。

这里的QGraphicsItemReset继承了QGraphicsPolygonItemQObject,通过重写 shape()paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget )
限制了绘图区域:

QPainterPath QGraphicsItemReset::shape() const
{QPainterPath path;path.addPolygon(Posfacet);path.setFillRule(Qt::WindingFill);return path;
}void QGraphicsItemReset::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget )
{//! 设置背景图片// 高质量渲染设置组合painter->setRenderHints(QPainter::Antialiasing |QPainter::TextAntialiasing |QPainter::SmoothPixmapTransform );// 2. 保存当前画布状态(保护后续绘制不受裁剪影响)painter->save();// painter.drawRoundedRect(polygon.boundingRect(),5,5);QRegion reg(Posfacet);// 2. 设置遮罩(限制绘制区域)// painter->setClipRegion(reg,Qt::IntersectClip);QPainterPath clipPath;clipPath.addPolygon(Posfacet); // 使用当前多边形作为裁剪区域clipPath.setFillRule(Qt::WindingFill);painter->setClipPath(clipPath);if(!backImage.isNull())painter->drawPixmap(0,0,backImage);// 5. 恢复画布状态(关闭裁剪)painter->restore();
}

从而实现了碎片的不规则形状。

  • 使用QPropertyAnimation 实现动画沿鼠标点向外延伸和掉落动画

QGraphicsItem类 中的setPos()方法不是元属性不能直接应用动画,所以需要创建个QObject元属性封装下:

 	//! 自定义元属性Q_PROPERTY(QPoint orientation READ getOrientation WRITE SetOrientation)QPoint getOrientation(){return orientation;}void SetOrientation(QPoint _orientation){orientation=_orientation; setPos(_orientation);}

在设置由 鼠标点到碎片中心向外延伸随机5像素 的动画效果:

void QGraphicsViewResetPrivate::moveFrameBetweenPoints(QGraphicsItemReset* frame, const QPoint& pointA, const QPoint& pointB,int time)
{// 计算两点之间的方向向量QPointF direction = QPointF(pointB - pointA);qreal length = sqrt(direction.x() * direction.x() + direction.y() * direction.y());// 标准化方向向量并乘以5像素if (length > 0) {direction = (direction / length) * QRandomGenerator::global()->bounded(5);}// 创建动画对象QPropertyAnimation *animation = new QPropertyAnimation((QObject*)frame, "orientation");animation->setDuration(QRandomGenerator::global()->bounded(401));  // 动画持续时间(毫秒)animation->setStartValue(frame->pos());animation->setEndValue(frame->pos() + direction.toPoint());animation->start(QAbstractAnimation::DeleteWhenStopped);
}

在设置 向下掉落 动画:

 QTimer::singleShot(dx,[&,frame](){// 创建动画对象QPropertyAnimation *animation = new QPropertyAnimation((QObject*)frame, "orientation");QObject::connect(animation,&QPropertyAnimation::finished,[&,frame](){//! 掉出界面后移出场景q_ptr->scene()->removeItem(frame);});animation->setDuration(4000);  // 动画持续时间(毫秒)animation->setStartValue(frame->pos());animation->setEndValue(frame->pos() + screenGeometry.bottomLeft());animation->start(QAbstractAnimation::DeleteWhenStopped);});

这里还使用了一个 QTimer::singleShot 就是为了等待前面的动画完成后再开始掉落。
这样一来整个鼠标点击屏幕碎裂掉落的效果就完成了。

效果展示

整个效果展示:
请添加图片描述
看起来还是太粗糙,碎片的玻璃层次感还需要改改。

总结

总体上并不是太复杂,涉及的内容都有现成的方法,只是细节上很耗时间,比如碎片上的多边形图像截取绘制,图片点击时的碎片移动距离,甚至为了实现声效愣是找了一圈都没有找到相关的素材。
但值得注意的也有

  • QOpenGLWidget 的使用优化

在测试的时候即使添加了 QOpenGLWidget 优化界面,
在界面频繁刷新还是会闪烁,界面黑一下,
后来发现需要在main.cpp添加 QCoreApplication::setAttribute(Qt::AA_UseOpenGLES) 配合使用,
具体优化项:

QOpenGLWidget* widget=new QOpenGLWidget();
// 在设置 QOpenGLWidget 后,强制启用 VSync
QSurfaceFormat format;
format.setSwapInterval(1); // 1 = 启用 VSync,0 = 禁用
widget->setFormat(format);
setViewport(widget); // 关键步骤:启用 GPU 加速
setViewportUpdateMode(QGraphicsView::FullViewportUpdate); // 强制全屏缓冲(减少闪烁)

到此完!
请添加图片描述

http://www.xdnf.cn/news/18384.html

相关文章:

  • Day22 顺序表与链表的实现及应用(含字典功能与操作对比)
  • 服务器无公网ip如何对外提供服务?本地网络只有内网IP,如何能被外网访问?
  • Vue.prototype 的作用
  • JUC之CompletableFuture【中】
  • Redis Reactor 模型详解【基本架构、事件循环机制、结合源码详细追踪读写请求从客户端连接到命令执行的完整流程】
  • FPGA 在情绪识别领域的护理应用(一)
  • 论文阅读系列(一)Qwen-Image Technical Report
  • 中和农信如何打通农业科技普惠“最后一百米”
  • 企业架构是什么?解读
  • 通过分布式系统的视角看Kafka
  • python黑盒包装
  • Matplotlib数据可视化实战:Matplotlib图表注释与美化入门
  • 抓取手机游戏相关数据
  • LWIP流程全解
  • java实现url 生成二维码, 包括可叠加 logo、改变颜色、设置背景颜色、背景图等功能,完整代码示例
  • 【运维进阶】Ansible 角色管理
  • 记一次 .NET 某自动化智能制造软件 卡死分析
  • 流程进阶——解读 49页 2023 IBM流程管理与变革赋能【附全文阅读】
  • Redis缓存加速测试数据交互:从前缀键清理到前沿性能革命
  • 微服务-07.微服务拆分-微服务项目结构说明
  • 236. 二叉树的最近公共祖先
  • 从密度到聚类:DBSCAN算法的第一性原理解析
  • 100202Title和Input组件_编辑器-react-仿低代码平台项目
  • git 创用操作
  • 【集合框架LinkedList底层添加元素机制】
  • Python网络爬虫全栈教程 – 从基础到实战
  • 网络编程day4
  • 电商平台接口自动化框架实践
  • Codeforces 斐波那契立方体
  • 寻找旋转排序数组中的最小值