用Qt图形视图框架开发拼图游戏

用Qt的图形视图框架(Graphics View Framework)做了一个拼图游戏DEMO,演示了:

  • QGraphicsView、QGraphicsScene、QGraphicsItem的基本用法
  • drag && drop
  • 自定义QGraphicsItem

先来看看效果吧:

现在,来看下代码了。

项目说明

如上图所示,项目名称为qPuzzle,三个源文件,main.cpp是入口,imageitem.h和imageitem.cpp实现了:

  • PuzzleImageItem,就是界面左上侧那两个可以拖动的碎片,支持拖动
  • PuzzlePart,用于接受拖放的item
  • PuzzlePartManager,管理可拖放的PuzzleImageItem,拖放到位后从QGraphicsScene中移除PuzzleImageItem

项目还有几个图片资源,model.png是带有拼图区域的房子图片,mode_1.png和model_2.png是房子上扣出来的小图片。

源码说明

分开来说吧,main()、PuzzleImageItem和PuzzlePartItem。

入口函数main

先看main.cpp吧:

class GraphicsView : public QGraphicsView
{
public:
    GraphicsView(QGraphicsScene *scene) : QGraphicsView(scene)
    {
    }

protected:
    virtual void resizeEvent(QResizeEvent *) Q_DECL_OVERRIDE
    {
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene(0, 0, 480, 360);
    PuzzleImageItem *image1 = new PuzzleImageItem(":/model_1.png", 60, 60, 1);
    image1->setPos(4, 4);
    scene.addItem(image1);
    PuzzleImageItem *image2 = new PuzzleImageItem(":/model_2.png", 60, 60, 2);
    image2->setPos(4, 70);
    scene.addItem(image2);

    QGraphicsPixmapItem *model = new QGraphicsPixmapItem(QPixmap(":/model.png"));
    scene.addItem(model);
    model->setPos(100, 100);

    PuzzlePartManager mgr(&scene);
    mgr.addSourceItems(1, image1);
    mgr.addSourceItems(2, image2);

    PuzzlePart *part1 = new PuzzlePart(&mgr, 60, 60, 1);
    part1->setPos(261, 149);
    part1->setZValue(2);
    scene.addItem(part1);

    PuzzlePart *part2 = new PuzzlePart(&mgr, 60, 60, 2);
    part2->setPos(231, 199);
    part2->setZValue(2);
    scene.addItem(part2);

    GraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
    view.setBackgroundBrush(QColor(230, 200, 167));
    view.setWindowTitle("House Puzzle");
    view.show();

    return a.exec();
}

main()方法创建了QGraphicsScene实例,构造各种item并添加到场景中,将QGraphicsView与QGraphicsScene关联起来,代码很直接,不多说了。

PuzzleImageItem

再来看看支持拖动的PuzzleImageItem的实现:

class PuzzleImageItem : public QGraphicsObject
{
public:
    PuzzleImageItem(const QString & imagePath, int w, int h, int partId);
    QRectF boundingRect() const Q_DECL_OVERRIDE;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) Q_DECL_OVERRIDE;

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) Q_DECL_OVERRIDE;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) Q_DECL_OVERRIDE;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) Q_DECL_OVERRIDE;

protected:
    QImage m_image;
    int m_width;
    int m_height;
    int m_partId;
};

PuzzleImageItem代表完整图片的一部分,它从QGraphicsObject继承而来,持有一个图片、宽、高以及图片的id(m_partId)。其中m_partId是碎片索引,在拖放到位后,PuzzlePartManager通过它来将界面左上角的碎片从视图中移除。

重写了boundingRect和paint方法,这是自定义QGraphicsItem时通常都需要做的。

重写了mousePressEvent,在它里面将鼠标形状修改为抓紧的小手:

void PuzzleImageItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    setCursor(Qt::ClosedHandCursor);
}

重写了mouseMoveEvent,在它里面组装QDrag和QMimeData:

void PuzzleImageItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton))
        .length() < QApplication::startDragDistance()) {
        return;
    }

    QDrag *drag = new QDrag((QObject*)event->widget());
    QMimeData *mime = new QMimeData;
    mime->setImageData(m_image);
    mime->setData(QString(QMetaType::typeName(QMetaType::Int)), QString("%1").arg(m_partId).toLatin1());
    drag->setMimeData(mime);

    drag->setPixmap(QPixmap::fromImage(m_image));
    drag->setHotSpot(QPoint(15, 30));

    drag->exec();
    setCursor(Qt::OpenHandCursor);
}

注意我在这里用QMimeData传递m_partId给接受拖放的PuzzlePartItem,这样可以区分碎片该放到哪个目标区域。在PuzzlePartItem的dragEnterEvent方法中有用到,代码如下:

void PuzzlePart::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
    const QMimeData *mime = event->mimeData();
    int partId = mime->data(QMetaType::typeName(QMetaType::Int)).toInt();
    if(mime->hasImage() && partId == m_partId)
    {
        event->setAccepted(true);
        m_dragOver = true;
        update();
    }
    else
    {
        event->setAccepted(false);
    }
}

这里可以留意一下使用QMimeData传递非典型类型数据的做法。

重写了mouseReleaseEvent,在它里面重新设置鼠标形状为打开的小手:

void PuzzleImageItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    setCursor(Qt::OpenHandCursor);
}

PuzzlePartItem

PuzzlePartItem定义了一个区域,用于接受拖放。它同样从QGraphicsObject继承,声明如下:

class PuzzlePart : public QGraphicsObject
{
public:
    PuzzlePart(PuzzlePartManager *mgr, int w, int h, int partId, QGraphicsItem *parent = 0);

    QRectF boundingRect() const Q_DECL_OVERRIDE;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) Q_DECL_OVERRIDE;

protected:
    void dragEnterEvent(QGraphicsSceneDragDropEvent *event) Q_DECL_OVERRIDE;
    void dragLeaveEvent(QGraphicsSceneDragDropEvent *event) Q_DECL_OVERRIDE;
    void dropEvent(QGraphicsSceneDragDropEvent *event) Q_DECL_OVERRIDE;

    PuzzlePartManager *m_sourcePartManager;
    int m_width;
    int m_height;
    int m_partId;
    QImage m_image;
    bool m_dragOver;
};

这个类也保留了一个id(m_partId),可以用来决定接受哪个PuzzleImageItem。前面贴出来的dragEnterEvent方法的代码里就用到了。dragEnterEvent方法在PuzzleImageItem被拖到PuzzlePartItem所在区域时触发。

当释放鼠标时,会触发dropEvent,代码如下:

void PuzzlePart::dropEvent(QGraphicsSceneDragDropEvent *event)
{
    m_dragOver = false;
    const QMimeData *mime = event->mimeData();
    int partId = mime->data(QMetaType::typeName(QMetaType::Int)).toInt();
    if(mime->hasImage() && partId == m_partId)
    {
        m_image = qvariant_cast<QImage>(event->mimeData()->imageData());
        m_sourcePartManager->removeItem(partId);
    }
    update();
}

我们在这里接收QMimeData里的图片,触发重新绘制,还调用PuzzlePartManager的removeItem将源item(PuzzleImageItem)从视图中移除。

重写dragLeaveEvent是为了在鼠标拖着源object离开自己时重绘。


好啦,这个demo基本就这样了。

需要项目源码的,可以关注我的订阅号“程序视界”,回复“qPuzzle”获取下载地址。

原文地址:https://www.cnblogs.com/hehehaha/p/6147402.html