QT5 实现卡片布局(CardStackLayout)

Qt拥有强大丰富的界面库。闲来无事,开发一款炫酷的天气预报程序。模拟天气,讯飞语音播报,卡片布局。现在开始慢慢更新,作为备忘录。


布局基类 QLayout

QLayout是QT所有布局的基类,要编写自己的布局类,您必须定义以下内容:

  • 数据结构以存储由布局处理的项。每一项都是QLayoutItem
  • addItem(),如何在布局中添加项目。
  • setGeometry(),如何执行布局。
  • sizeHint(),布局的首选大小。
  • itemAt(),如何遍历布局。
  • takeAt(),如何从布局中删除项目。
  • 为了支持子Item的高依赖于他们的宽,需要实现hasHeightForWidth()heightForWidth()
  • 在大多数情况下,您还将实现minimumSize()

布局管理器销毁时,也将会销毁它管理的Item。
Example: Border Layout Example, and Flow Layout Example.


为了实现自定义带动画的布局管理器,定义了继承自QLayout的CardStackLayout 。

CardStackLayout 类

CardStackLayout .h

#ifndef CARDSTACKLAYOUT_H
#define CARDSTACKLAYOUT_H

#include <QLayout>
#include <QRect>
#include <QMap>
#include <QDebug>

class CardStackLayout : public QLayout
{
    Q_OBJECT

public:
    // 定义布局风格,目前支持层叠布局(Cascade )
    enum CardStackLayoutStyle { Cascade };
    // 定义卡片排放方式
    enum Position
    {
        ZTop = 0x00000001,          // 在卡片顶层显示
        ZBottom = 0x00000010,       // 在卡片底层显示
        North = 0x00000100,         // 相对前一张卡片North(上方)布局
        West = 0x00100000           // 相对前一张卡片West(左侧)布局
    };
    typedef QMap<Position, int> tyPosValue;
    enum SizeType { MinimumSize, SizeHint };
    // 定义QLayoutItem包装体 
    struct ItemWrapper
    {
        ItemWrapper(QLayoutItem *i)
        {
            item = i;
        }
        QLayoutItem *item;
        tyPosValue   mapPos;
    };
    // 定义交换结构体
    struct ItemSwitch
    {
        ItemSwitch(){}
        CardStackLayout::ItemWrapper* item;
        QRect rcfrom;
        QRect rcto;
        ItemSwitch(const ItemSwitch &rhs)
        {
            *this = rhs;
        }
        ItemSwitch &operator= (const ItemSwitch &rhs)
        {
            if(&rhs == this)
                return *this;

            item=rhs.item;
            rcfrom = rhs.rcfrom;
            rcto = rhs.rcto;

            return *this;
        }
    };

    explicit CardStackLayout(QWidget *parent = Q_NULLPTR ,CardStackLayoutStyle style = Cascade);
    ~CardStackLayout();

    void addItem(QLayoutItem *item) override;
    void addWidget(QWidget *widget, Position position = ZBottom);

    void adjustWideget(QWidget *widget,long position, int offset);
    void adjustWideget(QWidget *widget,const tyPosValue& posvalue);
    void adjustWideget(int index,const tyPosValue& posvalue);

    void debugPut(QString tips);

    bool isRunningAnimation(){ return m_animation_relevant.runing; }
    void setUseAnimation(bool use) { m_animation_relevant.useani = use; }

    void setAllAlignment(bool horcenter = true, bool vercenter = true);
    void setDirectionSpacing(int north = 20, int west = 20);

    Qt::Orientations expandingDirections() const override;
    bool hasHeightForWidth() const override;
    int count() const override;
    QLayoutItem *itemAt(int index) const override;
    QLayoutItem *takeAt(int index) override;
    QSize minimumSize() const override;
    void setGeometry(const QRect &rect) override;
    QSize sizeHint() const override;

    void setNeedNonFirstTransparent(bool state) { m_non_first_transparent = state; changeNonFirstTransparent(); }
    bool needNonFirstTransparent(){ return m_non_first_transparent; }

protected slots:
    void reLayer();
    void animationFinished();

protected:
    void destoryAll();
    ItemWrapper *find(QWidget* item, int *index = NULL);
    void add(QLayoutItem *item, Position position);

    QSize calculateSize(SizeType sizeType) const;
    bool calculateItemRect(QRect prerc, ItemWrapper *p, QRect &rcnow) const;
    QRect calculateCombRect(QRect rclayout);

    void switchOrder(int oldindex, int newindex,ItemWrapper* p);
    void adjustItem(int index, ItemWrapper* p,long posflag, int offset);

    void geometryAnimation(QList<ItemSwitch> &lstmp);
    void changeNonFirstTransparent();

private:
    // 定义动画选项
    struct AnimationRelevant
    {
        bool useani;
        bool runing;
        short anitype;  // 0 geometry, 1 opacity
    };

    AnimationRelevant m_animation_relevant;
    bool m_horizontal_center;
    bool m_vertical_center;
    bool m_non_first_transparent;
    int m_north_spacing;
    int m_west_spacing;
    QList<ItemWrapper*> m_list;
    CardStackLayoutStyle m_cardstacklayout_style;
};

#endif // CARDSTACKLAYOUT_H

CardStackLayout .cpp

#include "cardstacklayout.h"
#include <cmath>
#include <QLabel>
#include <QPropertyAnimation>
#include <QParallelAnimationGroup>
#include <QGraphicsOpacityEffect>

CardStackLayout::CardStackLayout(QWidget *parent, CardStackLayout::CardStackLayoutStyle style)
    : QLayout(parent)
    , m_cardstacklayout_style(style)
    , m_horizontal_center(true)
    , m_vertical_center(true)
{
    m_animation_relevant.runing = false;
    m_animation_relevant.anitype = 1;
    m_animation_relevant.useani = true;
    m_non_first_transparent = true;
    m_north_spacing = 20;
    m_west_spacing = 20;

    setMargin(0);
    setSpacing(0);
}

CardStackLayout::~CardStackLayout()
{
    destoryAll();
}

void CardStackLayout::destoryAll()
{
    QLayoutItem *item = NULL;
    while (item = takeAt(0))
    {
        delete item;
    }
}

void CardStackLayout::addItem(QLayoutItem *item)
{
    add(item, ZBottom);
}

void CardStackLayout::addWidget(QWidget *widget, CardStackLayout::Position position)
{
    add(new QWidgetItem(widget), position);
}

void CardStackLayout::add(QLayoutItem *item, CardStackLayout::Position position)
{
    if(Cascade == m_cardstacklayout_style)
    {
        item->setAlignment(Qt::AlignHCenter);
        ItemWrapper *p = new ItemWrapper(item);

        if(ZTop == position)
        {
            m_list.push_front(p);
        }
        else
        {
            m_list.append(p);
        }
        reLayer();
    }

    // QWidget *w = item->widget();
    // if(w)
    // {
        // w->setSizePolicy(QSizePolicy::Fixed,QSizePolicy::Fixed);
        // w->show();
    // }

    changeNonFirstTransparent();

    invalidate();
}

bool CardStackLayout::hasHeightForWidth() const
{
    return false;
}

int CardStackLayout::count() const
{
    return m_list.size();
}

QLayoutItem *CardStackLayout::itemAt(int index) const
{
    ItemWrapper *it = m_list.value(index);
    if (it)
        return it->item;
    else
        return 0;
}

QLayoutItem *CardStackLayout::takeAt(int index)
{
    QLayoutItem *item = NULL;
    if (index >= 0 && index < m_list.size())
    {
        ItemWrapper *layoutStruct = m_list.takeAt(index);
        item = layoutStruct->item;
        delete layoutStruct;
    }
    return item;
}

void CardStackLayout::setGeometry(const QRect &rect)
{
    QLayout::setGeometry(rect);

    QRect rcpre, rcnow;
    QRect rccombination = calculateCombRect(rect);

    QPoint ptsrccenter = rccombination.center();
    QPoint ptdstcenter = rect.center();

    int woffset = abs(ptdstcenter.rx())-abs(ptsrccenter.rx());
    int hoffset = abs(ptdstcenter.ry())-abs(ptsrccenter.ry());

    if(!m_list.empty())
    {
        ItemWrapper *p = m_list.at(0);
        rcnow = rect;
        rcnow.setSize(p->item->geometry().size());

        if(m_horizontal_center)
        {
            rcnow.adjust(woffset,0,woffset,0);
        }
        if(m_vertical_center)
        {
            rcnow.adjust(0,hoffset,0,hoffset);
        }

        p->item->setGeometry(rcnow);
        rcpre = rcnow;
    }
    for (int i = 1; i < m_list.size(); ++i)
    {
        ItemWrapper *p = m_list.at(i);

        bool bok = calculateItemRect(rcpre,p,rcnow);
        if(!bok){ continue;}

        p->item->setGeometry(rcnow);
        rcpre = rcnow;
    }
}

QSize CardStackLayout::minimumSize() const
{
    return calculateSize(MinimumSize);
}

QSize CardStackLayout::sizeHint() const
{
    return calculateSize(SizeHint);
}

QSize CardStackLayout::calculateSize(CardStackLayout::SizeType sizeType) const
{
    QSize totalSize(0,0);
    QRect rcpre, rcnow,rect(0,0,0,0);

    for (int i = 0; i < m_list.size(); ++i)
    {
        ItemWrapper *wrapper = m_list.at(i);
        QSize itemSize;

        if (sizeType == MinimumSize)
        {
            itemSize = wrapper->item->minimumSize();
        }
        else // (sizeType == SizeHint)
        {
            itemSize = wrapper->item->sizeHint();
        }

        ItemWrapper *p = m_list.at(i);
        rcpre = (0 == i)? p->item->geometry():rcpre;
        bool bok = calculateItemRect(rcpre,p,rcnow);
        if(!bok){ continue;}
        rcpre = rcnow;

        totalSize = totalSize.expandedTo(rcnow.size());
    }
    return totalSize;
}

bool CardStackLayout::calculateItemRect(QRect rcpre, ItemWrapper *p, QRect &rcnow) const
{
    if(NULL == p)
    {
        Q_ASSERT(NULL!=p);
        return false;
    }

    QWidget *w = p->item->widget();
    rcnow = rcpre;

    tyPosValue::const_iterator it;
    it = p->mapPos.find(North);
    if(it!=p->mapPos.end())
    {
         rcnow.setTop(rcpre.top()+it.value());
    }
    else
    {
        rcnow.setTop(rcpre.top() + m_north_spacing);
    }

    it = p->mapPos.find(West);
    if(it!=p->mapPos.end())
    {
         rcnow.setLeft(rcpre.left()+it.value());
    }
    else
    {
        rcnow.setLeft(rcpre.left() + m_west_spacing);
    }

    if(NULL!=w)
    {
        rcnow.setRight(rcnow.left()+ w->size().width());
        rcnow.setBottom(rcnow.top()+ w->size().height());
    }
    else
    {
        QSize si = p->item->geometry().size();
        rcnow.setRight(rcnow.left()+ si.width());
        rcnow.setBottom(rcnow.top()+ si.height());
    }

    return true;
}

QRect CardStackLayout::calculateCombRect(QRect rclayout)
{
    QRect rcpre = rclayout, rcnow, rcmax = rclayout;

    if(!m_list.empty())
    {
        rcmax.setSize(m_list.at(0)->item->geometry().size());
        rcpre = rcmax;
    }
    for (int i = 1; i < m_list.size(); ++i)
    {
        ItemWrapper *p = m_list.at(i);

        bool bok = calculateItemRect(rcpre,p,rcnow);
        if(!bok){ continue;}

        rcmax = rcmax.united(rcnow);
        rcpre = rcnow;
    }

    return rcmax;
}

Qt::Orientations CardStackLayout::expandingDirections() const
{
    return Qt::Horizontal | Qt::Vertical;
}

void CardStackLayout::adjustWideget(QWidget *widget, long posflag, int offset)
{
   if(Q_NULLPTR == widget) return;
   int index;
   CardStackLayout::ItemWrapper *p = find(widget,&index);
   adjustItem(index,p,posflag,offset);
}

void CardStackLayout::adjustWideget(QWidget *widget, const CardStackLayout::tyPosValue &posvalue)
{
    if(Q_NULLPTR == widget) return;

    int index;
    ItemWrapper *p = find(widget,&index);

    tyPosValue::const_iterator it = posvalue.begin();
    while (it != posvalue.end() )
    {
        adjustItem(index,p,it.key(),it.value());
        ++it;
    }
}

void CardStackLayout::adjustWideget(int index, const CardStackLayout::tyPosValue &posvalue)
{
    if(index<0 || index>=m_list.size())
        return;

    ItemWrapper *p = m_list.at(index);
    tyPosValue::const_iterator it = posvalue.begin();
    while (it != posvalue.end() )
    {
        adjustItem(index,p,it.key(),it.value());
        ++it;
    }
}

void CardStackLayout::adjustItem(int index, CardStackLayout::ItemWrapper *p, long posflag, int offset)
{
    if(Q_NULLPTR == p || m_animation_relevant.runing)
        return;

    if(posflag & ZTop || posflag & ZBottom)
    {
        int newIndex = offset;
        int n = m_list.size();
        if(posflag & ZTop)
        {
            newIndex = n>newIndex? newIndex: n-1;
        }
        else
        {
            newIndex = newIndex>0?0:newIndex;
            newIndex = n>abs(newIndex)? (n+newIndex-1): 0;
        }
        switchOrder(index,newIndex,p);
    }
    if( posflag & North || posflag & West)
    {
         if(posflag & North)
         {
             p->mapPos[North] = offset;
         }
         if(posflag & West)
         {
             p->mapPos[West] = offset;
         }

         invalidate();
    }
}

void CardStackLayout::changeNonFirstTransparent()
{
    QGraphicsOpacityEffect* pOpacityEffect = NULL;
    qreal opa = m_non_first_transparent?0.1:1.0;
    for(auto it = m_list.begin(); it!=m_list.end();++it)
    {
        ItemWrapper *tmp = *it;
        QWidget *p = tmp->item->widget();
        if(NULL == p) continue;
        pOpacityEffect = (QGraphicsOpacityEffect*)p->graphicsEffect();
        if(NULL == pOpacityEffect) continue;
        if(it!=m_list.end()-1)
        {
            pOpacityEffect->setOpacity(opa);
        }
        else
        {
            pOpacityEffect->setOpacity(1.0);
        }
    }
}

void CardStackLayout::geometryAnimation(QList<ItemSwitch> &lstmp)
{
    if(lstmp.empty() || !m_animation_relevant.useani || m_animation_relevant.runing)
        return ;

    const CardStackLayout::ItemWrapper *tail = m_list.back();
    QParallelAnimationGroup *group = new QParallelAnimationGroup;
    for(auto it = lstmp.begin(); it!=lstmp.end(); ++it)
    {
        QWidget *w = it->item->item->widget();
        if(NULL==w) continue;
        QPropertyAnimation *animation = new QPropertyAnimation(w, "geometry");
        animation->setDuration((it->item)==tail?500:500);
        animation->setStartValue(it->rcfrom);
        animation->setEndValue(it->rcto);
        animation->setEasingCurve(QEasingCurve::OutInQuad);
        group->addAnimation(animation);
    }
    connect(group,SIGNAL(finished()),this,SLOT(reLayer()));
    connect(group,SIGNAL(finished()),this,SLOT(animationFinished()));
    m_animation_relevant.runing = true;
    group->start(QAbstractAnimation::DeleteWhenStopped);
}

void CardStackLayout::switchOrder(int oldindex, int newindex, CardStackLayout::ItemWrapper *p)
{
    if(newindex<0 || newindex >=m_list.size() || oldindex == newindex)
        return;

    QList<ItemSwitch> lstmp;

    if(true)
    {
        ItemSwitch stit;
        tyPosValue tyfrom,tyto = p->mapPos;
        QRect rcfrom,rcto = p->item->geometry();

        if(oldindex<newindex)
        {
            for(int i = oldindex+1; i<=newindex; ++i)
            {
                ItemWrapper* it = m_list.at(i);
                tyfrom = it->mapPos;
                rcfrom = it->item->geometry();
                it->mapPos = tyto;

                stit.item = it;
                stit.rcfrom = rcfrom;
                stit.rcto = rcto;
                lstmp.push_front(stit);

                tyto = tyfrom;
                rcto = rcfrom;
            }
            p->mapPos = tyto;
        }
        else
        {
            for(int i = oldindex-1;i>=newindex; --i)
            {
                ItemWrapper* it = m_list.at(i);
                tyfrom = it->mapPos;
                rcfrom = it->item->geometry();
                it->mapPos = tyto;

                stit.item = it;
                stit.rcfrom = rcfrom;
                stit.rcto = rcto;
                lstmp.push_front(stit);

                tyto = tyfrom;
                rcto = rcfrom;
            }
            p->mapPos = tyto;
        }

        stit.item = p;
        stit.rcfrom = p->item->geometry();
        stit.rcto = rcto;
        lstmp.push_back(stit);
    }

    for(auto it = m_list.begin(); it!=m_list.end();)
    {
        if( *it == p)
        {
           it = m_list.erase(it);
           m_list.insert(newindex,p);
           break;
        }
        ++it;
    }

    if(m_animation_relevant.useani)
    {
       geometryAnimation(lstmp);
    }
    else
    {
        reLayer();
        changeNonFirstTransparent();
    }
}

void CardStackLayout::reLayer()
{
    for(auto it = m_list.begin(); it!=m_list.end();++it)
    {
        QWidget* p = (*it)->item->widget();
        if( NULL!=p)
        {
            p->raise();
        }
    }
}

void CardStackLayout::animationFinished()
{
     m_animation_relevant.runing = false;
     changeNonFirstTransparent();
}

CardStackLayout::ItemWrapper *CardStackLayout::find(QWidget *item, int *index)
{
    ItemWrapper *p = Q_NULLPTR;
    int i = 0;
    for(auto it = m_list.begin(); it!=m_list.end();++it,++i)
    {
        ItemWrapper *tmp = *it;
        if( tmp->item->widget() == item)
        {
            p = *it;
            break;
        }
    }
    if(NULL!=index)
    {
        *index = i;
    }

    return p;
}

void CardStackLayout::debugPut(QString tips)
{
    qDebug() << tips;
    for(int i=0;i<m_list.size();++i)
    {
        ItemWrapper* it = m_list.at(i);
        //QString s = ((QLabel*)(it->item->widget()))->text();
        QRect rc = it->item->geometry();
        qDebug() << i << " " << it->item << /*s <<*/ rc.topLeft() << " " << rc.bottomRight();
    }
}

void CardStackLayout::setDirectionSpacing(int north, int west)
{
    m_north_spacing = north;
    m_west_spacing = west;

    invalidate();
}

void CardStackLayout::setAllAlignment(bool horcenter, bool vercenter)
{
    m_horizontal_center = horcenter;
    m_vertical_center = vercenter;
    invalidate();
}

猜你喜欢

转载自blog.csdn.net/u010383605/article/details/78636375