Qt 拖拽實現拼圖 【官方demo源碼超級詳細解讀】

前言:

如果不瞭解Qt drag-drop 的建議先看一下 Qt 實現拖放內容 drag - drop 【簡單明瞭】

否則看起來會一頭霧水

看一下官方的介紹:

譯文:這個例子是一個簡單的拼圖遊戲的實現,它使用了Qt的模型/視圖框架提供的對拖放的內置支持。拖放拼圖的例子展示了許多相同的特性,但是採用了另一種方法,即在應用程序級別使用Qt的拖放API來處理拖放操作。

這個拼圖的demo 還是能學到東西的

在這裏插入圖片描述

項目叫做 puzzle 大家可以去官方demo 裏找一下 玩一玩

項目的結構如下
在這裏插入圖片描述

main
mainwindow 主界面
piecesmodel 拼圖塊模型
puzzlewidget 拼圖窗口

main :

初始化了資源
實例化了主窗體
加載了一個圖片

圖片就是下面
在這裏插入圖片描述

mainWindow.h

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);

public slots:
    void openImage();
    void loadImage(const QString &path);
    void setupPuzzle();

private slots:
    void setCompleted();

private:
    void setupMenus();
    void setupWidgets();

    QPixmap puzzleImage;
    QListView *piecesList;
    PuzzleWidget *puzzleWidget;
    PiecesModel *model;
};

類也不復雜

private:
一個 存放 拼圖照片的 pixmap
一個 存放左邊拼圖塊的 listview
右邊的拼圖窗口
拼圖的塊模型

他是自定義了 模型 等會看

整個的結構是這樣的
在這裏插入圖片描述

#include "mainwindow.h"
#include "piecesmodel.h"
#include "puzzlewidget.h"
#include <QDebug>
#include <QtWidgets>
#include <stdlib.h>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    setupMenus();
    setupWidgets();
    model = new PiecesModel(puzzleWidget->pieceSize(), this);
    piecesList->setModel(model);

    setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
    setWindowTitle(tr("Puzzle"));
}

void MainWindow::openImage()
{
    const QString fileName =
        QFileDialog::getOpenFileName(this,
                                     tr("Open Image"), QString(),
                                     tr("Image Files (*.png *.jpg *.bmp)"));
    if (!fileName.isEmpty())
        loadImage(fileName);
}

void MainWindow::loadImage(const QString &fileName)
{
    QPixmap newImage;
    if (!newImage.load(fileName)) {
        QMessageBox::warning(this, tr("Open Image"),
                             tr("The image file could not be loaded."),
                             QMessageBox::Cancel);
        return;
    }
    puzzleImage = newImage;
    setupPuzzle();
}

void MainWindow::setCompleted()
{
    QMessageBox::information(this, tr("Puzzle Completed"),
                             tr("Congratulations! You have completed the puzzle!\n"
                                "Click OK to start again."),
                             QMessageBox::Ok);

    setupPuzzle();
}

void MainWindow::setupPuzzle()
{
    int size = qMin(puzzleImage.width(), puzzleImage.height());


    puzzleImage = puzzleImage.copy((puzzleImage.width() - size) / 2,
        (puzzleImage.height() - size) / 2, size, size).scaled(puzzleWidget->imageSize(),
            puzzleWidget->imageSize(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);

    qsrand(QCursor::pos().x() ^ QCursor::pos().y());

    model->addPieces(puzzleImage);
    puzzleWidget->clear();
}

void MainWindow::setupMenus()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));

    QAction *openAction = fileMenu->addAction(tr("&Open..."));
    openAction->setShortcuts(QKeySequence::Open);

    QAction *exitAction = fileMenu->addAction(tr("E&xit"));
    exitAction->setShortcuts(QKeySequence::Quit);

    QMenu *gameMenu = menuBar()->addMenu(tr("&Game"));

    QAction *restartAction = gameMenu->addAction(tr("&Restart"));

    connect(openAction, &QAction::triggered, this, &MainWindow::openImage);
    connect(exitAction, &QAction::triggered, qApp, &QCoreApplication::quit);
    connect(restartAction, &QAction::triggered, this, &MainWindow::setupPuzzle);
}

void MainWindow::setupWidgets()
{
    QFrame *frame = new QFrame;
    QHBoxLayout *frameLayout = new QHBoxLayout(frame);

    puzzleWidget = new PuzzleWidget(400);

    piecesList = new QListView;
    piecesList->setDragEnabled(true);
    piecesList->setViewMode(QListView::IconMode);
    piecesList->setIconSize(QSize(puzzleWidget->pieceSize() - 20, puzzleWidget->pieceSize() - 20));
    piecesList->setGridSize(QSize(puzzleWidget->pieceSize(), puzzleWidget->pieceSize()));
    piecesList->setSpacing(10);
    piecesList->setMovement(QListView::Snap);
    piecesList->setAcceptDrops(true);
    piecesList->setDropIndicatorShown(true);

    PiecesModel *model = new PiecesModel(puzzleWidget->pieceSize(), this);
    piecesList->setModel(model);

    connect(puzzleWidget, &PuzzleWidget::puzzleCompleted,
            this, &MainWindow::setCompleted, Qt::QueuedConnection);

    frameLayout->addWidget(piecesList);
    frameLayout->addWidget(puzzleWidget);
    setCentralWidget(frame);
}

把這個幾個函數的實現都挨個看吧

構造:

在這裏插入圖片描述

setupMenus()

在這裏插入圖片描述
頂部的菜單欄 沒啥說的

setupWidgets()

在這裏插入圖片描述

用了 水平佈局 把 左邊的 listView 和 右邊的 puzzleWidget 合起來

listview 設置了可以拖拽
設置了 view mode 是icon
設置了 icon 的大小 puzzleWidget->pieceSize() 是多少 等會去看這個類
設置 網格的大小
設置間距
設置 item 移動時 吸附到指定的網格上;
設置 item 在拖動和刪除項時是否顯示拖放指示器。

然後給 listview 設置自定義的 model

openImage()

在這裏插入圖片描述

打開圖片

loadImage()

在這裏插入圖片描述

把.h 裏面聲明的 puzzleImage 賦值 新讀進來的圖片

setupPuzzle()

在這裏插入圖片描述

這兩句的 意思是
把圖片 縮放爲 爲 size 的正方形
size 是 取最小的一方 比如 800*600 的圖片 那麼就是取 600
取 600 也不是從 0到600
而是去取 中間的 600
爲什麼?

(puzzleImage.width() - size) / 2
( 800 -600 /2) =100
從 100 的位置 開始取 取600 【100,700】

用筆在紙上畫一下就明白了

然後把縮放好的圖片 給到 model

model 的實現 我們下面看

PiecesModel.h

#include <QAbstractListModel>
#include <QList>
#include <QPixmap>
#include <QPoint>
#include <QStringList>

QT_BEGIN_NAMESPACE
class QMimeData;
QT_END_NAMESPACE

class PiecesModel : public QAbstractListModel
{
    Q_OBJECT

public:
    explicit PiecesModel(int pieceSize, QObject *parent = 0);

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool removeRows(int row, int count, const QModelIndex &parent) override;

    bool dropMimeData(const QMimeData *data, Qt::DropAction action,
                      int row, int column, const QModelIndex &parent) override;
    QMimeData *mimeData(const QModelIndexList &indexes) const override;
    QStringList mimeTypes() const override;
    int rowCount(const QModelIndex &parent) const override;
    Qt::DropActions supportedDropActions() const override;

    void addPiece(const QPixmap &pixmap, const QPoint &location);
    void addPieces(const QPixmap& pixmap);

private:
    QList<QPoint> locations;
    QList<QPixmap> pixmaps;

    int m_PieceSize;
};

不瞭解 自定義 model 的要去看一下 否則會看不懂

前面的 幾個函數 就是重載 基類的函數

在這裏插入圖片描述

只有這幾個是自己實現的

addPieces(const QPixmap& pixmap)

在這裏插入圖片描述
剛纔 我們處理好的圖片 就是傳遞給了這個函數

beginRemoveRows(QModelIndex(), 0, 24);
endRemoveRows();

只有我們重載了 基類的 removeRows 函數 上面的就必須要寫
在這裏插入圖片描述

for (int y = 0; y < 5; ++y) {
    for (int x = 0; x < 5; ++x) {
        QPixmap pieceImage = pixmap.copy(x*m_PieceSize, y*m_PieceSize, m_PieceSize, m_PieceSize);
        addPiece(pieceImage, QPoint(x, y));
    }
}

把傳進來的圖片 分成了 5行5列 的 25個 格子 每個格子的像素是 m_PieceSize
他是多少 等會 puzzleWidget 裏會說

addPiece(const QPixmap &pixmap, const QPoint &location)

在這裏插入圖片描述

然後把這些 圖片格子(拼圖塊) 以隨機的方式 插入到list裏面
有的是在頭部 有的是在尾部插入 打亂了順序

data(const QModelIndex &index, int role) const

在這裏插入圖片描述

獲取模型的數據 根據枚舉的不同 返回的類型不同
有 icon 有 pixmap 有 位置

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

userRole 是我們自定義的

removeRows(int row, int count, const QModelIndex &parent)

在這裏插入圖片描述

這裏可能有人看不懂了 我給你們弄個 gif
在這裏插入圖片描述

仔細看 邏輯是這樣的

當拖走了一個拼圖塊 左邊部分 那麼 剩餘的圖塊會進行一個排序 從拖走的位置 開始補齊
而不是 空着那個位置

mimeTypes() const

在這裏插入圖片描述

這裏要明白 必須要看我上面發的鏈接 這個就是包裝拖拽數據的類的密碼頭

mimeData(const QModelIndexList &indexes) const

在這裏插入圖片描述

包裝我們的數據 我們的拼圖爲啥能從 左邊的 widget 移動到 一個 widget

是因爲 drag 和 drop 的實現
其實就是把 左邊的 數據 發送到 右邊的窗口 在把他畫出來
這裏數據的封裝 必須用 QMimeData
這塊不明白的去看文章頭部的鏈接 看完就懂了

把 pixmap 和 位置 以數據流的形式寫到了 QbyteArray 然後給到 QMimeData
在這裏插入圖片描述

這個地方就用了 我們說的自定義的用戶的枚舉 來獲取不同的信息

又學到了一點

dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)

這個函數 其實是 拖到這裏放下的函數 因爲我們可以把拼圖從右邊拖回到左邊

在這裏插入圖片描述
在這裏插入圖片描述

先判斷 hasFormat mimeTypes 對不對
然後 把 包裝的數據 (拼圖)解包

把數據插入 然後 把每個拼圖 向後移動一個位置 把它塞進去

ok 這邊的整個類就結束了 對這塊不瞭解的 可能看不太懂
我覺得我說的夠詳細了

繼續看 右邊的拼圖類

PuzzleWidget.h

#include <QList>
#include <QPixmap>
#include <QPoint>
#include <QWidget>

QT_BEGIN_NAMESPACE
class QDragEnterEvent;
class QDropEvent;
class QMouseEvent;
QT_END_NAMESPACE

class PuzzleWidget : public QWidget
{
    Q_OBJECT

public:
    explicit PuzzleWidget(int imageSize, QWidget *parent = 0);
    void clear();

    int pieceSize() const;
    int imageSize() const;

signals:
    void puzzleCompleted();

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

private:
    int findPiece(const QRect &pieceRect) const;
    const QRect targetSquare(const QPoint &position) const;

    QList<QPixmap> piecePixmaps;
    QList<QRect> pieceRects;
    QList<QPoint> pieceLocations;
    QRect highlightedRect;
    int inPlace;
    int m_ImageSize;
};

這邊 還是重載了基類的方法

拖拽進入事件
拖拽離開事件
拖拽移動事件
放下事件
鼠標按壓事件
繪圖事件

一個個看吧

構造

在這裏插入圖片描述

設置 接收拖拽放下事件 這個必須要寫 不寫接收不到拖拽放下事件

設置窗口的固定大小 就是加載的圖片處理完後的的大小

dragEnterEvent(QDragEnterEvent *event)

在這裏插入圖片描述

還是判斷 mimeType
比如 快遞是我們要的東西 才能簽收

dragMoveEvent(QDragMoveEvent *event)

在這裏插入圖片描述

這裏是 拖拽移動時 繪製後面的 高亮矩形塊

painter 繪製背部高亮矩形框和拼圖

在這裏插入圖片描述

在這裏插入圖片描述

下面的 放下事件 和 點擊事件
都是 把數據包裝 刪除拼圖 各種或者 把數據拆開 添加進容器 繪製出來
和上面 model 的實現類似

void PuzzleWidget::dropEvent(QDropEvent *event)
{
    if (event->mimeData()->hasFormat("image/x-puzzle-piece")
        && findPiece(targetSquare(event->pos())) == -1) {

        QByteArray pieceData = event->mimeData()->data("image/x-puzzle-piece");
        QDataStream stream(&pieceData, QIODevice::ReadOnly);
        QRect square = targetSquare(event->pos());
        QPixmap pixmap;
        QPoint location;
        stream >> pixmap >> location;

        pieceLocations.append(location);
        piecePixmaps.append(pixmap);
        pieceRects.append(square);

        highlightedRect = QRect();
        update(square);

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

        if (location == QPoint(square.x()/pieceSize(), square.y()/pieceSize())) {
            inPlace++;
            if (inPlace == 25)
                emit puzzleCompleted();
        }
    } else {
        highlightedRect = QRect();
        event->ignore();
    }
}

int PuzzleWidget::findPiece(const QRect &pieceRect) const
{
    for (int i = 0; i < pieceRects.size(); ++i) {
        if (pieceRect == pieceRects[i])
            return i;
    }
    return -1;
}

void PuzzleWidget::mousePressEvent(QMouseEvent *event)
{
    QRect square = targetSquare(event->pos());
    int found = findPiece(square);

    if (found == -1)
        return;

    QPoint location = pieceLocations[found];
    QPixmap pixmap = piecePixmaps[found];
    pieceLocations.removeAt(found);
    piecePixmaps.removeAt(found);
    pieceRects.removeAt(found);

    if (location == QPoint(square.x()/pieceSize(), square.y()/pieceSize()))
        inPlace--;

    update(square);

    QByteArray itemData;
    QDataStream dataStream(&itemData, QIODevice::WriteOnly);

    dataStream << pixmap << location;

    QMimeData *mimeData = new QMimeData;
    mimeData->setData("image/x-puzzle-piece", itemData);

    QDrag *drag = new QDrag(this);
    drag->setMimeData(mimeData);
    drag->setHotSpot(event->pos() - square.topLeft());
    drag->setPixmap(pixmap);

    if (drag->start(Qt::MoveAction) == 0) {
        pieceLocations.insert(found, location);
        piecePixmaps.insert(found, pixmap);
        pieceRects.insert(found, square);
        update(targetSquare(event->pos()));

        if (location == QPoint(square.x()/pieceSize(), square.y()/pieceSize()))
            inPlace++;
    }
}

寫到這裏 基本的也都說完了
太長了 我也不想寫了 就到這吧

反正就是 你要看懂這個拼圖項目 首先要搞懂 自定義model 看一下 MVD 模型
瞭解 drop 和 drag 的機制

這些在我的其他我文章都有寫 可以看一下 然後在來看這個 就比較清晰了

drop 和 drag 的機制
自定義委託
mvd 結構

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章