目录导读
-
- 前言
- 涉及内容
- 具体实现
-
- 通过WInApi获取点击屏幕坐标
- 根据鼠标点位置,生成周围的随机离散点
- 通过OPencv的Subdiv2D类生成Voronoi图
- 根据Voronoi图点集合,生成所有碎片,并平铺到界面上
- 重写GraphicsItem图元控件实现了碎片的不规则形状和绘图。
- [使用QPropertyAnimation 实现动画沿鼠标点向外延伸和掉落动画](#使用QPropertyAnimation 实现动画沿鼠标点向外延伸和掉落动画)
- 效果展示
- 总结
-
- [QOpenGLWidget 的使用优化](#QOpenGLWidget 的使用优化)
前言
在以前玩过一个破解版的扫雷小游戏,当扫雷失败的时候,炸弹会弹出来把屏幕界面炸的粉碎,那界面动画相当出色,记忆尤新,每当我玩了一把win10版的扫雷后,我开始想这个电脑屏幕的碎片效果,用Qt来实现应该怎么实现,
于是偶尔抽点时间尝试了一下,实现了一个阉割版的示例。
之所以说是阉割版的,就是这碎片的效果不达标,完全没有屏幕玻璃碎片的感觉,但是还是实现了鼠标点击屏幕,屏幕碎开,然后掉落的一系列效果。
就像这样:
鼠标点击屏幕任意位置,屏幕直接碎开,由鼠标点击位置像外蔓延碎片并且掉落...
每块碎片只是简单的加了层阴影,显得不是太立体。
涉及内容
整个示例的效果主要是通过:
-
使用WIN API 通过鼠标钩子获取到鼠标点击的屏幕位置 。
在一开始软件启动时,是隐藏窗体的,直到第一次点击桌面后,获取到点击的坐标,然后软件界面覆盖整个电脑屏幕。
-
使用Opencv 的Subdiv2D类 根据鼠标点周围辐射点生成Voronoi图 ,实现碎片的网状蔓延效果。
这个功能主要参考OpenCV Subdiv2D 平面细分这篇文章中的说明,
再使用cv::intersectConvexConvex 方法对所有Voronoi图点集进行一个凸集求交集,移除屏幕外的碎片。
-
通过QGraphicsView控件 和QGraphicsItem图元 ,实现一块块的绘图碎片。
一开始我是用QFrame控件 和setMask方法 实现碎片的绘图效果,但是后面发现碎片数量过多时,再加上动画效果后,界面就开始卡顿了,还是改成了QGraphicsView控件 使用QOpenGLWidget控件优化了界面刷新。
-
再使用QPropertyAnimation 实现碎片的平移和掉落效果。
使用QPropertyAnimation 类实现setPos()方法 的动画效果,QGraphicsItem图元 不是QObject对象 ,在实现的时候还得优先继承QObject对象;
开发环境: Windows系统 Qt Creator 5.15.2 MSCV2019 X64
整个示例涉及的技术大体就这些内容,拆开来并不复杂。
具体实现
通过WInApi获取点击屏幕坐标
使用SetWindowsHookEx 和 UnhookWindowsHookEx 函数熟悉鼠标钩子,在点击屏幕后显示整个软件遮住整个电脑屏幕;
cpp
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图点集
cpp
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 图点集合
cpp
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图点集合,生成所有碎片,并平铺到界面上
cpp
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 继承了QGraphicsPolygonItem 和QObject ,通过重写 shape()
和 paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget )
限制了绘图区域:
cpp
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元属性封装下:
cpp
//! 自定义元属性
Q_PROPERTY(QPoint orientation READ getOrientation WRITE SetOrientation)
QPoint getOrientation(){return orientation;}
void SetOrientation(QPoint _orientation){orientation=_orientation; setPos(_orientation);}
在设置由 鼠标点到碎片中心向外延伸随机5像素 的动画效果:
cpp
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);
}
在设置 向下掉落 动画:
cpp
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)
配合使用,
具体优化项:
cpp
QOpenGLWidget* widget=new QOpenGLWidget();
// 在设置 QOpenGLWidget 后,强制启用 VSync
QSurfaceFormat format;
format.setSwapInterval(1); // 1 = 启用 VSync,0 = 禁用
widget->setFormat(format);
setViewport(widget); // 关键步骤:启用 GPU 加速
setViewportUpdateMode(QGraphicsView::FullViewportUpdate); // 强制全屏缓冲(减少闪烁)
到此完!