圖形化界面數獨(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: 背景顏色
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控件的演示:
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: 文字的字體和字號
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記錄,方便計算和判重

我們把上面兩個函數放入代碼中,運行效果如下:

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()

隨后我們逐行操作,先把每行的'\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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM