〇、引言
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