TkInter:销毁多个按钮后重新创建(可能不同数量的)按钮

1 投票
1 回答
926 浏览
提问于 2025-04-16 11:33

我刚开始学习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 来获取这个框架里的所有子元素。

撰写回答