Qt扫雷游戏
最近在学习Qt,于是想到将以前VS2013上写过的扫雷程序移植到Qt上.当时是模仿Window XP上自带的扫雷游戏写的,水平有限,代码写的比较水.现在把它移植到Qt平台上,实现了扫雷最基本功能,还有一些细节的东西没有深入去实现.话不多说先上一个效果图.
VS2013工程下载
Qt工程下载(linux)
GitHub项目
直接将Linux下的工程放到window下的Qt编译会出现两个问题:
warning: C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失
这个警告不打紧,关键是下面这个错误
error: C2065: “painter”: 未声明的标识符
折腾了好久,最后发现把程序中的中文注释去掉就好了。
下面来分析一下程序,首先来分析一下扫雷的一些基本操作,在看看程序界面如何实现.
首先自己定义了一个类来实现扫雷相关操作.
class CMineMap
{
public:
CMineMap();
~CMineMap();
void Create();
void Create(int,int,int);
void Restart();
private:
int ctnflag(int m, int n);
public:
int **Map;
int nMime;//雷数
int mx; //行数
int my; //列数
bool Onrbtdown(int m, int n);
bool Onlrbtdown(int m, int n);
bool Onlbtup(int m, int n);
bool iswin();
int winf;
int rMine;
bool first;
int timer;
};
Map成员变量用来保存雷区信息.以为雷区行数列数根据难易级别要动态改变,所以这里定义成指针,方便动态分配空间.当Map[x][y]取值为0-8时表示(x,y)这一格周围8格中的雷数,并且是已经被点开来了的,-1表示雷.若未被点开,则范围为99-108,99表示雷,100-108表示周围雷个数.若这个地方被插上小红旗,则范围为49-58.
布雷
上述类中nMine表示雷的数量,在雷区范围内生成nMine个随机坐标,把对应坐标的赋值成99,在没有雷的地方统计周围的雷数,并赋值为100-108.
点击事件
当一个格子(坐标为(x,y))被点下,如果个格子周围有雷,则Map[x][y]值为101-108,将这个值减去100就行了.如果周围都没有雷,就会爆出一大片,就是下面这种效果:
刚开始感觉很难实现,这形状完全不规则,一行行一列列去检测都难以实现.但仔细一想其实这个问题很好解决,当点下发现这地方周围都没雷的时候就递归的点击周围8个格子就行了.代码如下
bool CMineMap::Onlbtup(int m, int n)
{
...
if (Map[m][n] >= 101 && Map[m][n] <= 108)
{
Map[m][n] -= 100;
first = 0;
iswin();
return true;
}
if (Map[m][n] == 100)
{ //点击的地方周围都没雷,则递归点击周围8个
Map[m][n] -= 100;
Onlbtup(m - 1, n);
Onlbtup(m + 1, n);
Onlbtup(m, n - 1);
Onlbtup(m, n + 1);
Onlbtup(m - 1, n - 1);
Onlbtup(m + 1, n - 1);
Onlbtup(m - 1, n + 1);
Onlbtup(m + 1, n + 1);
}
if (Map[m][n] == 99)
{ //中雷了
...
}
return true;
}
当点击的格子是99那么恭喜你中雷了,把所有雷都显示出来就行了,具体做法就是把所有99都置为-1,如果插小红旗的地方,插错了的置为-2,代码如下
for (i = 0; i < mx; i++)
for (j = 0; j < my; j++)
{
if (Map[i][j] == 99)
Map[i][j] = -1;
if (Map[i][j]>49 && Map[i][j] < 60)
Map[i][j] = -2;
}
右击事件
右击一下插个红旗,在点一下小旗取消,实现起来很简单,点一下减50在点一下加50就行了.
左右键同时按下
如果一个格子周围所有雷都被标记,在这个格子上同时按下鼠标左右键,剩余未被点开的格子就会全被点开.实现起来也不难,调用函数周围都点一遍就行了.代码太费空间这里就不贴了.
扫雷的基本操作就介绍到这里,接下来看看界面是如何实现的.
绘制雷区
先看一下这张图片,这是当时我自己画的,有点丑,但画了好久,尽力了.
把这张图片作为资源添加进工程,在绘图函数中,根据Map的值,把图片对应部分贴到窗口对应位置.代码如下
void MainWindow::paintEvent(QPaintEvent *)
{
//实例 QPixmap
QPixmap bmpmap(":new/prefix1/res/item2.bmp");
QPixmap bmpnub(":new/prefix1/res/item1.bmp");
QPixmap bmpfame(":new/prefix1/res/item3.bmp");
QPixmap bmpface(":new/prefix1/res/item4.bmp");
//实例 QPainter 绘制类
QPainter painter(this);
//绘制雷区
for(int i=0;i<mineMap.mx;i++)
{
for(int j=0;j<mineMap.my;j++)
{
if (mineMap.Map[i][j] >= 0 && mineMap.Map[i][j] < 9)
{
painter.drawPixmap( i * 20+offsetx, j * 20 + 40+offsety, bmpmap, mineMap.Map[i][j] * 20, 0,20,20);
}
if (mineMap.Map[i][j]>90)
painter.drawPixmap(i * 20+offsetx, j * 20 + 40+offsety,bmpmap, 10 * 20, 0,20,20);
if (mineMap.Map[i][j] == -1)
painter.drawPixmap(i * 20+offsetx, j * 20 + 40+offsety,bmpmap, 9 * 20, 0,20,20);
if (mineMap.Map[i][j] == -2)
painter.drawPixmap(i * 20+offsetx, j * 20 + 40+offsety, bmpmap, 12 * 20, 0,20,20);
if (mineMap.Map[i][j] > 40 && mineMap.Map[i][j] < 60)
painter.drawPixmap(i * 20+offsetx, j * 20 + 40+offsety, bmpmap, 11 * 20, 0,20,20);
}
}
...
}
QPainter是Qt中的一个绘图类,用该类中的drawPixmap函数来贴图片.drawPixMap这个函数做的很完善,这个函数有11个同名函数,我用的是这一个
void drawPixmap(int x, int y, const QPixmap & pixmap, int sx, int sy, int sw, int sh)
这个函数用于从位图中截取一部分按原大小贴到指定位置.
绘制框架
这个放在雷区上面,根据雷区大小伸长缩短,左边显示剩余雷数,右边显示扫雷时间,中间放个表情.
数显示的话我自己画了个数码管
要显示剩余雷数,扫雷时间就把图片对应位置贴到框架上.
下面几个表情是从QQ表情包里扣出来的,稍微修改了下
上面四个表情分别表示游戏失败,游戏进行中,过关了,鼠标点下没放开时.但在这个程序中鼠标按下没放开的效果没有做,不过这不影响主体功能.
框架绘制代码如下:
//绘制框架
painter.drawPixmap( 0+offsetx, 0+offsety, bmpfame, 0, 0, 70, 40);
painter.drawPixmap(mineMap.mx * 10 - 20+offsetx, 0+offsety, bmpfame, 80, 0, 40, 40);
painter.drawPixmap(mineMap.mx * 20 - 70+offsetx, 0+offsety, bmpfame, 130, 0, 70, 40);
painter.drawPixmap(70+offsetx, 0+offsety, mineMap.mx * 10 - 90, 40, bmpfame, 70, 0, 10, 40);
painter.drawPixmap(mineMap.mx * 10 + 20+offsetx, 0+offsety, mineMap.mx * 10 - 90, 40, bmpfame, 70, 0, 10, 40);
painter.drawPixmap(mineMap.mx * 10 - 12+offsetx, 7+offsety, bmpface, mineMap.winf * 24, 0, 24, 24);
//绘制剩余雷数
if (rm < 0) rm = 0;
painter.drawPixmap(6+offsetx, 5+offsety, bmpnub, rm / 100 * 20, 0, 20, 28);
if (rm >= 100) rm %= 100;
painter.drawPixmap(26+offsetx, 5+offsety, bmpnub, rm / 10 * 20, 0, 20, 28);
painter.drawPixmap(46+offsetx, 5+offsety, bmpnub, rm % 10 * 20, 0, 20, 28);
//绘制扫雷时间
if (rt >= 1000) rt %= 1000;
painter.drawPixmap(mineMap.mx*20-66+offsetx, 5+offsety, bmpnub, rt / 100 * 20, 0, 20, 28);
if (rt >= 100) rt %= 100;
painter.drawPixmap(mineMap.mx*20 - 46+offsetx, 5+offsety, bmpnub, rt / 10 * 20, 0, 20, 28);
painter.drawPixmap(mineMap.mx*20 - 26+offsetx, 5+offsety, bmpnub, rt % 10 * 20, 0, 20, 28);
好了,整个界面就绘制完成了.接下来添加鼠标消息让程序动起来.
鼠标消息处理
Qt 中添加鼠标消息很容易,直接重载父类的mousePressEvent函数就好了.
通过event->button()和event->buttons()可以获取到底是哪个键被按下.
通过event->x()和event->y()获取鼠标按下的坐标.根据这个坐标计算出点到哪一格,然后调用扫雷类的相关操作.
另外,当游戏失败或过关时,可以通过点击表情从新开始游戏.
void MainWindow::mousePressEvent(QMouseEvent *event)
{
int px=event->x()-offsetx;
int py=event->y() -offsety;
int m = (px) / 20;
int n = (py) / 20 - 2;
if(event->buttons()==(Qt::LeftButton|Qt::RightButton))
{
if(mineMap.Onlrbtdown(m, n))
update();
}
else if(event->button()==Qt::LeftButton)
{
if(mineMap.Onlbtup(m,n))
{
if (!(rumtime->isActive() ))
rumtime->start(1000);//开始计时
update();
}
if (mineMap.winf==0 || mineMap.winf == 2)
{
if (rumtime->isActive() )
rumtime->stop();//停止计时
if (px > mineMap.mx * 10 - 15 && px< mineMap.mx * 10 + 15 && py>4 && py < 34)
{
mineMap.Restart();//点击表情图标重新开始
update();
}
}
}
else if(event->button()==Qt::RightButton)
{
if(mineMap.Onrbtdown(m, n))
update();
}
计时器
利用QTimer类实现计时功能,用connet函数设置QTimer超时执行on_sectime函数.on_sectime自己定义,但一定要在为private slots 类型.
private:
QTimer *rumtime;
private slots:
void on_sectime();
rumtime=new QTimer(this);
connect( rumtime,SIGNAL(timeout()), this, SLOT(on_sectime()) );
开始计时:
if (!(rumtime->isActive() ))
rumtime->start(1000);//开始计时
停止计时:
if (rumtime->isActive() )
rumtime->stop();//停止计时
on_sectime实现
void MainWindow::on_sectime()
{
mineMap.timer++;
update();
}
最后在添加一些菜单消息:重新开始 初级 中级 高级 退出等选项.
//重新开始
void MainWindow::on_actionRestart_triggered()
{
mineMap.Restart();
update();
}
重新布雷,刷新窗口
//高级
void MainWindow::on_actionAdvanced_triggered()
{
mineMap.Create(30,20,100);
setFixedSize(mineMap.mx*20+offsetx*2,mineMap.my*20+offsety+66);
}
更改行数列数雷数,调整窗口大小
到这里,程序就分析完啦.其实程序也不难,就几百行代码基本的功能就实现了.Qt这个开发环境很强大,自己才刚刚入门,很多功能不知道该怎么实现,还需要好好学习.
流云非晚