QML小案例 使用QML简单实现翻牌版扫雷游戏(一)
使用QML实现扫雷功能案例,C++类生成炸弹二维数据地图,判断是否按住炸弹,是否点到空白等数据处理,C++类处理篇
乘着最近不忙,学习学习QML的相关知识点,本来打算在项目中直接使用QML开发几个小窗体的,但是才学这玩意,把握不注,所以就打算还是先写几个案例学习学习先。
扫雷这个游戏大家都玩过,功能也简单,正好拿来练手。
分为两章节,当前为C++处理部分
QML简单介绍
Qt中的QML框架是一种用于描述应用程序用户界面的声明式编程语言,全称是Qt Meta-Object Language(Qt元对象语言)。它允许开发人员和设计人员创建高性能、流畅的动画和具有视觉吸引力的应用程序。
QML主要基于一些可视组件以及这些组件之间的交互、关联来描述用户界面。它是一种高可读性的语言,设计目的是使组件能够以动态方式互连,并且允许在用户界面中轻松地重用和定制组件。Qt QML模块为QML语言开发应用程序和库提供了一个框架,它定义并实现了语言及其引擎架构,并且提供了一个接口,允许应用开发者以自定义类型和集成JavaScript、C++代码的方式来扩展QML语言。
Qt Quick是QML类型和功能的标准库,包含了可视化类型、交互类型、动画、模型和视图、粒子特效和渲染特效等。在QML应用程序中,可以通过一个简单的import语句来使用Qt Quick模块提供的所有功能。Qt Quick模块提供了QML创建用户界面所需的所有基本类型,并允许使用QML语言创建用户界面的QML接口和使用C++语言扩展QML的C++接口。
总的来说,QML框架提供了一种高效的方式来开发用户界面,它结合了声明式编程的灵活性和Qt Quick的强大功能库,使得开发人员能够创建出具有丰富交互和视觉吸引力的应用程序。同时,QML也支持与JavaScript和C++的无缝整合,进一步增强了其开发效率和灵活性。
QML开发资料比较少,也就一些对界面要求比较高的公司会强调需要使用QML开发,我去年看招聘网站上也就三四个公司指定需要掌握QML技术,建议学习的时候多问文言一心或者老北鼻GPT。多了解一些控件使用。
扫雷案例
扫雷这个游戏代码量不高,算法上主要是随机生成一个布设指定数量炸弹的二维数组,循环实现一个9格内的炸弹数量统计和空白处的一个查找临近所有空白的功能。
写案例的时候参考的是蓝色的Win10的扫雷游戏,这个版的扫雷特别经典。敲代码累了就来一局。
获取素材
没找到免费的蓝色版本的扫雷游戏素材,手动截图也不好看,只好另辟蹊径找了一
个卡牌版的素材,大差不差的就将就用着。
素材来源:https://game-icons.net/
最终效果展示
由于素材是单色调的,所有整个窗体就显得很单调。
加载时的特效是随机生成的。炸弹个数限制为40个。
预处理
开发环境:Qt Creator 5.13.1,MinGW 64X
使用 C++类 处理数据 ,使用 QML显示界面
要在QML中使用C++类 ,类必须继承QObject对象,使用QT元对象
C++ 类生成数据
定义 元对象宏定义
声明元对象的通用模版,主要用于可供QML读取和修改C++类中变量声明
#define Attribute_INIT(__type__,__Attr__) \
Q_INVOKABLE __type__ get_##__Attr__() const {return this->__Attr__; qDebug()<<"get!";} \
Q_INVOKABLE void set_##__Attr__(__type__ _##__Attr__) { this->__Attr__=_##__Attr__; emit __Attr__##_Changed(_##__Attr__);}
#define PROPERTY_INIT(__type__,__Attr__) \
Q_PROPERTY(__type__ __Attr__ READ get_##__Attr__ WRITE set_##__Attr__( NOTIFY __Attr__##_Changed))
#define Signals_INIT(__type__,__Attr__) \
void __Attr__##_Changed(__type__);
定义 C++类
定义一个C++操作类 Operate_Connector,用来保存炸弹数量,地图宽带,地图高度,炸弹二维矩阵等参数。
Operate_Connector头文件
///操作类
class Operate_Connector:public QObject
{
Q_OBJECT
PROPERTY_INIT(int,Bomb_Count)
PROPERTY_INIT(int,Plat_Rows)
PROPERTY_INIT(int,Plat_Cols)
PROPERTY_INIT(int,animType)
Q_PROPERTY(QVector<QVector<int>> MapBomb MEMBER MapBomb)
public:
///必须实现QObject(parent) 否则无法设置id
Q_INVOKABLE Operate_Connector(QObject *parent = nullptr);
// Q_INVOKABLE int GetBombCount() {return this->Bomb_Count;}
// Q_INVOKABLE int SetBombCount(int _count) {this->Bomb_Count=_count;}
Attribute_INIT(int,Bomb_Count)
Attribute_INIT(int,Plat_Rows)
Attribute_INIT(int,Plat_Cols)
Attribute_INIT(int,animType)
///生成一个 int类型 二维矩阵
Q_INVOKABLE void reconstruction();
///是否按住炸弹
Q_INVOKABLE int is_Bomb(int index);
//加载时的动画特效
enum animEffects
{
//随机
random=0,
//从左到右
leftToright=1,
//从中心向左右散开
centerToSide=2,
//从中心向左右散开
centerToSpread=3
};
//Roster枚举类型注册
Q_ENUM(animEffects);
private:
///炸弹数量
int Bomb_Count ;
///地图宽带
int Plat_Rows;
///地图高度
int Plat_Cols;
///加载时的动画特效
int animType;
///二维矩阵
/// -1 炸弹
/// 0 空白
/// 1-8 九格之内炸弹数量
QVector<QVector<int>> MapBomb;
/// 初始化全为 -1
/// 当统计-1 个数等于炸弹个数时 胜利
QVector<int> VectBomb;
///列排序基准点
static int datum_mark;
/// 列排序
/// 适用于中心散开特效
static bool sortType(QPair<int,int> a1,QPair<int,int> a2);
///查找空白项
void findblank(QPair<int,int> item, QList<QPair<int,int>> &blankitems);
///验证炸弹数量
void verify_bomb();
public:
//!信号
signals:
Signals_INIT(int,Bomb_Count);
Signals_INIT(int,Plat_Rows);
Signals_INIT(int,Plat_Cols);
Signals_INIT(int,animType);
//!显示牌
Q_INVOKABLE void blank_clearing(int index,int board,int duration) const;
//!游戏胜利
void gameWin();
//!游戏失败
void gameFailure();
};
生成一个二维矩阵
初始化二维矩阵QVector<QVector<int>> MapBomb;数据
使用 #include <QRandomGenerator>生成随机数
并随机插入指定Bomb_Count个数的炸弹,
再循环统计每个单元格9格范围内的炸弹数量
void Operate_Connector::reconstruction()
{
if(Bomb_Count==0||Plat_Rows==0||Plat_Cols==0)
return;
datum_mark=ceil(Plat_Cols/2);
MapBomb=QVector<QVector<int>>(Plat_Rows, QVector<int>(Plat_Cols, 0));
VectBomb=QVector<int>(Plat_Cols*Plat_Rows, -1);
//加载特效
animType= QRandomGenerator::global()->bounded(4);
//随机插入炸弹
for(int B=0;B<Bomb_Count;B++)
{
do{
// 生成一个介于0(包含)和行数/列数(不包含)之间的随机整数
int r_row = QRandomGenerator::global()->bounded(Plat_Rows);
int r_col = QRandomGenerator::global()->bounded(Plat_Cols);
if(MapBomb[r_row][r_col]!=-1)
{
MapBomb[r_row][r_col]=-1;
break;
}
}while (true);
}
//统计当前项3*3矩形范围内的炸弹个数
for(int r=0;r<Plat_Rows;r++)
{
for(int c=0;c<Plat_Cols;c++)
{
//炸弹不统计
if(MapBomb[r][c]==-1)
continue;
int number=0;
//左上
if((r-1<Plat_Rows && r-1 >=0 ) &&
(c-1<Plat_Cols && c-1 >=0 ))
{
if(MapBomb[r-1][c-1]==-1)
number++;
}
//上
if((r-1<Plat_Rows && r-1 >=0 ) &&
(c<Plat_Cols && c >=0 ))
{
if(MapBomb[r-1][c]==-1)
number++;
}
//右上
if((r-1<Plat_Rows && r-1 >=0 ) &&
(c+1<Plat_Cols && c+1 >=0 ))
{
if(MapBomb[r-1][c+1]==-1)
number++;
}
//右
if((r<Plat_Rows && r >=0 ) &&
(c+1<Plat_Cols && c+1 >=0 ))
{
if(MapBomb[r][c+1]==-1)
number++;
}
//右下
if((r+1<Plat_Rows && r+1 >=0 ) &&
(c+1<Plat_Cols && c+1 >=0 ))
{
if(MapBomb[r+1][c+1]==-1)
number++;
}
//下
if((r+1<Plat_Rows && r+1 >=0 ) &&
(c<Plat_Cols && c >=0 ))
{
if(MapBomb[r+1][c]==-1)
number++;
}
//左下
if((r+1<Plat_Rows && r+1 >=0 ) &&
(c-1<Plat_Cols && c-1 >=0 ))
{
if(MapBomb[r+1][c-1]==-1)
number++;
}
//左
if((r<Plat_Rows && r >=0 ) &&
(c-1<Plat_Cols && c-1 >=0 ))
{
if(MapBomb[r][c-1]==-1)
number++;
}
MapBomb[r][c]=number;
}
}
}
点击判断点击的是不是炸弹
判断点击索引处是炸弹还是空白还是概率值
如果是空白则展开所有空白项,如果是概率就翻转自己。
炸弹游戏结束
void Operate_Connector::findblank(QPair<int,int> item, QList<QPair<int,int>> &blankitems)
{
int r=item.first;
int c=item.second;
//左上
if((r-1<Plat_Rows && r-1 >=0 ) &&
(c-1<Plat_Cols && c-1 >=0 ) &&
MapBomb[r-1][c-1]!=-1)
{
if(!blankitems.contains(QPair<int,int>(r-1,c-1)))
{
blankitems.append(QPair<int,int>(r-1,c-1));
if(MapBomb[r-1][c-1]==0)
findblank(QPair<int,int>(r-1,c-1),blankitems);
}
}
//上
if((r-1<Plat_Rows && r-1 >=0 ) &&
(c<Plat_Cols && c >=0 ) &&
MapBomb[r-1][c]!=-1)
{
if(!blankitems.contains(QPair<int,int>(r-1,c)))
{
blankitems.append(QPair<int,int>(r-1,c));
if(MapBomb[r-1][c]==0)
findblank(QPair<int,int>(r-1,c),blankitems);
}
}
//右上
if((r-1<Plat_Rows && r-1 >=0 ) &&
(c+1<Plat_Cols && c+1 >=0 ) &&
MapBomb[r-1][c+1]!=-1)
{
if(!blankitems.contains(QPair<int,int>(r-1,c+1)))
{
blankitems.append(QPair<int,int>(r-1,c+1));
if(MapBomb[r-1][c+1]==0)
findblank(QPair<int,int>(r-1,c+1),blankitems);
}
}
//右
if((r<Plat_Rows && r >=0 ) &&
(c+1<Plat_Cols && c+1 >=0 &&
MapBomb[r][c+1]!=-1))
{
if(!blankitems.contains(QPair<int,int>(r,c+1)))
{
blankitems.append(QPair<int,int>(r,c+1));
if(MapBomb[r][c+1]==0)
findblank(QPair<int,int>(r,c+1),blankitems);
}
}
//右下
if((r+1<Plat_Rows && r+1 >=0 ) &&
(c+1<Plat_Cols && c+1 >=0 &&
MapBomb[r+1][c+1]!=-1))
{
if(!blankitems.contains(QPair<int,int>(r+1,c+1)))
{
blankitems.append(QPair<int,int>(r+1,c+1));
if(MapBomb[r+1][c+1]==0)
findblank(QPair<int,int>(r+1,c+1),blankitems);
}
}
//下
if((r+1<Plat_Rows && r+1 >=0 ) &&
(c<Plat_Cols && c >=0 ) &&
MapBomb[r+1][c]!=-1)
{
if(!blankitems.contains(QPair<int,int>(r+1,c)))
{
blankitems.append(QPair<int,int>(r+1,c));
if(MapBomb[r+1][c]==0)
findblank(QPair<int,int>(r+1,c),blankitems);
}
}
//左下
if((r+1<Plat_Rows && r+1 >=0 ) &&
(c-1<Plat_Cols && c-1 >=0 ) &&
MapBomb[r+1][c-1]!=-1)
{
if(!blankitems.contains(QPair<int,int>(r+1,c-1)))
{
blankitems.append(QPair<int,int>(r+1,c-1));
if(MapBomb[r+1][c-1]==0)
findblank(QPair<int,int>(r+1,c-1),blankitems);
}
}
//左
if((r<Plat_Rows && r >=0 ) &&
(c-1<Plat_Cols && c-1 >=0 ) &&
MapBomb[r][c-1]!=-1)
{
if(!blankitems.contains(QPair<int,int>(r,c-1)))
{
blankitems.append(QPair<int,int>(r,c-1));
if(MapBomb[r][c-1]==0)
findblank(QPair<int,int>(r,c-1),blankitems);
}
}
}
bool Operate_Connector::sortType(QPair<int,int> a1,QPair<int,int> a2)
{
return abs(datum_mark-a1.second)>abs(datum_mark-a2.second);
}
//验证是否已经打开了所有项
void Operate_Connector::verify_bomb()
{
int _BombCount=0;
for(int i=0;i<VectBomb.count();i++)
{
if(VectBomb[i]==-1)
_BombCount++;
}
if(_BombCount==Bomb_Count)
emit gameWin();
}
#include <QPair>
int Operate_Connector::is_Bomb(int index)
{
//QML的ceil与Qt的Ceil不一样
int row= ceil(index/Plat_Rows);
int col=index-row*Plat_Cols;
if(row>=Plat_Rows || col>=Plat_Cols)
return -1;
//默认中心散开特效 centerSpread
QList<QPair<int,int>> blankitems;
if(MapBomb[row][col]==-1)
goto gameFailure;
blankitems.append(QPair<int,int>(row,col));
if(MapBomb[row][col]==0)
{
datum_mark=col;
//先找到空白附件的所有空白项
findblank(QPair<int,int>(row,col), blankitems);
//按照列间隔排序 可以不用排序
qSort(blankitems.begin(),blankitems.end(),sortType);
}
//计算时长
for(int s=0;s<blankitems.count();s++)
{
int index=blankitems[s].first*Plat_Rows+blankitems[s].second;
int board=MapBomb[blankitems[s].first][blankitems[s].second];
VectBomb[index]=board;
int duration=ceil(((double)abs(blankitems[s].second-col)/Plat_Cols)*1000)+200;
emit blank_clearing(index,board,duration);
}
verify_bomb();
// qDebug()<<"[Row] : "<<row<<" [Col] : "<<col;
// emit blank_clearing(index,MapBomb[row][col],200);
return MapBomb[row][col];
gameFailure:
//按住炸弹 全展开
for(int r=0;r<Plat_Rows;r++)
{
for(int c=0;c<Plat_Cols;c++)
{
blankitems.append(QPair<int,int>(r,c));
}
}
qSort(blankitems.begin(),blankitems.end(),sortType);
//计算时长
for(int s=0;s<blankitems.count();s++)
{
int index=blankitems[s].first*Plat_Rows+blankitems[s].second;
int board=MapBomb[blankitems[s].first][blankitems[s].second];
VectBomb[index]=board;
int duration=ceil(((double)abs(blankitems[s].second-col)/Plat_Cols)*1000)+200;
qDebug()<<duration;
emit blank_clearing(index,board,duration);
}
emit gameFailure();
return MapBomb[row][col];
}
C++类中主要涉及到元对象的使用:
Q_INVOKABLE用于注册方法,Q_PROPERTY用于注册变量Q_ENUM用于注册枚举
同时需要再main文件中注册实例qmlRegisterType<Operate_Connector>("DAL_Connector", 1, 0, "Operate_Connector");
这样就能在QML文件中内直接调用C++类
主要信号和槽函数前面可以不加Q_INVOKABLE,也能识别
值得注意的是:
- 在QML文件中使用
Connections链接C++类信号时必须on+首字母大写函数名,括号内可直接使用信号参数
如上例
Connections
{onBlank_clearing: {
// console.log("index : ",index) // 当C++信号被发射时,这里将被调用
// console.log("board : ",board)
// console.log("duration : ",duration)
}onGameWin: { }onGameFailure:{ }
}
QML实现界面篇 : QML小案例 使用QML简单实现翻牌版扫雷游戏(二)
更多推荐
所有评论(0)