zoukankan      html  css  js  c++  java
  • 图形化界面数独(GUI)(一)

    〇、引言

    QwQ

    我们即将用 Python 写一个GUI图形界面数独!(第一部分)

    设计效果:
    hjtiy-vc7oi.gif

    关键词汇:tkinter库、python方法判重、GUI界面简单设计

    本文为课后总结,除个人解释和思路外,内容均为上课老师讲解提供,请勿转载!!

    一、tkinter库及数独界面设计

    1.GUI界面创建

    tkinter,一个神奇的东西。Python自带的控件,只需要调用:

    import tkinter as tk
    

    我们首先要创建一个图形界面的根界面。我们可以:

    root = tk.Tk()
    root.title("数独游戏")
    

    此时root是一个GUI界面的类,root.title()就是给图形界面加标题。

    效果:无!

    当你创建这个界面的时候,你会发现这个揭=界面很快就会被关掉(电脑好一点的话根本看不到它出现),这个时候你需要让这个界面保持工作,也就是我们让他进入服务器式的循环中不关闭。我们可以将这行代码加在后面:

    tk.mainloop()
    

    此时效果:

    非常朴素的开始

    2.初识控件

    你看到的一些奇怪的文字框,按钮啥的,都是用控件组成。我们所看到上文的演示的控件主要是按钮控件(Button)。我们也相应的介绍一下其他常用的控件:

    (1) label

    只是一个文字框

    label = tk.Label(root, fg='red', bg='blue',
         width=10, height=2, text='标签示例', font=('Tempus Sans ITC', 12))
    label.pack()
    
    fg: 字体颜色
    bg: 背景颜色
     宽度
    height: 高度
    text: 文字内容
    font: 字体(字体,字号)
    

    label属于tk.Label类,需要用label.pack()打包然后在界面中显示。显示情况如下:

    (2) entry

    可输入文字框

    pwd = tk.StringVar()
    entry = tk.Entry(root, textvariable=pwd, relief=tk.RAISED)
    pwd.set("输入框示例")
    entry.pack()
    
    textvariable: 框中初始所填的文字
    relief: 形态/状态
    

    在使用界面编程的时候,有些时候是需要跟踪变量的值的变化,以保证值的变更随时可以显示在界面上。由于python无法做到这一点,所以使用了tcl的相应的对象,也就是StringVar、BooleanVar、DoubleVar、IntVar所需要起到的作用[1]。我们这里用的是StringVar()。

    relief表示形态或者状态,其实就是图形框的样式。常见的有"FLAT", "RAISED", "SUNKEN", "SOLID", "RIDGE", "GROOVE",但注意修改时这些变量都是tkinter内部的。其样式效果如下(下面是用的按钮展示的):

    Entry控件的演示:
    Entrykongjian.gif

    (3) Botton

    按钮,可用于执行命令

    def buttonclicked():
        return True
    btn = tk.Button(root, text="按钮示例", relief=tk.SOLID, bd=2, command=buttonclicked).pack()
    
    text: 显示的文字
    relief: 形态/样式
    bd: 按钮的边缘宽度(borderwidth)
    command: 回调函数,当你按下这个按钮后会执行的函数
    font: 文字的字体和字号
     宽度
    height: 高度
    bg: 背景颜色
    fg: 字体颜色
    

    效果:

    代码:

    import tkinter as tk
    
    root = tk.Tk()
    root.title("数独游戏")
    
    #Label
    label = tk.Label(root, fg='red', bg='blue', width=10, height=2, text='标签示例', font=('Tempus Sans ITC', 12))
    label.pack()
    
    #Entry
    pwd = tk.StringVar()
    entry = tk.Entry(root, textvariable=pwd, relief=tk.RAISED)
    pwd.set("输入框示例")
    entry.pack()
    
    #Button
    def buttonclicked():
        return True
    btn = tk.Button(root, text="按钮示例", relief=tk.SOLID, bd=2, command=buttonclicked).pack()
    
    tk.mainloop()
    
    (4) 更多

    更多控件:

    Button	        按钮控件;在程序中显示按钮。
    Label	        标签控件;可以显示文本和位图
    Entry	        输入控件;用于显示简单的文本内容
    Text	        文本控件;用于显示多行文本
    Radiobutton	单选按钮控件;显示一个单选的按钮状态
    Checkbutton	多选框控件;用于在程序中提供多项选择框
    Listbox	        列表框控件;在Listbox窗口小部件是用来显示一个字符串列表给用户
    Frame	        框架控件;在屏幕上显示一个矩形区域,多用来作为容器
    Canvas	        画布控件;显示图形元素如线条或文本
    Menubutton	菜单按钮控件,由于显示菜单项
    Menu	        菜单控件;显示菜单栏,下拉菜单和弹出菜单
    Message	        消息控件;用来显示多行文本,与label比较类似
    Scale	        范围控件;显示一个数值刻度,为输出限定范围的数字区间
    Scrollbar	滚动条控件,当内容超过可视化区域时使用,如列表框
    Toplevel	容器控件;用来提供一个单独的对话框,和Frame比较类似
    Spinbox	        输入控件;与Entry类似,但是可以指定输入范围值
    PanedWindow	PanedWindow是一个窗口布局管理的插件,可以包含一个或者多个子控件
    LabelFrame	labelframe 是一个简单的容器控件。常用于复杂的窗口布局
    tkMessageBox	用于显示你应用程序的消息框
    

    更多标准属性:

    属性	    描述
    Dimension   控件大小
    Color	    控件颜色
    Font	    控件字体
    Anchor	    锚点
    Relief	    控件样式
    Bitmap	    位图
    Cursor	    光标
    

    更多集合管理:

    几何方法    描述
    pack()	    包装
    grid()	    网格
    place()	    位置
    
    3.数独界面设计

    我们发现,我们目标效果的界面可以大体分为三个界面:

    对于大的分区我们可以用Frame控件来调整和分区。

    Python Tkinter 框架(Frame)控件在屏幕上显示一个矩形区域,多用来作为容器[2]。我们可以用Frame框架来分区。这时,我们根据刚刚的分区效果来看,我们可以这样写:

    import tkinter as tk
    
    root = tk.Tk()
    root.title("数独游戏")
    
    frametop = tk.Frame(root)
    # 上部框架建设
    frametop.pack(side=tk.TOP, pady = 10)
    # side指该框架要放在哪里,我们可以选择TOP, BOTTOM, LEFT, RIGHT
    # pady是指与垂直边距,相似的,padx是指水平边距
    
    framemiddle = tk.Frame(root)
    framemiddle.pack(side=tk.TOP, pady=15)
    
    framebottom = tk.Frame(root)
    framebottom.pack(side=tk.TOP, pady=5)
    
    tk.mainloop()
    

    效果:空界面

    因为此时我们并没有在各个框架上添加内容。我们可以通过加控件来丰富我们的框架。对于我们数独游戏界面来说,我们可以这样写:(部分代码解释见备注,控件表示请见上文表格)

    import tkinter as tk
    
    root = tk.Tk()
    root.title("数独游戏")
    
    N = 9
    
    frametop = tk.Frame(root)
    # 上部的框架建设
    gridvar = [[tk.StringVar() for column in range(N)] for row in range(N)]
    # 对于大九宫格的所有的显示都是大多动态的,我们分别定义一个StringVar()来存储
    frame = [tk.Frame(frametop) for row in range(N)]
    # 这里涉及框架的嵌套,请见下文详解(1)
    grid = [[tk.Button(frame[row], width = 3, textvariable = gridvar[row][column],
         relief = tk.GROOVE, command = lambda row = row,
              column = column:gridclick(row, column),
              font = ('Helvetica', '12')) for column in range(N)] for row in range(N)]
    # grid这里为控件列表,代表每个小格对应的Button控件,lambda的解释请看下文详解(2)
    for row in range(N):
        for column in range(N):
            grid[row][column].pack(side = tk.LEFT)
            # 分别对每个控件进行打包显示
        frame[row].pack(side = tk.TOP)
        # 对嵌套的框架进行打包显示
    frametop.pack(side=tk.TOP, pady = 10)
    # 大框架进行打包显示
    
    framemiddle = tk.Frame(root)
    # 中部的框架建设
    selections = [tk.Button(framemiddle, width = 3, text = '%d' % number, relief = tk.RAISED,
         command = lambda number=number:numberclick(selections[number - 1]),
              font = ('Helvetica', '12')) for number in range(1, 10)]
    # 设立九个“选项”按钮
    for each in selections:
        each.pack(side = tk.LEFT)
    framemiddle.pack(side=tk.TOP, pady = 15)
    # 层层打包显示
    
    framebottom = tk.Frame(root)
    # 底部的框架建设
    erase = tk.Button(framebottom, text = '删除', relief = tk.RAISED,
         font = ('HeiTi', '14', 'bold'), width = 7, height = 1, bg = 'darkgreen', fg = 'white')
    # 删除键
    erase.pack(side = tk.LEFT, padx = 15)
    check = tk.Button(framebottom, text = '核查', relief = tk.RAISED,
         font = ('HeiTi', '14', 'bold'), width = 7, height = 1, bg = 'darkblue', fg = 'white')
    # 核查键
    check.pack(side = tk.LEFT, padx = 15)
    ok = tk.Button(framebottom, text = '退出', relief = tk.RAISED, command = exit,
         font = ('HeiTi', '14', 'bold'), width = 7, height = 1, bg = 'darkred', fg = 'white')
    # 退出键
    ok.pack(side = tk.LEFT, padx = 15)
    framebottom.pack(side = tk.TOP, pady = 5)
    # 依次打包
    
    tk.mainloop()
    

    详解:

    (1)

    frametop = tk.Frame(root)
    frame = [tk.Frame(frametop) for row in range(N)]
    

    这两行代码第一行是在root中添加一个框架,而第二行是在root下的一个框架中添加一个子框架,我们可以将其视作嵌套框架。这样方便我们对button们进行排版。框架建构也可以进行批量操作。

    (2)

    grid = [[tk.Button(frame[row], width = 3, textvariable = gridvar[row][column],
         relief = tk.GROOVE, command = lambda row = row,
              column = column:gridclick(row, column),
              font = ('Helvetica', '12')) for column in range(N)] for row in range(N)]
    

    lambda:lambda是定义了匿名函数。一般我们用lambda来定义单行函数比如

    >>> lambda x : x + 1 (1)
    2
    

    这里第一个x代表函数的变量,冒号后面表示函数表达式或者单行函数所要调用的东西,也就是说这里的用法相当于定义了:

    def g(x):
        return x + 1 
    

    我们源代码上如此用法实际上是为了方便调用函数,可以用方括号内所定义的变量来作为形参调用函数。

    不过在上述代码中,我们numberclick,gridclick函数还未编写,按钮的功能函数还未完善。革命尚未完成,同志还需努力

    二、内部构造架构

    1.numberclick -- 选项架构

    我们的期望效果是点击选项架构后,再点击大九宫格中的某个格子时,将选项中的数字填入其大九宫格格子中。那我们可以让程序记住我们选项中选的数字,然后再填入。我们numberclick就是用来记录选中的数字的

    def numberclick(selectionbutton):
        # selectionbutton是我们点中的Button类
        for i in range(N):
            selections[i]["relief"] = tk.RAISED
        # 先将所有数字按钮和控制按钮恢复状态
        erase["relief"] = tk.RAISED
        # 恢复删除按钮显示状态
        selectionbutton["relief"] = tk.SOLID
        # 将点击的数字按钮设置成SOLID
    

    这时,我们就将我们选中的格子用SOLID的方式记录下来,方便我们填数

    2.gridclick -- 大九宫格架构

    选择所需填的数以后,我们在点击大九宫格的格子时,就可以将标记过的选项数字填入其中。

    def gridclick(row, column):
        number = ''
        # 一般首次点击选项之前都没有可选的number,那么初始化为''
        for i in range(N):
            if selections[i]["relief"] == tk.SOLID:
                number = '%d' % (i + 1)
                break
        # 这一for循环寻找选项中的标记项,然后将其下标+1(下标为0-8,我们数字实为1-9)作为待填项,注意我们之前用的时StringVar(),所有我们此时要修改也是str类型的,所以转换成str
        gridvar[row][column].set(number)
        # 将大九宫格对应行列的StringVar()类改为number
        if number == '':
            layout[row][column] = 0
        # 当然,如果没有数的话强制转换是肯定不行的
        else :
            layout[row][column] = int(number)
        # 这里layout表示现在的情况,这是我们定义的全局变量,以int记录,方便计算和判重
    

    我们把上面两个函数放入代码中,运行效果如下:

    gongneng1.gif

    3.eraseclick -- 删除键功能架构
    def eraseclick(event):
        for i in range(N):
            selections[i]["relief"] = tk.RAISED
        # 点击删除后会将所有标记去掉,这样在填数的时候number变量为空
    erase.bind("<Button-1>", eraseclick)
    

    erase 是我们上文的所讲的Button控件,bind是将其他函数和控件绑定在一起的函数。其参数中 "<Button-1>" 表示左键点击, "<Button-2>" 指右键点击。也就是说,当我左键点击删除键时,程序运行eraseclick函数。

    4.checkclick -- 核查功能架构
    def checkclick(event):
        correct = verify()
        if correct:
            showinfo('核查结果', '答案正确') 
            print("答案正确")
        else:
            showinfo('核查结果', '答案不正确')  
            print("答案不正确")
    check.bind("<Button-1>", checkclick)
    

    注意:这里的最后一句是函数外的。这句话相当于初始化,一定不要将此句错缩进进函数中

    verify()是一个返回Bool值的自定义函数,表示我们的填入是否是正确的。

    这里涉及showinfo()函数,这是跳出一个提示窗口,其中形参第一个是窗口名称,第二个是提示内容,效果如下:
    showinfo_.gif

    4.readlayout -- 读入架构

    我们做数独肯定不是一张空空的表格来让我们填的,而是有初始定下的几个数字。我们可以将题目提前存在文件中(这里我们将文件命名为"sodoku.txt",储存方式如下:

    8
      36     
     7  9 2  
     5   7   
        457
       1   3 
      1    68
      85   1
     9    4  
    

    首先我们打开文件,这里我们直接将文件名储存在filename变量中作为参数。并将其以行读入成列表:

        layoutfile = open(filename, 'r')
        lines = layoutfile.readlines()
    

    随后我们逐行操作,先把每行的' '去掉,然后对行内每个字符进行统计和存储。注:可能存在一行中没有数字的可能,所以要看本行是否有数字,最后返回其数组。完整代码:

    def readlayout(filename):
        layoutfile = open(filename, 'r')
        lines = layoutfile.readlines()
        for row in range(N):
            line = lines[row].strip('
    ')
            if line != '': # 除去空行情况
                for i in range(len(line)):
                    if line[i] != '' and line[i] != ' ':
                        layout[row][i] = int(line[i])
        return layout
    

    我们显示数字时,要把预先给出的数字做处理,使得其不能被改动。我们对每行每列的layout进行判定,若其中有数字,则将其性质该为不可变性(tk.DISABLED),其代码如下:

    def showlayout(layout, gridvar):
        for row in range(N):
            for column in range(N):
                gridvar[row][column].set(str(layout[row][column]) if (layout[row][column] != 0) else '') # 将已经有的预填入表中
                if layout[row][column] != 0:
                    grid[row][column]["state"] = tk.DISABLED
    

    三、核查答案正确性:判重

    我们现在可以填数字了,现在我们需要对答案进行正确性核查。我们要保证在同行,同列和同九宫中不重复数字为1~9。首先我们要分别取到各行,各列,各九宫的数字。我们分别用3个函数来判定行,列与和九宫中数的正确性,其返回值为一个Bool变量。

    我们每行用切片的方式切出,然后将9个数sort一下,排序后应该一定是1,2,3,4,5,6,7,8,9。

    def verifyrow(): # 按行取
        correct = True
        for row in range(N):
            line = layout[row].copy()
            line.sort()
            if line != [1, 2, 3, 4, 5, 6, 7, 8, 9]:
                correct = False
        return correct
    
    def verifycolumn(): # 按列取
        correct = True
        for column in range(N):
            line = list((np.array(layout))[:, column])
            line.sort()
            if line != [1, 2, 3, 4, 5, 6, 7, 8, 9]:
                correct = False
        return correct
    
    def verifyblock(): # 按九宫取
        correct = True
        for blockindex in range(N):
            block = getblock(blockindex)
            line = list((np.array(layout))[block[0] : block[1] + 1, block[2] : block[3] + 1].reshape(N))
            # 这里是切片,切出对应行和列。然后将其重塑成一个一维数组,再转成list方便sort
            line.sort()
            if line != [1, 2, 3, 4, 5, 6, 7, 8, 9]:
                correct = False
        return correct
    
    
    def getblock(index): # 这个函数是用来获得区块所在的行的开始和结束,列的开始和结束
        rowstart = index // 3 * 3
        rowend = rowstart + 2
        columnstart = index % 3 * 3
        columnend = columnstart + 2
        return rowstart, rowend, columnstart, columnend
    

    最终我们将行,列,九宫所得的正确性综合一下,确定最终核查结果,即:

    def verify():
        return verifyrow() & verifycolumn() & verifyblock()
    

    这样,我们就大体将主要的效果搞出来了。所有代码综合起来如下:

    '''
    writer : yizimi - yuanxin
    Instructor : Mr. Mao, Palace of Tang Dynasty and CITers
    '''
    
    import tkinter as tk
    import numpy as np
    from tkinter.messagebox import showinfo
    
    N = 9
    layout = [[0 for j in range(N)] for k in range(N)]
    
    root = tk.Tk()
    root.title("数独游戏")
    
    frametop = tk.Frame(root)
    gridvar = [[tk.StringVar() for column in range(N)] for row in range(N)]
    frame = [tk.Frame(frametop) for row in range(N)]
    grid = [[tk.Button(frame[row], width = 3, textvariable = gridvar[row][column],
         relief = tk.GROOVE, command = lambda row = row,
              column = column:gridclick(row, column),
              font = ('Helvetica', '12')) for column in range(N)] for row in range(N)]
    for row in range(N):
        for column in range(N):
            grid[row][column].pack(side = tk.LEFT)
        frame[row].pack(side = tk.TOP)
    frametop.pack(side=tk.TOP, pady = 10)
    
    framemiddle = tk.Frame(root)
    selections = [tk.Button(framemiddle, width = 3, text = '%d' % number, relief = tk.RAISED,
         command = lambda number=number:numberclick(selections[number - 1]),
              font = ('Helvetica', '12')) for number in range(1, 10)]
    for each in selections:
        each.pack(side = tk.LEFT)
    framemiddle.pack(side=tk.TOP, pady = 15)
    
    
    framebottom = tk.Frame(root)
    erase = tk.Button(framebottom, text = '删除', relief = tk.RAISED,
         font = ('HeiTi', '14', 'bold'), width = 7, height = 1, bg = 'darkgreen', fg = 'white')
    erase.pack(side = tk.LEFT, padx = 15)
    check = tk.Button(framebottom, text = '核查', relief = tk.RAISED,
         font = ('HeiTi', '14', 'bold'), width = 7, height = 1, bg = 'darkblue', fg = 'white')
    check.pack(side = tk.LEFT, padx = 15)
    ok = tk.Button(framebottom, text = '退出', relief = tk.RAISED, command = exit,
         font = ('HeiTi', '14', 'bold'), width = 7, height = 1, bg = 'darkred', fg = 'white')
    ok.pack(side = tk.LEFT, padx = 15)
    framebottom.pack(side = tk.TOP, pady = 5)
    
    def gridclick(row, column):
        number = ''
        for i in range(N):
            if selections[i]["relief"] == tk.SOLID:
                number = '%d' % (i + 1)
                break
        gridvar[row][column].set(number)
        if number == '':
            layout[row][column] = 0
        else :
            layout[row][column] = int(number)
    
    
    def numberclick(selectionbutton):
        for i in range(N):
            selections[i]['relief'] = tk.RAISED
        erase["relief"] = tk.RAISED
        selectionbutton["relief"] = tk.SOLID
    
    def eraseclick(event):
        for i in range(N):
            selections[i]['relief'] = tk.RAISED
    
    
    def checkclick(event):
        correct = verify() 
        if correct:
            showinfo("核查结果", "答案正确")
            print("correct")
        else:
            showinfo("核查结果", "答案不正确")
            print("wrong")
    check.bind("<Button-1>", checkclick)
    
    def readlayout(filename):
        layoutfile = open(filename, 'r')
        lines = layoutfile.readlines()
        for row in range(N):
            line = lines[row].strip('
    ')
            if line != '':
                for i in range(len(line)):
                    if line[i] != '' and line[i] != ' ':
                        layout[row][i] = int(line[i])
        return layout
    
    def showlayout(layout, gridvar):
        for row in range(N):
            for column in range(N):
                gridvar[row][column].set(str(layout[row][column]) if (layout[row][column] != 0) else '')
                if layout[row][column] != 0:
                    grid[row][column]["state"] = tk.DISABLED
    
    
    def verifyrow():
        correct = True
        for row in range(N):
            line = layout[row].copy()
            line.sort()
            if line != [1, 2, 3, 4, 5, 6, 7, 8, 9]:
                correct = False
        return correct
    
    def verifycolumn():
        correct = True
        for column in range(N):
            line = list((np.array(layout))[:, column])
            line.sort()
            if line != [1, 2, 3, 4, 5, 6, 7, 8, 9]:
                correct = False
        return correct
    
    def verifyblock():
        correct = True
        for blockindex in range(N):
            block = getblock(blockindex)
            line = list((np.array(layout))[block[0] : block[1] + 1, block[2] : block[3] + 1].reshape(N))
            line.sort()
            if line != [1, 2, 3, 4, 5, 6, 7, 8, 9]:
                correct = False
        return correct
    
    
    def getblock(index):
        rowstart = index // 3 * 3
        rowend = rowstart + 2
        columnstart = index % 3 * 3
        columnend = columnstart + 2
        return rowstart, rowend, columnstart, columnend
    
    def verify():
        return verifyrow() & verifycolumn() & verifyblock()
    
    erase.bind("<Button-1>", eraseclick)
    
    layout = readlayout('sudoku.txt')
    showlayout(layout, gridvar)
    
    tk.mainloop()
    
    # 但是还没有结束哦,我们下一期讲解如何自动填写正确答案QwQ

    upd.12.15: 图形化界面数独(GUI)(二)出现啦!想看下一期的同学可以继续继续学习辣!

    参考文献及网站:
    [0].老师精彩的课上讲解和资料(不方便透露其相关信息)
    [1].https://blog.csdn.net/Eider1998/article/details/104725180/
    [2].https://www.runoob.com/python/python-tk-frame.html

  • 相关阅读:
    Android基础-Android Bitmap高效加载策略
    Android基础-Android进程间通信方式
    Android基础-Android虚拟机及编译过程
    Android基础-View测量、布局及绘制原理
    Android基础-Window、Activity、DecorView以及ViewRoot之间的关系
    Android基础-LruCache原理解析
    Android基础-IntentService详解
    Android基础-AsyncTask详解
    linux 校准时间
    网站自动识别移动端访问并跳转
  • 原文地址:https://www.cnblogs.com/yizimi/p/14110513.html
Copyright © 2011-2022 走看看