Qt扫雷游戏

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这个开发环境很强大,自己才刚刚入门,很多功能不知道该怎么实现,还需要好好学习.

流云非晚

  • 23
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值