简体   繁体   中英

(Python tkinter canvas widget ) How do I change color for 1 of 9 square once it is clicked?

I am trying to build a simple Tic Tac Toe game using python, almost there now except the last visual affect:

I need to be able to modify any of 9 squares showing on the canvas when I click it . Have searched a bunch of example on stack... well...none worked I use a dictionary to map the cursor:

self.map = {(0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 0): 3, (1, 1): 4, (1, 2): 5, (2, 0): 6, (2, 1): 7,(2, 2): 8}

First square on top left is 0 looks like this won't work: id= self.canvas.create_rectangle() does not work on this. self.canvas.itemconfig(id,fill='bule')

I am seeking a effective way to access the fill or the clicked square not the rest. Thanks

All my codes are attached here:

import tkinter
import random


    class Game(object):
        """
        Enter the class docstring here
        """
        block_size = 100
        def __init__(self, parent):
            parent.title('Tic Tac Toe')
            self.parent = parent

            self.initialize_game()

        def initialize_game(self):
            # These are the initializations that need to happen
            # at the beginning and after restarts
            self.board = [None, None, None, None, None, None, None, None, None]  # game board as a instance variable
            self.map = {(0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 0): 3, (1, 1): 4, (1, 2): 5, (2, 0): 6, (2, 1): 7,
                        (2, 2): 8}  # map to self.board
            self.top_frame = tkinter.Frame(self.parent)
            self.top_frame.pack(side=tkinter.TOP)

            # add restart button on top frame
            restart_button = tkinter.Button(self.top_frame, text='Restart', width=20,
                                            command=self.restart)
            restart_button.pack()  # register restart_button with geometry manager

            # create bottom frame for group the label below
            self.bottom_frame=tkinter.Frame(self.parent)
            self.bottom_frame.pack(side=tkinter.BOTTOM)

            # create label for displaying game result text
            self.my_lbl=tkinter.Label(self.bottom_frame,text=None)
            self.my_lbl.pack()

            # create a canvas to draw our board on the top frame
            self.canvas = tkinter.Canvas(self.top_frame,
                                         width=self.block_size * 3,
                                         height=self.block_size * 3)
            # draw 3x3 visible blocks on the canvas
            for ro in range(3):
                for col in range(3):
                    tag=self.canvas.create_rectangle(self.block_size * col,
                                                 self.block_size * ro,
                                                 self.block_size * (col + 1),
                                                 self.block_size * (ro + 1))
            # bind entire canvas with left click  handler (play function)
            self.canvas.bind("<Button-1>", self.play)
            self.canvas.pack()                  # register canvas with a geometry manager

        def board_full(self):
            if None not in self.board:
                return True  # true for full
            else:
                return False  # false for not full


        def possible_moves(self):
            """return: list of possible moves"""
            possible_moves = []  # list for possible moves
            for i in range(0, 9):
                if self.board[i] is None:  # if cell un-taken
                    possible_moves.append(i)  # append the cell number to list
                else:
                    pass  # cell taken, don't append
            return possible_moves  # return list of possible moves

        def pc_move(self):
            m = True
            while m:
                pc_move = random.randint(0, 8)  # random generate a number from 0 to 8
                if pc_move in self.possible_moves():  # if the number is a possible move
                    self.board[pc_move] = 'O'  # do it
                    m = False  # exit loop
                else:  # not a possible movie
                    continue  # re-do
            return self

        def draw_out(self):
            """to be deleted"""
            print(self.board[0:3])
            print(self.board[3:6])
            print(self.board[6:9])

        def play(self, event):  # This method is invoked when the user clicks on a square.
            """
            when the player clicks on a un-taken square, this method first translate cursor into cell number,
            then update game board and check game result based on condition
            :parameter: click
            :return: updated game object
            """
            print('clicked', event.y, event.x)  # to be deleted
            # after the click: part 1     human play first
            my_move = self.map[(event.y // self.block_size, event.x // self.block_size)]  # map cursor
            if self.board[my_move] is None:  # check if cell is empty
                self.board[my_move] = 'X'  # if cell empty mark X for my play,  PC use O
                #self.canvas.itemconfigure(fill='blue')
                # self.draw_out()              # delete this line later
            else:  # if the cell taken, do nothing until click on empty square
                return None
            #check game result and board full:
            if (self.board_full()is True) or(
                        self.check_game()is not None):
                self.canvas.unbind("<Button-1>")    # when win, lost, tie occur,disable handler
                print(self.check_game())             # DEBUGGING DELETE
            else:
                pass
            # part 2: while not filled, PC make one move right after my move:
            self.possible_moves()  # check possible moves for PC
            self.pc_move()  # pc make move
            self.draw_out()  # DELETE LATER
            # part3: check game result and board full
            if (self.board_full()is True) or(
                        self.check_game()is not None):
                self.canvas.unbind("<Button-1>")    # when win, lost, tie occur,disable handler
                print(self.check_game())             # DEBUGGING DELETE
            else:
                pass
            return self  # when board is filled, return

        def check_game(self):
            """
            Check if the game is won or lost or a tie
            Return:  win, lose, tie, none 
            """
            result=None
            if (self.board[0] == self.board[1] == self.board[2] == 'X') or (
                                self.board[3] == self.board[4] == self.board[5] == 'X') or (
                                self.board[6] == self.board[7] == self.board[8] == 'X') or (
                                self.board[0] == self.board[3] == self.board[6] == 'X') or (
                                self.board[1] == self.board[4] == self.board[7] == 'X') or (
                                self.board[2] == self.board[5] == self.board[8] == 'X') or (
                                self.board[0] == self.board[4] == self.board[8] == 'X') or (
                                self.board[2] == self.board[4] == self.board[6] == 'X'):
                result = 'You win!'  # player win
                self.my_lbl.config(text=result)
            elif (self.board[0] == self.board[1] == self.board[2] == 'O') or (
                                self.board[3] == self.board[4] == self.board[5] == 'O') or (
                                self.board[6] == self.board[7] == self.board[8] == 'O') or (
                                self.board[0] == self.board[3] == self.board[6] == 'O') or (
                                self.board[1] == self.board[4] == self.board[7] == 'O') or (
                                self.board[2] == self.board[5] == self.board[8] == 'O') or (
                                self.board[0] == self.board[4] == self.board[8] == 'O') or (
                                self.board[2] == self.board[4] == self.board[6] == 'O'):
                result = 'You lost!'  # player lose
                self.my_lbl.config(text=result)
            else:
                if self.board_full()is True:
                    result = "It's a tie!"  # tie
                    self.my_lbl.config(text=result)
                else:
                    pass
            return result


        def restart(self):
            """ Reinitialize the game and board after restart button is pressed """
            self.top_frame.destroy()
            self.bottom_frame.destroy()
            self.initialize_game()


    def main():
        root = tkinter.Tk()  # Instantiate a root window
        my_game = Game(root)  # Instantiate a Game object
        root.mainloop()  # Enter the main event loop


    if __name__ == '__main__':
        main()

There are at least two ways to solve this problem.

First, you can use find_closest to find the closest object to the cursor. There are two things to keep in mind with this solution. First, you must give it canvas coordinates, not window coordinates (which is what the event object has). Second, you need to either give the rectangles a fill color, or make sure they don't overlap. find_closest looks for colored pixels, so if there's no fill color it will search until it finds a border. If the borders overlap (which yours do), it may find the wrong square (ie: the left edge of the right-most square overlaps the right edge of the middle square, and due to stacking order it will be considered closest).

So, give your rectangles a fill color (eg: self.canvas.create_rectangle(..., fill='white') ), and then you can find the square under the cursor like this:

def play(self, event):
    ...
    cx = self.canvas.canvasx(event.x)
    cy = self.canvas.canvasy(event.y)
    cid = self.canvas.find_closest(cx,cy)[0]
    self.canvas.itemconfigure(cid, fill="blue")
    ...

The other thing you can do is to bind to each square individually with tag_bind , and have the binding pass the number to your function. Again, the rectangle needs a fill color, though for a different reason. The clicks only register on non-transparent regions, so without a fill color the only time it will register a click is when you click on the border:

def initialize_game(self):
    ...
    tag=self.canvas.create_rectangle(self.block_size * col,
                                     self.block_size * ro,
                                     self.block_size * (col + 1),
                                     self.block_size * (ro + 1), fill="white")
    self.canvas.tag_bind(tag, "<1>", lambda event, tag=tag: self.play(event, tag))
    ...

...
def play(self, event, tag):
    ...
    self.canvas.itemconfigure(tag, fill="blue")
    ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM