QT 实现电子相册(四)--实现播放动画

简介

前问提到了PicAnimationWid,PicButton,PreListWid等类,这里介绍如何实现这些类,并串联起来达到幻灯片放映的效果。

SlideShowDlg

在左侧目录树右击时弹出菜单,新增幻灯片播放的选项,点击后会弹出SlideShowDlg。SlideShowDlg的构造函数如下

  1. SlideShowDlg::SlideShowDlg(QWidget *parent, QTreeWidgetItem *first_item,
  2. QTreeWidgetItem *last_item): QDialog(parent),
  3. ui(new Ui::SlideShowDlg),_first_item(first_item),_last_item(last_item)
  4. {
  5. ui->setupUi(this);
  6. }

_first_item表示播放的第一个item
_last_item表示播放的最后一个item
先将slideshow展示出来看看效果,所以在ProTreeWidget的构造函数里添加动作

  1. _action_slideshow = new QAction(QIcon(":/icon/slideshow.png"), tr("轮播图播放"),this);
  2. connect(_action_slideshow, &QAction::triggered, this, &ProTreeWidget::SlotSlideShow);

在点击槽函数里完善右键点击逻辑

  1. void ProTreeWidget::SlotItemPressed(QTreeWidgetItem *pressedItem, int column)
  2. {
  3. qDebug() << "ProTreeWidget::SlotItemPressed" << endl;
  4. if(QGuiApplication::mouseButtons() == Qt::RightButton) //判断是否为右键
  5. {
  6. QMenu menu(this);
  7. qDebug() << "menu addr is " << &menu << endl;
  8. int itemtype = (int)(pressedItem->type());
  9. if (itemtype == TreeItemPro)
  10. {
  11. _right_btn_item = pressedItem;
  12. menu.addAction(_action_import);
  13. menu.addAction(_action_setstart);
  14. menu.addAction(_action_closepro);
  15. menu.addAction(_action_slideshow);
  16. menu.exec(QCursor::pos()); //菜单弹出位置为鼠标点击位置
  17. }
  18. }
  19. }

实现槽函数SlotSlideShow,n内部创建一个SlideShowDlg智能指针对象,然后设置为模态对话框,并且最大化显示。

  1. void ProTreeWidget::SlotSlideShow(){
  2. if(!_right_btn_item){
  3. return;
  4. }
  5. auto *right_pro_item = dynamic_cast<ProTreeItem*>(_right_btn_item);
  6. auto * last_child_item = right_pro_item->GetLastPicChild();
  7. if(!last_child_item){
  8. return;
  9. }
  10. qDebug()<< "last child item name is " << last_child_item->GetPath()<< endl;
  11. auto * first_child_item = right_pro_item->GetFirstPicChild();
  12. if(!first_child_item){
  13. return;
  14. }
  15. qDebug()<< "first child item name is " << first_child_item->GetPath()<< endl;
  16. _slide_show_dlg = std::make_shared<SlideShowDlg>(this, first_child_item, last_child_item);
  17. _slide_show_dlg->setModal(true);
  18. _slide_show_dlg->showMaximized();
  19. }

SlotSlideShow里根据right_pro_item获取当前项目下第一个子item和最后一个子item,这个算法并不复杂,right_pro_item为项目的root item,所以可以递归的获取root下第一个非文件夹item节点和最后一个非文件夹item节点。
遍历根目录所有一级子节点,如果第一个一级子节点为文件夹则递归查找,直到找到,如果第一个一级子节点下不存在图片类型的item,那么

  1. ProTreeItem *ProTreeItem::GetFirstPicChild()
  2. {
  3. if(this->type() == TreeItemPic){
  4. return nullptr;
  5. }
  6. auto child_count = this->childCount();
  7. if(child_count == 0){
  8. return nullptr;
  9. }
  10. for(int i = 0; i < child_count-1; i++){
  11. auto * first_child = this->child(i);
  12. auto * first_tree_child = dynamic_cast<ProTreeItem*>(first_child);
  13. auto item_type = first_tree_child->type();
  14. if(item_type == TreeItemPic){
  15. return first_tree_child;
  16. }
  17. first_child = first_tree_child->GetFirstPicChild();
  18. if(!first_child){
  19. continue;
  20. }
  21. first_tree_child = dynamic_cast<ProTreeItem*>(first_child);
  22. return first_tree_child;
  23. }
  24. return nullptr;
  25. }

获取最后一个子节点逻辑类似

  1. ProTreeItem *ProTreeItem::GetLastPicChild()
  2. {
  3. if(this->type() == TreeItemPic){
  4. return nullptr;
  5. }
  6. auto child_count = this->childCount();
  7. if(child_count == 0){
  8. return nullptr;
  9. }
  10. for(int i = child_count-1; i >= 0; i--){
  11. auto* last_child = this->child(i);
  12. auto * last_tree_item = dynamic_cast<ProTreeItem*>(last_child);
  13. int item_type = last_tree_item->type();
  14. if(item_type == TreeItemPic){
  15. return last_tree_item;
  16. }
  17. last_child = last_tree_item->GetLastPicChild();
  18. if(!last_child){
  19. continue;
  20. }
  21. last_tree_item = dynamic_cast<ProTreeItem*>(last_child);
  22. return last_tree_item;
  23. }
  24. return nullptr;
  25. }

此时点击幻灯片播放菜单就会弹出这个对话框了,为了样式美观我们添加qss样式

  1. SlideShowDlg {
  2. color:rgb(231,231,231);
  3. background-color:rgb(46,47,48);
  4. }

PicAnimationWid

接下来我们需要在SlideShowDlg的动画区域添加动画逻辑,类PicAnimationWid为图片动画展示窗口,继承于QWidget,构造函数比较简单

  1. PicAnimationWid::PicAnimationWid(QWidget *parent) : QWidget(parent),_factor(0.0),
  2. _cur_item(nullptr),_b_start(false)
  3. {
  4. _timer = new QTimer(this);
  5. connect(_timer, &QTimer::timeout, this, &PicAnimationWid::TimeOut);
  6. }
  1. _factor为动画因子,控制图片渐隐效果
  2. _b_start控制动画是否播放
  3. _cur_item 表示当前要绘制显示的ProTreeItem对象。
  4. 启动了一个定时器,然后定时回调TimeOut函数
    同样的道理析构函数需要实现定时器的停止

    1. PicAnimationWid::~PicAnimationWid(){
    2. _timer->stop();
    3. // delete _timer;
    4. }

    定时器回调函数

    1. void PicAnimationWid::TimeOut()
    2. {
    3. if(!_cur_item){
    4. Stop();
    5. update();
    6. return;
    7. }
    8. //qDebug()<<"_factor is " << _factor << endl;
    9. _factor = _factor+0.01;
    10. if(_factor >= 1){
    11. _factor = 0;
    12. auto * cur_pro_item = dynamic_cast<ProTreeItem*>(_cur_item);
    13. auto * next_pro_item = cur_pro_item->GetNextItem();
    14. if(!next_pro_item){
    15. Stop();
    16. update();
    17. return;
    18. }
    19. SetPixmap(next_pro_item);
    20. update();
    21. return;
    22. }
    23. update();
    24. }

    该函数每次对factor增加0.01,进而控制动画,如果factor变为1说明已经完成一张图片的消失和另一张的展示,需要更新下一组两张图片用来做渐隐渐现的效果。
    update函数是基类的刷新函数,会触发paintEvent函数,这个函数功能之后介绍。先介绍SetPixmap函数,该函数用来加载两张图片做渐变效果。
    实现SetPixmap设置要绘制的图片

    1. void PicAnimationWid::SetPixmap(QTreeWidgetItem *item)
    2. {
    3. if(!item){
    4. return;
    5. }
    6. auto * tree_item = dynamic_cast<ProTreeItem*>(item);
    7. auto path = tree_item->GetPath();
    8. _pixmap1.load(path);
    9. _cur_item = tree_item;
    10. if(_map_items.find(path) == _map_items.end()){
    11. _map_items[path]=tree_item;
    12. qDebug() << "SetPixmap path is " << path << endl;
    13. emit SigUpPreList(item);
    14. }
    15. emit SigSelectItem(item);
    16. auto * next_item = tree_item->GetNextItem();
    17. if(!next_item){
    18. return;
    19. }
    20. auto next_path = next_item->GetPath();
    21. _pixmap2.load(next_path);
    22. if(_map_items.find(next_path) == _map_items.end()){
    23. _map_items[next_path] = next_item;
    24. emit SigUpPreList(next_item);
    25. }
    26. }

    因为要双缓冲绘图,所以要缓存两张图片,用_pixmap1和_pixmap2缓存。
    实现_pixmap1渐隐,_pixmap2渐现。
    SigUpPreList信号是用来通知下方预览框更新预览图,因为我们要做的是上方播放动画后,下方会更新预览图。
    当前正在播放的图在下方预览图有选中提示,所以SigSelectItem信号是用来通知下方预览图选中效果。
    接下来要实现开始函数,让动画动起来

    1. void PicAnimationWid::Start()
    2. {
    3. emit SigStart();
    4. emit SigStartMusic();
    5. _factor = 0;
    6. _timer->start(25);
    7. _b_start = true;
    8. }
  5. SigStart信号用来通知右上方按钮的显示播放还是暂停状态,之后在处理信号连接问题。
  6. _factor为动画因子
  7. _b_start被设置为true
  8. 定时器每隔25ms更新一次
  9. SigStartMusic信号用来更新音乐,之后再处理信号连接问题。

同样实现一个停止动画的逻辑

  1. void PicAnimationWid::Stop()
  2. {
  3. emit SigStop();
  4. emit SigStopMusic();
  5. _timer->stop();
  6. _factor = 0;
  7. _b_start = false;
  8. }

我们接下来要实现双缓冲绘图的逻辑

  1. void PicAnimationWid::paintEvent(QPaintEvent *event)
  2. {
  3. if(_pixmap1.isNull()){
  4. return;
  5. }
  6. QPainter painter(this);
  7. painter.setRenderHint(QPainter::Antialiasing, true);
  8. QRect rect = geometry();
  9. int w = rect.width();
  10. int h = rect.height();
  11. _pixmap1=_pixmap1.scaled(w,h,Qt::KeepAspectRatio);
  12. int alpha = 255 * (1.0f - _factor);
  13. //qDebug()<<"_pixmap1.size()" << _pixmap1.size() << endl;
  14. QPixmap alphaPixmap(_pixmap1.size());
  15. alphaPixmap.fill(Qt::transparent);
  16. QPainter p1(&alphaPixmap);
  17. p1.setCompositionMode(QPainter::CompositionMode_Source);
  18. p1.drawPixmap(0, 0, _pixmap1);
  19. p1.setCompositionMode(QPainter::CompositionMode_DestinationIn);
  20. p1.fillRect(alphaPixmap.rect(), QColor(0, 0, 0, alpha));
  21. p1.end();
  22. int x = (w - _pixmap1.width()) / 2;
  23. int y = (h - _pixmap1.height()) / 2;
  24. painter.drawPixmap(x, y, alphaPixmap);
  25. if(_pixmap2.isNull()){
  26. return;
  27. }
  28. _pixmap2=_pixmap2.scaled(w,h,Qt::KeepAspectRatio);
  29. alpha = 255 * (_factor);
  30. QPixmap alphaPixmap2(_pixmap2.size());
  31. alphaPixmap2.fill(Qt::transparent);
  32. QPainter p2(&alphaPixmap2);
  33. p2.setCompositionMode(QPainter::CompositionMode_Source);
  34. p2.drawPixmap(0, 0, _pixmap2);
  35. p2.setCompositionMode(QPainter::CompositionMode_DestinationIn);
  36. p2.fillRect(alphaPixmap2.rect(), QColor(0, 0, 0, alpha));
  37. p2.end();
  38. x = (w - _pixmap2.width()) / 2;
  39. y = (h - _pixmap2.height()) / 2;
  40. painter.drawPixmap(x, y, alphaPixmap2);
  41. }

所谓双缓冲绘图逻辑如下:

  1. 提前加载好图片的两个pixmap分别为_pixmap1和_pixmap2。然后基于现在的widget大小做等比拉伸。
  2. 创建两个pixmap用作遮盖,分别为alphaPixmap和alphaPixmap2,将他们填充为透明的颜色。
  3. 分别创建两个画刷,然后绑定alphaPixmap和alphaPixmap2,用画刷分别绘制_pixmap1和_pixmap2。
  4. CompositionMode_DestinationIn表示遮罩的模式为显示重叠区域,CompositionMode_Source表示原图的绘制模式。
  5. 最后根据alpha值分别p2和p1的两个矩形区域设置透明度。
  6. 最后统一用一个painter分别绘制两个alphaPixmap和alphaPixmap2。

我们回到SlideShowDlg的ui文件,将动画显示区的widget升级为PicAnimationWid类即可。
然后在其构造函数里添加对动画区域的调用

  1. ui->picAnimation->SetPixmap(_first_item);
  2. ui->picAnimation->Start();

然后我们运行程序右键目录树的root项目选择幻灯片播放就可以看到动画效果了。

预览图类PreListWid

PreListWid类是从QListWidget派生而来。我们将SlideShowDlg中的preListWidget升级为PreListWid类型。
然后在ui文件中设置其高度为固定的110,因为之前我们设置其父窗口widget高度为固定的120,之后再设置其最大宽度为1677215,一个很大的数,列表不换行。
属性配置如下
https://cdn.llfc.club/1675062597828.jpg

构造函数

  1. PreListWid::PreListWid(QWidget *parent):QListWidget(parent),_global(0),_last_index(17)
  2. {
  3. this->setViewMode(QListWidget::IconMode);//设置内容为图片
  4. this->setIconSize(QSize(PREICON_SIZE, PREICON_SIZE));//设置图片的大小
  5. this->setSpacing(5);//设置每个item之间的间隔大小
  6. connect(this,&PreListWid::itemPressed, this, &PreListWid::SlotItemPressed);
  7. }
  1. 构造函数里设置视图模式为图片模式
  2. _global为计数器,统计累计加入列表的item数量,为每个item生成计数id。
  3. _last_index为上一次选择的item的id,因为全屏模式下列表框最多显示17个item,那么初始为17。该变量主要用于控制PreListWid是否横向移动,以及移动多少像素,因为上方展示的图片在下方的预览图可能在屏幕外,所以要移动PreListWid做显示效果。
  4. 连接了PreListWid的itemPressed信号,所以当item被点击后会触发SlotItemPressed函数。

PreListItem

为了实现我们自己的功能,所以PreListWid的item要自己实现,PreListItem继承于QListWidgetItem类。
其基本功能包括

  1. PreListItem::PreListItem(const QIcon &icon, const QString &text,const int &index,
  2. QListWidget *view , int type ):
  3. QListWidgetItem (icon,"",view,type),_path(text),_index(index)
  4. {
  5. }
  6. int PreListItem::GetIndex()
  7. {
  8. return _index;
  9. }
  10. QString PreListItem::GetPath()
  11. {
  12. return _path;
  13. }

_path表示item代表的文件路径
_index表示item的索引,也就是之前提到的id

动画区域和预览区域联动

为使动画区域和预览区域联动,在SlideShowDlg的构造函数里添加信号和槽函数连接逻辑

  1. auto * prelistWid = dynamic_cast<PreListWid*>(ui->preListWidget);
  2. connect(ui->picAnimation, &PicAnimationWid::SigUpPreList, prelistWid,&PreListWid::SlotUpPreList);
  3. connect(ui->picAnimation, &PicAnimationWid::SigSelectItem, prelistWid, &PreListWid::SlotUpSelect);
  4. //连接下方预览条点击与上方动画区图片显示
  5. connect(prelistWid, &PreListWid::SigUpSelectShow,
  6. ui->picAnimation, &PicAnimationWid::SlotUpSelectShow);
  1. 连接了PicAnimationWid的SigUpPreList信号,可以实现上面动画播放时将图像的预览图添加到列表中的效果。
  2. 连接了PicAnimationWid的SigSelectItem信号,可以实现上面动画播放时根据图像显示预览图选中效果。
  3. 连接了PreListWid的SigUpSelectShow信号,可以实现点击下方预览图,上方显示对应的动画效果。

先实现添加逻辑

  1. void PreListWid::SlotUpPreList(QTreeWidgetItem *tree_item)
  2. {
  3. if(!tree_item){
  4. qDebug() << "tree_item is empty" << endl;
  5. return;
  6. }
  7. auto * pro_item = dynamic_cast<ProTreeItem*>(tree_item);
  8. auto path = pro_item->GetPath();
  9. auto iter = _set_items.find(path);
  10. if(iter != _set_items.end()){
  11. qDebug() << "path " <<path<< " exists" << endl;
  12. return;
  13. }
  14. AddListItem(path);
  15. }

根据传入的tree_item判断路径是否存在,如果存在则返回,不存在则调用AddListItem将item加入listwidget里。

  1. void PreListWid::AddListItem(const QString &path)
  2. {
  3. QPixmap src_pixmap(path);
  4. src_pixmap = src_pixmap.scaled(PREICON_SIZE,PREICON_SIZE,Qt::KeepAspectRatio);
  5. QPixmap dst_pixmap(QSize(PREICON_SIZE, PREICON_SIZE));
  6. auto src_width = src_pixmap.width();
  7. auto src_height = src_pixmap.height();
  8. auto dist_width = dst_pixmap.width();
  9. auto dist_height = dst_pixmap.height();
  10. dst_pixmap.fill(QColor(220,220,220, 50));
  11. QPainter painter(&dst_pixmap);
  12. auto x = (dist_width-src_width)/2;
  13. auto y = (dist_height-src_height)/2;
  14. painter.drawPixmap(x,y,src_pixmap);
  15. _global++;
  16. PreListItem *pItem = new PreListItem(QIcon(dst_pixmap),path,_global,this);
  17. pItem->setSizeHint(QSize(PREITEM_SIZE,PREITEM_SIZE));
  18. this->addItem(pItem);
  19. _set_items[path] = pItem;
  20. if(_global==1){
  21. _pos_origin = this->pos();
  22. }
  23. }
  1. 因为图片的大小宽高不同,做拉伸时产生的空白区域不一样,那么我们统一用宽高为PREICON_SIZE的正方形绘制然后填充默认的背景色作为dist_pixmap。
  2. 然后用一个painter绑定这个dist_pixmap,计算和原图形src_pixmap的差值,让原图形src_pixmap居中绘制在dist_pixmap上。
  3. 然后构造PreListItem对象,将路径放入集合中。
  4. 如果_global为1说明时第一张预览图,需要记录一下预览图在其父窗口的位置,这样我们可以根据屏幕最右侧的预览图和该图的位置差值移动PreList
    Wid。

更新预览图选中效果

  1. void PreListWid::SlotUpSelect(QTreeWidgetItem *tree_item)
  2. {
  3. if(!tree_item){
  4. qDebug() << "tree_item is empty" << endl;
  5. return;
  6. }
  7. auto * pro_item = dynamic_cast<ProTreeItem*>(tree_item);
  8. auto path = pro_item->GetPath();
  9. auto iter = _set_items.find(path);
  10. if(iter == _set_items.end()){
  11. qDebug() << "path " <<path<< " not exists" << endl;
  12. return;
  13. }
  14. auto * list_item = dynamic_cast<PreListItem*>(iter.value());
  15. auto index = list_item->GetIndex();
  16. if(index > 17){
  17. auto pos_cur = this->pos();
  18. this->move(pos_cur.x()-(index-_last_index)*100, pos_cur.y());
  19. _last_index = index;
  20. }else{
  21. this->move(_pos_origin);
  22. _last_index = 17;
  23. }
  24. this->setCurrentItem(iter.value());
  25. }

判断当前播放的图像对应预览图的item的索引是否大于17,如果大于17说明该item在屏幕外,因为一个屏幕最多容纳17个item,所以要移动PreListWid。
移动的方法就是当前索引减去上一次的索引差值乘以100,因为一个item的宽度为100,纵坐标不变。同时设置当前item为选中状态。

点击预览图显示对应图片

PreListWid响应点击的槽函数

  1. void PreListWid::SlotItemPressed(QListWidgetItem *item)
  2. {
  3. if(QGuiApplication::mouseButtons() != Qt::LeftButton){
  4. return;
  5. }
  6. auto * list_item = dynamic_cast<PreListItem*>(item);
  7. auto cur_index = list_item->GetIndex();
  8. auto path = list_item->GetPath();
  9. this->setCurrentItem(item);
  10. emit SigUpSelectShow(path);
  11. }

点击item时发送SigUpSelectShow信号通知动画去显示选择的图片。
触发PicAnimationWid的槽函数SlotUpSelectShow

  1. void PicAnimationWid::SlotUpSelectShow(QString path)
  2. {
  3. qDebug()<<"SlotUpSelectShow path is " << path << endl;
  4. auto iter = _map_items.find(path);
  5. if(iter == _map_items.end()){
  6. return;
  7. }
  8. UpSelectPixmap(iter.value());
  9. update();
  10. }

内部调用UpSelectPixmap更新图片为选中的图片,并且调用update刷新界面。

  1. void PicAnimationWid::UpSelectPixmap(QTreeWidgetItem *item)
  2. {
  3. if(!item){
  4. return;
  5. }
  6. auto * tree_item = dynamic_cast<ProTreeItem*>(item);
  7. auto path = tree_item->GetPath();
  8. _pixmap1.load(path);
  9. _cur_item = tree_item;
  10. if(_map_items.find(path) == _map_items.end()){
  11. _map_items[path]=tree_item;
  12. qDebug() << "SetPixmap path is " << path << endl;
  13. }
  14. auto * next_item = tree_item->GetNextItem();
  15. if(!next_item){
  16. return;
  17. }
  18. auto next_path = next_item->GetPath();
  19. _pixmap2.load(next_path);
  20. if(_map_items.find(next_path) == _map_items.end()){
  21. _map_items[next_path] = next_item;
  22. }
  23. }

到此就实现了幻灯片放映和下方预览图的交互效果。播放和暂停,以及切换操作等留给下一篇。

源码链接

源码链接
https://gitee.com/secondtonone1/qt-learning-notes

热门评论

热门文章

  1. windows环境搭建和vscode配置

    喜欢(587) 浏览(1691)
  2. 解密定时器的实现细节

    喜欢(566) 浏览(1780)
  3. C++ 类的继承封装和多态

    喜欢(588) 浏览(2488)
  4. slice介绍和使用

    喜欢(521) 浏览(1662)
  5. Linux环境搭建和编码

    喜欢(594) 浏览(5329)

最新评论

  1. 泛型算法的定制操作 secondtonone1:lambda和bind是C11新增的利器,善于利用这两个机制可以极大地提升编程安全性和效率。
  2. 基于锁实现线程安全队列和栈容器 secondtonone1:我是博主,你认真学习的样子的很可爱,哈哈,我画的是链表由空变成1个的情况。其余情况和你思考的类似,只不过我用了一个无效节点表示tail的指向,最初head和tail指向的都是这个节点。
  3. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  4. 利用内存模型优化无锁栈 卡西莫多的礼物:感谢博主指点,好人一生平安o(* ̄▽ ̄*)ブ
  5. 类和对象 陈宇航:支持!!!!

个人公众号

个人微信