如果不了解Qt drag-drop 的建议先看一下 Qt 实现拖放内容 drag - drop 【简单明了】
否则看起来会一头雾水
看一下官方的介绍:
译文:这个例子是一个简单的拼图游戏的实现,它使用了Qt的模型/视图框架提供的对拖放的内置支持。拖放拼图的例子展示了许多相同的特性,但是采用了另一种方法,即在应用程序级别使用Qt的拖放API来处理拖放操作。
这个拼图的demo 还是能学到东西的
项目叫做 puzzle 大家可以去官方demo 里找一下 玩一玩
main
mainwindow 主界面
piecesmodel 拼图块模型
puzzlewidget 拼图窗口
初始化了资源
实例化了主窗体
加载了一个图片
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
#include
#include
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);
}
把这个几个函数的实现都挨个看吧
用了 水平布局 把 左边的 listView 和 右边的 puzzleWidget 合起来
listview 设置了可以拖拽
设置了 view mode 是icon
设置了 icon 的大小 puzzleWidget->pieceSize() 是多少 等会去看这个类
设置 网格的大小
设置间距
设置 item 移动时 吸附到指定的网格上;
设置 item 在拖动和删除项时是否显示拖放指示器。
然后给 listview 设置自定义的 model
打开图片
把.h 里面声明的 puzzleImage 赋值 新读进来的图片
这两句的 意思是
把图片 缩放为 为 size 的正方形
size 是 取最小的一方 比如 800*600 的图片 那么就是取 600
取 600 也不是从 0到600
而是去取 中间的 600
为什么?
(puzzleImage.width() - size) / 2
( 800 -600 /2) =100
从 100 的位置 开始取 取600 【100,700】
用笔在纸上画一下就明白了
然后把缩放好的图片 给到 model
model 的实现 我们下面看
#include
#include
#include
#include
#include
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 的要去看一下 否则会看不懂
前面的 几个函数 就是重载 基类的函数
只有这几个是自己实现的
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 里会说
然后把这些 图片格子(拼图块) 以随机的方式 插入到list里面
有的是在头部 有的是在尾部插入 打乱了顺序
获取模型的数据 根据枚举的不同 返回的类型不同
有 icon 有 pixmap 有 位置
userRole 是我们自定义的
仔细看 逻辑是这样的
当拖走了一个拼图块 左边部分 那么 剩余的图块会进行一个排序 从拖走的位置 开始补齐
而不是 空着那个位置
这里要明白 必须要看我上面发的链接 这个就是包装拖拽数据的类的密码头
包装我们的数据 我们的拼图为啥能从 左边的 widget 移动到 一个 widget
是因为 drag 和 drop 的实现
其实就是把 左边的 数据 发送到 右边的窗口 在把他画出来
这里数据的封装 必须用 QMimeData
这块不明白的去看文章头部的链接 看完就懂了
把 pixmap 和 位置 以数据流的形式写到了 QbyteArray 然后给到 QMimeData
这个地方就用了 我们说的自定义的用户的枚举 来获取不同的信息
又学到了一点
这个函数 其实是 拖到这里放下的函数 因为我们可以把拼图从右边拖回到左边
先判断 hasFormat mimeTypes 对不对
然后 把 包装的数据 (拼图)解包
把数据插入 然后 把每个拼图 向后移动一个位置 把它塞进去
ok 这边的整个类就结束了 对这块不了解的 可能看不太懂
我觉得我说的够详细了
继续看 右边的拼图类
#include
#include
#include
#include
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;
};
这边 还是重载了基类的方法
拖拽进入事件
拖拽离开事件
拖拽移动事件
放下事件
鼠标按压事件
绘图事件
一个个看吧
设置 接收拖拽放下事件 这个必须要写 不写接收不到拖拽放下事件
设置窗口的固定大小 就是加载的图片处理完后的的大小
还是判断 mimeType
比如 快递是我们要的东西 才能签收
这里是 拖拽移动时 绘制后面的 高亮矩形块
下面的 放下事件 和 点击事件
都是 把数据包装 删除拼图 各种或者 把数据拆开 添加进容器 绘制出来
和上面 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 结构