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

QT-UI 轮播窗口

目标 实现一个像网易云音乐播放器一样的卡片轮播窗口

1.分析UI布局

垂直布局,该布局主要分为两个窗口,上面是显示图片的滚动窗口,下面是显示图片当前索引的索引窗口,明显上面的窗口需要占据剩余的所有空间,所以主布局是一个垂直布局,滚动窗口只是一个窗口没有任何布局,导航条窗口是水平布局,负责管理所有的导航点

void CoverFlowWidget::BuildUI()
{// 创建根布局 垂直布局QVBoxLayout* root = new QVBoxLayout(this);// 创建滚动窗口scroll_ = new QWidget(this);scroll_->setMouseTracking(true);scroll_->resize(780, 220);root->addWidget(scroll_, 1);                                                                    // 占据除去索引框的所有空间// 创建圆点导航条(水平布局)dotBar_ = new QWidget(this);dotBar_->setMouseTracking(true);QHBoxLayout* dotLayout = new QHBoxLayout(dotBar_);                                                  dotLayout->setContentsMargins(0, 0, 0, 0);dotLayout->setSpacing(10);root->addWidget(dotBar_, 0, Qt::AlignHCenter);                                                  // 索引框在水平居中
}

1.1 滚动窗口(图片绘制)

重点在于图片位置的绘制,只会展示三张图片,那么该如何绘制这种近大远小的感觉呢?

也就是在整体的窗口大小确定时计算出三张图片在窗口中的布局显示

也就是三个矩形的位置

也就是下面这种感觉

图片堆叠的效果如下

1.1.1 确定矩形框位置和大小

首先要根据整个窗口的大小来确认

左右两侧预留够足够大小的宽度,上下两侧预留够足够大小的宽度,最后得到的是中心图片的宽度

根据中心图层的大小获取到侧边图片的大小,然后根据这三个图片的大小获取对应的矩形位置

void CoverFlowWidget::updateRects() 
{// 计算中心与侧边大小与位置(保持上下留白)const int Width = scroll_->width(), Hight = scroll_->height();const int margin = 12;// 让左右各露出固定宽度(更接近参考图)const int peek = qRound(Width * 0.17);                                                              // 左右各露出 ~17%(可调)// 中心尺寸:整体宽度扣掉左右露出szCenter_ = QSizeF(Width - 2 * peek, qMin(220, Hight - 2 * margin));// 侧边比例(略小于中心)const qreal sideScale = 0.78;                                                                       // 可调 0.74~0.82szSide_ = QSizeF(szCenter_.width() * sideScale, szCenter_.height() * sideScale);rcCenter_ = QRectF((Width - szCenter_.width()) / 2.0,(Hight - szCenter_.height()) / 2.0,szCenter_.width(), szCenter_.height());rcLeft_ = QRectF(LEFT_MARGIN,(Hight - szSide_.height()) / 2.0,szSide_.width(), szSide_.height());rcRight_ = QRectF(Width - szSide_.width() - RIGHT_MARGIN,(Hight - szSide_.height()) / 2.0,szSide_.width(), szSide_.height());
}

1.1.2 设置过渡动画

在动画里进行实时的绘制,动画开启时会伴随着数值的改变,其实就是虚幻中的线性插值,从0到1进行插值,插值的时间就是动画的速度罢了,当动画结束后重置图片索引即可

anim_ = new QVariantAnimation(this);
anim_->setDuration(320);
anim_->setStartValue(0.0);
anim_->setEndValue(1.0);connect(anim_, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) {t_ = v.toReal(); update();});connect(anim_, &QVariantAnimation::finished, this, [this] {// 动画结束后,落位为新的中心center_ = (center_ + (dir_ > 0 ? 1 : -1) + slides_.size()) % slides_.size();t_ = 0.0; update();});

什么时候进行一次动画的播放是用定时器控制的,当然这是自动轮播

connect(&timer_, &QTimer::timeout, this, [this]{if (!hovering_) next();});void CoverFlowWidget::next() 
{if (slides_.size() <= 1 || anim_->state() == QAbstractAnimation::Running) return;dir_ = +1; anim_->stop(); anim_->setStartValue(0.0); anim_->setEndValue(1.0); anim_->start();
}

1.1.3 绘制UI

观察下面的绘制可以大致推断一下原理也就是说,每一帧都会绘制一张图片,而每一张图片都会带有一些偏移,这里如果不清除掉之前绘制的图片,就很容易导致出现残影的效果

所以首先对画笔进行设置

QPainter p(this);
p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, true);p.setCompositionMode(QPainter::CompositionMode_Source);                                                     // 覆盖目标区域
p.fillRect(rect(), Qt::transparent);                                                                        // 清空画布
p.setCompositionMode(QPainter::CompositionMode_SourceOver);                                                 // 与背景自然融合

然后获取要绘制的三张图片,由于我们的轮播是从左到右进行的,而center_就是中间那张图片的索引

const int n = slides_.size();                                                                               // 获取图片的大小
int idxC = center_;                                                                                         // 当前图片
int idxL = (center_ - 1 + n) % n;                                                                           // 左边那一张
int idxR = (center_ + 1) % n;                                                                               // 右边那一张

图片移动的感觉实际上是每一帧进行绘制不同位置和大小的图片造成的视觉效果,就像翻小人书一样

得到动画中矩形的位置,目前最左边的矩形位置是不变的,中间的图片往左边偏移,右边的图片往中间进行偏移

偏移开始就是动画开始的时候,动画开始的每一帧都进行图片位置的计算

// 计算动画中的矩形(dir_ 决定谁往中心、谁往侧边)
QRectF rcC = rcCenter_, rcToSide, rcToCenter, rcStatic;                                                     // 创建三个矩形,分别对应“去侧边”、“到中心”、“静态”
int    idxToSide = idxC, idxToCenter = (dir_ > 0 ? idxR : idxL), idxStatic = (dir_ > 0 ? idxL : idxR);      // 获取对应的图片索引(左边的图片是保持不动的)// 然后根据起始坐标和结束坐标做线性插值,得到动画中的矩形坐标
if (dir_ > 0) 
{             // 向右:中心 -> 左;右 -> 中心;左保持rcToSide = lerp(rcCenter_, rcLeft_, t_);rcToCenter = lerp(rcRight_, rcCenter_, t_);rcStatic = rcLeft_;
}
else 
{                   // 向左:中心 -> 右;左 -> 中心;右保持rcToSide = lerp(rcCenter_, rcRight_, t_);rcToCenter = lerp(rcLeft_, rcCenter_, t_);rcStatic = rcRight_;
}

线性插值

QRectF CoverFlowWidget::lerp(const QRectF& a, const QRectF& b, qreal t) const 
{return QRectF(a.x() + (b.x() - a.x()) * t,a.y() + (b.y() - a.y()) * t,a.width() + (b.width() - a.width()) * t,a.height() + (b.height() - a.height()) * t);
}

绘制UI

首先基础知识是后绘制的UI在先绘制的UI之前

最后的三张图片,实际上就是三个圆角矩形框,只需要创建绘制路径,然后将上面得到的矩形的起点拿到,最后根据图片索引获取到图片直接进行绘制即可

auto drawRounded = [&](const QPixmap& pm, const QRectF& r){QPainterPath path;                                                                                              // 创建圆角矩形路径path.addRoundedRect(r, radius_, radius_);                                                                       // 画圆角矩形p.save();p.setClipPath(path);                                                                                            // 只在当前区域进行绘制// 简单的“裁切铺满”效果(保持比例裁剪)QPixmap scaled = pm.scaled(r.size().toSize(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);        // 缩放图片p.drawPixmap(QPointF(r.x(), r.y()), scaled);p.restore();};// 先画静态侧边(在底层)
drawRounded(slides_[idxStatic], rcStatic);// 再画“去侧边”的(在中层)
drawRounded(slides_[idxToSide], rcToSide);// 最后画“到中心”的(在顶层,保证中心遮住其它)
drawRounded(slides_[idxToCenter], rcToCenter);

最后在动画没有启动时也就是一开始也会进行UI的绘制

// 未在动画中时,直接三张:左、右、中心(中心最后画)
if (!anim_->state()) {drawRounded(slides_[idxL], rcLeft_);drawRounded(slides_[idxR], rcRight_);drawRounded(slides_[idxC], rcCenter_);
}

完整代码如下

void CoverFlowWidget::paintEvent(QPaintEvent*) 
{QPainter p(this);p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, true);p.setCompositionMode(QPainter::CompositionMode_Source);                                                     // 覆盖目标区域p.fillRect(rect(), Qt::transparent);                                                                        // 清空画布p.setCompositionMode(QPainter::CompositionMode_SourceOver);                                                 // 与背景自然融合if (slides_.isEmpty()) return;const int n = slides_.size();                                                                               // 获取图片的大小int idxC = center_;                                                                                         // 当前图片int idxL = (center_ - 1 + n) % n;                                                                           // 左边那一张int idxR = (center_ + 1) % n;                                                                               // 右边那一张// 计算动画中的矩形(dir_ 决定谁往中心、谁往侧边)QRectF rcC = rcCenter_, rcToSide, rcToCenter, rcStatic;                                                     // 创建三个矩形,分别对应“去侧边”、“到中心”、“静态”int    idxToSide = idxC, idxToCenter = (dir_ > 0 ? idxR : idxL), idxStatic = (dir_ > 0 ? idxL : idxR);      // 获取对应的图片索引(左边的图片是保持不动的)// 然后根据起始坐标和结束坐标做线性插值,得到动画中的矩形坐标if (dir_ > 0) {             // 向右:中心 -> 左;右 -> 中心;左保持rcToSide = lerp(rcCenter_, rcLeft_, t_);rcToCenter = lerp(rcRight_, rcCenter_, t_);rcStatic = rcLeft_;}else {                   // 向左:中心 -> 右;左 -> 中心;右保持rcToSide = lerp(rcCenter_, rcRight_, t_);rcToCenter = lerp(rcLeft_, rcCenter_, t_);rcStatic = rcRight_;}auto drawRounded = [&](const QPixmap& pm, const QRectF& r){QPainterPath path;                                                                                              // 创建圆角矩形路径path.addRoundedRect(r, radius_, radius_);                                                                       // 画圆角矩形p.save();p.setClipPath(path);                                                                                            // 只在当前区域进行绘制// 简单的“裁切铺满”效果(保持比例裁剪)QPixmap scaled = pm.scaled(r.size().toSize(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);        // 缩放图片p.drawPixmap(QPointF(r.x(), r.y()), scaled);p.restore();};// 先画静态侧边(在底层)drawRounded(slides_[idxStatic], rcStatic);// 再画“去侧边”的(在中层)drawRounded(slides_[idxToSide], rcToSide);// 最后画“到中心”的(在顶层,保证中心遮住其它)drawRounded(slides_[idxToCenter], rcToCenter);// 未在动画中时,直接三张:左、右、中心(中心最后画)if (!anim_->state()) {drawRounded(slides_[idxL], rcLeft_);drawRounded(slides_[idxR], rcRight_);drawRounded(slides_[idxC], rcCenter_);}
}

1.2 滚动窗口(自定义标签)

由于绘制的图片不支持点击状态

所以这里创建一个可支持标签,通过标签的缩放和移动来模拟绘制

1.2.1 标签的移动

确认矩形框的位置和大小,依据轮播的图片进行缩放并重新设置到标签中,标签在进行位置的移动

完整的代码如下

void CoverFlowWidget::startAnimation()
{for (FlowLabel* label : labels_) label->hide();const int n = slides_.size();                                                                               // 获取图片的大小if(n < 3) return;                                                                                           // 至少需要三个图片才能旋转int idxC = center_;                                                                                         // 当前图片int idxL = (center_ - 1 + n) % n;                                                                           // 左边那一张int idxR = (center_ + 1) % n;                                                                               // 右边那一张// 计算动画中的矩形(dir_ 决定谁往中心、谁往侧边)QRectF rcC = rcCenter_, rcToSide, rcToCenter, rcStatic;                                                     // 创建三个矩形,分别对应“去侧边”、“到中心”、“静态”int    idxToSide = idxC, idxToCenter = (dir_ > 0 ? idxR : idxL), idxStatic = (dir_ > 0 ? idxL : idxR);      // 获取对应的图片索引(左边的图片是保持不动的)// 然后根据起始坐标和结束坐标做线性插值,得到动画中的矩形坐标if (dir_ > 0){// 向右:中心 -> 左;右 -> 中心;左保持rcToSide = lerp(rcCenter_, rcLeft_, t_);rcToCenter = lerp(rcRight_, rcCenter_, t_);rcStatic = rcLeft_;}else{// 向左:中心 -> 右;左 -> 中心;右保持rcToSide = lerp(rcCenter_, rcRight_, t_);rcToCenter = lerp(rcLeft_, rcCenter_, t_);rcStatic = rcRight_;}// 画圆角矩形auto drawRounded = [&](const int idx, const QRectF& r){FlowLabel* label = labels_[idx];QPixmap pm = slides_[idx];QPixmap scaled = pm.scaled(r.size().toSize(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);        // 缩放图片label->setMaximumSize(r.size().toSize());label->setPixmap(scaled);//label->move(QPoint(r.x(), r.y()));label->setGeometry(r.toRect());label->show();label->raise();};// 先画静态侧边(在底层)drawRounded(idxStatic, rcStatic);// 再画“去侧边”的(在中层)drawRounded(idxToSide, rcToSide);// 最后画“到中心”的(在顶层,保证中心遮住其它)drawRounded(idxToCenter, rcToCenter);// 未在动画中时,直接三张:左、右、中心(中心最后画)if (!anim_->state()) {drawRounded(idxL, rcLeft_);drawRounded(idxR, rcRight_);drawRounded(idxC, rcCenter_);}
}

1.3 创建导航栏

当图片素材被导入时索引点也就同时添加进来了,通过更新按钮的状态来设置按钮的选中状态,所以在图片的每一次过渡动画完结的时候就可以更新按钮的状态了,如下图所示,当索引点被点击时会立即更新当前图片的索引,然后根据当前图片的索引重新计算位置即可

导航栏的代码

static QString dotStytle(R"(QPushButton {background-color: white;border-radius: 5px;}QPushButton[selected=true] {background-color: red;}QPushButton[selected=false] {background-color: white;}
)");connect(anim_, &QVariantAnimation::finished, this, [this]{// 动画结束后,落位为新的中心UpdateDotState(dots_[center_], false);center_ = (center_ + (dir_ > 0 ? 1 : -1) + slides_.size()) % slides_.size();t_ = 0.0; /*update();*/ startAnimation();// 更新下一帧索引UpdateDotState(dots_[center_], true);void CoverFlowWidget::LoadSlide()
{for (int i = 1; i <= 10; ++i){addSlide(QPixmap(QString(":/Images/picturewall/%1.png").arg(i)));}
}void CoverFlowWidget::addSlide(const QPixmap& p) 
{slides_.push_back(p);FlowLabel* label = new FlowLabel(this);label->setPixmap(p);labels_.push_back(label);updateRects();//update();startAnimation();// 添加索引QPushButton* dot = new QPushButton(dotBar_);dot->setFixedSize(10, 10);dot->setStyleSheet(dotStytle);dot->setCursor(Qt::PointingHandCursor);dots_.push_back(dot);dotBar_->layout()->addWidget(dot);UpdateDotState(dots_[center_], true);connect(dot, &QPushButton::clicked, this, [this, dot]{int idx = dots_.indexOf(dot);if (idx == center_) return;UpdateDotState(dots_[center_], false);dir_ = (idx > center_ ? 1 : -1);center_ = idx;UpdateDotState(dots_[center_], true);t_ = 0.0; startAnimation();});
}

1.4 创建左右按钮

这两个按钮需要实时的处于最上层,也就是说,对标签进行移动时,最后要进行按钮的绘制

btnPrev_ = new QPushButton(scroll_);
btnPrev_->setFixedSize(20, 20);
btnPrev_->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft));
btnPrev_->setCursor(Qt::PointingHandCursor);
btnPrev_->raise();
btnNext_ = new QPushButton(scroll_);
btnNext_->setFixedSize(20, 20);
btnNext_->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
btnNext_->setCursor(Qt::PointingHandCursor);
btnNext_->raise();connect(btnPrev_, &QPushButton::clicked, this, [this] { prev(); });
connect(btnNext_, &QPushButton::clicked, this, [this] { next(); });

当导航栏添加完所有的dot时,这时会在过一段时间后触发布局的调整,但是滚动区域的布局也会随机的占据剩余分配的空间,这里要在调整了之后立即触发布局调整,布局调整完毕后,将按钮挪到指定的位置

dotBar_->updateGeometry();
layout()->invalidate();
layout()->activate();                                                                           // 强制布局重新计算
updateRects();
btnPrev_->move(CalcuBtnPos(true));
btnNext_->move(CalcuBtnPos(false));

计算按钮的位置

QPoint CoverFlowWidget::CalcuBtnPos(bool isLeft)
{if (isLeft) return QPoint(LEFT_MARGIN, (height() - 20) / 2);else return QPoint(scroll_->width() - RIGHT_MARGIN - 20, (height() - 20) / 2);
}

2. 完结

这样一个滚动窗口就做好了

最终演示效果如下


文章转载自:

http://04JalZ2f.rzbcz.cn
http://oh1Ctf3E.rzbcz.cn
http://2NrRt1Ru.rzbcz.cn
http://PGpORqyU.rzbcz.cn
http://Lx0AOJmw.rzbcz.cn
http://vBTwth5L.rzbcz.cn
http://EJ2a5fMG.rzbcz.cn
http://N4h69y6n.rzbcz.cn
http://Uw9h6a5Q.rzbcz.cn
http://sFbKZ0eP.rzbcz.cn
http://Lp0ovGOw.rzbcz.cn
http://48Ixfg2e.rzbcz.cn
http://9wo44F1m.rzbcz.cn
http://MaW5MaAp.rzbcz.cn
http://5mwtbJMy.rzbcz.cn
http://BOebRZSj.rzbcz.cn
http://v1MG69oC.rzbcz.cn
http://lyAhDbCf.rzbcz.cn
http://pnEciomB.rzbcz.cn
http://nF63zTPU.rzbcz.cn
http://ci91F3d7.rzbcz.cn
http://IVz6rMbW.rzbcz.cn
http://aoQCnNhu.rzbcz.cn
http://vFkrWE1Q.rzbcz.cn
http://AjKjD2D4.rzbcz.cn
http://nGGwSRBW.rzbcz.cn
http://4W0wchc2.rzbcz.cn
http://nsrJeSBs.rzbcz.cn
http://lTxJuNRF.rzbcz.cn
http://QZVBfUgl.rzbcz.cn
http://www.dtcms.com/a/388463.html

相关文章:

  • Nginx动静分离实验步骤
  • 硬件驱动——I.MX6ULL裸机启动(7)(ADC相关设置)
  • 重读生成概率模型1----基础概念
  • File (文件)• Open (打开)•
  • DNS 服务原理与部署实战:从基础到主从架构搭建
  • 《黑夜君临》网络测试:XSX表现优于PS5及PS5 Pro
  • HDLBits-移位寄存器
  • C++宽度优先搜索算法(BFS算法):FloodFill问题模型
  • ThreadLocal 的工作原理
  • Windows 11 下载安装 CosyVoice2,一键启动
  • 《Vuejs设计与实现》第 16 章(解析器) 下
  • JavaSE——图书系统项目
  • PHP 中 Class 的使用说明
  • Android入门到实战(九):实现书架页——RecyclerView + GridLayoutManager + 本地数据库
  • 日常开发-20250917
  • 基于SpringBoot+Vue的近郊农场共享管理系统(Echarts图形化分析)
  • AI开发实战:从数据准备到模型部署的完整经验分享
  • 【漏洞预警】大华DSS数字监控系统 user_edit.action 接口敏感信息泄露漏洞分析
  • RFID赋能光伏电池片制造智能化跃迁
  • 大数据 + 分布式架构下 SQL 查询优化:从核心技术到调优体系
  • FPGA硬件设计-DDR
  • 卫星通信天线的跟踪精度,含义、测量和计算
  • 忘记MySQL root密码,如何急救并保障备份?
  • Java 异步编程实战:Thread、线程池、CompletableFuture、@Async 用法与场景
  • 贪心算法应用:硬币找零问题详解
  • while语句中的break和continue
  • 10cm钢板矫平机:一场“掰直”钢铁的微观战争
  • Python实现计算点云投影面积
  • C++底层刨析章节二:迭代器原理与实现:STL的万能胶水
  • 学习Python中Selenium模块的基本用法(14:页面打印)