Qt -DFS可视化

博客主页:【夜泉_ly】
本文专栏:【暂无】
欢迎点赞👍收藏⭐关注❤️

在这里插入图片描述

目录

      • 前言
      • 关于如何sleep
      • 实现思路
      • Pixmaps
        • pixmaps.h
        • pixmaps.cpp
      • MapSquare
        • mapsquare.h
        • mapsquare.cpp
      • dfsthread
        • dfsthread.h
        • dfsthread.cpp
          • run dfs
          • 其他
        • Widget
          • Unit
          • 其他

Qt -DFS算法可视化

完整代码:https://gitee.com/Ye_Quan/my-cpp-project/tree/master/Qt-experiments/dfs-visualizer

前言

本来,我是在搞我的战棋小游戏的,
如果大家玩过战棋应该知道,
当我们点击一个单位时,
一般会显示出一片区域,
表示这个单位的移动范围(以及可攻击目标等等)。

我当时就想到了用DFS和BFS,
然后就写了。

之后我想,
既然已经在游戏中实现了DFS,
那能不能以现有代码为基础,
写一个DFS算法可视化呢?
感觉很有意思,
所以有了本文。

当然,虽然标题叫做DFS可视化,
但是代码部分大多是在对战棋游戏的实现进行试验,
因此存在很多‘多余’的设计,大家看看就行。

关于如何sleep

既然是可视化,
那必定不能等DFS完了再更新地图状态,
因此我决定在每次更新了一个格子的状态后,
就暂停一会儿再继续DFS。

结果问题来了,
怎么做到每次都暂停一会儿?

我本来想直接在DFS中直接加sleep就好了:
在这里插入图片描述
然后程序卡了,
DFS完了才更新界面。
我以为是没用信号触发的问题,
改成用信号触发地格更新:
在这里插入图片描述
结果还是不行。
为什么?
最开始我以为和Linux中多线程竞争锁的问题类似,
大概描述一下就是:
抢锁 - 我抢到了! - 我释放 - 欸我离得近,我再抢!
然后发现了一个本质的区别:
这里只有一个线程啊🤣!

问题出在事件队列:
当前这个DFS就是事件队列的一个事件,
而DFS发送的信号所触发的事件,
不过是去事件队列后面排队罢了,
那现在执行的是谁?
还是DFS!
我DFS没跑完,
你们后面的事件都别想跑!

所以用QTimer?
我这是DFS。。。
如果用QTimer,那得搞个stack,
然后用类似非递归版的DFS来做吧?

我最终的解决方案是,
使用子线程跑DFS,
这样就不会阻塞主线程的事件队列了。

实现思路

那么大致分为几个模块

首先是图片模块,
这里搞的单例模式,
用于加载图片给所有人用。
为什么提前加载?
因为不这样做会很卡。。。

然后是地图格模块,
我感觉地图格在战棋游戏中还挺重要的,
所以把很多属性存进去了,
这里只是个DFS可视化,
所有只保留关键的一些属性。

然后是子线程模块,
主要作用是进行DFS和发送信号。

最后就是widget主窗口,
进行初始化地图,处理用户交互等工作。

Pixmaps

比较简单,比较作用就是加载和存储图片。
注意这里不能用饿汉模式,
图片的加载必须放在QApplication对象创建之后。

pixmaps.h
#ifndef PIXMAPS_H
#define PIXMAPS_H
#include <QPixmap>
class Pixmaps {static Pixmaps *getInstance();
private:Pixmaps();Pixmaps(const Pixmaps&) = delete;static void* _instance;
public:QPixmap map0, map1, map2, map3, dst, src;const int map_width = 100, map_height = 100,unit_width = 80, unit_height = 80;
};
#endif // PIXMAPS_H
pixmaps.cpp
#include "pixmaps.h"
void* Pixmaps::_instance = nullptr; // 这个不能放.h   --  会造成重定义问题// 报错会提示在其他包含了这个.h文件中, 重定义了_instance
Pixmaps* Pixmaps::getInstance(){if(_instance == nullptr){// 加锁。。。懒得加了if(_instance == nullptr){_instance = new Pixmaps;}}return (Pixmaps*)_instance;
}Pixmaps::Pixmaps(): map0("://image/map0.png"), map1("://image/map1.png"), map2("://image/map2.png"), map3("://image/map3.png"), map_src("://image/map_src.png"), map_dst("://image/map_dst.png")
{map0 = map0.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map1 = map1.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map2 = map2.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map3 = map3.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map_dst = map_dst.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);map_src = map_src.scaled(map_width, map_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}

单例,只是为了让别人更方便的用图片罢了,
所以不用搞太复杂 - - 至少这里不用。

MapSquare

地格类

mapsquare.h
#ifndef MAPSQUARE_H
#define MAPSQUARE_H
#include <QLabel>
#include "pixmaps.h"class MapSquare : public QLabel
{Q_OBJECT
public:MapSquare(QWidget *parent = nullptr);enum UNIT_TYPE{UNIT_TYPE_EMPTY = 0,UNIT_TYPE_PLAYER1 = 1,UNIT_TYPE_PLAYER2 = 2};enum STATUS{STATUS_UNCHECKABLE = -1,STATUS_EMPTY = 0,STATUS_MOVE = 1,STATUS_ATTACK = 2,};void setStatus(STATUS new_status, bool update_pixmap = true);void setUnitType(UNIT_TYPE new_type);void setIndex(int r, int c);void setSrc(int r = -1, int c = -1);void setUnit(void* unit = nullptr);STATUS status(){return _status;}UNIT_TYPE unit_type(){return _unit_type;}int r(){return index_r;}int c(){return index_c;}int src_r() {return _src_r;}int src_c() {return _src_c;}void* unit() {return _unit;}
private:UNIT_TYPE _unit_type;STATUS _status;int index_r, index_c;int _src_r = -1, _src_c = -1;void* _unit = nullptr;
};
#endif // MAPSQUARE_H

嗯,基本是直接cv过来的,enum都没改,
毕竟要改的话太麻烦了。

这里保留了我们需要用的,
_unit_type,即地格上面有什么,
无、阵营1(这里就是起点)、阵营2(这里就是终点)。

_status,即地格的状态,
不可达请添加图片描述、空请添加图片描述、可抵达 请添加图片描述、可攻击请添加图片描述(这里用来表示DFS找到的路径)。

r和c,表示地格在地图中的坐标。

_src_r、_src_c、_unit,这里不用管。

mapsquare.cpp
#include "mapsquare.h"MapSquare::MapSquare(QWidget *parent): QLabel(parent) {int w = Pixmaps::getInstance()->map_width,h = Pixmaps::getInstance()->map_height;setGeometry(-w, -h, w, h);setStatus(STATUS_EMPTY);_unit_type = UNIT_TYPE_EMPTY;
}void MapSquare::setStatus(STATUS new_status, bool update_pixmap){_status = new_status;if(update_pixmap){if(_status == STATUS_UNCHECKABLE){setPixmap(Pixmaps::getInstance()->map0);} else if(_status == STATUS_EMPTY){setPixmap(Pixmaps::getInstance()->map1);} else if(_status == STATUS_MOVE){setPixmap(Pixmaps::getInstance()->map2);} else if(_status == STATUS_ATTACK){setPixmap(Pixmaps::getInstance()->map3);}}
}void MapSquare::setUnitType(UNIT_TYPE new_type) {_unit_type = new_type;
}void MapSquare::setIndex(int r, int c) {index_r = r;index_c = c;
}void MapSquare::setSrc(int r, int c) {_src_r = r;_src_c = c;
}void MapSquare::setUnit(void *unit) {_unit = unit;
}

都是简单的赋值,应该不必多说吧。

dfsthread

用于DFS的线程,本篇的核心。

dfsthread.h
#ifndef DFSTHREAD_H
#define DFSTHREAD_H#include "mapsquare.h"
#include <QObject>
#include <QWidget>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>class DfsThread : public QThread
{Q_OBJECT
public:explicit DfsThread(QWidget *parent = nullptr);void init(QVector<QVector<MapSquare*>>& _map, int _r, int _c, int _max_step);public:void pause();void resume();void oneStep(bool);protected:void run() override;bool dfs(int r, int c, int step, int max_step);signals:void updateSquare(int r, int c, MapSquare::STATUS status);void dstNotFound();private:QVector<QVector<MapSquare*>>* map; // 这里要不断更改指向, 所以不能用引用QVector<QVector<bool>> visited;int src_r, src_c, max_step;int r_map, c_map;static const int dx[4];static const int dy[4];
private:bool one_step = false;bool paused = false;QMutex mutex;QWaitCondition pauseCond;
};#endif // DFSTHREAD_H

讲讲成员变量:
map用来存地图。
visited用来标记当前走过的路径。
src_r, src_c 就是起点坐标。
max_step 是最多走多少步。
r_map, c_map 是地图行、列的数量。
dx,dy 用于控制方向。

嗯,后面几个是用来控制线程的,
为了达到暂停、单步执行的效果,
用到了锁和条件变量。

dfsthread.cpp
run dfs
void DfsThread::run()
{{QMutexLocker locker(&mutex);while (paused) {pauseCond.wait(&mutex);}}if (!map) return;if (false == dfs(src_r, src_c, 0, max_step)){emit dstNotFound();}
}

先检查被暂停没有,然后开始DFS。

bool DfsThread::dfs(int r, int c, int step, int max_step)
{{QMutexLocker locker(&mutex);while (paused) {pauseCond.wait(&mutex);}}

同样,每次DFS前检查一下被暂停没有。

    if (step > max_step) return false;if (r < 0 || r >= r_map || c < 0 || c >= c_map) return false;if (visited[r][c]) return false;const QVector<QVector<MapSquare*>>& mapRef = *map;if (mapRef[r][c]->status() == MapSquare::STATUS_UNCHECKABLE) return false;visited[r][c] = true; // 标记为已访问

对当前地格是否可走进行判定,
如果可走,在visited中标记。
接下来的地格将不包括:请添加图片描述

    // 检查是否找到目标 (不同阵营)if (mapRef[r][c]->unit_type() != MapSquare::UNIT_TYPE_EMPTY &&mapRef[r][c]->unit_type() != mapRef[src_r][src_c]->unit_type()) {// 找到目标!emit updateSquare(r, c, MapSquare::STATUS_ATTACK);mapRef[r][c]->setSrc(src_r, src_c);QThread::msleep(300);return true;  // 成功找到路径}

如果找到目标,
发出信号将地格 请添加图片描述 标记为STATUS_ATTACK 请添加图片描述,
并开始返回。

    if(mapRef[r][c]->status() != MapSquare::STATUS_MOVE){emit updateSquare(r, c, MapSquare::STATUS_MOVE);mapRef[r][c]->setSrc(src_r, src_c);if(one_step) {pause();} else {QThread::msleep(50); // 暂停一段时间}}

如果没找到,
发出信号将地格 请添加图片描述 标记为STATUS_MOVE 请添加图片描述,
并判断是否是单步执行,
如果是,直接暂停线程,
如果不是,那么休眠50ms,
展现出地格是一步步被改变的,
而不是一下就dfs完了。

    for (int i = 0; i < 4; i++) {int x = dx[i] + r;int y = dy[i] + c;if (dfs(x, y, step + 1, max_step)) {// 到了这里, 说明找到了// 那么接着修改地格状态, 并返回 trueemit updateSquare(r, c, MapSquare::STATUS_ATTACK);QThread::msleep(300);return true;}}// 恢复状态visited[r][c] = 0;return false;
}
其他
#include "dfsthread.h"
#include <QDebug>const int DfsThread::dx[4] = {0, 0, 1, -1};
const int DfsThread::dy[4] = {1, -1, 0, 0};DfsThread::DfsThread(QWidget *parent): QThread(parent), map(nullptr)
{
}void DfsThread::init(QVector<QVector<MapSquare*>>& _map, int _r, int _c, int _max_step){map = &_map;src_r = _r;src_c = _c;max_step = _max_step;r_map = _map.size();c_map = _map[0].size();// 这里每次都得重新设置一下visited = QVector<QVector<bool>>(r_map, QVector<bool>(c_map, false));
}void DfsThread::pause(){QMutexLocker locker(&mutex);paused = true;
}void DfsThread::resume(){QMutexLocker locker(&mutex);paused = false;pauseCond.wakeAll();
}void DfsThread::oneStep(bool o){one_step = o;
}

我试了一下,
似乎不加锁和条件变量也行,
不过为了安全还是加上吧。

Widget

UI界面史中史,设计得就是依托。

完整的可以去我的代码仓库看,
这里就不列出来了。

Unit
struct Unit : public QPushButton{Unit(QWidget *parent=nullptr): QPushButton(parent){}int r = -1, c = -1;int move_range = 5;MapSquare::UNIT_TYPE unit_type;
};

这个类本来单独在一个文件里的,被我合过来了。

关于这个类,我写了几个函数:

void initUnit(Unit* unit, MapSquare::UNIT_TYPE unit_type);
void moveUnit(Unit* unit, int r, int c);
void connectUnit(Unit* &unit);

一个是初始化,
一个是移动,
一个是连接信号和槽(继承自按钮,可点击)。

initUnit :

void Widget::initUnit(Widget::Unit *unit, MapSquare::UNIT_TYPE unit_type)
{if(unit_type == MapSquare::UNIT_TYPE_PLAYER1)unit->setIcon(Pixmaps::getInstance()->map_src);else if(unit_type == MapSquare::UNIT_TYPE_PLAYER2)unit->setIcon(Pixmaps::getInstance()->map_dst);elseqDebug() << "初始化单位为未定义类型";unit->setIconSize(QSize(80, 80));unit->setStyleSheet("border : transparent;");unit->unit_type = unit_type;
}

传入一个Unit*,和单位类型,
就能初始化一个单位。

moveUnit

void Widget::moveUnit(Widget::Unit *unit, int r, int c)
{if(unit->r != -1 && unit->c != -1) {map[unit->r][unit->c]->setUnitType(MapSquare::UNIT_TYPE_EMPTY);map[unit->r][unit->c]->setUnit();}unit->setGeometry(map[r][c]->geometry());unit->r = r;unit->c = c;unit->raise();map[r][c]->setUnitType(unit->unit_type);map[r][c]->setUnit(unit);
}

首先得判断是否刚初始化,
如果已经设置过坐标,
那么得先把原地格的状态清空,
(感觉还能封装,地格可以提供一个状态清空函数)
然后才能移动单位,
并重新设置相关属性。

connectUnit

void Widget::connectUnit(Widget::Unit* &unit)
{connect(unit, &QPushButton::clicked, this, [&](){qDebug() << "unit clicked ...";if(dfsThreadIsRunning()) return;MapSquare::STATUS _status = map[unit->r][unit->c]->status();if(_status == MapSquare::STATUS_EMPTY) {resetMap(); // 重置地图dfsThread.init(map, unit->r, unit->c, unit->move_range); // 每次都要初始化dfsThread.start(); // 不能用run, 会阻塞主线程} else {resetMap(); // 重置地图}});
}

单位被点击,
如果当前地格状态为空,
则开始DFS。
如果地格状态为其他的,
暂时不做处理。

其他
connect(&dfsThread, &DfsThread::updateSquare, this, [this](int r, int c, MapSquare::STATUS status) {qDebug() << "子线程来信号了!" << status;map[r][c]->setStatus(status);
});

这个放在构造函数里,
子线程发来信号,
主线程进行地格的状态更新。

在这里插入图片描述


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RHCSA Linux 系统 文件系统权限

1. 文件的一般权限 &#xff08;1&#xff09;文件权限标识解读 drwxr - xr - x. 12 root root 144 Feb 17 16:51 usr ➤d&#xff1a;文件类型&#xff08;d 表示目录&#xff09; ➤rwx&#xff1a;文件所有者权限&#xff08;读 r&#xff0c;写 w&#xff0c;执行 x&am…

华为云IoT平台与MicroPython实战:从MQTT协议到物联网设备开发

目录 前言 1. 华为云 1.1. 创建实例 1.2. 创建产品 1.3. 编辑服务模型 1.4. 注册设备 1.4.1. 复制设备连接参数 1.5. 连接参考代码 2. micropython版-物联网 2.1. 环境搭建 2.2. 实现步骤 2.3. 示例代码 结语 前言 物联网&#xff08;IoT&#xff09;技术的快速发…

2025-04-30 AIGC-如何做短片视频

摘要: 2025-04-30 AIGC-如何做短片视频 如何做短片视频: 一、画图修图 1.保存视频&#xff08;无水保存&#xff09; 2.文案提取&#xff08;提取文案&#xff09; 3. DeepSeek(提示词&#xff09; 4.小梦Ai&#xff08;图片视频&#xff09; 5.修图Ai 6.扩图Ai 7.养生…

硬件工程师面试常见问题(10)

第四十六问&#xff1a;锁存器&#xff0c;触发器&#xff0c;寄存器三者的区别 触发器&#xff1a;能够存储一位二值信号的基本单元电路统称为 "触发器"。&#xff08;单位&#xff09; 锁存器&#xff1a;一位触发器只能传送或存储一位数据&#xff0c;而在实际工…

外部访问 Kubernetes 集群中 MQ 服务的方案

外部访问 Kubernetes 集群中 MQ 服务的方案 当您在 Kubernetes 集群中部署了消息队列服务&#xff08;如 RabbitMQ、Kafka、ActiveMQ 等&#xff09;后&#xff0c;以下是外部客户端访问这些服务的几种可靠方法&#xff1a; 一、基础访问方案 1. NodePort 方式暴露服务 # M…

论文笔记(八十二)Transformers without Normalization

Transformers without Normalization 文章概括Abstract1 引言2 背景&#xff1a;归一化层3 归一化层做什么&#xff1f;4 动态 Tanh &#xff08;Dynamic Tanh (DyT)&#xff09;5 实验6 分析6.1 DyT \text{DyT} DyT 的效率6.2 tanh \text{tanh} tanh 和 α α α 的消融实验…

软考中级-软件设计师 操作系统(手写笔记)

第一章&#xff1a;基础知识 第二章&#xff1a;进程管理 状态转换图 进程同步机制 信号量机制 信号量题 死锁 第三章&#xff1a;存储管理 基础知识 分页存储管理 分段存储管理 段页式存储管理 页面置换算法 第四章&#xff1a;文件管理 基础知识 索引分配 空闲存储空间的管…

ubuntu 部署moodle

通过地址https://download.moodle.org/releases/latest/选择下载&#xff0c;下载两种压缩包都特别慢&#xff08;有可能无法下载&#xff09;。 可以使用下面git下载项目 注意图中php、mysql等版本要求&#xff0c;本次采用Ubuntu22.04下 nginxphp8.2mysql8.4部署 mkdir /var…

python实战项目67:空气质量在线检测平台js逆向

python实战项目67:空气质量在线检测平台js逆向 一、需求介绍二、完整代码一、需求介绍 项目需求是获取某个城市(以北京市为例)历年(2013年12月至2025年4月)的空气质量数据,字段包括日期、AQI、质量等级、PM2.5、PM10、NO2、CO、SO2等。改网站的网址是“https://www.aqis…

【Linux】记录一个有用PS1

PS1 是用来定义shell提示符的环境变量 下面是一个带有颜色和丰富信息的 Linux PS1 配置示例&#xff0c;包含用户名、主机名、路径、时间、Git 分支和退出状态提示&#xff1a; # 添加到 ~/.bashrc 文件末尾 PS1\[\e[1;32m\]\u\[\e[m\] # 绿色粗体用户名 PS…

Python PyTorch库【机器学习框架】全面深入讲解与实践

一、PyTorch 核心概念 1. 定义与发展背景 PyTorch 是由 Facebook AI Research (FAIR) 开发的开源机器学习框架&#xff0c;2016 年首次发布。其核心特性包括&#xff1a; 动态计算图&#xff08;Define-by-Run&#xff09;GPU 加速张量计算自动微分系统丰富的神经网络模块 …

呼叫中心座席管理系统:智能升级,高效服务

在数字化转型加速的今天&#xff0c;客户服务体验已成为企业竞争力的核心要素。传统 呼叫中心系统 依赖硬件设备、人工操作的模式已无法满足高效、智能、灵活的现代企业需求。畅信达呼叫中心 座席管理系统 V5.0应运而生&#xff0c;以WEBRTC软电话接入、智能座席辅助、知识库管…

时态--00--总述

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 时态句子结构时态标志词 时态 句子结构 时态标志词

算法每日一题 | 入门-顺序结构-字母转换

字母转换 题目描述 输入一个小写字母&#xff0c;输出其对应的大写字母。例如输入 q[回车] 时&#xff0c;会输出 Q。 输入格式 无 输出格式 无 输入输出样例 #1 输入 #1 q输出 #1 QC 首先我们要知道&#xff0c;C字符的所有转换形式都是依照ASCII码来的。 所以&…

晶振:从消费电子到航天领域的时间精度定义者

从手表到卫星&#xff1a;晶振如何在不同领域定义时间精度 在时间的长河中&#xff0c;人类对时间精度的追求永无止境。从古老的日晷到如今精密的计时仪器&#xff0c;每一次进步都离不开技术的革新。而晶振&#xff0c;作为现代计时的核心元件&#xff0c;在不同领域发挥着至…

短视频矩阵系统贴牌开发实战:批量剪辑文件夹功能设计与实现

摘要&#xff1a;在短视频矩阵系统的开发中&#xff0c;批量处理功能是提升运营效率的关键。本文将深入探讨如何实现基于文件夹的短视频批量剪辑功能&#xff0c;涵盖技术选型、核心功能实现及代码示例。 一、需求背景与场景价值 在短视频矩阵运营场景中&#xff0c;运营者常面…

读书笔记--华为从偶然到必然之创新与技术开发阅读有感

最近继续阅读一本讲述华为研发投资与管理实践方面的书籍&#xff0c;分享给大家。华为在创新与技术研发方面有体系化、系统化和延续性。创新是企业的生命线&#xff0c;是企业发展的不竭动力&#xff0c;同时将企业文化与创新精神进行了融合&#xff0c;华为的企业文化强调以客…

基于DeepSeek与HTML的可视化图表创新研究

一、研究背景 在当今数字化时代&#xff0c;数据呈指数级增长&#xff0c;广泛渗透于社会各个领域。无论是商业运营、科学研究&#xff0c;还是公共管理等方面&#xff0c;海量数据蕴含着丰富的潜在价值&#xff0c;成为驱动决策优化、推动业务发展、促进科学创新的关键要素。数…

K8S - 命名空间实战 - 从资源隔离到多环境管理

引言 在传统的物理机或虚拟机环境中&#xff0c;不同业务应用共享资源&#xff0c;容易导致权限冲突、资源争用和管理混乱。Kubernetes 通过 命名空间&#xff08;Namespace&#xff09;实现资源逻辑隔离&#xff0c;将集群划分为多个虚拟子集群&#xff0c;从而解决以下问题&…

Unity3D仿星露谷物语开发40之割草动画

1、目标 当Player选择Scythe后&#xff0c;鼠标悬浮在草上&#xff0c;会显示绿色光标。鼠标左击&#xff0c;会触发割草的动画。 2、优化Settings.cs脚本 添加以下两行代码&#xff1a; // Reaping&#xff08;收割&#xff09; public const int maxCollidersToTestPerRe…