TkInter:销毁多个按钮后重新创建(可能不同数量的)按钮
我刚开始学习Tkinter,正在写一个简单的翻转方块游戏来掌握基础。这个游戏的想法是,当我启动应用时,会出现一个“开始新游戏”的按钮、一个退出按钮和一个网格大小的滑块。我可以选择一个网格大小,然后点击开始,这样就会出现一个n*n的红色或绿色按钮(方块)网格。点击其中一个按钮,我就能改变这个方块的颜色,以及与它相连的四个方块的颜色。一旦所有的方块都变成绿色,这些方块就会消失,让我可以开始新游戏。
现在问题来了,当我赢得一局游戏后,开始新游戏,或者在一局游戏进行中开始新游戏时,新方块会出现,但当我点击其中一个时,我会看到以下内容:
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python2.6/lib-tk/Tkinter.py", line 1413, in __call__
return self.func(*args)
File "/home/adejong/workspace/test2/view.py", line 116, in on_click
self.view.update()
File "/home/adejong/workspace/test2/view.py", line 84, in update
button.set_colour(View.UP)
File "/home/adejong/workspace/test2/view.py", line 112, in set_colour
self.gridButton.config(bg=colour)
File "/usr/lib/python2.6/lib-tk/Tkinter.py", line 1205, in configure
return self._configure('configure', cnf, kw)
File "/usr/lib/python2.6/lib-tk/Tkinter.py", line 1196, in _configure
self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
TclError: invalid command name ".33325424.33325640.33327872"
我很确定这和我清空网格的方式有关,但我搞不清楚具体问题出在哪里。最开始我无法在开始新游戏时显示按钮,所以这可能和那个有关。
这是发生问题的模块:
# @todo make wrappers for all Tkinter widgets
from Tkinter import *
from observerPattern import Observer
# USE Grid to organise widgets into a grid
class View(Observer):
UP = "red"
DOWN = "green"
## @brief initialises the GUI
# @param master ie the root
#@ model, a pointer to the model
def __init__(self, master, model):
View.instance = self
self.model = model
self.master = master
self.frame = Frame(self.master)
self.frame.grid() #size frame to fit the given text, and make itself visibl
self.optionsGrid = Frame(self.frame)
self.gameGrid = Frame(self.frame)
self.optionsGrid.grid(row = 0, column = 0)
self.gameGrid.grid(row = 1, column = 0)
self.gridSizeScale = Scale(self.optionsGrid, label = "grid size", from_ = 2, to = 20, bigincrement = 1,
showvalue = True, orient = HORIZONTAL)
self.quit = Button(self.optionsGrid, text="QUIT", fg="red", command=self.frame.quit)
self.newGame = Button(self.optionsGrid, text="Start New Game", command=self.init_game)
self.objective = Label(self.optionsGrid, text="Make all the tiles green")
self.newGame.grid(row=0, column=0)
self.quit.grid(row=0, column=3)
self.gridSizeScale.grid(row=0, column=1, columnspan=2)
self.objective.grid(row=1, column=1, columnspan=2)
self.gameButtons = []
self.update()
# start a new game by re building the grid
def init_game(self):
size = self.gridSizeScale.get()
self.model.init_model(size)
self.objective.config(text = "Make all the tiles green")
print "MODEL INITIALISED"
#for i in range(len(self.gameButtons)):
# self.gameButtons[i].grid_forget()
for button in self.gameButtons:
#button.destroy()
#self.gameGrid.grid_forget(button)
button.__del__()
for button in range(size * size):
buttonRow = int(button / size)
buttonCol = button % size
state = self.model.getTileState(buttonRow, buttonCol)
if state == self.model.UP:
initialColour = View.UP
else:
initialColour = View.DOWN
newButton = GridButton(self.gameGrid, butRow = buttonRow, butCol = buttonCol, initColour = initialColour, model = self.model, view = self )
self.gameButtons.append(newButton)
print self.gameButtons
self.gameGrid.grid()
## @brief gets the only View instance. A static method. Dont really need this
# @param None
# @returns the singleton View object
@staticmethod
def getInstance():
if hasattr(View, 'instance') and View.instance != None:
return View.instance
else:
return View()
# make sure the tiles are the right colour etc
def update(self):
for button in self.gameButtons:
state = self.model.getTileState(button.row, button.col)
if state == self.model.UP:
button.set_colour(View.UP)
else:
button.set_colour(View.DOWN)
if self.model.check_win() == True and self.model.tilesInitialised:
for button in self.gameButtons:
#button.destroy()
#self.gameGrid.grid_forget(button)
button.__del__()
#self.gameGrid.grid_forget()
print "slaves", self.gameGrid.grid_slaves()
self.objective.config(text = "You Win!")
self.master.update()
print "YOU WIN!"
# a wrapper i made so i could pass parameters into the button commands
class GridButton(Button):
def __init__(self, master, butRow, butCol, initColour, model, view, *args, **kw):
Button.__init__(self, master)
self.gridButton = Button(master, command=self.on_click, *args, **kw)
self.gridButton.grid(row = butRow, column = butCol )
self.set_colour(initColour)
self.row = butRow
self.col = butCol
self.model = model
self.view = view
def set_colour(self, colour):
self.gridButton.config(bg=colour)
def on_click(self):
self.model.flip_tiles(Row = self.row, Col = self.col)
self.view.update()
def __del__(self):
self.gridButton.destroy()
del self
这是重置的代码,供参考:
from Tkinter import *
from model import Model
from view import View
from controller import Controller
import sys
root = Tk() #enter the Tkinter event loop
model = Model()
view = View(root, model)
#controller = Controller(root, model, view)
root.mainloop()
from Tkinter import *
import random
class Model:
UP = 1
DOWN = 0
def __init__(self, gridSize=None):
Model.instance = self
self.init_model(gridSize)
def init_model(self, gridSize):
self.size = gridSize
self.tiles = []
self.won = False
self.tilesInitialised = False
if gridSize != None:
self.init_tiles()
## @brief gets the only Model instance. A static method
# @param None
# @returns the singleton Model object
@staticmethod
def getInstance(gridSize = None):
if hasattr(Model, 'instance') and Model.instance != None:
return Model.instance
else:
return Model(gridSize)
#initially init tile vals randomly but since not all problems can be solved
# might want to start with a soln and work backwards
def init_tiles(self):
# i should also make sure they're not all 0 to start with
self.tiles = []
for row in range(self.size):
rowList = []
for col in range(self.size):
rowList.append(random.randint(Model.DOWN, Model.UP))
self.tiles.append(rowList)
self.check_win()
if self.won == True:
self.init_tiles()
self.tilesInitialised = True
# tile indexing starts at 0
def flip_tiles(self, selected = None, Row = None, Col = None):
if selected == None and (Row == None and Col == None):
raise IOError("Need a tile to flip")
elif selected != None:
neighbours = self.get_neighbours(selected)
for r in neighbours:
for c in r:
if self.tiles[r][c] == Model.DOWN:
self.tiles[r][c] = Model.UP
elif self.tiles[r][c] == Model.UP:
self.tiles[r][c] = Model.DOWN
else:
selectedTile = Row, Col
neighbours = self.get_neighbours(selectedTile)
for tile in neighbours:
r = tile[0]
c = tile[1]
if self.tiles[r][c] == Model.DOWN:
self.tiles[r][c] = Model.UP
elif self.tiles[r][c] == Model.UP:
self.tiles[r][c] = Model.DOWN
# selected is a tuple (row, column)
# returns a list of tuples of tiles to flip
def get_neighbours(self, selected):
row = selected[0]
col = selected[1]
tilesToFlip = []
for modifier in range(-1,2):
rowIndex = row + modifier
if rowIndex < 0:
pass
elif rowIndex > self.size - 1 :
pass
else:
final = rowIndex, col
tilesToFlip.append(final)
for modifier in range(-1,2):
colIndex = col + modifier
if colIndex < 0:
pass
elif colIndex > self.size - 1:
pass
else:
final = row, colIndex
tilesToFlip.append(final)
neighbours = set(tilesToFlip)
return neighbours
def check_win(self):
self.won = True
#everytime a tile is selected
for row in range(len(self.tiles)):
for col in range(len(self.tiles)):
if self.tiles[row][col] == Model.UP:
self.won = False
break
return self.won
def getTileState(self, buttonRow, buttonCol):
return self.tiles[buttonRow][buttonCol]
任何帮助都会非常感谢。
1 个回答
1
看起来你没有重置 self.game_buttons
。你在 __init__
里把它设置成了一个空列表,但其实应该在 init_game
里做这个重置。因为没有重置,第二次运行游戏时,self.view.update()
会遍历一个包含了两次游戏按钮的列表。由于有一半的按钮已经不存在了,所以当你第一次尝试改变一个已经被删除的按钮的颜色时,就会出现错误。
顺便说一下,有个很简单的方法来管理这些按钮,就是把所有按钮放在一个内部框架里。这样你只需要删除这个框架,就能一并删除它里面的所有按钮。还有一个好处是,你不需要维护一个按钮的列表,因为你可以通过 winfo_children
来获取这个框架里的所有子元素。