个人实现的QT拼图游戏(开源),QT拖拽事件详解

文章目录

      • 效果图
      • 引言
        • 玩法
      • 拖拽概念
        • 基本概念
        • 如何在Qt中使用拖放
        • 注意事项
      • 游戏关键问题
      • 总结

效果图

请添加图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c6dd66befd314442adf07e1dec0d550c.png
在这里插入图片描述
在这里插入图片描述

引言

  • 在学习QT demo时,发现有一个拼图demo,介绍拖拽事件的。以此为蓝本加了亿点修饰,就诞生了这个游戏。
玩法
  • 游戏为拼图游戏,分为俩种模式(闯关与休闲)。
  • 闯关模式:在规定的时间内完成拼图,共有四关,有三种难度,每种难度所需的时间不一致。
  • 休闲模式:玩家可以自定义图片与难度,没有时间限制。

拖拽概念

基本概念
  • 在Qt中,拖放(Drag and Drop)是一种非常直观的方式来处理对象的移动或复制。拖放可以在单个应用程序内进行,也可以在不同应用程序之间进行。Qt为此提供了一组丰富的API来支持拖放操作。
  1. 拖动 (Drag)
  • 开始一个拖动操作,通常是当用户在一个可拖动的组件上按下鼠标按钮,并移动一定距离时。在Qt中,你需要创建一个QDrag对象,并指定要拖动的数据。
  1. 放下 (Drop)
  • 放下操作发生在拖动过程的最后,当用户释放鼠标按钮时。如果释放位置是一个可以接受放下的组件(一个设置为接受放下的QWidget或者QGraphicsItem),那么会发生放下操作。
  1. MIME 数据
  • 拖动和放下的数据是通过MIME(Multipurpose Internet Mail Extensions)类型封装的。在Qt中通常使用QMimeData对象来处理拖放的数据。
如何在Qt中使用拖放
  1. 启用组件的拖放
  • 首先,确保你的QWidget派生类允许拖放。使用setDragEnabled(true)可以使得组件可以被拖动,使用setAcceptDrops(true)使得组件可以接收放下。
  1. 处理拖动事件
  • 在源组件中,你需要重写mousePressEventmouseMoveEvent。这些本是处理鼠标事件的函数在此也被用来发起拖动。在鼠标移动事件中,你可以使用QDrag来开始拖动操作,并将QMimeData附加到QDrag对象。
void SourceWidget::mouseMoveEvent(QMouseEvent *event) {
    if (!(event->buttons() & Qt::LeftButton)) {
        return;
    }
    QDrag *drag = new QDrag(this);
    QMimeData *mimeData = new QMimeData;
    // 设置数据 mimeData->setData(...) 或 mimeData->setText(...)
    drag->setMimeData(mimeData);
    // 开始拖动操作
    Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
}
  1. 处理放下事件
  • 在目标组件中,你需要重写几个事件处理函数以处理放下事件:dragEnterEventdragMoveEvent(可选)和dropEvent。通过这些事件,你可以确定是否接受拖动进来的数据,以及如何处理这些数据。
void TargetWidget::dragEnterEvent(QDragEnterEvent *event) {
    if (event->mimeData()->hasFormat("custom/format")) {
        event->acceptProposedAction();
    }
}
void TargetWidget::dropEvent(QDropEvent *event) {
    const QMimeData *mimeData = event->mimeData();
    // 处理放下的数据 mimeData->data(...) 或 mimeData->text()
    event->acceptProposedAction();
}
注意事项
  • 你也许会需要处理dragLeaveEvent,用来处理拖动物体离开组件时的事件。
  • 拖放事件与标准的鼠标事件是相互独立的,在处理拖放事件时不会影响鼠标事件的处理。
  • 拖放操作可以包括图片、文本、HTML等多种数据类型,基本上任何种类的数据都可以通过MIME数据进行传输。
  • 要实现跨不同应用程序的拖放,需要确保所有参与的应用程序都能理解相关的MIME类型。

游戏关键问题

  • 游戏的总体结构是怎么样的
  • 界面主要由俩块组成,左边为一个QListView设置了继承于QAbstractListModel的代理模型,右边为一个QWidget
  • 游戏维护了一个全局的结构体指针中,该结构体用于保存游戏的信息,如模式,难度,当前关卡等信息。
  • 游戏实现的主要难点就是拖拽的实现
  1. 如何将一张图片分割为指定的x*x的图片
    //  计算新的图像大小,取原始图片宽高的最小值作为新的尺寸size
    int size = qMin(pixmap.width(), pixmap.height());
    // 从原始图片中剪切出一个大小为 sizexsize 的部分作为新的puzzleImage,
    // 重新调整新的puzzleImage大小为puzzleWidget的imageSize,使用Qt::SmoothTransformation平滑缩放
    pixmap = pixmap.copy((pixmap.width() - size) / 2, (pixmap.height() - size) / 2, size, size)
                 .scaled(puzzleWidget->imageSize(), puzzleWidget->imageSize(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    // 制作每一片拼图片段。m_PieceSize图片大小,m行列数
    for (int y = 0; y < m; ++y)
    {
        for (int x = 0; x < m; ++x)
        {
            QPixmap pieceImage = pixmap.copy(x * m_PieceSize, y * m_PieceSize, m_PieceSize, m_PieceSize);
            addPiece(pieceImage, QPoint(x, y));
        }
    }
  1. 如何判断拼图是否完成
  • 在切割图片的时候,我们已经将将图片正确位置存放到图片中,只需要全局维护一个计数器,当计数器等于拼图数量时,即是完成。
    // 图片资源结构体
    struct Piece
    {
        QPixmap pixmap;
        QRect rect;
        QPoint location;
        Piece() {}
        Piece(QPixmap Vpixmap, QPoint Vlocation, QRect Vrect) : pixmap(Vpixmap), location(Vlocation), rect(Vrect) {}
        Piece(const Piece &other)
        {
            pixmap = other.pixmap;
            rect = other.rect;
            location = other.location;
        }
    };
  • 计数器的增加规则是:若是当前图片所有在矩形与存放的位置相同,计数器+1
void PuzzleWidget::addInPlace(Piece piece)
{
    if (piece.location == piece.rect.topLeft() / pieceSize())
    {
        inPlace++;
        if (inPlace == MacroDf::getCloum() * MacroDf::getCloum())
            emit puzzleCompleted();
    }
}
  1. 图片是如何出现在widget上的
  • 通过绘制实现,pieces存放的是保存的图片结构体列表,highlightedRect为高亮区域。
void PuzzleWidget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.fillRect(event->rect(), Qt::white);

    if (highlightedRect.isValid())
    {
        painter.setBrush(QColor("#98FB98"));
        painter.setPen(Qt::NoPen);
        painter.drawRect(highlightedRect.adjusted(0, 0, -1, -1));
    }

    for (const Piece &piece : pieces)
    {
        painter.drawPixmap(piece.rect, piece.pixmap);
    }
}
  1. widget窗口上图片是如何拖动的
  • 在鼠标点击事件中,先判断当前点击的位置是否存在图片,若是有就去存好的图片链表中获取该图片的资源,创建拖动操作的数据对象
void PuzzleWidget::mousePressEvent(QMouseEvent *event)
{
    // 获取鼠标点击位置的方块
    QRect square = targetSquareMove(event->pos());
    // 查找方块是否有图片
    int found = findPiece(square);

    if (found == -1)
        return;
    // 移除找到的拼图块
    Piece piece = pieces.takeAt(found);
    // 如果拼图块的位置与方块的顶点位置一致,表示该拼图块为正确位置,移除时更新完成计数位
    if (piece.location == square.topLeft() / pieceSize())
        inPlace--;

    update(square);
    // 将拼图块的图像和位置信息存入数据流
    QByteArray itemData;
    QDataStream dataStream(&itemData, QIODevice::WriteOnly);

    dataStream << piece.pixmap << piece.location << piece.rect;
    // 创建拖动操作的数据对象
    QMimeData *mimeData = new QMimeData;
    mimeData->setData("DJ-NB", itemData);
    // 创建拖动操作
    QDrag *drag = new QDrag(this);
    drag->setMimeData(mimeData);
    drag->setHotSpot(event->pos() - square.topLeft());
    drag->setPixmap(piece.pixmap);
    // 判断拖动操作的结果是否为Qt::IgnoreAction,表示拖拽失败,将拼图块放回原位置
    if (drag->exec(Qt::MoveAction) == Qt::IgnoreAction) // 拖放到其他应用程序。我们使用Qt::IgnoreAction来限制它。
    {
        pieces.insert(found, piece);
        update(targetSquareMove(event->pos()));

        if (piece.location == QPoint(square.x() / pieceSize(), square.y() / pieceSize()))
            inPlace++;
    }
}
  1. 图片是如何拖入widget以及交换图片的
  • dropEvent事件中,先检查数据格式是否正确,再判断当前要放入的位置是否存在图片,不存在图片直接加入到列表中就行,若是存在,则需要交换俩个图片的信息,同时要判断计数位。
void PuzzleWidget::dropEvent(QDropEvent *event)
{
    // 检查事件是否含有我们需要的数据格式
    if (event->mimeData()->hasFormat("DJ-NB"))
    {
        // 接受事件默认的复制动作
        event->setDropAction(Qt::MoveAction);
        event->accept();
        auto square = targetSquareMove(event->pos()); // 目标位置
        int existingPieceIndex = findPiece(square);   // 寻找目标位置是否有拼图块

        // 从拖放事件的数据中读取拼图块的信息
        QByteArray pieceData = event->mimeData()->data("DJ-NB");
        QDataStream dataStream(&pieceData, QIODevice::ReadOnly);
        // 将拼图块添加到列表中或与现有拼图块交换
        if (existingPieceIndex == -1)
        {
            // 目标位置没有拼图块,直接放置新拼图块
            Piece piece;
            piece.rect = targetSquareMove(event->pos());
            dataStream >> piece.pixmap >> piece.location;
            // 将拼图块添加到列表中
            pieces.append(piece);
            // 清除高亮的区域并更新拼图块的区域
            highlightedRect = QRect();
            update(piece.rect);
            // 如果拼图块放置在正确的位置
            addInPlace(piece);
        }
        else
        {
            // 目标位置已有拼图块,和拖入的拼图块互换位置
            // 起始位置资源
            Piece piece;
            dataStream >> piece.pixmap >> piece.location >> piece.rect;
            // 目标位置资源
            Piece rPic = pieces[existingPieceIndex];
            // 删除掉原有的,以便重新写入新值
            if (rPic.location == rPic.rect.topLeft() / pieceSize())
                inPlace--;
            pieces.takeAt(existingPieceIndex);
            // 数据交互
            Piece tempPiece = piece;
            piece.location = rPic.location;
            piece.pixmap = rPic.pixmap;
            rPic.location = tempPiece.location;
            rPic.pixmap = tempPiece.pixmap;
            // 存放俩组数据
            pieces.append(piece);
            pieces.append(rPic);
            // 重绘涉及的区域
            highlightedRect = QRect();
            update(piece.rect);
            update(rPic.rect);
            // 如果拼图块放置在正确的位piece
            addInPlace(rPic);
            addInPlace(piece);
        }
    }
    else
    {
        highlightedRect = QRect();
        // 不是我们支持的数据格式,保留默认行为
        event->ignore();
    }
}
  1. list以拖入widget中的图片如何删除,更新链表视图的
  • 在继承与QAbstractListModel的代理中的removeRows函数实现
bool PiecesModel::removeRows(int row, int count, const QModelIndex &parent)
{
    if (parent.isValid())
        return false;

    if (row >= piece.size() || row + count <= 0)
        return false;
    // 修剪beginRow和endRow,限制在有效范围内。
    int beginRow = qMax(0, row);
    int endRow = qMin(row + count - 1, piece.size() - 1);
    // 调用beginRemoveRows()告知视图将移除行,开始行beginRow和结束行endRow。
    beginRemoveRows(parent, beginRow, endRow);
    // 循环移除
    while (beginRow <= endRow)
    {
        piece.removeAt(beginRow);
        ++beginRow;
    }
    // 调用endRemoveRows()告知视图完成移除行。
    endRemoveRows();
    return true;
}
  1. 如何将widget拖回list
  • 上述中我们在widget的点击事件中直接创建了拖拽数据,那么我们只需要在listdropMimeData实现存放的逻辑就行
bool PiecesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    // 检查mime数据是否包含正确的格式:"DJ-NB"
    if (!data->hasFormat("DJ-NB"))
        return false;
    // 检查拖放操作:
    if (action == Qt::IgnoreAction)
        return true;
    // 只允许插入第一列:
    if (column > 0)
        return false;
    // 判断插入行的尾部位置endRow:
    int endRow;
    // 如果是根节点:
    if (!parent.isValid())
    {
        if (row < 0)
            endRow = piece.size();
        else
            endRow = qMin(row, piece.size());
    }
    else // 如果是子节点:
    {
        endRow = parent.row();
    }
    // 解析mime数据,读取 pixmap 图片和位置 location:
    QByteArray encodedData = data->data("DJ-NB");
    QDataStream stream(&encodedData, QIODevice::ReadOnly);
    // 通过 begin/endInsertRows函数更新模型,插入数据:
    while (!stream.atEnd())
    {
        QPixmap pixmap;
        QPoint location;
        QRect rect;
        // 从数据流中读数据
        stream >> pixmap >> location >> rect;
        Piece pie(pixmap, location, rect);
        // 若是以存在则返回不加入
        for (auto point : piece)
        {
            if (point.location == location)
            {
                return false;
            }
        }
        beginInsertRows(QModelIndex(), endRow, endRow);
        piece.insert(endRow, pie);
        endInsertRows();
        ++endRow;
    }
    return true;
}
  1. widget中如何判断当前位置,以及图片中的矩形数据怎么存放
  • 通过鼠标的点击获取的点,得到以图片为大小的当前位置左上角坐标,矩形大小也是每张图片的大小
const QRect PuzzleWidget::targetSquareMove(const QPoint &position) const
{
    // point除以一个数是往前进位,这会导致坐标出现问题,所以要用Int处理
    int x = position.x() / pieceSize();
    int y = position.y() / pieceSize();
    auto pointNew = QPoint(x, y);
    auto point = pointNew * pieceSize();
    auto resultRect = QRect(point.x(), point.y(), pieceSize(), pieceSize());
    return resultRect;
}

总结

  • 这个游戏的用了周末俩天时间做完,后面用了一天修了点BUG,细节还是很多的,像计时器如何使用,富文本内容如何显示,弹窗的事件处理等,主要还是用于理解拖拽事件,当然你也可以直接去看QT 的demo,那个没我这么复杂,搜drag就行,不过它那个有几个明显的问题,我这都优化了。
  • 知识理应共享,大家相互学习,源码在此哦。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/334742.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LLVM 环境配置

这里选择下载源码, 然后编译的安装方式。 下载地址 (在这里可以找到多版本, 多平台的LLVM下载资源) # 解压源码 sudo tar xvf llvm-project-17.0.6.src.tar.xz # 新建安装目录 sudo mkdir -p /usr/local/llvm # 新建编译目录 sudo mkdir -p llvm-project-17.0.6.src/build #…

非线性最小二乘问题的数值方法 —— 狗腿法 Powell‘s Dog Leg Method (I - 原理与算法)

Title: 非线性最小二乘问题的数值方法 —— 狗腿法 Powell’s Dog Leg Method (I - 原理与算法) 文章目录 I. 前言II. 线搜索类型和信赖域类型1. 线搜索类型 —— 最速下降法2. 信赖域类型3. 柯西点 III. 狗腿法的原理1. 狗腿法的构建2. 狗腿法的优化说明3. 狗腿法的插值权重 I…

Java中打印图案最常用的25个图案程序

Java是公认的最流行的编程语言&#xff0c;因为它的简单性和多功能性。还可以使用它开发各种应用程序&#xff0c;包括Web、移动和桌面应用程序。此外&#xff0c;Java为开发人员提供了强大的工具来轻松高效地创建复杂的程序。Java最有前途的特性之一是它能够创建可以以特定格式…

Linux环境下部署Tomcat(详细图文)

目录 一、下载地址 1.服务器不能联网情况下载 2.服务器能够联网 二、安装 1. Tomcat解压 2. Tomcat目录说明&#xff1a; 3. 重命名解压后的文件名 4. 配置环境变量 5. 修改配置文件 6.启动Tomcat 7.访问Tomcat 8. 停止Tomcat 一、下载地址 1.服务器不能联网情况下…

PyTorch视觉工具箱:图像变换与上采样技术详解(1)

目录 Pytorch中Vision functions详解 pixel_shuffle 用途 用法 使用技巧 注意事项 参数 数学理论公式 示例代码及输出 pixel_unshuffle 用途 用法 使用技巧 注意事项 参数 数学理论公式 示例代码及输出 pad 用途 用法 使用技巧 注意事项 参数 示例代码…

SMT回流焊工艺之回流温度曲线

引言 在SMT生产流程中&#xff0c;如何控制回焊炉的温度是非常重要的一环&#xff0c;好的炉温曲线图意味着可以形成良好的焊点。 上一期分享&#xff08;SMT回流焊温度解析之锡膏焊接特性&#xff09;中&#xff0c;我们着重介绍了SMT回流工艺中的锡膏焊接部分。本期内容主要…

Leetcode2957. 消除相邻近似相等字符

Every day a Leetcode 题目来源&#xff1a;2957. 消除相邻近似相等字符 解法1&#xff1a;遍历 分类讨论 遍历字符串 word&#xff0c;比较相邻的 3 个元素 word[i - 1]、word[i] 和 word[i 1]&#xff0c;记 left_distance abs(mid - left)&#xff0c;right_distance…

大模型背景下计算机视觉年终思考小结(二)

1. 引言 尽管在过去的一年里大模型在计算机视觉领域取得了令人瞩目的快速发展&#xff0c;但是考虑到大模型的训练成本和对算力的依赖&#xff0c;更多切实的思考是如果在我们特定的小规模落地场景下的来辅助我们提升开发和落地效率。本文从相关数据集构造&#xff0c;预刷和生…

rust使用protobuf

前言 c,java,go 等直接是用 &#xff0c;具体就不说了&#xff0c;这章主要讲述rust 使用protobuf 这章主要讲述2种 1 > protoc protoc-gen-rust plugin 2> protoc prost-build 1&#xff1a;环境 win10 rustrover64 25-2 下载地址 https://github.com/protocolbu…

《WebKit 技术内幕》之四(3): 资源加载和网络栈

3. 网络栈 3.1 WebKit的网络设施 WebKit的资源加载其实是交由各个移植来实现的&#xff0c;所以WebCore其实并没有什么特别的基础设施&#xff0c;每个移植的网络实现是非常不一样的。 从WebKit的代码结构中可以看出&#xff0c;网络部分代码的确比较少的&#xff0c;它们都在…

2.4 网络层03

2.4 网络层03 2.4.7 路由表 1、什么是路由&#xff1f; 路由就是报文从源端到目的端的路径。当报文从路由器到目的网段有多条路由可达时&#xff0c;路由器可以根据路由表中最佳路由进行转发。 2、什么是路由表&#xff1f; 在计算机网络中&#xff0c;路由表&#xff08…

鸿蒙原生应用/元服务实战-AGC中几个菜单栏的关系

大家是否清楚AGC这几个菜单栏的相互关系&#xff1f; 我的元服务&#xff1a;点击后跳转到“我的应用”列表中的“HarmonyOS”页签&#xff0c;并且过滤出元服务。开发者可以在此模块中管理和运营元服务&#xff0c;例如创建元服务、发布元服务等。 我的应用&#xff1a;开发者…

2024最新Java高频面试题总结(附答案PDF)春招面试必备!

《Java面试全解析》1000道 面试题大全详解 本人是 2009 年参加编程工作的&#xff0c;一路上在技术公司摸爬滚打&#xff0c;前几年一直在上海&#xff0c;待过的公司有 360 和游久游戏&#xff0c;因为自己家庭的原因&#xff0c;放弃了阿里钉钉团队的 offer 回到了西安。 从…

Qt事件过滤

1.相关说明 监控鼠标进入组件、出组件、点击组件、双击组件的事件&#xff0c;需要重写eventFilter函数 2.相关界面 3.相关代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui-&…

解决国内Linux服务器无法使用Github的方法

解决思路&#xff1a;修改Host https://www.ipaddress.com/ 利用上面的网站查询github.com和raw.githubusercontent.com的DNS解析的IP地址 最后&#xff0c;修改服务器的/etc/hosts 添加如下两行&#xff1a; 140.82.112.3 github.com 185.199.108.133 raw.githubuserconte…

04 MyBatisPlus之逻辑删除+锁+防全表更新/删除+代码生成插件

1 逻辑删除 1. 1 什么是逻辑删除 , 以及逻辑删除和物理删除的区别? 逻辑删除&#xff0c;可以方便地实现对数据库记录的逻辑删除而不是物理删除。逻辑删除是指通过更改记录的状态或添加标记字段来模拟删除操作&#xff0c;从而保留了删除前的数据&#xff0c;便于后续的数据…

flink operator 拉取阿里云私有镜像(其他私有类似)

创建 k8s secret kubectl --namespace flink create secret docker-registry aliyun-docker-registry --docker-serverregistry.cn-shenzhen.aliyuncs.com --docker-usernameops_acr1060896234 --docker-passwordpasswd --docker-emailDOCKER_EMAIL注意命名空间指定你使用的 我…

MeterSphere本地化部署实践

项目结构 搭建本地环境 安装JDK11&#xff0c;配置好JDK环境&#xff0c;系统同时支持JDK8和JDK11安装IEAD&#xff0c;配置JDK环境配置maven环境,IDEA配置(解压可以直接使用)无限重置IDEA试用期配置redis环境(解压可以直接使用) 配置kafka环境 安装mysql-5.7环境&#xff…

Java并发基础:一文讲清util.concurrent包的作用

java.util.concurrent包是 Java 中用于并发编程的重要工具集&#xff0c;提供了线程池、原子变量、并发集合、同步工具类、阻塞队列等一系列高级并发工具类&#xff0c;使用这些工具类可以极大地简化并发编程的难度&#xff0c;减少出错的可能性&#xff0c;提高程序的效率和可…

街机模拟游戏逆向工程(HACKROM)教程:[13]68K汇编-jmp指令

在68K汇编中&#xff0c;有多个可以改变PC寄存器的指令&#xff1a; jmp 该指令在之前的章节已经介绍&#xff0c;该指令可以把目的操作数传递到PC寄存器&#xff0c;实现程序的流程控制。 bra 该指令的作用与jmp几乎相同&#xff0c;同样可以把目的操作数传递到PC寄存器&a…