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 结构

发布了194 篇原创文章 · 获赞 443 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/weixin_42837024/article/details/105558694