图形化界面数独(GUI)(一)
〇、引言
QwQ
我们即将用 Python 写一个GUI图形界面数独!(第一部分)
设计效果:
关键词汇: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: 背景颜色
width: 宽度
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控件的演示:
(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: 文字的字体和字号
width: 宽度
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记录,方便计算和判重
我们把上面两个函数放入代码中,运行效果如下:
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()函数,这是跳出一个提示窗口,其中形参第一个是窗口名称,第二个是提示内容,效果如下:
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()
随后我们逐行操作,先把每行的'\n'去掉,然后对行内每个字符进行统计和存储。注:可能存在一行中没有数字的可能,所以要看本行是否有数字,最后返回其数组。完整代码:
def readlayout(filename):
layoutfile = open(filename, 'r')
lines = layoutfile.readlines()
for row in range(N):
line = lines[row].strip('\n')
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('\n')
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