欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

用python tkinter组件实现扫雷游戏

程序员文章站 2024-03-18 12:21:52
...

首先介绍扫雷游戏规则。扫雷游戏界面是有若干行和列的方块矩阵,用矩阵的行列号定位矩阵中的方块。每个方块都有一些状态,例如有无雷、有无标记等,用一个2维列表记录这些状态,为了和方块矩阵形成对应关系,把矩阵的第1个序号称作行号,第2个序号称作列号。矩阵中一些块下隐藏着雷,把这些块称作“有雷块”,同样块下无雷的块称作“无雷块”。每个块都有相邻的块,最多8个,在边角有5个或3个,把这些块称作某块的“相邻块”。本程序将使用按钮代替方块,将使用“有雷按钮”、“无雷按钮”和“相邻按钮”等名称。玩家如判断某块有雷,可用鼠标右键单击该块,使该块显示红旗。如怀疑某块有雷,右击方块两次,使该块显示问号。用鼠标左键单击标有红旗的块,程序不做任何处理,即使该块无雷被错标红旗。用鼠标左键单击未标红旗的块或标有问号的块,称作打开该方块。如左击到有雷块,该块将显示背景为红色的雷,说明这个雷被错误点击,然后显示所有未被红旗标记的雷,被红旗标记却无雷的块显示打叉的雷,表示标记错误,有雷而且被正确标记的块保留显示红旗,游戏结束。如左击到无雷块,则调用左击事件函数来计算左击块的所有相邻有雷块的数量(即雷数),请注意,是查看真实的雷数,不受相邻块中那些错标红旗无雷块的影响。把得到的雷数显示在左击块的上方。如被左击块的所有相邻块都是无雷块(雷数为0),左击块则显示空白。由于相邻的这些块无雷,因此可以被左击打开,但错误标记了红旗的无雷块不能被打开。所有这些需要打开的方块由程序模拟单击打开,采用递归调用单击事件函数方法,来查看这些块的所有相邻有雷块的数量,在递归调用中可能还需要继续递归调用,直到若干显示空白的方块区域被一圈显示数字的方块所包围。当某块显示数字n,表示该块的所有相邻块中n块有雷,如玩家已在该块所有相邻块中为n块标记了红旗,认定这些块下有雷,无论这些标记正确与否,玩家都可双击这个显示数字n的块,相当于对被双击块的所有相邻、未打开且未标记红旗的块(包括带问号的块)都进行一次左键单击操作。如玩家在相邻块中把红旗错误标记到1个无雷块上,那么所有相邻块中一定存在1个没被红旗标记的有雷块,对这个有雷块左键单击,游戏就结束了,没标记红旗的有雷块显示底色为红色的雷,表示点击此雷出错。在WinXP中的经典扫雷游戏中,双击是鼠标左右键同时按下。现在的笔记本电脑鼠标触摸板不能实现鼠标左右键同时按下,因此本程序的双击是鼠标左键双击。玩家胜利的条件是:全部有雷块都被红旗标记,没有被错误标记红旗的无雷块,所有无雷块都被单击打开。
本程序模拟WinXP中的扫雷程序,只想表达设计扫雷游戏的基本思路,因此仅实现了大部分功能,包括游戏菜单下的“开局”、“初级”、“中级”、“高级”和“退出”共5个菜单项,没实现的菜单项有“自定义”、“标记”、“颜色”、“声音”和“扫雷英雄谱”共6项,除了“自定义”,其它都是辅助功能,用处不大。本想要实现“自定义”功能,但自定义要求玩家输入地雷矩阵的行数、列数和矩阵中雷的个数,因此要用到一个对话框,但tkinte中无此组件,要自己设计,增加不少代码,和扫雷游戏编程思路关系不大,最后放弃了。另外帮助菜单也比较简单,仅包括一个简单消息框。WinXP中的扫雷程序共有3级,初级矩阵为9x9,10个雷,中级16x16,40个雷,高级30x16,99个雷。级别偏难,因此参考4399中的扫雷游戏,改为初级矩阵为6x6,5个雷,中级9x9,15个雷,高级12x12,33个雷。另外,WinXP的扫雷程序中,重玩按钮显示的是一个图形,有2个动作:高兴和悲伤。本程序按钮显示的是文本:重玩。
本程序使用python tkinter按钮(Button)组件组成按钮矩阵(第154-162行)。初始所有按钮是立体状,按钮显示为空。当左击按钮(打开按钮),按钮变为平面,按钮显示其所有相邻有雷按钮数。用3维列表mineMap记录每个按钮的雷状态和相邻雷数,以2行2列按钮矩阵为例说明3维列表格式:mineMap=[[[0,-1],[0,-1]],[[0,-1],[0,-1]]],列表项为map[i][j][k],当k=0列表项是矩阵i行j列按钮的雷状态,当k=1是相邻雷数。雷状态是负数表示该按钮有雷,-1=有雷无标记 ,-2=有雷标记红旗,-3=有雷标记问号,是正数表示按钮无雷,0=无标记未打开 ,1=表示已打开,2=标记红旗,3=标记问号,请注意,这7种状态是互斥的,因此能用一个整数表示7种雷状态;相邻雷数=-1表示该按钮未打开,即未计算该按钮所有相邻有雷按钮数,为正数表示其已被打开,其值为该按钮所有相邻有雷按钮数(0-8),为0按钮显示空。函数putMine初始化3维列表。第8行用状态值[0,-1]初始化列表,0表示无雷未被标记未打开,-1表示未计算所有相邻有雷按钮数。第9行用来布雷,从总按钮数 (numOfrow * numOfCol)随机取出若干数,具体取出多少数决定于初始雷数,然后把各个数转换为列表的行列数,把在该位置按钮的雷状态由0改为-1,表示该位置有无标记的雷。游戏界面如下:
用python tkinter组件实现扫雷游戏
第167行生成主窗口。第170-184行生成菜单。第185-189行生成全局变量,第190行建立Label类对象显示地雷数。第192行建立Label类对象显示扫雷所用秒数。第193行建立Button类对象做为重玩按钮。第194-196行从文件生成PhotoImage类对象,用来在按钮上显示雷、带叉的雷以及红旗的图形。最后调用函数setGameLevel(6,6,5),在程序运行后将游戏设置为初级。有一点需要注意,仅拷贝源程序代码不能使程序正确运行,因为按钮所显示的3个图形,小红旗、地雷和打叉的地雷,需要从外部文件建立PhotoImage对象。这3个文件在程序所在文件夹的子文件夹pic中,文件名分别是:红旗.png、地雷2.png和地雷3.png。读者可自己建立这3个文件,图形大小为20*20,采用png文件目的是使背景能够透明,使png文件图形背景透明的网址:http://www.aigei.com/bgremover/。当然读者也可下载源文件,网址是:https://download.csdn.net/download/geng_zhaoying/12927644
下面介绍函数setGameLevel,它有3个参数分别是矩阵行数、列数和雷数。它完成两个任务,第1是程序运行后将游戏设为初级,第2是修改游戏等级。二者区别是修改游戏等级时要把前边等级按钮矩阵删除,再建立新矩阵。而程序运行后将游戏等级设为初级时还未有按钮矩阵,无法删除矩阵。如当前游戏等级和要修改等级相同,即行列数和雷数都相同,没有必要删除再重建(第144行)。如查到记录按钮对象的字典为空,说明程序刚运行还未建立按钮矩阵,因此无法删除按钮矩阵(第147行)。第154-162行是创建按钮矩阵。如numOfrow和numOfCol改变,主窗体尺寸、秒表和重玩按钮位置都要改变,第163-165行完成此功能。最后调用函数reSet()。
现在介绍函数reSet(),在重玩游戏前调用此函数完成游戏初始化。第115行用label组件显示所设定游戏等级的初始雷数,以后显示未标记的雷数,每用红旗标记一个雷,该值减1。第116行将秒表清0。第117行将3维列表mineMap清空,第118行调用函数putMine(),初始化3维列表mineMap,并重新布雷。第119-124行将在上次游戏中按钮被改变的属性恢复为初始状态:立体状、显示为空和背景色为银灰色。第125行将变量gameOver设置为false,使按钮能响应事件函数。最后3条语句和秒表有关。
秒表的工作原理参见本人博文:博文“在python tkinter窗口实现多线程秒表的两种方法”的第2种方法再讨论。第127行创建一个Timer类对象timer。timer在**状态,关闭主窗口结束程序,会抛出异常,为避免此种情况,timer.setDaemon(True)语句使子线程成为主线程(主程序)的守护线程(第128行),当主进程结束后,子线程会随之结束。语句timer.start()在左击按钮事件函数btnClick中(第53行),用来启动定时器,启动后timer延迟1秒后将调用函数count()在子线程中运行,退出该函数,子线程结束。子线程只能启动一次,如子线程已启动处于**状态,再次启动子线程,会抛出异常。这里必须要判定子线程不是在**状态(第51行),才能执行timer.start()。在启动前令isTimerRun为True。在函数count中,如while语句满足循环条件,函数count将一直在子线程中运行,完成秒表计数功能。只要修改全局变量isTimerRun=false,就可退出函数,结束子线程,秒表计数结束。游戏无论是胜利还是失败都要结束秒表,在第57行、第78行和第112行令isTimerRun=false,使秒表停止计数。
菜单项初级、中级和高级的事件函数都是setGameLevel(row=0,col=0,mine=0),见第175-177行,但用lambda表达式给出的实参不同,修改实参的值可以修改游戏等级的行列数和地雷数。菜单项开局和重玩按钮的事件函数都是reSet(),因行列数和地雷数不变,只需要为重玩游戏做初始化工作。
在扫雷游戏中,按钮要响应鼠标右键单击、鼠标左键单击和双击事件。程序第154-162行语句中生成按钮矩阵,在建立按钮对象的语句中,用command指定左键单击事件函数是btnClick(x,y)(第158行),用lambda表达式为左键单击事件函数的两个参数提供实参,实参是被单击按钮所在位置的行列号,可参考本人博文:python3.8的tkinter按钮事件函数实现多个参数。还为按钮绑定鼠标右键单击和鼠标左键双击事件(第160- 161行),指定的事件函数是同一个函数but_RDclick(event,x=row,y=col),在事件函数中根据鼠标事件传递的参数event.num区分左右键,来确定当前是哪个事件调用了事件函数。可参考本人博文:增加鼠标右击按钮事件函数包含event和其它参数。函数but_RDclick的参数x和y是被单击按钮的行列号。
首先看和鼠标左键单击按钮事件函数btnClick(x,y)的相关函数。要计算被单击按钮所有相邻有雷按钮数,必须首先要找到这些相邻按钮,函数getAroundBut(x,y)完成这个工作(第11-13行),该函数返回列表,列表中包含x行y列按钮的所有相邻按钮的行列号。相邻按钮最多为8个,如按钮在边角可能有5个或3个。如某按钮行(列)号为n,其相邻按钮的行(列)号可能为n-1,n,n+1,行(列)号不能为负,为负取0,也不能大于等于最大行(列)号,否则取值最大行(列)号-1。当然相邻按钮不包括自己。函数getNumOfAroundMine(x,y)得到x行y列(被左击)按钮所有相邻有雷按钮数(第33-46行),当左击了有雷按钮,退出函数返回0(第35行),否则返回1(第46行)。第36行得到所有相邻按钮行列号的列表,逐一判断该列表中每一个按钮的雷状态值,如其为负,表明是雷,雷数+1。最后将所得相邻有雷按钮数保存到和左击按钮相对应mineMap列表的’相邻雷数’项中(第41行)。如所有相邻有雷按钮数为0,所有这些相邻按钮都可以被左击打开,即每个相邻按钮都需要计算自己的所有相邻有雷按钮数,可由程序递归调用函数getNumOfAroundMine来完成(第45行)。再看单击按钮事件函数btnClick,如游戏结束、按钮标记了红旗或按钮已打开,单击这些按钮后将退出该函数(第49-50行),第54行调用函数getNumOfAroundMine计算该按钮所有相邻有雷按钮数,返回0,表示点击了有雷按钮,游戏失败,调用函数showAllMines(函数在第14行),显示所有未被标记的雷,被左击的有雷按钮底色变红(第15行),错误标记红旗的无雷按钮显示打叉的雷,正确标记的有雷按钮保留红旗不变。函数btnClick最后设置一些变量值(第56-58行)。如单击了无雷按钮,则调用函数showNumOfMine,在所有已计算了所有相邻雷数的按钮上显示这个雷数(第60行),调用函数isWin判断是否胜利(第61行)。
最后介绍函数RClick_LDoubleClick(evt,x,y),是处理鼠标右击和鼠标左键双击的函数。如果evt.num== 1,是左键,是鼠标左键双击按钮事件,否则是右键单击事件。
先看双击显示数字n的按钮,只有n!=0且该按钮已被打开,双击才有效(第67行)。第68-72行计算被双击按钮的所有相邻按钮上显示红旗的数量(可能标记有错),如判断有n个红旗(第73行),双击就相当于对被双击按钮的所有相邻按钮(包括显示问号按钮)均进行1次左键单击按钮操作,即调用函数getNumOfAroundMine,如其返回值是0,相当于单击了有雷按钮,如果该按钮未被红旗标记(第75行),说明被标记为红旗的n个按钮中有一个标记不正确,游戏结束。
再看右击按钮事件。多次右击未打开的按钮,按钮显示按规律循环,如右击前按钮显示为空,右击后按钮显示红旗;如右击前按钮显示红旗,右击后按钮显示问号;如右击前按钮显示问号,右击后按钮显示为空。但实际上右击前,按钮下的雷有7种状态,右击后按钮属性也就有7种变化。如用判断语句根据右击前按钮下的雷状态得到右击后按钮属性的变化,需不少代码。为减少代码,因此建立一个字典如下:

myDict={-1:(-2,p,''),-2:(-3,'','?'),-3:(-1,'',''),0:(2,p,''),2:(3,'','?'),3:(0,'','')}

按钮的雷状态作为字典的键,字典的值是元组,有3个值,分别为单击后的雷状态值、按钮显示图形的PhotoImage对象和按钮的text属性值,例如,右击时按钮的雷状态为-3(有雷标记问号),右击后雷状态为-1(有雷无标记),后两项都为空字符串,因此按钮显示为空。另外字典只有6项,因右击时雷状态为1时,表示按钮下无雷已被单击打开,不做处理(第84行)。有了字典,右击按钮,得到按钮下的雷状态当前值,从字典取出单击后的雷状态值、按钮应显示的图形和按钮属性text对应的字符串。全部程序代码如下。再次提醒仅拷贝源程序不能运行,原因见前边解释。

from threading import Timer
import time
import random
import tkinter as tk               
import tkinter.messagebox         #第8行中[0,-1]是按钮的雷状态和相邻雷数量,雷状态:-1=有雷,-2=有雷标????,接下行
def putMine():#布雷。              -3=有雷标❔,0=无雷未打开,1=无雷已打开,2=无雷标????,3=无雷标❔;接下行
    global numOfrow,numOfCol,mineMap      #相邻雷数量:-1未计算相邻地雷数,0-8相邻按钮下的雷数
    mineMap=[[[0,-1] for j in range(numOfCol)] for i in range(numOfrow)]      #矩阵初始化,无雷,未计算雷数  
    for i in random.sample(range(numOfrow*numOfCol), mines):  #随机取出若干(mines指定)数,组成列表,逐次赋给i
        mineMap[i//numOfCol][i%numOfCol][0]=-1      #将随机序号转为行列号,在该行列处按钮下布雷,-1=有雷无标记  
def getAroundBut(x, y):                       #返回列表,列表中包含x行y列按钮的所有相邻块(最多为8个)的行列号  
    return [(i,j) for i in range(max(0,x-1),min(numOfrow-1,x+1)+1)       #行号最小为0不为负,最大numOfrow-1
        for j in range(max(0,y-1),min(numOfCol-1,y+1)+1) if i !=x or j !=y]    #不包括自己,即x行y列方块
def showAllMines(i,j):                          #(i,j)是被鼠标左击按钮的坐标
    buttons[i,j]['background']='red'            #该按钮背景变红,表示被左击的雷
    for row in range(numOfrow):                 #row为行,0到numOfrow-1
        for col in range(numOfCol):             #col为列,0到numOfCol-1
            if mineMap[row][col][0]==-1 or mineMap[row][col][0]==-3:    #雷被正确标记为????,不显示雷                
                buttons[row,col]['relief']='groove'                     #按钮变为平面,不再有立体感
                buttons[row,col]['image']=p2
            if mineMap[row][col][0]==2:                                 #不是雷被标记为????,显示雷有红叉        
                buttons[row,col]['relief']='groove'                     #按钮变为平面,不再有立体感
                buttons[row,col]['image']=p3
def showNumOfMine():                            #使每个按钮都显示其相邻按钮下的雷数
    for row in range(numOfrow):                 #row为行,0到numOfrow-1,col为列,0到numOfCol-1
        for col in range(numOfCol):          #下句条件是该按钮未打开或标记为?,且计算了该按钮所有相邻块下的雷数
            if (mineMap[row][col][0]==0 or mineMap[row][col][0]==3) and mineMap[row][col][1]>=0:
                mineMap[row][col][0]=1          #设置为打开状态
                buttons[row,col]['text']=mineMap[row][col][1]   #显示该按钮相邻按钮下的雷数
                if mineMap[row][col][1]==0:                     #如果雷数为0,显示空白
                    buttons[row,col]['text']=''                                 
                buttons[row,col]['relief']='groove'             #按钮变为平面,不再有立体感
def getNumOfAroundMine(x,y):        #得到x行y列处按钮所有相邻按钮(最多为8个)下的地雷数
    if mineMap[x][y][0]<0:          #按钮的雷状态值<0,说明此按钮下有雷,点击到雷,游戏结束
        return 0
    aroundBut =getAroundBut(x, y)   #列表aroundBut中是x行y列按钮所有相邻按钮(最多为8个)的行列号
    mineSum = 0                     #变量mineSum记录地雷数,初始为0
    for i, j in aroundBut:
        if mineMap[i][j][0]<0:      #该项为-1=有雷、-2=有雷标????,-3=有雷标❔,所以是负数表示块下有雷
            mineSum += 1
    mineMap[x][y][1]=mineSum            #该项为-1未计算相邻地雷数,为0到8是相邻地雷数     
    if mineSum == 0:         #如其相邻按钮下雷数为0,其相邻所有按钮都要打开,要计算这些要打开的按钮所有相邻按钮下雷数
        for i, j in aroundBut:
            if mineMap[i][j][1]==-1:          #该按钮未被计算过其相邻地雷数
                getNumOfAroundMine(i,j)       #递归调用 
    return 1                 
def btnClick(x,y):              #所有鼠标左键单击按钮的事件函数,有两个参数,被点击按钮所在位置行列号
    global gameOver,timer,isTimerRun   #如果游戏结束,不再响应鼠标左键单击及双击和鼠标右键单击事件
    if gameOver or mineMap[x][y][0]==-2 or 0<mineMap[x][y][0]<3:    #游戏结束、做了雷标记按钮都不处理
        return
    if not timer.is_alive():            #子线程只能启动一次,即线程不是**的时候,才能启动子线程
        isTimerRun=True
        timer.start()                   #启动子线程
    if getNumOfAroundMine(x,y)==0:      #返回值为0,左击了有雷的按钮,游戏结束
        showAllMines(x,y)               #将所有雷显示出来
        gameOver=True                   #游戏结束标志
        isTimerRun=False
        label['text']='你输了'
        return
    showNumOfMine()
    isWin()
def RClick_LDoubleClick(evt,x,y):          #处理鼠标右击和鼠标左键双击事件
    global gameOver,isTimerRun                
    if gameOver:                           #如果游戏结束,不再响应鼠标左键单击及双击和鼠标右键单击事件
        return
    if evt.num==1:                         #=1,是左键,是鼠标左键双击按钮事件            
        if mineMap[x][y][1]!=0 and mineMap[x][y][0]==1:     #被双击按钮邻近按钮有雷且已被单击打开
            aroundBut =getAroundBut(x, y)  #列表aroundBut中是x行y列按钮的所有相邻按钮(最多为8个)的行列号
            mineFlagSum = 0                #变量mineFlagSum记录相邻按钮被标记为红旗标的数量,初始为0
            for i, j in aroundBut:          #计算被双击按钮所有相邻按钮被标记为红旗标的数量
                if mineMap[i][j][0]==-2 or mineMap[i][j][0]==2:             #-2有雷标????,2无雷标????
                    mineFlagSum += 1                                        #只要被????标记,+1
            if mineFlagSum==mineMap[x][y][1]:       #后一项是被双击按钮显示的所有相邻按钮的雷数
                for i, j in aroundBut:              #对所有相邻未标记红旗按钮均进行一次左键单击按钮操作
                    if getNumOfAroundMine(i,j)==0 and mineMap[i][j][0]==-1:  #如是无标记的雷,游戏结束
                        showAllMines(i,j)           #将所有雷显示出来,没标记出来的雷底色变红
                        gameOver=True               #游戏结束标志
                        isTimerRun=False
                        label['text']='你输了'
                        return
            showNumOfMine()
        isWin()
    else:                                   #=3是右键,是鼠标右键单击按钮事件,为未打开的按钮做标记       
        if mineMap[x][y][0]==1:             #该按钮已被打开,已显示其相邻按钮下的地雷数,不能做标记
            return
        m=mineMap[x][y][0]          #不知何故,必须用m作为myDict键,用mineMap[i][j][0]直接作键出错
        myDict={-1:(-2,p,''),-2:(-3,'','?'),-3:(-1,'',''),0:(2,p,''),2:(3,'','?'),3:(0,'','')}
        mineMap[x][y][0]=myDict[m][0]    
        buttons[x,y]['image']=myDict[m][1]
        buttons[x,y]['text']=myDict[m][2]
        isWin()
def isWin():
    global mines,numOfrow,numOfCol,gameOver,isTimerRun
    sumOfmineWithFlag=0                 #所有按钮下有地雷被标????的总数
    sumOfNomineWithFlag=0               #所有按钮下无地雷被标????的总数
    sumOfOpenBlock=0                    #所有被打开的按钮的总数,等于所有按钮数-所有雷数
    for i in range(numOfrow):           #i为行,0到numOfrow-1
        for j in range(numOfCol):       #j为列,0到numOfCol-1
            if mineMap[i][j][0]==-2:    #如果该按钮下地雷被标????
                sumOfmineWithFlag+=1
            elif mineMap[i][j][0]==2:   #如果该按钮下无地雷被标????
                sumOfNomineWithFlag+=1
            elif mineMap[i][j][0]==1:   #如果该按钮已被打开
                sumOfOpenBlock+=1
    s=mines-(sumOfmineWithFlag+sumOfNomineWithFlag)     #s为未被标记的地雷数
    if s<0:                             #如无雷也被标记红旗,可能出现标记红旗的按钮数大于地雷数,雷数不能为负
        s=0                             #标记为????的所有块>实际雷数,仍显示0个雷
    label['text']=str(s)                #显示剩余地雷数
    if sumOfmineWithFlag==mines and sumOfOpenBlock==numOfCol*numOfrow-mines:        #胜利条件是:(接下行)
        label['text']='你赢了'          #正确标记雷的按钮数=雷的实际数量和单击打开按钮数=按钮总数-雷的实际数量
        gameOver=True
        isTimerRun=False
def reSet():                        #在重玩游戏前调用此程序完成游戏初始化
    global mines,numOfrow,numOfCol,gameOver,buttons,mineMap,timer,isTimerRun
    label['text']=str(mines)        #初始显示该游戏等级初始的雷数,用红旗标记一个雷,该值减1
    label1['text']='0'              #游戏重新开始,秒表清0
    mineMap=[]   #记录指定行列数方块矩阵中每个方块的两个状态值,是3维矩阵,mineMap[i][j][k],i行号j列号,k=0或1    
    putMine()    #初始化mine_map,并在列表中随机增加地雷
    for row in range(numOfrow):                     #row为行,0到numOfrow-1
        for col in range(numOfCol):                 #col为列,0到numOfCol-1
            buttons[row,col]['text']=''             #按钮标题显示为空                                
            buttons[row,col]['relief']='raised'     #按钮立体状
            buttons[row,col]['image']=''            #去掉按钮显示的图形
            buttons[row,col]['bg']='Silver'         #按钮背景为银白色,以上按钮属性在完成游戏后都有可能被改变
    gameOver=False
    isTimerRun=False
    timer=Timer(1, count)   #建立Timer类对象,1秒后将调用函数count()在子线程中运行,可参见我的有关多线程秒表的博文
    timer.setDaemon(True)   #成为主线程的守护线程,当主进程结束后,子线程也会随之结束。否则计数未停止前,关闭窗口,会抛出异常
def count():                #该函数完成秒表功能,将运行在子线程中
    global isTimerRun       #启动子线程代码timer.start()在函数btnClick(x,y)中
    k=0
    while isTimerRun and k<1000:      #isTimerRun=Ture且秒数<1000,将一直计算秒数。退出该函数,子线程结束,计算秒数结束
        k+=1
        label1['text']=str(k)           #将秒数显示在label1
        time.sleep(1)                   #休眠1秒。
def do_job():
    s='左击方块,是雷游戏结束,否则显示相邻8块地雷数,空相邻无雷\n'+\
    '右击方块标记红旗表示有雷,再右击变?,再右击无标记\n'+\
    '方块显示n,相邻8块标记n块有雷,左键双击等效单击邻近8块无雷块'+\
    '\n如雷标记有错,游戏结束。                保留所有版权'
    tkinter.messagebox.showinfo(title="帮助",message=s)
def setGameLevel(row=0,col=0,mine=0):
    global mines,numOfrow,numOfCol,buttons
    if mines==mine and numOfrow==row and numOfCol==col:         #如果新行列数、地雷数和旧的相同,不必删所有按钮重建
        reSet()
        return
    if len(buttons)!=0:                         #初始还未创建按钮,无按钮可删
        for r in range(numOfrow):               #删除所有按钮
            for c in range(numOfCol):
                buttons[r,c].destroy()
    numOfrow=row                                #设置新行列数、雷数
    numOfCol=col
    mines=mine
    for row in range(numOfrow):         #row为行,0到numOfrow-1,col为列,0到numOfCol-1
        for col in range(numOfCol):     #创建新的numOfrow行,numOfCol列按钮矩阵
            def but_RDclick(event,x=row,y=col): #鼠标右击和鼠标左键双击的事件函数,参数x,y默认值是按钮的行列数
                RClick_LDoubleClick(event,x,y)              #所有事件函数都调用同一函数
            button=tk.Button(root,command=lambda x=row,y=col:btnClick(x,y),bg="Silver",fg='red',font=("Arial",20))
            button.place(x=20+col*32,y=45+row*32,width=30,height=30)
            button.bind("<Button-3>", but_RDclick)                  #鼠标右击按钮事件绑定事件函数为but_RDclick
            button.bind("<Double-Button-1>", but_RDclick)           #鼠标左键双击按钮事件绑定事件函数为but_RDclick        
            buttons[row,col]=button                                 #字典,键为行列号,值为该行列号的按钮对象
    root.geometry(f"{numOfCol*32+40}x{numOfrow*32+65}")             #numOfrow和numOfCol改变,主窗体尺寸也要改变
    label1.place(x=f"{numOfCol*32-15}",y=5,width=40,height=30)      #numOfrow和numOfCol改变,秒表和重玩按钮位置也要改变
    button1.place(x=f"{numOfCol*17-10}",y=5,width=50,height=30)     #在字符串前加f,字符串中的{}内容是表达式
    reSet()
root = tk.Tk()                  #初始化窗口
root.title('扫雷')              #窗口标题
root.resizable(width=False,height=False)            #设置窗口是否可变,宽不可变,高不可变,默认为True
menubar = tk.Menu(root)         #创建一个菜单栏,可把它理解成一个容器,在窗口的上方,可放置多个能下拉菜单项
cameMenu = tk.Menu(menubar, tearoff=0)  #创建1个能下拉菜单项,点击后显示下拉菜单,下拉菜单包括多个子菜单项
menubar.add_cascade(label='游戏', menu=cameMenu)    #将能下拉菜单项放入menubar,并指定其名称为:游戏
cameMenu.add_command(label='重玩', command=reSet)  
cameMenu.add_separator()                        #添加一条分隔线,上句为能下拉菜单项的第一个子菜单项:重玩
cameMenu.add_command(label='初级', command=lambda row=6,col=6,mine=5:setGameLevel(row,col,mine))
cameMenu.add_command(label='中级', command=lambda row=9,col=9,mine=15:setGameLevel(row,col,mine))
cameMenu.add_command(label='高级', command=lambda row=12,col=12,mine=33:setGameLevel(row,col,mine))
cameMenu.add_command(label='自定义')    #修改以上3条语句,可修改每级的行数、列数和地雷数
cameMenu.add_separator()    
cameMenu.add_command(label='退出', command=root.quit) # 用tkinter里面自带的quit()函数
helpMenu = tk.Menu(menubar, tearoff=0)  #创建第2个能下拉菜单项,点击后显示下拉菜单,下拉菜单可包括多个子菜单项
menubar.add_cascade(label='帮助', menu=helpMenu)    #将能下拉菜单项放入menubar,并指定其名称为:帮助
helpMenu.add_command(label='关于本游戏', command=do_job)  #能下拉菜单项的第一个子菜单项:关于本游戏
root.config(menu=menubar)                                #让菜单显示出来
gameOver=isTimerRun=False       #初始游戏结束标志为假,定时器是否运行为假
numOfrow=numOfCol=mines=0       #方块矩阵的行列数和雷数,以下3个变量必须为0,确保函数setGameLevel第2条语句不成立
timer=0  #此变量将引用在reSet()函数中定义的Timer对象,可取消该条语句,不会出错,这条语句只为了说明其是全局变量
buttons={}   #字典,键为行列号,值为该行列号的按钮对象
mineMap=[]   #记录指定行列数方块矩阵中每个方块的两个状态值,是3维矩阵,mineMap[i][j][k],i行号j列号,k=0或1
label=tk.Label(root,text=str(mines),bd='5',fg='red',font=("Arial",15))
label.place(x=10,y=5,width=60,height=30)
label1=tk.Label(root,text='0',bd='5',fg='red',font=("Arial",15))
button1=tk.Button(root,text='重玩',command=reSet,fg='red',font=("Arial",15))
p = tk.PhotoImage(file='pic/红旗.png') 
p2= tk.PhotoImage(file='pic/地雷2.png')
p3= tk.PhotoImage(file='pic/地雷3.png')
setGameLevel(6,6,5)     #修改函数的3个参数值,改变程序开始扫雷的等级,这里是初级
root.mainloop()