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

环境配置 :MinGW + QT 5.12
效果图(左边是QListWidget传统拖拽样式,右边是自定义拖拽样式):

这种自定义拖拽样式的灵感来自于Chrome浏览器的书签栏。文章中所使用的自定义QListWidget来自于:自定义QListWidget实现item被hover时改变图标样式(模仿网易云音乐选项列表)(方法二)中的TestListWidget类的拓展(在之前的基础上加入了拖拽的代码)。本文中拖拽的特点是:拖拽即选中


实现的功能及方法:

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

拖拽时缩略图thumbnail类:

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


这里需要说明一下:QListWidget是鼠标press时item就会被选中,自定义的TestListWidget类重写了mousePressEvent和mouseReleaseEvent使得item在鼠标release时才会被选中。至于为什么这样做,是因为Chrome浏览器的书签栏以及网易云音乐的选项列表都是在鼠标release时才会触发选中…
正是由于item在鼠标release时才会被选中,所以才会出现 拖拽即选中拖拽不影响选中 两种情况,而QListWidget是鼠标press时item就会被选中,所以只能是 拖拽即选中(拖拽的条件是press,导致item在触发拖拽时就被选中)。


(0)TestListWidgetItem类继承自QListWidgetItem

  • TestListWidgetItem.h文件:
class TestListWidgetItem : public QListWidgetItem
{
    //Q_OBJECT  //由于QListWidgetItem没有QObject属性,所以Q_OBJECT需要注释掉
public:
    explicit TestListWidgetItem(QListWidget *view = nullptr);
    void setUpIcon(const QIcon &icon, const QIcon &icon_hover);

    QIcon Img;
    QIcon Img_hover;
};
  • TestListWidgetItem.c文件:
TestListWidgetItem::TestListWidgetItem(QListWidget *view) :
    QListWidgetItem(view)
{
}

void TestListWidgetItem::setUpIcon(const QIcon &icon, const QIcon &icon_hover)
{
    Img = icon;
    Img_hover = icon_hover;
    setIcon(Img);
}

(1)TestListWidget类继承自QListWidget

  • TestListWidget.h文件:
class TestListWidget : public QListWidget
{
    Q_OBJECT

public:
    explicit TestListWidget(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("TestListWidget/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;
    QRect oldHighlightedRect;
    QRect theHighlightedRect;
    int theHighlightedRow = -2;
    int theDragRow = -1;

    const QRect targetRect(const QPoint &position) const;
};
  • TestListWidget.c文件:
TestListWidget::TestListWidget(QWidget *parent) :
    QListWidget(parent)
{
    //setMouseTracking(true);  //注释与否没有影响
    //setDragEnabled(true);  //注释与否没有影响
    setAcceptDrops(true);
    //setDropIndicatorShown(false);  //注释与否没有影响
    //setDefaultDropAction(Qt::MoveAction);  //注释与否没有影响
}

//拖拽起点
void TestListWidget::mousePressEvent(QMouseEvent *event)
{
    if(event->buttons() & Qt::LeftButton){
        startPos = event->pos();
    }
}

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

    TestListWidgetItem *item = static_cast<TestListWidgetItem *>(itemAt(event->pos()));
    setCurrentItem(item);
}

void TestListWidget::mouseMoveEvent(QMouseEvent *event)
{
    if(event->buttons() & Qt::LeftButton){
//[1]超过规定距离才会触发拖拽,防止手滑...
        if((event->pos() - startPos).manhattanLength() < QApplication::startDragDistance()) return;
//[1]

        TestListWidgetItem *theDragItem = static_cast<TestListWidgetItem *>(itemAt(startPos));
        setCurrentItem(theDragItem);  //拖拽即选中
        
        theDragRow = row(theDragItem);
        
//[2]这部分是把拖拽的数据放在QMimeData容器中(参考Qt例程puzzle,使用QByteArray和QDataStream感觉很方便)
        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);
//[2]


//[3]设置拖拽时的缩略图,thumbnail类(找机会我会写一篇单独的文章介绍)是继承自QWidget的类椭圆形半透明窗口,使用grab()将QWidget变成QPixmap。
        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));
//[3]

        //拖拽开始!
        if(drag->exec(Qt::MoveAction) == Qt::MoveAction){
        }
    }
}

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

//当拖拽离开QListWidget时,需要update以保证DropIndicator消失
void TestListWidget::dragLeaveEvent(QDragLeaveEvent *event)
{
    theHighlightedRow = -2;
    update(theHighlightedRect);

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

    event->accept();
}

//拖拽移动时刷新以更新DropIndicator
void TestListWidget::dragMoveEvent(QDragMoveEvent *event)
{
    TestListWidget *source = qobject_cast<TestListWidget *>(event->source());
    if (source && source == this) {

        oldHighlightedRect = theHighlightedRect;
        theHighlightedRect = targetRect(event->pos());

        //offset() = 19(这个数值是我调用父类的dropEvent(event)一次一次试出来的,我觉得公式应该是19 = 40 / 2 - 1, 其中40是item行高)
        if(event-o>pos().y() >= offset()){

            theHighlightedRow = row(itemAt(event->pos() - QPoint(0, offset())));

            if(oldHighlightedRect != theHighlightedRect){
                update(oldHighlightedRect);  //刷新旧区域使DropIndicator消失
                update(theHighlightedRect);  //刷新新区域使DropIndicator显示
            }else
                update(theHighlightedRect);
        }else{
            theHighlightedRow = -1;
            update(QRect(0, 0, width(), 80));  //仅刷新第一行
        }

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

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

        IsDraging = false;  //拖拽完成

        theHighlightedRow = -2;
        update(theHighlightedRect);  //拖拽完成,刷新以使DropIndicator消失

        QListWidget::dropEvent(event);  //因为是拖拽即选中,所以可以直接调用父类的dropEvent(event)

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

const QRect TestListWidget::targetRect(const QPoint &position) const
{
    //40是item的行高
    if(position.y() >= offset())
        return QRect(0, (position.y() - offset()) / 40 * 40, width(), 2 * 40);
    else
        return QRect(0, 0, width(), 40);
}

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

在这里插入图片描述

  • TestItemDelegate.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;
};
  • TestItemDelegate.c文件:
TestItemDelegate::TestItemDelegate(QObject *parent)
    : QStyledItemDelegate(parent)
{
}

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

    QRect rect = option.rect;

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

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

        TestListWidgetItem *item = static_cast<TestListWidgetItem *>(dragWidget->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{
        TestListWidgetItem *item = static_cast<TestListWidgetItem *>(dragWidget->item(index.row()));
        item->setIcon(item->Img);
    }

//begin drag
    if(isDraging){
        int theDragRow = dragWidget->dragRow();
        int UpRow = dragWidget->highlightedRow();
        int DownRow = UpRow + 1;
        int rowCount = dragWidget->count() - 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){
                //到达头部,三角形向下移动一个WIDTH的距离,以使分隔符宽度*2
                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{
                //正常情况,组成下半部分(+1是根据实际情况修正)
                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);
}


(3)使用TestListWidgetItem和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);

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

    TestItemDelegate *delegate = new TestItemDelegate();
    listwidget->setItemDelegate(delegate);

    TestListWidgetItem *item1 = new TestListWidgetItem(listwidget);
    item1->setUpIcon(QIcon(":/listBar_Icon/1.png"), QIcon(":/listBar_Icon/1_hover.png"));
    item1->setText("发现音乐");

    TestListWidgetItem *item2 = new TestListWidgetItem(listwidget);
    item2->setUpIcon(QIcon(":/listBar_Icon/2.png"), QIcon(":/listBar_Icon/2_hover.png"));
    item2->setText("私人FM");

    TestListWidgetItem *item3 = new TestListWidgetItem(listwidget);
    item3->setUpIcon(QIcon(":/listBar_Icon/3.png"), QIcon(":/listBar_Icon/3_hover.png"));
    item3->setText("朋友");

    TestListWidgetItem *item4 = new TestListWidgetItem(listwidget);
    item4->setUpIcon(QIcon(":/listBar_Icon/4.png"), QIcon(":/listBar_Icon/4_hover.png"));
    item4->setText("MV");

    TestListWidgetItem *item5 = new TestListWidgetItem(listwidget);
    item5->setUpIcon(QIcon(":/listBar_Icon/5.png"), QIcon(":/listBar_Icon/5_hover.png"));
    item5->setText("本地音乐");

    TestListWidgetItem *item6 = new TestListWidgetItem(listwidget);
    item6->setUpIcon(QIcon(":/listBar_Icon/6.png"), QIcon(":/listBar_Icon/6_hover.png"));
    item6->setText("下载管理");

    TestListWidgetItem *item7 = new TestListWidgetItem(listwidget);
    item7->setUpIcon(QIcon(":/listBar_Icon/7.png"), QIcon(":/listBar_Icon/7_hover.png"));
    item7->setText("我的音乐云盘");

    TestListWidgetItem *item8 = new TestListWidgetItem(listwidget);
    item8->setUpIcon(QIcon(":/listBar_Icon/8.png"), QIcon(":/listBar_Icon/8_hover.png"));
    item8->setText("我的收藏");

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

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

环境配置 :MinGW + QT 5.12
  • 6
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以通过以下步骤实现QListWidget拖拽功能: 1. 设置QListWidget拖拽模式为QtWidgets.QAbstractItemView.DragDrop模式,可以在类的构造函数中使用setDragDropMode()函数实现。 2. 设置QListWidget中Item的拖拽行为,默认情况下QListWidgetItem是可以被拖拽并创建副本的,可以通过重载QListWidgetItem的mimeData()函数实现自定义拖拽数据。 3. 设置QListWidget拖拽事件,可以使用dragEnterEvent()、dragLeaveEvent()、dragMoveEvent()和dropEvent()等函数实现。 下面是一个简单的例子,演示如何在pyqt6中实现QListWidget拖拽功能: ``` python from PyQt6 import QtCore, QtGui, QtWidgets class MyListWidget(QtWidgets.QListWidget): def __init__(self, parent=None): super().__init__(parent) self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: item = self.itemAt(event.pos()) if item and item.isSelected(): # 记录拖拽项的数据 mimeData = QtCore.QMimeData() mimeData.setText(item.text()) drag = QtGui.QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - item.pos()) # 开始拖拽操作 drag.exec_(QtCore.Qt.MoveAction) def dragEnterEvent(self, event): if event.mimeData().hasText(): event.acceptProposedAction() def dragMoveEvent(self, event): if event.mimeData().hasText(): event.acceptProposedAction() def dropEvent(self, event): if event.mimeData().hasText(): itemText = event.mimeData().text() item = QtWidgets.QListWidgetItem(itemText, self) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled) event.acceptProposedAction() if __name__ == '__main__': app = QtWidgets.QApplication([]) win = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(win) listWidget = MyListWidget(win) listWidget.addItem('item1') listWidget.addItem('item2') listWidget.addItem('item3') layout.addWidget(listWidget) win.show() app.exec_() ``` 在上面的例子中,重载了MyListWidget类,通过重载mousePressEvent()函数记录待拖拽项的数据,然后在拖拽事件中实现拖拽的各个操作。通过这些步骤,就可以实现QListWidget拖拽功能了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值