简介

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

SlideShowDlg

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

SlideShowDlg::SlideShowDlg(QWidget *parent, QTreeWidgetItem *first_item,
                           QTreeWidgetItem *last_item): QDialog(parent),
    ui(new Ui::SlideShowDlg),_first_item(first_item),_last_item(last_item)
{
    ui->setupUi(this);
}

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

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

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

void ProTreeWidget::SlotItemPressed(QTreeWidgetItem *pressedItem, int column)
{
    qDebug() << "ProTreeWidget::SlotItemPressed" << endl;
    if(QGuiApplication::mouseButtons() == Qt::RightButton)   //判断是否为右键
        {
            QMenu menu(this);
            qDebug() << "menu addr is " << &menu << endl;
            int itemtype = (int)(pressedItem->type());
            if (itemtype == TreeItemPro)
            {
                _right_btn_item = pressedItem;
                menu.addAction(_action_import);
                menu.addAction(_action_setstart);
                menu.addAction(_action_closepro);
                menu.addAction(_action_slideshow);
                menu.exec(QCursor::pos());   //菜单弹出位置为鼠标点击位置
            }
    }
}

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

void ProTreeWidget::SlotSlideShow(){
    if(!_right_btn_item){
        return;
    }
    auto *right_pro_item = dynamic_cast<ProTreeItem*>(_right_btn_item);

    auto * last_child_item = right_pro_item->GetLastPicChild();
    if(!last_child_item){
        return;
    }

    qDebug()<< "last child item name is " << last_child_item->GetPath()<< endl;

    auto * first_child_item = right_pro_item->GetFirstPicChild();
    if(!first_child_item){
        return;
    }

    qDebug()<< "first child item name is " << first_child_item->GetPath()<< endl;

    _slide_show_dlg = std::make_shared<SlideShowDlg>(this, first_child_item, last_child_item);
    _slide_show_dlg->setModal(true);
    _slide_show_dlg->showMaximized();

}

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

ProTreeItem *ProTreeItem::GetFirstPicChild()
{
    if(this->type() == TreeItemPic){
        return nullptr;
    }

    auto child_count = this->childCount();
    if(child_count == 0){
        return nullptr;
    }

    for(int i = 0; i < child_count-1; i++){
        auto * first_child = this->child(i);
        auto * first_tree_child = dynamic_cast<ProTreeItem*>(first_child);
        auto item_type = first_tree_child->type();
        if(item_type == TreeItemPic){
            return first_tree_child;
        }

        first_child = first_tree_child->GetFirstPicChild();
        if(!first_child){
            continue;
        }

        first_tree_child = dynamic_cast<ProTreeItem*>(first_child);
        return first_tree_child;
    }

    return nullptr;
}

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

ProTreeItem *ProTreeItem::GetLastPicChild()
{
    if(this->type() == TreeItemPic){
        return nullptr;
    }

    auto child_count = this->childCount();
    if(child_count == 0){
        return nullptr;
    }

    for(int i = child_count-1; i >= 0; i--){
        auto* last_child = this->child(i);
        auto * last_tree_item = dynamic_cast<ProTreeItem*>(last_child);
        int item_type = last_tree_item->type();
        if(item_type == TreeItemPic){
            return last_tree_item;
        }

        last_child = last_tree_item->GetLastPicChild();
        if(!last_child){
            continue;
        }

        last_tree_item = dynamic_cast<ProTreeItem*>(last_child);
        return last_tree_item;
    }

    return nullptr;
}

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

SlideShowDlg {
    color:rgb(231,231,231);
    background-color:rgb(46,47,48);
}

PicAnimationWid

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

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

    PicAnimationWid::~PicAnimationWid(){
     _timer->stop();
    //  delete _timer;
    }
    

    定时器回调函数

    void PicAnimationWid::TimeOut()
    {
     if(!_cur_item){
         Stop();
         update();
         return;
     }
     //qDebug()<<"_factor is " << _factor << endl;
    
     _factor = _factor+0.01;
    
     if(_factor >= 1){
         _factor = 0;
         auto * cur_pro_item = dynamic_cast<ProTreeItem*>(_cur_item);
         auto * next_pro_item = cur_pro_item->GetNextItem();
         if(!next_pro_item){
             Stop();
             update();
             return;
         }
         SetPixmap(next_pro_item);
         update();
         return;
     }
     update();
    }
    

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

    void PicAnimationWid::SetPixmap(QTreeWidgetItem *item)
    {
     if(!item){
         return;
     }
    
     auto * tree_item = dynamic_cast<ProTreeItem*>(item);
     auto path = tree_item->GetPath();
     _pixmap1.load(path);
     _cur_item = tree_item;
     if(_map_items.find(path) == _map_items.end()){
         _map_items[path]=tree_item;
         qDebug() << "SetPixmap path is " << path << endl;
          emit SigUpPreList(item);
     }
    
     emit SigSelectItem(item);
    
     auto * next_item = tree_item->GetNextItem();
     if(!next_item){
         return;
     }
    
     auto next_path = next_item->GetPath();
     _pixmap2.load(next_path);
     if(_map_items.find(next_path) == _map_items.end()){
          _map_items[next_path] = next_item;
          emit SigUpPreList(next_item);
     }
    }
    

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

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

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

void PicAnimationWid::Stop()
{
    emit SigStop();
    emit SigStopMusic();
    _timer->stop();
    _factor = 0;
    _b_start = false;
}

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

void PicAnimationWid::paintEvent(QPaintEvent *event)
{
    if(_pixmap1.isNull()){
        return;
    }

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing, true);

    QRect rect = geometry();

    int w = rect.width();
    int h = rect.height();
    _pixmap1=_pixmap1.scaled(w,h,Qt::KeepAspectRatio);
    int alpha = 255 * (1.0f - _factor);
    //qDebug()<<"_pixmap1.size()" << _pixmap1.size() << endl;
    QPixmap alphaPixmap(_pixmap1.size());
    alphaPixmap.fill(Qt::transparent);

   QPainter p1(&alphaPixmap);
   p1.setCompositionMode(QPainter::CompositionMode_Source);
   p1.drawPixmap(0, 0, _pixmap1);
   p1.setCompositionMode(QPainter::CompositionMode_DestinationIn);
   p1.fillRect(alphaPixmap.rect(), QColor(0, 0, 0, alpha));
   p1.end();

   int x = (w - _pixmap1.width()) / 2;
   int y = (h - _pixmap1.height()) / 2;
   painter.drawPixmap(x, y, alphaPixmap);

   if(_pixmap2.isNull()){
        return;
   }

   _pixmap2=_pixmap2.scaled(w,h,Qt::KeepAspectRatio);
   alpha = 255 * (_factor);
   QPixmap alphaPixmap2(_pixmap2.size());
   alphaPixmap2.fill(Qt::transparent);
   QPainter p2(&alphaPixmap2);
   p2.setCompositionMode(QPainter::CompositionMode_Source);
   p2.drawPixmap(0, 0, _pixmap2);
   p2.setCompositionMode(QPainter::CompositionMode_DestinationIn);
   p2.fillRect(alphaPixmap2.rect(), QColor(0, 0, 0, alpha));
   p2.end();
    x = (w - _pixmap2.width()) / 2;
    y = (h - _pixmap2.height()) / 2;
    painter.drawPixmap(x, y, alphaPixmap2);
}

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

  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类即可。 然后在其构造函数里添加对动画区域的调用

    ui->picAnimation->SetPixmap(_first_item);
    ui->picAnimation->Start();

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

预览图类PreListWid

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

构造函数

PreListWid::PreListWid(QWidget *parent):QListWidget(parent),_global(0),_last_index(17)
{
    this->setViewMode(QListWidget::IconMode);//设置内容为图片
    this->setIconSize(QSize(PREICON_SIZE, PREICON_SIZE));//设置图片的大小
    this->setSpacing(5);//设置每个item之间的间隔大小
    connect(this,&PreListWid::itemPressed, this, &PreListWid::SlotItemPressed);
}
  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类。 其基本功能包括

PreListItem::PreListItem(const QIcon &icon, const QString &text,const int &index,
                         QListWidget *view , int type ):
    QListWidgetItem (icon,"",view,type),_path(text),_index(index)
{

}

int PreListItem::GetIndex()
{
    return _index;
}

QString PreListItem::GetPath()
{
    return _path;
}

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

动画区域和预览区域联动

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

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

先实现添加逻辑

void PreListWid::SlotUpPreList(QTreeWidgetItem *tree_item)
{
    if(!tree_item){
        qDebug() << "tree_item is empty" << endl;
        return;
    }

    auto * pro_item = dynamic_cast<ProTreeItem*>(tree_item);
    auto path = pro_item->GetPath();
    auto iter = _set_items.find(path);
    if(iter != _set_items.end()){
        qDebug() << "path " <<path<< " exists" << endl;
        return;
    }

    AddListItem(path);
}

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

void PreListWid::AddListItem(const QString &path)
{
    QPixmap src_pixmap(path);
    src_pixmap = src_pixmap.scaled(PREICON_SIZE,PREICON_SIZE,Qt::KeepAspectRatio);
    QPixmap dst_pixmap(QSize(PREICON_SIZE, PREICON_SIZE));
    auto src_width = src_pixmap.width();
    auto src_height = src_pixmap.height();

    auto dist_width = dst_pixmap.width();
    auto dist_height = dst_pixmap.height();

    dst_pixmap.fill(QColor(220,220,220, 50));
    QPainter painter(&dst_pixmap);

    auto x = (dist_width-src_width)/2;
    auto y = (dist_height-src_height)/2;
    painter.drawPixmap(x,y,src_pixmap);

    _global++;
    PreListItem *pItem = new PreListItem(QIcon(dst_pixmap),path,_global,this);
    pItem->setSizeHint(QSize(PREITEM_SIZE,PREITEM_SIZE));
    this->addItem(pItem);
    _set_items[path] = pItem;

    if(_global==1){
        _pos_origin = this->pos();
    }
}
  1. 因为图片的大小宽高不同,做拉伸时产生的空白区域不一样,那么我们统一用宽高为PREICON_SIZE的正方形绘制然后填充默认的背景色作为dist_pixmap。
  2. 然后用一个painter绑定这个dist_pixmap,计算和原图形src_pixmap的差值,让原图形src_pixmap居中绘制在dist_pixmap上。
  3. 然后构造PreListItem对象,将路径放入集合中。
  4. 如果_global为1说明时第一张预览图,需要记录一下预览图在其父窗口的位置,这样我们可以根据屏幕最右侧的预览图和该图的位置差值移动PreList Wid。

更新预览图选中效果

void PreListWid::SlotUpSelect(QTreeWidgetItem *tree_item)
{
    if(!tree_item){
        qDebug() << "tree_item is empty" << endl;
        return;
    }

    auto * pro_item = dynamic_cast<ProTreeItem*>(tree_item);
    auto path = pro_item->GetPath();
    auto iter = _set_items.find(path);
    if(iter == _set_items.end()){
        qDebug() << "path " <<path<< " not exists" << endl;
        return;
    }

    auto * list_item = dynamic_cast<PreListItem*>(iter.value());
    auto index = list_item->GetIndex();

    if(index > 17){
        auto pos_cur = this->pos();
        this->move(pos_cur.x()-(index-_last_index)*100, pos_cur.y());
        _last_index = index;

    }else{
       this->move(_pos_origin);
        _last_index = 17;
    }
    this->setCurrentItem(iter.value());
}

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

点击预览图显示对应图片

PreListWid响应点击的槽函数

void PreListWid::SlotItemPressed(QListWidgetItem *item)
{
    if(QGuiApplication::mouseButtons() != Qt::LeftButton){
        return;
    }

    auto * list_item = dynamic_cast<PreListItem*>(item);
    auto cur_index = list_item->GetIndex();
    auto path = list_item->GetPath();

     this->setCurrentItem(item);
    emit SigUpSelectShow(path);
}

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

void PicAnimationWid::SlotUpSelectShow(QString path)
{
    qDebug()<<"SlotUpSelectShow path is " << path << endl;
    auto iter = _map_items.find(path);
    if(iter == _map_items.end()){
            return;
    }

    UpSelectPixmap(iter.value());
    update();
}

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

void PicAnimationWid::UpSelectPixmap(QTreeWidgetItem *item)
{
    if(!item){
        return;
    }

    auto * tree_item = dynamic_cast<ProTreeItem*>(item);
    auto path = tree_item->GetPath();
    _pixmap1.load(path);
    _cur_item = tree_item;
    if(_map_items.find(path) == _map_items.end()){
        _map_items[path]=tree_item;
        qDebug() << "SetPixmap path is " << path << endl;
    }

    auto * next_item = tree_item->GetNextItem();
    if(!next_item){
        return;
    }

    auto next_path = next_item->GetPath();
    _pixmap2.load(next_path);
    if(_map_items.find(next_path) == _map_items.end()){
         _map_items[next_path] = next_item;
    }
}

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

源码链接

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

results matching ""

    No results matching ""