Puzzle

Puzzle

//Puzzle.pro

QT+=widgets

SOURCES += \
    main.cpp \
    MainWindow.cpp \
    PiecesList.cpp \
    PuzzleWidget.cpp

HEADERS += \
    PiecesList.h \
    PuzzleWidget.h \
    MainWindow.h

RESOURCES += \
    image.qrc

/*main.cpp
 * 程序说明:官方拖动的例子,自己加了中文注释
 * 程序构造:有一个主窗口,里面有两个主要区域,一个源拖动区域,一个目的拖动区域,
 * 这里认为左边的部件是源拖动区域,右边的是目的拖动区域,源拖动区域中有
 * 源图片被裁切的图片的碎片,下面我都会用碎片来称呼。将碎片拖动到目的区域,
 *  (不是固定的,也可以从目的拖动区域,拖动碎片到源拖动区域)因为目的区域被
 * 分成了5*5的方块,拖进目的区域时会在鼠标所属方块形成粉色的投影.源区域被拖的碎
 * 片会被移除.
 *
 * 单词说明:
 * PiecesList:碎片列表,也可以理解成源区域
 * PuzzleWidget:目的区域
 *
 *
 */
#include <QApplication>

 #include "MainWindow.h"

 int main(int argc, char *argv[])
 {
     //Q_INIT_RESOURCE(puzzle);

     QApplication app(argc, argv);
     MainWindow window;
     //加载源图片
     window.openImage(":/Image/1.jpg");
     window.show();
     return app.exec();
 }

//MainWindow.h
#ifndef MAINWINDOW_H
 #define MAINWINDOW_H

 #include <QPixmap>
 #include <QMainWindow>

 class PiecesList;
 class PuzzleWidget;
 class QListWidgetItem;

 class MainWindow : public QMainWindow
 {
     Q_OBJECT

 public:
     MainWindow(QWidget *parent = 0);

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

 private slots:
     void setCompleted();

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

     QPixmap puzzleImage;
     PiecesList *piecesList;
     PuzzleWidget *puzzleWidget;
 };

 #endif

//MainWindow.cpp

#include <QtCore>
#include <QFileDialog>
#include <QFrame>
#include <QHBoxLayout>

#include <QMessageBox>

#include <stdlib.h>
#include "MainWindow.h"
#include "PiecesList.h"
#include "PuzzleWidget.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    //一开始就设置菜单
    setupMenus();
    //初始化部件
    setupWidgets();
    //设置默认布局属性(x,y轴不能变化大小,例如按钮)
    //如果一个部件被加到了layout中,因为有layout中默认
    //设置了策略所以就用layout中的布局策略,其他就采用这个默认布局
    setSizePolicy(QSizePolicy(QSizePolicy::Fixed,/*x轴伸缩因子:为最大填充*/
                              QSizePolicy::Fixed/*y轴伸缩因子:为最大填充*/));
    //设置标题
    setWindowTitle(tr("Puzzle"));
}

void MainWindow::openImage(const QString &path)
{
    QString fileName = path;

    //路径为空就打开文件对话框,让你选图片
    if (fileName.isNull())
        fileName = QFileDialog::getOpenFileName
                (this,
                 tr("Open Image"), "", "Image Files (*.png *.jpg *.bmp)");

    //文件名不为空
    if (!fileName.isEmpty()) {
        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());
    //功能:设置源图片为缩放的正方形的图片
    //深拷贝:我是这么理解的,浅拷贝就像个指针,只保存个地址
    //而深拷贝是拷贝整个值.为什么会有拷贝这种东西呢?其实就是
    //不想被人看到实现细节,不然操作指针好了,也是为了简便,
    //不用直接面对一堆指针.
    //copy:深拷贝一张图片然后赋值给源图片 copy(Rect(x,y,width,height)
    //因为加载来的源图片不一定是正方形的,要处理成正方形的
    //怎么处理呢?假如加载的源图的宽大一些,大宽减去小高,得到大了多少,
    //除2就能对半分,然后就能得到要的x坐标(如果高大一些,得到的就是y坐标)
    //另一个坐标一定是0,大减大,不就是0嘛。
    //saled(width,height,纵横比,转换模式)
    puzzleImage = puzzleImage.copy
            ((puzzleImage.width() - size)/2,
             (puzzleImage.height() - size)/2, size, size).scaled
            (400,
             400,
             Qt::IgnoreAspectRatio/*指定大小完全填充,会造成拉伸*/
             , Qt::SmoothTransformation
             /*利用双线性滤波对得到的图像进行变换,不懂*/);

    //将存储碎片的列表清空
    piecesList->clear();


    for (int y = 0; y < 5; ++y) {
        for (int x = 0; x < 5; ++x) {
            //以指定大小的深拷贝方式来依次切割源图片
            QPixmap pieceImage = puzzleImage.copy(x*80, y*80, 80, 80);
            //切好后就成了碎片,加入碎片列表,连同切割坐标
            piecesList->addPiece(pieceImage, QPoint(x, y));
        }
    }

    //这个随机是想怎样?
    //seed(随机种子):屏幕坐标的x,y异或
    qsrand(QCursor::pos().x() ^ QCursor::pos().y());

    //听说前置++,快一点
    //count():加入碎片列表的碎片个数
         for (int i = 0; i < piecesList->count(); ++i) {
             //没见过的随机,可能比较可靠吧
             //达到某个数值,int()显式转换一下,再跟1比较
             if (int(2.0*qrand()/(RAND_MAX+1.0)) == 1) {
                 //从碎片列表中取出来
                 QListWidgetItem *item = piecesList->takeItem(i);
                 //再添加到列表开头
                 //看样子这个piecesList是链表
                 //tackItem有点像是删除链表中一个结点,删除了一个引用而已
                 //操作会很快吧
                 piecesList->insertItem(0, item);
             }
         }

    //清空目的区域
    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"));

    //打开动作启动,会调用openImage()
    connect(openAction, SIGNAL(triggered()), this, SLOT(openImage()));
    //离开动作启动,会调用quit()
    connect(exitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
    //重新开始启动,会调用setupPuzzle();
    connect(restartAction, SIGNAL(triggered()), this, SLOT(setupPuzzle()));
}

void MainWindow::setupWidgets()
{
    //设置中间框架类,这是个过渡类,就是用来加东西的
    QFrame *frame = new QFrame;
    //添加水平布局,里面加的东西会自动排成一排
    QHBoxLayout *frameLayout = new QHBoxLayout(frame);

    //定义碎片列表
    piecesList = new PiecesList;
    //定义目的区域
    puzzleWidget = new PuzzleWidget;

    //当目的区域发出信号puzzleCompleted,这个主窗口就调用
    //setCompleted()
    //Qt::QueuedConnection:当puzzleWidget控件返回到
    //主窗口接收方线程的事件循环时调用该插槽。插槽在主窗口这个接收方的线程中执行。
    connect(puzzleWidget/*目的区域*/, SIGNAL(puzzleCompleted()),
            this, SLOT(setCompleted()), Qt::QueuedConnection);

    //框架加入
    //加入碎片列表
    frameLayout->addWidget(piecesList);
    //加入目的区域
    frameLayout->addWidget(puzzleWidget);
    //添加框架进这个主窗口,成为这个主窗口的中间部件
    setCentralWidget(frame);
}

//PuzzleWidget.h
#ifndef PUZZLEWIDGET_H
 #define PUZZLEWIDGET_H

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

 class QDragEnterEvent;
 class QDropEvent;
 class QMouseEvent;

 class PuzzleWidget : public QWidget
 {
     Q_OBJECT

 public:
     PuzzleWidget(QWidget *parent = 0);
     void clear();

 signals:
     void puzzleCompleted();

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

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

 #endif

//PuzzelWidget.cpp
#include <QtGui>
#include "PuzzleWidget.h"

 PuzzleWidget::PuzzleWidget(QWidget *parent)
     : QWidget(parent)
 {
     //为了能drag,一定要设置接受拖放
     setAcceptDrops(true);
     //固定部件大小
     setMinimumSize(400, 400);
     setMaximumSize(400, 400);
 }

 void PuzzleWidget::clear()
 {
     //清空操作,将变量清空归零啊
     //调用界面更新
     //吧啦吧啦
     pieceLocations.clear();
     piecePixmaps.clear();
     pieceRects.clear();
     highlightedRect = QRect();
     inPlace = 0;
     update();
 }

 void PuzzleWidget::dragEnterEvent(QDragEnterEvent *event)
 {
     if (event->mimeData()->hasFormat("image/x-puzzle-piece"))
         event->accept();
     else
         event->ignore();
 }

 void PuzzleWidget::dragLeaveEvent(QDragLeaveEvent *event)
 {
     QRect updateRect = highlightedRect;
     //不高亮
     highlightedRect = QRect();
     update(updateRect);
     event->accept();
 }

 void PuzzleWidget::dragMoveEvent(QDragMoveEvent *event)
 {
     //方块大小是80*80的
     //得到要更新的区域
     //它由高亮区域与当前鼠标坐标形成80*80大小的矩形的并集组成
     //取两矩形中坐标最左上,最左下,最右上,最右下组成updateRect
     QRect updateRect =
             highlightedRect.united
             (targetSquare(event->pos()));
     //mimeData中包含要处理的数据且没有在方块列表中
     //换句话说就是没被拖进目标区域的碎片现在正在被
     //拖进目的区域
     if (event->mimeData()->hasFormat("image/x-puzzle-piece")
         && findPiece(targetSquare(event->pos())) == -1) {

         //高亮矩形获得值
         highlightedRect = targetSquare(event->pos());
         event->setDropAction(Qt::MoveAction);
         event->accept();
     } else {
         //不高亮
         highlightedRect = QRect();
         event->ignore();
     }

     //更新部份界面
     update(updateRect);
 }

 void PuzzleWidget::dropEvent(QDropEvent *event)
 {
     //mimeData中包含要处理的数据且没有在方块列表中
     //换句话说就是没被拖进目标区域的碎片现在正在被
     //拖进目的区域
     if (event->mimeData()->hasFormat("image/x-puzzle-piece")
         && findPiece(targetSquare(event->pos())) == -1) {
         //从mimeData中取数据
         QByteArray pieceData = event->mimeData()->data
            ("image/x-puzzle-piece");
         QDataStream dataStream(&pieceData, QIODevice::ReadOnly);
         //取得要放碎片的矩形
         QRect square = targetSquare(event->pos());
         QPixmap pixmap;
         QPoint location;
         dataStream >> pixmap >> location;

         //在paintEvent中好一块画出来
         //pieceLocation[0]对应piecePixmap[0]
         //pieceLocation[1]对应piecePixmap[1]
         //             .
         //             .
         //             .
         //加入坐标集
         pieceLocations.append(location);
         //加入碎片列集
         piecePixmaps.append(pixmap);
         //加入方块集
         pieceRects.append(square);
         //去高亮
         highlightedRect = QRect();
         //因为已经被放下了,直接更新这个区域就行
         update(square);

         //通知源区域
         event->setDropAction(Qt::MoveAction);
         event->accept();
         //通知结束

         //对比序列来得出结论
         //比如,squre为第一行第二个矩形 (80,0,80,80)
         //则对比一下是源图片左上角的坐标序列吗  这里得出square的序列是(1,0)
         //序列相同,就是放对了地方,不同就不对
         if (location == QPoint(square.x()/80, square.y()/80)) {
             inPlace++;
             //达到一定条件发送通关信号给主窗口
             if (inPlace == 25)
                 emit puzzleCompleted();
         }
     } else {
         //不高亮
         highlightedRect = QRect();
         event->ignore();
     }
 }

 int PuzzleWidget::findPiece(const QRect &pieceRect) const
 {
     //size:方块列表中方块的个数   只有其上有碎片,才会加入方块列表
     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);
     //对比序列来得出结论
     //比如,squre为第一行第二个矩形 (80,0,80,80)
     //则对比一下是源图片左上角的坐标序列吗  这里得出square的序列是(1,0)
     //序列相同,就是放对了地方,不同就不对
     if (location == QPoint(square.x()/80, square.y()/80))
         inPlace--;
     //更新这块区域
     update(square);

     //写入mimeData
     QByteArray itemData;
     QDataStream dataStream(&itemData, QIODevice::WriteOnly);

     dataStream << pixmap << location;

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

     //启动Drag
     QDrag *drag = new QDrag(this);
     drag->setMimeData(mimeData);
     //得到偏移,让鼠标在拖动的时候是正确的
     //设置成QPoint(0,0)就会在拖动时,鼠标始终在碎片的左上角。不自然
     drag->setHotSpot(event->pos() - square.topLeft());
     //为拖动时显示图片而设置,其他可以设置成其他cute一点的图片
     //随你喜欢
     drag->setPixmap(pixmap);

     if (!(drag->exec(Qt::MoveAction) == Qt::MoveAction)) {
         //不是拖动,就将数据插回原位
         pieceLocations.insert(found, location);
         piecePixmaps.insert(found, pixmap);
         pieceRects.insert(found, square);
////////////////////////////////////////////////////////
         //只更新鼠标所在方块吗???
         //好像这里有点不对,说不上来
         //这是对的,从piecelist通信回来,只有从鼠标上来判断了
//         update(targetSquare(event->pos()));
         //这样也问题不大,反正是插回原位
         update(square);
///////////////////////////////////////////////////////////////////
         //location应该被pieceslist处理过了吧,主界面将切割坐标传给了piecelist
         //piecelist一定是除了60之类的,将其转换成一个序列
         //用序列来比较是否放对应
         if (location == QPoint(square.x()/80, square.y()/80))
             inPlace++;
     }
 }

 void PuzzleWidget::paintEvent(QPaintEvent *event)
 {
     QPainter painter;
     painter.begin(this);
     //整个目的部件设置成白色
     painter.fillRect(event->rect(), Qt::white);

     //高亮
     if (highlightedRect.isValid()) {
         //设置投影的背景的颜色为粉红色,即高亮矩形的颜色
         painter.setBrush(QColor("#ffcccc"));
         painter.setPen(Qt::NoPen);
         //设置投影背景的矩形大小
         //依据的是现有矩形的大小来调整的,
         //0,0,-1,-1表示X,Y不变,Width-1,Height-1
         painter.drawRect(highlightedRect.adjusted(0, 0, -1, -1));
     }

     //方块上有碎片的才画出来
     for (int i = 0; i < pieceRects.size(); ++i) {
         painter.drawPixmap(pieceRects[i], piecePixmaps[i]);
     }
     painter.end();
 }

 const QRect PuzzleWidget::targetSquare(const QPoint &position) const
 {
     //目标方块 Rect(x,y,width,height)
     //positino.x()/80有点像mainwindow.cpp中随机函数那个int()
     //这里隐式转换后再乘80
     //就是要利用有失真的效果来得到坐标
     //比如传来的坐标positon(100,100)
     //position.x()为100
     //除80得1.25,隐式转成了1
     //1*80=80,怎么样,这个传来的坐标被包括在了这个矩形(80,80,80,80)中
     //而在目标区域中,矩形是限死的,
     //(0,0,80,80)~(0,4,80,80)
     //         .
     //         .
     //(4,0,80,80)~(4,4,80,80)
     return QRect(position.x()/80 * 80, position.y()/80 * 80, 80, 80);
 }

//PiecesList.h
#ifndef PIECESLIST_H
 #define PIECESLIST_H

 #include <QListWidget>

//继承自listwidget
 class PiecesList : public QListWidget
 {
     Q_OBJECT

 public:
     PiecesList(QWidget *parent = 0);
     void addPiece(QPixmap pixmap, QPoint location);

 protected:
     void dragEnterEvent(QDragEnterEvent *event);
     void dragMoveEvent(QDragMoveEvent *event);
     void dropEvent(QDropEvent *event);
     //从源区域拖动到目的区域
     void startDrag(Qt::DropActions supportedActions);
 };

 #endif

//PiecesList.cpp
#include <QtGui>

 #include "PiecesList.h"

 PiecesList::PiecesList(QWidget *parent)
     : QListWidget(parent)
 {
      //为了能drag,一定要设置可以拖放
     setDragEnabled(true);
     //图标模式
     setViewMode(QListView::IconMode);
     //图标大小
     setIconSize(QSize(60, 60));
     //间隙
     setSpacing(10);
     //为了能drag,一定要设置接受拖放
     setAcceptDrops(true);
    //此属性保存在拖放项目和拖放时是否显示拖放指示器。
     setDropIndicatorShown(true);
 }

 void PiecesList::dragEnterEvent(QDragEnterEvent *event)
 {
     if (event->mimeData()->hasFormat("image/x-puzzle-piece"))
         event->accept();
     else
         event->ignore();
 }

 void PiecesList::dragMoveEvent(QDragMoveEvent *event)
 {
     if (event->mimeData()->hasFormat("image/x-puzzle-piece")) {
         event->setDropAction(Qt::MoveAction);
         //发回目的区域
         event->accept();
     } else
         event->ignore();
 }

 void PiecesList::dropEvent(QDropEvent *event)
 {
     if (event->mimeData()->hasFormat("image/x-puzzle-piece")) {
         //从目的区域拖来,放到了源区域
         QByteArray pieceData = event->mimeData()->data
                 ("image/x-puzzle-piece");
         QDataStream dataStream(&pieceData, QIODevice::ReadOnly);
         QPixmap pixmap;
         QPoint location;
         dataStream >> pixmap >> location;

         //加入源区域,也就是碎片列表
         addPiece(pixmap, location);

         //发回目的区域
         event->setDropAction(Qt::MoveAction);
         event->accept();
     } else
         event->ignore();
 }

 void PiecesList::addPiece(QPixmap pixmap, QPoint location)
 {
	//添加到源区域的item
     QListWidgetItem *pieceItem = new QListWidgetItem(this);
     pieceItem->setIcon(QIcon(pixmap));
     pieceItem->setData(Qt::UserRole, QVariant(pixmap));
     pieceItem->setData(Qt::UserRole+1, location);
     pieceItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable
                         | Qt::ItemIsDragEnabled);
 }

 //从源区域拖动碎片到目的区域
 void PiecesList::startDrag(Qt::DropActions /*supportedActions*/)
 {
     QListWidgetItem *item = currentItem();

     QByteArray itemData;
     QDataStream dataStream(&itemData, QIODevice::WriteOnly);
     //从item取图片数据
     QPixmap pixmap =(item->data(Qt::UserRole)).value<QPixmap>();
     //从item中取坐标数据
     QPoint location = item->data(Qt::UserRole+1).toPoint();

     dataStream << pixmap << location;

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

     QDrag *drag = new QDrag(this);
     drag->setMimeData(mimeData);
     //拖动时,鼠标定位在图片pixmap的中间
     drag->setHotSpot(QPoint(pixmap.width()/2, pixmap.height()/2));
     drag->setPixmap(pixmap);

     //返回的是movaction时,把当前Item连同其当前行容器从列表中删除
     if (drag->exec(Qt::MoveAction) == Qt::MoveAction)
         delete takeItem(row(item));
 }