拖拽之路(三):自定义QListView实现美观的拖拽样式(拖拽即选中)

环境配置 :MinGW + QT 5.12
效果图(左边是QListView传统拖拽样式(感觉很不正常),右边是自定义拖拽样式):

这种自定义拖拽样式的灵感来自于Chrome浏览器的书签栏。本文中拖拽的特点是:拖拽即选中

实现功能及方法:

  • 拖拽功能实现:继承QListView(重写drag事件)
  • item在release时被选中:继承QListView (重写mousePressEvent和mouseReleaseEvent)
  • item在被hover时改变图标样式:继承QStandardItem + 继承QStyledItemDelegate
  • 绘制dropIndicator:继承QListView(使用update()进行控制) + 继承QStyledItemDelegate (使用画笔进行绘制)

拖拽时缩略图thumbnail类:

推荐其它关于拖拽的文章:


这里需要说明一下:QListView是鼠标press时item就会被选中,自定义的TestListView类重写了mousePressEvent和mouseReleaseEvent使得item在鼠标release时才会被选中。至于为什么这样做,是因为Chrome浏览器的书签栏以及网易云音乐的选项列表都是在鼠标release时才会触发选中…

正是由于item在鼠标release时才会被选中,所以才会出现 拖拽即选中拖拽不影响选中 两种情况,而QListView是鼠标press时item就会被选中,所以只能是 拖拽即选中(拖拽的条件是press,导致item在触发拖拽时就被选中)。


(1)TestListViewItem类继承自QStandardItem

  • TestListViewItem.h文件:
class TestListViewItem : public QStandardItem
{
public:
    explicit TestListViewItem();
    explicit TestListViewItem(const QIcon &icon, const QIcon &icon_hover, const QString &text);

    QIcon Img;
    QIcon Img_hover;
};
  • TestListViewItem.c文件:
TestListViewItem::TestListViewItem()
    :QStandardItem()
{
}

TestListViewItem::TestListViewItem(const QIcon &icon, const QIcon &icon_hover, const QString &text)
    :QStandardItem()
{
    Img = icon;
    Img_hover = icon_hover;
    setIcon(Img);
    setText(text);
}

(2)TestListView类继承自QListView

  • TestListView.h文件:
class TestListView : public QListView
{
    Q_OBJECT

public:
    explicit TestListView(QWidget *parent = nullptr);

    bool isDraging() const {return IsDraging;}
    int offset() const {return 19;}
    int highlightedRow() const {return theHighlightedRow;}
    int dragRow() const {return theDragRow;}
    static QString myMimeType() { return QStringLiteral("TestListView/text-icon-icon_hover"); }

protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void dragEnterEvent(QDragEnterEvent *event) override;
    void dragLeaveEvent(QDragLeaveEvent *event) override;
    void dragMoveEvent(QDragMoveEvent *event) override;
    void dropEvent(QDropEvent *event) override;

private:
    QPoint startPos;
    bool IsDraging = false;
    int theHighlightedRow = -2;
    int oldHighlightedRow = -2;
    int theDragRow = -1;
    int theInsertRow = -1;
};
  • TestListView.c文件:
TestListView::TestListView(QWidget *parent) :
    QListView(parent)
{
//    setMouseTracking(true);
//    setDragEnabled(true);
    setAcceptDrops(true);
//    setDropIndicatorShown(false);
//    setDefaultDropAction(Qt::MoveAction);
}

//记录拖拽初始位置
void TestListView::mousePressEvent(QMouseEvent *event)
{
    if(event->buttons() & Qt::LeftButton){
        startPos = event->pos();
    }
}

void TestListView::mouseReleaseEvent(QMouseEvent *event)
{
    if((event->pos() - startPos).manhattanLength() > 5) return;

    QModelIndex index = indexAt(event->pos());
    setCurrentIndex(index);  //鼠标relesse时才选中
}

void TestListView::mouseMoveEvent(QMouseEvent *event)
{
    if(event->buttons() & Qt::LeftButton){
        if((event->pos() - startPos).manhattanLength() < QApplication::startDragDistance()) return;

        QModelIndex theDragIndex = indexAt(startPos);
        theDragRow = theDragIndex.row();

        setCurrentIndex(theDragIndex);  //拖拽即选中

        QStandardItemModel *listModel = qobject_cast<QStandardItemModel *>(model());
        TestListViewItem *theDragItem = static_cast<TestListViewItem *>(listModel->item(theDragRow));

//[1]把拖拽数据放在QMimeData容器中
        QString text = theDragItem->text();
        QIcon icon = theDragItem->Img;
        QIcon icon_hover = theDragItem->Img_hover;
        QByteArray itemData;
        QDataStream dataStream(&itemData, QIODevice::WriteOnly);
        dataStream << text << icon << icon_hover;

        QMimeData *mimeData = new QMimeData;
        mimeData->setData(myMimeType(), itemData);
//[1]

//[2]设置拖拽时的缩略图
        thumbnail *DragImage = new thumbnail(this);
        DragImage->setupthumbnail(icon_hover, text);
        //DragImage->setIconSize(18);  //default:20
        QPixmap pixmap = DragImage->grab();

        QDrag *drag = new QDrag(this);
        drag->setMimeData(mimeData);
        drag->setPixmap(pixmap);
        drag->setHotSpot(QPoint(pixmap.width() / 2, pixmap.height() / 2));
//[2]

        //删除的行需要根据theInsertRow和theDragRow的大小关系来判断(这个也是我根据实际情况测试发现的)
        if(drag->exec(Qt::MoveAction) == Qt::MoveAction){
            int theRemoveRow = -1;
            if(theInsertRow < theDragRow) theRemoveRow = theDragRow + 1;
            else theRemoveRow = theDragRow;
            model()->removeRow(theRemoveRow);
        }
    }
}

void TestListView::dragEnterEvent(QDragEnterEvent *event)
{
    TestListView *source = qobject_cast<TestListView *>(event->source());
    if (source && source == this) {
        //IsDraging(标志位)判断是否正在拖拽
        IsDraging = true;
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

void TestListView::dragLeaveEvent(QDragLeaveEvent *event)
{
    oldHighlightedRow = theHighlightedRow;
    theHighlightedRow = -2;

    //之前QListWidget用的是update(QRect),这里用的是update(QModelIndex),当然这里也可以使用update(QRect),只是想换一种方法而已
    update(model()->index(oldHighlightedRow, 0));  //UpRow
    update(model()->index(oldHighlightedRow + 1, 0));  //DownRow

    IsDraging = false;  //IsDraging(标志位)判断是否正在拖拽

    theInsertRow = -1;
    event->accept();
}

void TestListView::dragMoveEvent(QDragMoveEvent *event)
{
    TestListView *source = qobject_cast<TestListView *>(event->source());
    if (source && source == this) {

        oldHighlightedRow = theHighlightedRow;
        theHighlightedRow = indexAt(event->pos() - QPoint(0, offset())).row();

        //offset() = 19 = 40 / 2 - 1,其中40是行高
        if(event->pos().y() >= offset()){

            if(oldHighlightedRow != theHighlightedRow){
                //刷新旧区域使dropIndicator消失
                update(model()->index(oldHighlightedRow, 0));
                update(model()->index(oldHighlightedRow + 1, 0));

                //刷新新区域使dropIndicator显示
                update(model()->index(theHighlightedRow, 0));
                update(model()->index(theHighlightedRow + 1, 0));
            }else{
                update(model()->index(theHighlightedRow, 0));
                update(model()->index(theHighlightedRow + 1, 0));
            }

            theInsertRow = theHighlightedRow + 1;
        }else{
            theHighlightedRow = -1;
            update(model()->index(0, 0));
            update(model()->index(1, 0));
            theInsertRow = 0;
        }

        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

void TestListView::dropEvent(QDropEvent *event)
{
    TestListView *source = qobject_cast<TestListView *>(event->source());
    if (source && source == this){

        IsDraging = false;  //IsDraging(标志位)判断是否正在拖拽

        oldHighlightedRow = theHighlightedRow;
        theHighlightedRow = -2;

        //刷新旧区域使dropIndicator消失
        update(model()->index(oldHighlightedRow, 0));
        update(model()->index(oldHighlightedRow + 1, 0));

        
        if(theInsertRow == theDragRow || theInsertRow == theDragRow + 1) return;

        //这里我像QListWidget那样调用父类dropEvent(event)发现不起作用(原因尚不明),没办法,只能删除旧行,插入新行
        //if(theSelectedRow == theDragRow){
            //QListView::dropEvent(event);
            //return;
        //}

        QString text;
        QIcon icon, icon_hover;
        QByteArray itemData = event->mimeData()->data(myMimeType());
        QDataStream dataStream(&itemData, QIODevice::ReadOnly);
        dataStream >> text >> icon >> icon_hover;

        model()->insertRow(theInsertRow);

        QStandardItemModel *listModel = qobject_cast<QStandardItemModel *>(model());
        listModel->setItem(theInsertRow, 0, new TestListViewItem(icon, icon_hover, text));

        setCurrentIndex(model()->index(theInsertRow, 0));  //插入行保持选中状态

        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

(3)TestItemDelegate类继承自QStyledItemDelegate,主要是为了绘制dropIndicator。图示为dropIndicator组成:

在这里插入图片描述

  • QStyledItemDelegate.h文件:
#define POLYGON 4   //等腰三角形直角边长
#define WIDTH 1     //分隔符粗细的一半

class TestItemDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    TestItemDelegate(QObject *parent = nullptr);

protected:
    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
};
  • QStyledItemDelegate.c文件:
TestItemDelegate::TestItemDelegate(QObject *parent)
    : QStyledItemDelegate(parent)
{
}

void TestItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
    TestListView *dragView = qobject_cast<TestListView *>(option.styleObject);
    bool isDraging = dragView->isDraging();

    QRect rect = option.rect;

    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setPen(Qt::NoPen);

    if(option.state & (QStyle::State_MouseOver | QStyle::State_Selected)){

        QStandardItemModel *listModel = qobject_cast<QStandardItemModel *>(dragView->model());
        TestListViewItem *item = static_cast<TestListViewItem *>(listModel->item(index.row()));
        item->setIcon(item->Img_hover);

        if(option.state & QStyle::State_MouseOver){
        }
        if(option.state & QStyle::State_Selected){
            painter->setBrush(QColor(180, 0, 0));
            painter->drawRect(rect.topLeft().x(), rect.topLeft().y(), 4, rect.height());

            painter->setBrush(QColor(230, 231, 234));
            painter->drawRect(rect.topLeft().x() + 4, rect.topLeft().y(), rect.width() - 4, rect.height());

        }
    }else{
        QStandardItemModel *listModel = qobject_cast<QStandardItemModel *>(dragView->model());
        TestListViewItem *item = static_cast<TestListViewItem *>(listModel->item(index.row()));
        item->setIcon(item->Img);
    }

//begin drag
    if(isDraging){
        int theDragRow = dragView->dragRow();
        int UpRow = dragView->highlightedRow();
        int DownRow = UpRow + 1;
        int rowCount = dragView->model()->rowCount() - 1;

//绘制DropIndicator
        if(index.row() == UpRow && index.row() != theDragRow - 1 && index.row() != theDragRow){
            painter->setBrush(QColor(66, 133, 244));

            if(UpRow == rowCount){
                //到达尾部,三角形向上移动一个WIDTH的距离,以使分隔符宽度*2
                QPolygon trianglePolygon_bottomLeft;
                trianglePolygon_bottomLeft << QPoint(rect.bottomLeft().x(), rect.bottomLeft().y() - (POLYGON + WIDTH) + 1 - WIDTH);
                trianglePolygon_bottomLeft << QPoint(rect.bottomLeft().x(), rect.bottomLeft().y() - WIDTH + 1 - WIDTH);
                trianglePolygon_bottomLeft << QPoint(rect.bottomLeft().x() + POLYGON, rect.bottomLeft().y() - WIDTH + 1 - WIDTH);

                QPolygon trianglePolygon_bottomRight;
                trianglePolygon_bottomRight << QPoint(rect.bottomRight().x() + 1, rect.bottomRight().y() - (POLYGON + WIDTH) + 1 - WIDTH);
                trianglePolygon_bottomRight << QPoint(rect.bottomRight().x() + 1, rect.bottomRight().y() - WIDTH + 1 - WIDTH);
                trianglePolygon_bottomRight << QPoint(rect.bottomRight().x() - POLYGON + 1, rect.bottomRight().y() - WIDTH + 1 - WIDTH);

                painter->drawRect(rect.bottomLeft().x(), rect.bottomLeft().y() - 2 * WIDTH + 1, rect.width(), 2 * WIDTH);  //rect
                painter->drawPolygon(trianglePolygon_bottomLeft);
                painter->drawPolygon(trianglePolygon_bottomRight);
            }
            else {
                //正常情况,组成上半部分(+1是根据实际情况修正)
                QPolygon trianglePolygon_bottomLeft;
                trianglePolygon_bottomLeft << QPoint(rect.bottomLeft().x(), rect.bottomLeft().y() - (POLYGON + WIDTH) + 1);
                trianglePolygon_bottomLeft << QPoint(rect.bottomLeft().x(), rect.bottomLeft().y() - WIDTH + 1);
                trianglePolygon_bottomLeft << QPoint(rect.bottomLeft().x() + POLYGON, rect.bottomLeft().y() - WIDTH + 1);

                QPolygon trianglePolygon_bottomRight;
                trianglePolygon_bottomRight << QPoint(rect.bottomRight().x() + 1, rect.bottomRight().y() - (POLYGON + WIDTH) + 1);
                trianglePolygon_bottomRight << QPoint(rect.bottomRight().x() + 1, rect.bottomRight().y() - WIDTH + 1);
                trianglePolygon_bottomRight << QPoint(rect.bottomRight().x() - POLYGON + 1, rect.bottomRight().y() - WIDTH + 1);

                painter->drawRect(rect.bottomLeft().x(), rect.bottomLeft().y() - WIDTH + 1, rect.width(), WIDTH);  //rect
                painter->drawPolygon(trianglePolygon_bottomLeft);
                painter->drawPolygon(trianglePolygon_bottomRight);
            }
        }
        else if(index.row() == DownRow && index.row() != theDragRow + 1 && index.row() != theDragRow){
            painter->setBrush(QColor(66, 133, 244));

            if(DownRow == 0){
                //reach the head
                QPolygon trianglePolygon_topLeft;
                trianglePolygon_topLeft << QPoint(rect.topLeft().x(), rect.topLeft().y() + (POLYGON + WIDTH) + WIDTH);
                trianglePolygon_topLeft << QPoint(rect.topLeft().x(), rect.topLeft().y() + WIDTH + WIDTH);
                trianglePolygon_topLeft << QPoint(rect.topLeft().x() + POLYGON, rect.topLeft().y() + WIDTH + WIDTH);

                QPolygon trianglePolygon_topRight;
                trianglePolygon_topRight << QPoint(rect.topRight().x() + 1, rect.topRight().y() + (POLYGON + WIDTH) + WIDTH);
                trianglePolygon_topRight << QPoint(rect.topRight().x() + 1, rect.topRight().y() + WIDTH + WIDTH);
                trianglePolygon_topRight << QPoint(rect.topRight().x() - POLYGON + 1, rect.topRight().y() + WIDTH + WIDTH);

                painter->drawRect(rect.topLeft().x(), rect.topLeft().y(), rect.width(), 2 * WIDTH);  //rect
                painter->drawPolygon(trianglePolygon_topLeft);
                painter->drawPolygon(trianglePolygon_topRight);
            }
            else{
                //normal
                QPolygon trianglePolygon_topLeft;
                trianglePolygon_topLeft << QPoint(rect.topLeft().x(), rect.topLeft().y() + (POLYGON + WIDTH));
                trianglePolygon_topLeft << QPoint(rect.topLeft().x(), rect.topLeft().y() + WIDTH);
                trianglePolygon_topLeft << QPoint(rect.topLeft().x() + POLYGON, rect.topLeft().y() + WIDTH);

                QPolygon trianglePolygon_topRight;
                trianglePolygon_topRight << QPoint(rect.topRight().x() + 1, rect.topRight().y() + (POLYGON + WIDTH));
                trianglePolygon_topRight << QPoint(rect.topRight().x() + 1, rect.topRight().y() + WIDTH);
                trianglePolygon_topRight << QPoint(rect.topRight().x() - POLYGON + 1, rect.topRight().y() + WIDTH);

                painter->drawRect(rect.topLeft().x(), rect.topLeft().y(), rect.width(), WIDTH);  //rect
                painter->drawPolygon(trianglePolygon_topLeft);
                painter->drawPolygon(trianglePolygon_topRight);
            }
        }
        QStyledItemDelegate::paint(painter, option, index);
        return;
    }
//end drag

    QStyledItemDelegate::paint(painter, option, index);
}

(4)使用TestListViewItem、TestListView和TestItemDelegate

  • 主窗口.h文件:
class test : public QWidget
{
    Q_OBJECT
public:
    explicit test(QWidget *parent = nullptr);

private:
    void initUi();
};
  • 主窗口.c文件:
test::test(QWidget *parent) : QWidget(parent)
{
    initUi();
}

void test::initUi()
{
    setFixedSize(250, 600);

    QStandardItemModel *listModel = new QStandardItemModel();
    listModel->setItem(0, 0, new TestListViewItem(QIcon(":/listBar_Icon/1.png"), QIcon(":/listBar_Icon/1_hover.png"), "发现音乐"));
    listModel->setItem(1, 0, new TestListViewItem(QIcon(":/listBar_Icon/2.png"), QIcon(":/listBar_Icon/2_hover.png"), "私人FM"));
    listModel->setItem(2, 0, new TestListViewItem(QIcon(":/listBar_Icon/3.png"), QIcon(":/listBar_Icon/3_hover.png"), "朋友"));
    listModel->setItem(3, 0, new TestListViewItem(QIcon(":/listBar_Icon/4.png"), QIcon(":/listBar_Icon/4_hover.png"), "MV"));
    listModel->setItem(4, 0, new TestListViewItem(QIcon(":/listBar_Icon/5.png"), QIcon(":/listBar_Icon/5_hover.png"), "本地音乐"));
    listModel->setItem(5, 0, new TestListViewItem(QIcon(":/listBar_Icon/6.png"), QIcon(":/listBar_Icon/6_hover.png"), "下载管理"));
    listModel->setItem(6, 0, new TestListViewItem(QIcon(":/listBar_Icon/7.png"), QIcon(":/listBar_Icon/7_hover.png"), "我的音乐云盘"));
    listModel->setItem(7, 0, new TestListViewItem(QIcon(":/listBar_Icon/8.png"), QIcon(":/listBar_Icon/8_hover.png"), "我的收藏"));

    TestListView *listView = new TestListView(this);
    listView->setIconSize(QSize(25, 25));
    listView->setFocusPolicy(Qt::NoFocus);  //这样可禁用tab键和上下方向键并且除去复选框
    listView->setFixedHeight(320);
    listView->setFont(QFont("宋体", 10, QFont::DemiBold));
    listView->setStyleSheet(
                //"*{outline:0px;}"  //除去复选框
                "QListView{background:rgb(245, 245, 247); border:0px; margin:0px 0px 0px 0px;}"
                "QListView::Item{height:40px; border:0px; padding-left:14px; color:rgba(200, 40, 40, 255);}"
                "QListView::Item:hover{color:rgba(40, 40, 200, 255); padding-left:14px;}"
                "QListView::Item:selected{color:rgba(40, 40, 200, 255); padding-left:15px;}"
                );

    TestItemDelegate *delegate = new TestItemDelegate();
    listView->setItemDelegate(delegate);
    listView->setModel(listModel);

    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->setSpacing(0);
    layout->addWidget(listView);
    layout->setContentsMargins(0, 0, 0, 0);
    setLayout(layout);
}

如果想要接触更多关于拖拽的代码,在Qt例程中搜索“drag”。推荐看一下例程puzzle的两种实现方法(一种是继承QListWidget,另一种是QListView + 继承QAbstractListModel)。
在这里插入图片描述

环境配置 :MinGW + QT 5.12
  • 8
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值