天天看點

拖拽之路(原生之初一):自定義QListWidget實作美觀的拖拽樣式

環境配置 :MinGW + QT 5.12

效果圖(左邊是QListWidget傳統拖拽樣式,右邊是自定義拖拽樣式):

拖拽之路(原生之初一):自定義QListWidget實作美觀的拖拽樣式
拖拽之路(原生之初一):自定義QListWidget實作美觀的拖拽樣式

這種自定義拖拽樣式的靈感來自于Chrome浏覽器的書簽欄。這篇文章命名為 “原生之初” 是因為沒有加入 設定item在滑鼠release時選中 以及 設定item在hover狀态下改變圖示樣式 的代碼,實作了最基本的自定義拖拽樣式。本文中拖拽的特點是:拖拽即選中。

實作功能及方法:

  • 拖拽功能實作:繼承QListWiget(重寫drag事件)
  • 繪制dropIndicator:繼承QListWiget(使用update()進行控制) + 繼承QStyledItemDelegate (使用畫筆進行繪制)

拖拽時縮略圖thumbnail類:

  • Qt繪制形狀不規則視窗(一)

下面幾篇文章除了 “原生之初” 都加入了 設定item在滑鼠release時選中 以及 設定item在hover狀态下改變圖示樣式 的代碼:

  • 拖拽之路(原生之初一):自定義QListWidget實作美觀的拖拽樣式
  • 拖拽之路(原生之初二):自定義QListView實作美觀的拖拽樣式
  • 拖拽之路(一):自定義QListWidget實作美觀的拖拽樣式(拖拽即選中)
  • 拖拽之路(二):自定義QListWidget實作美觀的拖拽樣式(拖拽不影響選中)
  • 拖拽之路(三):自定義QListView實作美觀的拖拽樣式(拖拽即選中)
  • 拖拽之路(四):自定義QListView實作美觀的拖拽樣式(拖拽不影響選中)
  • 拖拽之路(五):自定義QListWidget實作美觀的拖拽樣式(拖拽不影響選中 + doAutoScroll)

(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"); }

protected:
    void dragEnterEvent(QDragEnterEvent *event) override;
    void dragLeaveEvent(QDragLeaveEvent *event) override;
    void dragMoveEvent(QDragMoveEvent *event) override;
    void dropEvent(QDropEvent *event) override;
    void startDrag(Qt::DropActions supportedActions) override;

private:
    bool IsDraging = false;
    QRect oldHighlightedRect;
    QRect theHighlightedRect;
    int theHighlightedRow = -1;
    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);
}

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();
    }
}

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

    update(theHighlightedRect);

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

    event->accept();
}

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->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消失

        //因為是拖拽即選中,是以可以調用父類dropEvent(event)
        QListWidget::dropEvent(event);
        
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

//使用startDrag()則不需要判斷拖拽距離
void TestListWidget::startDrag(Qt::DropActions)
{
    QListWidgetItem *theDragItem = currentItem();
    theDragRow = row(theDragItem);

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

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

//[2]設定拖拽時的縮略圖,thumbnail類(找機會我會寫一篇單獨的文章介紹)是繼承自QWidget的類橢圓形半透明視窗,使用grab()将QWidget變成QPixmap。
    thumbnail *DragImage = new thumbnail(this);
    DragImage->setupthumbnail(icon, 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]

    if(drag->exec(Qt::MoveAction) == Qt::MoveAction){
    }
}

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)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;}
    int selectedRow() const {return theSelectedRow;}
    static QString myMimeType() { return QStringLiteral("TestListWidget/text-icon"); }

protected:
    void mousePressEvent(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;h
    QRect oldHighlightedRect;
    QRect theHighlightedRect;
    int theHighlightedRow = -1;
    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);
}

void TestListWidget::mousePressEvent(QMouseEvent *event)
{
    QListWidget::mousePressEvent(event);  //繼承父類mousePressEvent(event)
    if(event->buttons() & Qt::LeftButton){
        startPos = event->pos();
    }
}

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

        QListWidgetItem *theDragItem = currentItem();
        theDragRow = row(theDragItem);

        QString text = theDragItem->text();
        QIcon icon = theDragItem->icon();
        QByteArray itemData;
        QDataStream dataStream(&itemData, QIODevice::WriteOnly);
        dataStream << text << icon;

        QMimeData *mimeData = new QMimeData;
        mimeData->setData(myMimeType(), itemData);

        thumbnail *DragImage = new thumbnail(this);
        DragImage->setupthumbnail(icon, 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));

        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();
    }
}

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

    update(theHighlightedRect);

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

    event->accept();
}

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->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消失

        //因為是拖拽即選中,是以可以調用父類dropEvent(event)
        QListWidget::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);
}
           

(3)TestItemDelegate類繼承自QStyledItemDelegate,主要是為了繪制dropIndicator。圖示為dropIndicator組成:

拖拽之路(原生之初一):自定義QListWidget實作美觀的拖拽樣式
  • 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)){

        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());

        }
    }

//begin drag
    if(isDraging){
        int theDragRow = dragWidget->dragRow();
        int UpRow = dragWidget->highlightedRow();
        int DownRow = UpRow + 1;
        int rowCount = dragWidget->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){
                //到達頭部,三角形向下移動一個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);
}
           

(4)使用TestListWidget和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);

    QListWidgetItem *item1 = new QListWidgetItem(listwidget);
    item1->setIcon(QIcon(":/listBar_Icon/1_hover.png"));
    item1->setText("發現音樂");

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

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

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

    QListWidgetItem *item5 = new QListWidgetItem(listwidget);
    item5->setIcon(QIcon(":/listBar_Icon/5_hover.png"));
    item5->setText("本地音樂");

    QListWidgetItem *item6 = new QListWidgetItem(listwidget);
    item6->setIcon(QIcon(":/listBar_Icon/6_hover.png"));
    item6->setText("下載下傳管理");

    QListWidgetItem *item7 = new QListWidgetItem(listwidget);
    item7->setIcon(QIcon(":/listBar_Icon/7_hover.png"));
    item7->setText("我的音樂雲盤");

    QListWidgetItem *item8 = new QListWidgetItem(listwidget);
    item8->setIcon(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)。

拖拽之路(原生之初一):自定義QListWidget實作美觀的拖拽樣式
環境配置 :MinGW + QT 5.12

繼續閱讀