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