Module tkdraw.screen

Simple draw graphical user interface (GUI) based on tkinter, synchroneous.

Test this module using python3 -m tkdraw.screen

The online documentation is available under python3:

$ python3
>>> import tkdraw.screen as tkd
>>> help(tkd.Screen)
or
>>> help(tkd.Screen.FUNC_NAME)

Copyright 2018-2022, Vincent Loechner. Distributed under the MIT license (see LICENSE) https://github.com/vincentloechner/pytkdraw.git

Expand source code
"""Simple draw graphical user interface (GUI) based on tkinter, synchroneous.

Test this module using `python3 -m tkdraw.screen`

The online documentation is available under python3:
```
$ python3
>>> import tkdraw.screen as tkd
>>> help(tkd.Screen)
or
>>> help(tkd.Screen.FUNC_NAME)
```

Copyright 2018-2022, Vincent Loechner.
Distributed under the MIT license (see LICENSE)
https://github.com/vincentloechner/pytkdraw.git
"""

import tkinter as tk
import queue


# pylint: disable=too-many-ancestors
# The tk canvas has too many already.
class Screen(tk.Canvas):
    """Main class for a window containing a board (2D grid).

    The 2D grid is pythonesque: always starts at top-left position (0, 0), in
    row-major order, ends at (height-1, width-1) included. All coordinates and
    sizes are expressed as iterable objects (a tuple or a list for example) of
    two integers.

    To open a window containing just an array of pixels, set the `pixels`
    parameter to 1 and `grid` to False.
    This class inherits from tkinter's canvas class.

    Example:
        ```
        import tkdraw.screen
        g = tkdraw.screen.Screen()
        g.message("hello")
        g.wait_event()
        g.close()
        ```
    Args:
        size ([int, int]): a couple (height, width) specifying the size of
            the grid (default: (8, 8))
        pixels (int): number of pixels of a square (default: 100)
        grid (bool): if True, prints a one-pixel grid to separate the tiles:
            horizontal and vertical lines (default: True) - the real size of
            the window is also increased by one pixel to draw the right/bottom
            lines


    Returns:
        The window object.

    Notes:
        Since this class inherits from tkinter's canvas class, more inherited
            documentation will be displayed below this paragraph. You can
            directly call the canvas methods to perform advanced drawings in
            this canvas (expert mode, be warned that a couple of coordinates
            (y, x) in tkdraw has to be transformed into two integers x+1, y+1
            in tkinter). You may refer to the documentation of tkinter
            describing the Canvas widget here:
            <https://docs.python.org/3/library/tkinter.html>
    """

    # pylint: disable=too-many-instance-attributes
    # it is reasonable here, and many are private.
    def __init__(
        self, size=(8, 8), pixels=100, grid=True
    ):
        # some private methods below, to react to asynchronous events:

        # private: the user clicked on a square
        def _click(evenement):
            # min to handle user clicking on the last pixel:
            i = min((evenement.y-1)//self.pixels, self.size[0]-1)
            j = min((evenement.x-1)//self.pixels, self.size[1]-1)
            # add the event (line, column) to the queue
            self._eventq.put(("click", (i, j)))
            # and leave the mainloop
            self.root.quit()

        # private: the user hit a key
        def _key(evenement):
            # put the event in the queue
            self._eventq.put(("key", evenement.keysym))
            # and leave the mainloop
            self.root.quit()

        # private: the user closed the window
        def _async_end():
            # put the END event in the queue
            self._eventq.put(("END", None))
            # and close
            self.close()

        # private: regularily check if some events are pending in the queue
        # and wake up
        def _checke():
            self._after_id = self.root.after(1000, _checke)
            # stop mainloop
            self.root.quit()

        #################################
        # INIT
        self.size = size
        self.pixels = pixels
        self._after_id = None
        self._gap = int(grid)  # 1 more pixel if grid is True

        self._eventq = queue.Queue()
        self._idd = None
        self._souris = (0, 0)

        self.root = tk.Tk()
        # you can create other widgets here if you want to
        # self.frame = tk.Frame(root)

        # creates THE canvas:
        tk.Canvas.__init__(
                self,
                self.root,
                height=self.size[0]*self.pixels+self._gap,
                width=self.size[1]*self.pixels+self._gap,
                background="#ddd",
                takefocus=True,
                borderwidth=0,
                highlightthickness=1)
        self.pack()
        # self.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
        self.focus_set()

        # binds the click function to the click event
        self.bind("<Button-1>", _click)
        # binds the key function to the keypress event
        self.bind("<Any-KeyPress>", _key)

        # checke will be called once per second
        self._after_id = self.root.after(1000, _checke)
        # ensure that async_end is called if the window is killed
        self.root.protocol("WM_DELETE_WINDOW", _async_end)

        # draw the original state
        if grid:
            self.draw_grid()

    def __enter__(self):
        """Internal: With -as: statement compatibility."""
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Internal: With -as: statement compatibility."""
        self.close()

    def __del__(self):
        """Internal: if the object is freeed by python for some reason."""
        self.close()

    def close(self):
        """Close this window.

        Args:
            None

        Returns:
            None
        """
        if self._after_id is not None:
            self.root.after_cancel(self._after_id)
            self._after_id = None
        if self.root is not None:
            self.root.destroy()
            self.root = None

    def message(self, message):
        """Display a message in a box and wait for the user to click somewhere.

        Args:
            message (str): the string to print

        Returns:
            bool: True if the user triggered a normal event, False if the user
                closed the window abruptly.
        """
        # private: the user clicked in the box
        # pylint: disable=unused-argument
        # it's an event, don't care which one.
        def _c(event):
            self._eventq.put("ok")
            self.root.quit()

        if not self.root:
            raise InterruptedError("window killed")
        msg = tk.Message(
                self.root, text=message,
                padx=20, pady=20,
                relief=tk.RAISED, borderwidth=5,
                )
        # mouse click:
        msg.bind("<Button-1>", _c)
        # m.grid(row=0, column=0)
        msg.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
        while True:
            evt = self.wait_event()
            if evt[0] == "END":
                # the window has been closed
                # put the end msg back in the queue and return False
                self._eventq.put(evt)
                return False
            # all the other events just close the message box and return True:
            msg.destroy()
            return True

    ###########################################################################
    # HIGH LEVEL INTERFACE                                                    #
    # This function can be used to print whole standard boards.               #
    # Matrices should contain integers (-> representing pieces colors/players)#
    # or the None special value (-> nothing there)                            #
    ###########################################################################

    # by default define those 10 colors:
    DEFAULT_COLOR = ["black", "white", "red", "green", "blue",
                     "yellow", "cyan", "magenta", "orange", "darkgrey"]
    """A list of ten colors used for ten players identified by numbers."""

    def draw_grid(
        self, matrix=None, grid=True
    ):
        """Draw a complete grid of pieces, using the 10 default colors.

        Args:
            matrix (list of list of int): the players pieces matrix, of the
                specified size. If matrix is None (default), just ignores it.
            grid (boolean): if True (default) draws a horizontal+vertical grid
                to separate the tiles.

        Returns:
            list of int: the graphical objects that were created for the pieces
                (grid excluded).
        """
        if not self.root:
            raise InterruptedError("window killed")
        # erase everything for a start
        self.delete(tk.ALL)
        # gap is used by draw_tile to fill the inside of a tile (including
        # borders, or not
        if grid:
            for i in range(self.size[0]+self._gap):
                self.create_line(1, i*self.pixels+1,
                                 self.size[1]*self.pixels+1, i*self.pixels+1,
                                 width=1)
            for i in range(self.size[1]+self._gap):
                self.create_line(i*self.pixels+1, 1,
                                 i*self.pixels+1,
                                 self.size[0]*self.pixels+1+self._gap,
                                 width=1)
        # draw the pieces
        lobj = []
        if matrix is not None:
            for i in range(self.size[0]):
                for j in range(self.size[1]):
                    if matrix[i][j] is not None:
                        lobj.append(self.draw_piece((i, j), matrix[i][j],
                                    refresh=False))
        # update just once at the end, for performance
        self.update()
        return lobj

    def erase(
        self
    ):
        """Erase everything from the window.

        All graphical objects that were created are deleted.

        Args:
            None

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")
        self.delete(tk.ALL)
        # redraw the grid if it was there:
        if self._gap == 1:
            self.draw_grid()

    ###########################################################################
    # INTERMEDIATE LEVEL INTERFACE                                            #
    # draw various objects on a board (2D grid)                               #
    ###########################################################################
    def draw_piece(
        self, pos, player=0,
        color=None,
        refresh=True
    ):
        """Draw a piece in position pos=(line, column), player-colored.

        Usage notice: don't use this function if your tiles are too small, you
        won't see the piece.

        Args:
            pos ([int, int]): a couple (line, column) specifying the grid
                position. (0, 0) = top-left position.
            player (int, optional): player number (default: 0)
                player is used only if color is NOT given.
            color (str, optional): fill color of the piece. If a color is
                given, player is ignored.
            refresh (bool): refresh the window after drawing (default: True)

        Returns:
            int: the ID of the graphical object (circle) that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")
        if color is None:
            if isinstance(player, int):
                color = self.DEFAULT_COLOR[player % len(self.DEFAULT_COLOR)]
            else:
                color = self.DEFAULT_COLOR[0]

        bord = self.pixels//10+1
        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to draw outside the window!")

        obj = self.create_oval(j*self.pixels+bord+1,
                               i*self.pixels+bord+1,
                               (j+1)*self.pixels-bord+1,
                               (i+1)*self.pixels-bord+1,
                               width=1, fill=color)
        if refresh:
            self.update()
        return obj

    def move_piece(
        self, obj, pos, refresh=True
    ):
        """Move a piece (obj ID) in grid position pos=(line, column).

        Args:
            obj (int): a previously created piece ID
            pos ([int, int]): new grid position, a couple (line, column)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to move a piece outside the window!")
        bord = self.pixels//10+1
        self.coords(
            obj,
            j*self.pixels+bord+1,
            i*self.pixels+bord+1,
            (j+1)*self.pixels-bord+1,
            (i+1)*self.pixels-bord+1
            )
        if refresh:
            self.update()

    def draw_tile(
        self, pos,
        color="black",
        border=0,
        refresh=True
    ):
        """Fill a tile in position pos=(line, column) with a color.

        Args:
            pos ([int, int]): grid position (line, column)
            color (str, optional): color of the tile (default: "black")
            border (int, optional): border thickness (default: 0) - borders may
                overlap over neighboring tiles
            refresh (bool): refresh the window after drawing (default: True)

        Returns:
            int: the ID of the colored tile
        """
        if not self.root:
            raise InterruptedError("window killed")

        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to fill a tile outside the window!")

        obj = self.create_rectangle(j*self.pixels+1+self._gap,
                                    i*self.pixels+1+self._gap,
                                    (j+1)*self.pixels+1,
                                    (i+1)*self.pixels+1,
                                    width=border, fill=color)
        if refresh:
            self.update()
        return obj

    def move_tile(
        self, obj, pos, refresh=True
    ):
        """Move a colored tile to another grid position.

        Args:
            obj (int): a previously created tile ID
            pos ([int, int]): new position in the grid (line, column)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to move a tile outside the window!")
        self.coords(
            obj,
            j*self.pixels+1++self._gap,
            i*self.pixels+1++self._gap,
            (j+1)*self.pixels+1,
            (i+1)*self.pixels+1
            )
        if refresh:
            self.update()

    ###########################################################################
    # low level interface:                                                    #
    # draw pixels, lines, circles, etc.                                       #
    ###########################################################################
    # pylint: disable=too-many-arguments
    # self doesn't count, and 3 are optionial
    def draw_line(
        self, x_1, x_2, color="black", thickness=1, refresh=True
    ):
        """Draw a line between x_1=(l1, c1) and x_2=(l2, c2) (excluded).

        Args:
            x_1 ([int, int]): pixel-wise positions (line, column) of the first
                point. (0,0) = top-left position.
            x_2 ([int, int]): pixel-wise positions (line, column) of the second
                point, excluded.
            color (str, optional): line color (default: "black")
            thickness (int, optional): thickness of the line (default: 1)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            int: the ID of the graphical object that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")

        obj = self.create_line(x_1[1]+1, x_1[0]+1, x_2[1], x_2[0],
                               width=thickness, fill=color)
        if refresh:
            self.update()
        return obj

    def draw_circle(
        self, x_1, x_2,
        color="black", border=1, refresh=True
    ):
        """Draw a circle in the bounding box of x_1=(l1,c1) and x_2 (excluded).

        Args:
            x_1 ([int, int]): pixel-wise positions (line, column) of the first
                point. (0,0) = top-left position.
            x_2 ([int, int]): pixel-wise positions (line, column) of the second
                point, excluded.
            color (str, optional): color of the inside (default: "black").
                If color == None, don't fill the circle.
            border (int, optional): border thickness (default: 1) - borders can
                overflow over the given pixels coordinates
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            int: the ID of the graphical object that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")

        obj = self.create_oval(x_1[1]+1, x_1[0]+1, x_2[1], x_2[0],
                               width=border, fill=color)
        if refresh:
            self.update()
        return obj

    def draw_text(
        self, position, text,
        color="black", fontname="Purisa", fontsize=11, refresh=True
    ):
        """Draw a text centered at a given position=(line, column).

        Args:
            position ([int, int]): pixel-wise position (line, column).
                (0,0) = top-left position.
            text (str): text to print
            color (str, optional): text color (default: "black")
            fontname (str, optional): font name (default: "Purisa")
            fontsize (int, optional): font size (default: 11pt)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            int: the ID of the graphical object that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")

        obj = self.create_text(position[1]+1, position[0]+1,
                               text=text,
                               font=(fontname, fontsize),
                               fill=color)
        if refresh:
            self.update()
        return obj

    # pylint: disable=invalid-name
    # I'm too lazy to write 'background/foreground'.
    def bg(
        self, obj, before=1, refresh=True
    ):
        """Send a graphical object (ID) to the background.

        Args:
            obj (int): object ID returned by an object creation method
            before (int, optional): the object behind which to hide (default:
                all)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        self.tag_lower(obj, before)
        if refresh:
            self.update()

    def fg(
        self, obj, after=None, refresh=True
    ):
        """Send a graphical object (ID) to the foreground.

        Args:
            obj (int): object ID returned by an object creation method
            after (int, optional): the object after which to show up (default:
                all)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        if after:
            self.tag_raise(obj, after)
        else:
            self.tag_raise(obj)

        if refresh:
            self.update()

    def refresh(
        self
    ):
        """Refresh all objects previously drawn in the window.

        Note:
            You can create several successive objects without refreshing by
                passing the extra argument "refresh=False" to the drawing
                functions, and then refresh only once using this function.
                Good for speed, especially if you draw many things.

        Args:
            None

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        self.update()

    # pylint: disable=invalid-name
    # I'm too lazy to write 'remove'.
    def rm(
        self, obj, refresh=True
    ):
        """Delete a graphical object.

        Args:
            obj (int): an object ID (returned by an object creation method)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        self.delete(obj)
        if refresh:
            self.update()

    ###########################################################################
    # Main I/O function                                                       #
    ###########################################################################
    def wait_event(self, delay=None):
        """Wait for the user to interact with the window.

        The tracked events are only keypress and mouse click (left button).

        Args:
            delay (int, optional): waiting time in ms (default: wait forever)

        Returns:
            If the delay expires (when given)

            - `None`

            Else, returns a couple, the first element being always a string:

            - `("click", (line, column))`

                if the user clicks on a tile of the board, where (line, column)
                    are the integer coordinates of the clicked tile

            - `("key", letter)`

                if the user hits a key, where letter is a string containing a
                    description of the key. If you're looking for a specific
                    key, you can give a try to the demo - running
                    `'python3 -m tkdraw'` will print all occuring events in the
                    console

            - `("END", None)`

                if the user closes the window
        """
        # private: called when the timer expires
        def _delay_expire():
            self._eventq.put(None)
            self._idd = None
            self.root.quit()

        # trigger the timer
        self._idd = None
        if delay is not None:
            if not self.root:
                raise InterruptedError("window killed")
            self._idd = self.root.after(delay, _delay_expire)
        # This is Tk's main loop
        # checks the events and get out if something happens
        while True:
            ####################
            try:
                # get something from the event queue
                ret = self._eventq.get(False)
                # cancels the timer if I got something
                if self._idd is not None and self.root is not None:
                    self.root.after_cancel(self._idd)
                return ret
            except queue.Empty:
                # no event in the queue, continue waiting (go to mainloop)
                pass
            ####################
            if not self.root:
                raise InterruptedError("window killed")
            # back to the mainloop, it will stop if I get an event
            self.root.mainloop()
            ####################

    def mouse_position(self):
        """Return the mouse position (pixel-wise).

        Note:
            The returned coordinates can be negative or greater than the window
                size, when the mouse is out of the window but the window still
                selected.

        Args:
            None

        Returns:
            a couple ([int, int]): pixel-wise position (line, column) relative
        to (0,0) = top-left position in the window.
        """
        # private: the mouse moved
        def _motion(event):
            self._souris = (event.y, event.x)

        if not self.root:
            raise InterruptedError("window killed")
        self.refresh()
        try:
            return self._souris
        except AttributeError:
            self.bind("<Motion>", _motion)
            return self._souris


###########################################################################
# Test program: 8x8 board, click to place/remove black and white pieces   #
###########################################################################
if __name__ == "__main__":
    SIZE = 8
    PIXEL_SIZE = 100

    def main():
        """Main function, testing the module."""
        # open the window containing a board
        # use tkdraw.screen.Screen() if you include this as a module with
        # 'import tkdraw.screen'
        win = Screen((SIZE, SIZE), PIXEL_SIZE)

        # checkered tiles:
        for i in range(SIZE):
            for j in range(i % 2, SIZE, 2):
                win.draw_tile((i, j), color="yellow", refresh=False)
        # refresh only once for performance
        win.refresh()

        # welcome message
        msg = win.draw_text((PIXEL_SIZE//2, PIXEL_SIZE+PIXEL_SIZE//2),
                          "Hello!\nclick here ->")

        # main loop: wait for user clicks and draw pieces.
        # tab = 2D grid containing the graphical object IDs
        # if you click again on a piece it will be deleted, same player plays
        tab = [[None]*SIZE for i in range(SIZE)]
        player = 1  # white player starts, black = -1.
        while True:
            evt = win.wait_event()
            # demo: print the content of the event couple
            print(evt)

            # stop when:
            if evt[0] == "END":
                break
            # or when:
            if evt[0] == "key" and evt[1] in ["q", "Q", "Escape"]:
                break

            # ignore all other events but mouse click
            if evt[0] != "click":
                continue

            lig, col = evt[1]
            if tab[lig][col] is None:
                tab[lig][col] = win.draw_piece(evt[1], player)
                # player : 1 -> -1 -> 1 -> ...
                player = -player
            else:
                # remove the piece at position (lig, col)
                win.rm(tab[lig][col])
                tab[lig][col] = None
                # and same player plays again

            # remove the previous message
            win.rm(msg, refresh=False)
            # and put a new one
            msg = win.draw_text((PIXEL_SIZE//2, PIXEL_SIZE+PIXEL_SIZE//2),
                              [0, "white player", "black player"][player])

        # at the end, close the window properly
        win.close()

    main()

Classes

class Screen (size=(8, 8), pixels=100, grid=True)

Main class for a window containing a board (2D grid).

The 2D grid is pythonesque: always starts at top-left position (0, 0), in row-major order, ends at (height-1, width-1) included. All coordinates and sizes are expressed as iterable objects (a tuple or a list for example) of two integers.

To open a window containing just an array of pixels, set the pixels parameter to 1 and grid to False. This class inherits from tkinter's canvas class.

Example

import tkdraw.screen
g = tkdraw.screen.Screen()
g.message("hello")
g.wait_event()
g.close()

Args

size : [int, int]
a couple (height, width) specifying the size of the grid (default: (8, 8))
pixels : int
number of pixels of a square (default: 100)
grid : bool
if True, prints a one-pixel grid to separate the tiles: horizontal and vertical lines (default: True) - the real size of the window is also increased by one pixel to draw the right/bottom lines

Returns

The window object.

Notes

Since this class inherits from tkinter's canvas class, more inherited documentation will be displayed below this paragraph. You can directly call the canvas methods to perform advanced drawings in this canvas (expert mode, be warned that a couple of coordinates (y, x) in tkdraw has to be transformed into two integers x+1, y+1 in tkinter). You may refer to the documentation of tkinter describing the Canvas widget here: https://docs.python.org/3/library/tkinter.html

Construct a canvas widget with the parent MASTER.

Valid resource names: background, bd, bg, borderwidth, closeenough, confine, cursor, height, highlightbackground, highlightcolor, highlightthickness, insertbackground, insertborderwidth, insertofftime, insertontime, insertwidth, offset, relief, scrollregion, selectbackground, selectborderwidth, selectforeground, state, takefocus, width, xscrollcommand, xscrollincrement, yscrollcommand, yscrollincrement.

Expand source code
class Screen(tk.Canvas):
    """Main class for a window containing a board (2D grid).

    The 2D grid is pythonesque: always starts at top-left position (0, 0), in
    row-major order, ends at (height-1, width-1) included. All coordinates and
    sizes are expressed as iterable objects (a tuple or a list for example) of
    two integers.

    To open a window containing just an array of pixels, set the `pixels`
    parameter to 1 and `grid` to False.
    This class inherits from tkinter's canvas class.

    Example:
        ```
        import tkdraw.screen
        g = tkdraw.screen.Screen()
        g.message("hello")
        g.wait_event()
        g.close()
        ```
    Args:
        size ([int, int]): a couple (height, width) specifying the size of
            the grid (default: (8, 8))
        pixels (int): number of pixels of a square (default: 100)
        grid (bool): if True, prints a one-pixel grid to separate the tiles:
            horizontal and vertical lines (default: True) - the real size of
            the window is also increased by one pixel to draw the right/bottom
            lines


    Returns:
        The window object.

    Notes:
        Since this class inherits from tkinter's canvas class, more inherited
            documentation will be displayed below this paragraph. You can
            directly call the canvas methods to perform advanced drawings in
            this canvas (expert mode, be warned that a couple of coordinates
            (y, x) in tkdraw has to be transformed into two integers x+1, y+1
            in tkinter). You may refer to the documentation of tkinter
            describing the Canvas widget here:
            <https://docs.python.org/3/library/tkinter.html>
    """

    # pylint: disable=too-many-instance-attributes
    # it is reasonable here, and many are private.
    def __init__(
        self, size=(8, 8), pixels=100, grid=True
    ):
        # some private methods below, to react to asynchronous events:

        # private: the user clicked on a square
        def _click(evenement):
            # min to handle user clicking on the last pixel:
            i = min((evenement.y-1)//self.pixels, self.size[0]-1)
            j = min((evenement.x-1)//self.pixels, self.size[1]-1)
            # add the event (line, column) to the queue
            self._eventq.put(("click", (i, j)))
            # and leave the mainloop
            self.root.quit()

        # private: the user hit a key
        def _key(evenement):
            # put the event in the queue
            self._eventq.put(("key", evenement.keysym))
            # and leave the mainloop
            self.root.quit()

        # private: the user closed the window
        def _async_end():
            # put the END event in the queue
            self._eventq.put(("END", None))
            # and close
            self.close()

        # private: regularily check if some events are pending in the queue
        # and wake up
        def _checke():
            self._after_id = self.root.after(1000, _checke)
            # stop mainloop
            self.root.quit()

        #################################
        # INIT
        self.size = size
        self.pixels = pixels
        self._after_id = None
        self._gap = int(grid)  # 1 more pixel if grid is True

        self._eventq = queue.Queue()
        self._idd = None
        self._souris = (0, 0)

        self.root = tk.Tk()
        # you can create other widgets here if you want to
        # self.frame = tk.Frame(root)

        # creates THE canvas:
        tk.Canvas.__init__(
                self,
                self.root,
                height=self.size[0]*self.pixels+self._gap,
                width=self.size[1]*self.pixels+self._gap,
                background="#ddd",
                takefocus=True,
                borderwidth=0,
                highlightthickness=1)
        self.pack()
        # self.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
        self.focus_set()

        # binds the click function to the click event
        self.bind("<Button-1>", _click)
        # binds the key function to the keypress event
        self.bind("<Any-KeyPress>", _key)

        # checke will be called once per second
        self._after_id = self.root.after(1000, _checke)
        # ensure that async_end is called if the window is killed
        self.root.protocol("WM_DELETE_WINDOW", _async_end)

        # draw the original state
        if grid:
            self.draw_grid()

    def __enter__(self):
        """Internal: With -as: statement compatibility."""
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Internal: With -as: statement compatibility."""
        self.close()

    def __del__(self):
        """Internal: if the object is freeed by python for some reason."""
        self.close()

    def close(self):
        """Close this window.

        Args:
            None

        Returns:
            None
        """
        if self._after_id is not None:
            self.root.after_cancel(self._after_id)
            self._after_id = None
        if self.root is not None:
            self.root.destroy()
            self.root = None

    def message(self, message):
        """Display a message in a box and wait for the user to click somewhere.

        Args:
            message (str): the string to print

        Returns:
            bool: True if the user triggered a normal event, False if the user
                closed the window abruptly.
        """
        # private: the user clicked in the box
        # pylint: disable=unused-argument
        # it's an event, don't care which one.
        def _c(event):
            self._eventq.put("ok")
            self.root.quit()

        if not self.root:
            raise InterruptedError("window killed")
        msg = tk.Message(
                self.root, text=message,
                padx=20, pady=20,
                relief=tk.RAISED, borderwidth=5,
                )
        # mouse click:
        msg.bind("<Button-1>", _c)
        # m.grid(row=0, column=0)
        msg.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
        while True:
            evt = self.wait_event()
            if evt[0] == "END":
                # the window has been closed
                # put the end msg back in the queue and return False
                self._eventq.put(evt)
                return False
            # all the other events just close the message box and return True:
            msg.destroy()
            return True

    ###########################################################################
    # HIGH LEVEL INTERFACE                                                    #
    # This function can be used to print whole standard boards.               #
    # Matrices should contain integers (-> representing pieces colors/players)#
    # or the None special value (-> nothing there)                            #
    ###########################################################################

    # by default define those 10 colors:
    DEFAULT_COLOR = ["black", "white", "red", "green", "blue",
                     "yellow", "cyan", "magenta", "orange", "darkgrey"]
    """A list of ten colors used for ten players identified by numbers."""

    def draw_grid(
        self, matrix=None, grid=True
    ):
        """Draw a complete grid of pieces, using the 10 default colors.

        Args:
            matrix (list of list of int): the players pieces matrix, of the
                specified size. If matrix is None (default), just ignores it.
            grid (boolean): if True (default) draws a horizontal+vertical grid
                to separate the tiles.

        Returns:
            list of int: the graphical objects that were created for the pieces
                (grid excluded).
        """
        if not self.root:
            raise InterruptedError("window killed")
        # erase everything for a start
        self.delete(tk.ALL)
        # gap is used by draw_tile to fill the inside of a tile (including
        # borders, or not
        if grid:
            for i in range(self.size[0]+self._gap):
                self.create_line(1, i*self.pixels+1,
                                 self.size[1]*self.pixels+1, i*self.pixels+1,
                                 width=1)
            for i in range(self.size[1]+self._gap):
                self.create_line(i*self.pixels+1, 1,
                                 i*self.pixels+1,
                                 self.size[0]*self.pixels+1+self._gap,
                                 width=1)
        # draw the pieces
        lobj = []
        if matrix is not None:
            for i in range(self.size[0]):
                for j in range(self.size[1]):
                    if matrix[i][j] is not None:
                        lobj.append(self.draw_piece((i, j), matrix[i][j],
                                    refresh=False))
        # update just once at the end, for performance
        self.update()
        return lobj

    def erase(
        self
    ):
        """Erase everything from the window.

        All graphical objects that were created are deleted.

        Args:
            None

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")
        self.delete(tk.ALL)
        # redraw the grid if it was there:
        if self._gap == 1:
            self.draw_grid()

    ###########################################################################
    # INTERMEDIATE LEVEL INTERFACE                                            #
    # draw various objects on a board (2D grid)                               #
    ###########################################################################
    def draw_piece(
        self, pos, player=0,
        color=None,
        refresh=True
    ):
        """Draw a piece in position pos=(line, column), player-colored.

        Usage notice: don't use this function if your tiles are too small, you
        won't see the piece.

        Args:
            pos ([int, int]): a couple (line, column) specifying the grid
                position. (0, 0) = top-left position.
            player (int, optional): player number (default: 0)
                player is used only if color is NOT given.
            color (str, optional): fill color of the piece. If a color is
                given, player is ignored.
            refresh (bool): refresh the window after drawing (default: True)

        Returns:
            int: the ID of the graphical object (circle) that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")
        if color is None:
            if isinstance(player, int):
                color = self.DEFAULT_COLOR[player % len(self.DEFAULT_COLOR)]
            else:
                color = self.DEFAULT_COLOR[0]

        bord = self.pixels//10+1
        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to draw outside the window!")

        obj = self.create_oval(j*self.pixels+bord+1,
                               i*self.pixels+bord+1,
                               (j+1)*self.pixels-bord+1,
                               (i+1)*self.pixels-bord+1,
                               width=1, fill=color)
        if refresh:
            self.update()
        return obj

    def move_piece(
        self, obj, pos, refresh=True
    ):
        """Move a piece (obj ID) in grid position pos=(line, column).

        Args:
            obj (int): a previously created piece ID
            pos ([int, int]): new grid position, a couple (line, column)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to move a piece outside the window!")
        bord = self.pixels//10+1
        self.coords(
            obj,
            j*self.pixels+bord+1,
            i*self.pixels+bord+1,
            (j+1)*self.pixels-bord+1,
            (i+1)*self.pixels-bord+1
            )
        if refresh:
            self.update()

    def draw_tile(
        self, pos,
        color="black",
        border=0,
        refresh=True
    ):
        """Fill a tile in position pos=(line, column) with a color.

        Args:
            pos ([int, int]): grid position (line, column)
            color (str, optional): color of the tile (default: "black")
            border (int, optional): border thickness (default: 0) - borders may
                overlap over neighboring tiles
            refresh (bool): refresh the window after drawing (default: True)

        Returns:
            int: the ID of the colored tile
        """
        if not self.root:
            raise InterruptedError("window killed")

        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to fill a tile outside the window!")

        obj = self.create_rectangle(j*self.pixels+1+self._gap,
                                    i*self.pixels+1+self._gap,
                                    (j+1)*self.pixels+1,
                                    (i+1)*self.pixels+1,
                                    width=border, fill=color)
        if refresh:
            self.update()
        return obj

    def move_tile(
        self, obj, pos, refresh=True
    ):
        """Move a colored tile to another grid position.

        Args:
            obj (int): a previously created tile ID
            pos ([int, int]): new position in the grid (line, column)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        i, j = pos
        if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
            raise ValueError("trying to move a tile outside the window!")
        self.coords(
            obj,
            j*self.pixels+1++self._gap,
            i*self.pixels+1++self._gap,
            (j+1)*self.pixels+1,
            (i+1)*self.pixels+1
            )
        if refresh:
            self.update()

    ###########################################################################
    # low level interface:                                                    #
    # draw pixels, lines, circles, etc.                                       #
    ###########################################################################
    # pylint: disable=too-many-arguments
    # self doesn't count, and 3 are optionial
    def draw_line(
        self, x_1, x_2, color="black", thickness=1, refresh=True
    ):
        """Draw a line between x_1=(l1, c1) and x_2=(l2, c2) (excluded).

        Args:
            x_1 ([int, int]): pixel-wise positions (line, column) of the first
                point. (0,0) = top-left position.
            x_2 ([int, int]): pixel-wise positions (line, column) of the second
                point, excluded.
            color (str, optional): line color (default: "black")
            thickness (int, optional): thickness of the line (default: 1)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            int: the ID of the graphical object that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")

        obj = self.create_line(x_1[1]+1, x_1[0]+1, x_2[1], x_2[0],
                               width=thickness, fill=color)
        if refresh:
            self.update()
        return obj

    def draw_circle(
        self, x_1, x_2,
        color="black", border=1, refresh=True
    ):
        """Draw a circle in the bounding box of x_1=(l1,c1) and x_2 (excluded).

        Args:
            x_1 ([int, int]): pixel-wise positions (line, column) of the first
                point. (0,0) = top-left position.
            x_2 ([int, int]): pixel-wise positions (line, column) of the second
                point, excluded.
            color (str, optional): color of the inside (default: "black").
                If color == None, don't fill the circle.
            border (int, optional): border thickness (default: 1) - borders can
                overflow over the given pixels coordinates
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            int: the ID of the graphical object that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")

        obj = self.create_oval(x_1[1]+1, x_1[0]+1, x_2[1], x_2[0],
                               width=border, fill=color)
        if refresh:
            self.update()
        return obj

    def draw_text(
        self, position, text,
        color="black", fontname="Purisa", fontsize=11, refresh=True
    ):
        """Draw a text centered at a given position=(line, column).

        Args:
            position ([int, int]): pixel-wise position (line, column).
                (0,0) = top-left position.
            text (str): text to print
            color (str, optional): text color (default: "black")
            fontname (str, optional): font name (default: "Purisa")
            fontsize (int, optional): font size (default: 11pt)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            int: the ID of the graphical object that was created.
        """
        if not self.root:
            raise InterruptedError("window killed")

        obj = self.create_text(position[1]+1, position[0]+1,
                               text=text,
                               font=(fontname, fontsize),
                               fill=color)
        if refresh:
            self.update()
        return obj

    # pylint: disable=invalid-name
    # I'm too lazy to write 'background/foreground'.
    def bg(
        self, obj, before=1, refresh=True
    ):
        """Send a graphical object (ID) to the background.

        Args:
            obj (int): object ID returned by an object creation method
            before (int, optional): the object behind which to hide (default:
                all)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        self.tag_lower(obj, before)
        if refresh:
            self.update()

    def fg(
        self, obj, after=None, refresh=True
    ):
        """Send a graphical object (ID) to the foreground.

        Args:
            obj (int): object ID returned by an object creation method
            after (int, optional): the object after which to show up (default:
                all)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        if after:
            self.tag_raise(obj, after)
        else:
            self.tag_raise(obj)

        if refresh:
            self.update()

    def refresh(
        self
    ):
        """Refresh all objects previously drawn in the window.

        Note:
            You can create several successive objects without refreshing by
                passing the extra argument "refresh=False" to the drawing
                functions, and then refresh only once using this function.
                Good for speed, especially if you draw many things.

        Args:
            None

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        self.update()

    # pylint: disable=invalid-name
    # I'm too lazy to write 'remove'.
    def rm(
        self, obj, refresh=True
    ):
        """Delete a graphical object.

        Args:
            obj (int): an object ID (returned by an object creation method)
            refresh (bool, optional): refresh the window after drawing
                (default: True)

        Returns:
            None
        """
        if not self.root:
            raise InterruptedError("window killed")

        self.delete(obj)
        if refresh:
            self.update()

    ###########################################################################
    # Main I/O function                                                       #
    ###########################################################################
    def wait_event(self, delay=None):
        """Wait for the user to interact with the window.

        The tracked events are only keypress and mouse click (left button).

        Args:
            delay (int, optional): waiting time in ms (default: wait forever)

        Returns:
            If the delay expires (when given)

            - `None`

            Else, returns a couple, the first element being always a string:

            - `("click", (line, column))`

                if the user clicks on a tile of the board, where (line, column)
                    are the integer coordinates of the clicked tile

            - `("key", letter)`

                if the user hits a key, where letter is a string containing a
                    description of the key. If you're looking for a specific
                    key, you can give a try to the demo - running
                    `'python3 -m tkdraw'` will print all occuring events in the
                    console

            - `("END", None)`

                if the user closes the window
        """
        # private: called when the timer expires
        def _delay_expire():
            self._eventq.put(None)
            self._idd = None
            self.root.quit()

        # trigger the timer
        self._idd = None
        if delay is not None:
            if not self.root:
                raise InterruptedError("window killed")
            self._idd = self.root.after(delay, _delay_expire)
        # This is Tk's main loop
        # checks the events and get out if something happens
        while True:
            ####################
            try:
                # get something from the event queue
                ret = self._eventq.get(False)
                # cancels the timer if I got something
                if self._idd is not None and self.root is not None:
                    self.root.after_cancel(self._idd)
                return ret
            except queue.Empty:
                # no event in the queue, continue waiting (go to mainloop)
                pass
            ####################
            if not self.root:
                raise InterruptedError("window killed")
            # back to the mainloop, it will stop if I get an event
            self.root.mainloop()
            ####################

    def mouse_position(self):
        """Return the mouse position (pixel-wise).

        Note:
            The returned coordinates can be negative or greater than the window
                size, when the mouse is out of the window but the window still
                selected.

        Args:
            None

        Returns:
            a couple ([int, int]): pixel-wise position (line, column) relative
        to (0,0) = top-left position in the window.
        """
        # private: the mouse moved
        def _motion(event):
            self._souris = (event.y, event.x)

        if not self.root:
            raise InterruptedError("window killed")
        self.refresh()
        try:
            return self._souris
        except AttributeError:
            self.bind("<Motion>", _motion)
            return self._souris

Ancestors

  • tkinter.Canvas
  • tkinter.Widget
  • tkinter.BaseWidget
  • tkinter.Misc
  • tkinter.Pack
  • tkinter.Place
  • tkinter.Grid
  • tkinter.XView
  • tkinter.YView

Class variables

var DEFAULT_COLOR

A list of ten colors used for ten players identified by numbers.

Methods

def bg(self, obj, before=1, refresh=True)

Send a graphical object (ID) to the background.

Args

obj : int
object ID returned by an object creation method
before : int, optional
the object behind which to hide (default: all)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

None

Expand source code
def bg(
    self, obj, before=1, refresh=True
):
    """Send a graphical object (ID) to the background.

    Args:
        obj (int): object ID returned by an object creation method
        before (int, optional): the object behind which to hide (default:
            all)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")

    self.tag_lower(obj, before)
    if refresh:
        self.update()
def close(self)

Close this window.

Args

None

Returns

None

Expand source code
def close(self):
    """Close this window.

    Args:
        None

    Returns:
        None
    """
    if self._after_id is not None:
        self.root.after_cancel(self._after_id)
        self._after_id = None
    if self.root is not None:
        self.root.destroy()
        self.root = None
def draw_circle(self, x_1, x_2, color='black', border=1, refresh=True)

Draw a circle in the bounding box of x_1=(l1,c1) and x_2 (excluded).

Args

x_1 : [int, int]
pixel-wise positions (line, column) of the first point. (0,0) = top-left position.
x_2 : [int, int]
pixel-wise positions (line, column) of the second point, excluded.
color : str, optional
color of the inside (default: "black"). If color == None, don't fill the circle.
border : int, optional
border thickness (default: 1) - borders can overflow over the given pixels coordinates
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

int
the ID of the graphical object that was created.
Expand source code
def draw_circle(
    self, x_1, x_2,
    color="black", border=1, refresh=True
):
    """Draw a circle in the bounding box of x_1=(l1,c1) and x_2 (excluded).

    Args:
        x_1 ([int, int]): pixel-wise positions (line, column) of the first
            point. (0,0) = top-left position.
        x_2 ([int, int]): pixel-wise positions (line, column) of the second
            point, excluded.
        color (str, optional): color of the inside (default: "black").
            If color == None, don't fill the circle.
        border (int, optional): border thickness (default: 1) - borders can
            overflow over the given pixels coordinates
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        int: the ID of the graphical object that was created.
    """
    if not self.root:
        raise InterruptedError("window killed")

    obj = self.create_oval(x_1[1]+1, x_1[0]+1, x_2[1], x_2[0],
                           width=border, fill=color)
    if refresh:
        self.update()
    return obj
def draw_grid(self, matrix=None, grid=True)

Draw a complete grid of pieces, using the 10 default colors.

Args

matrix : list of list of int
the players pieces matrix, of the specified size. If matrix is None (default), just ignores it.
grid : boolean
if True (default) draws a horizontal+vertical grid to separate the tiles.

Returns

list of int
the graphical objects that were created for the pieces (grid excluded).
Expand source code
def draw_grid(
    self, matrix=None, grid=True
):
    """Draw a complete grid of pieces, using the 10 default colors.

    Args:
        matrix (list of list of int): the players pieces matrix, of the
            specified size. If matrix is None (default), just ignores it.
        grid (boolean): if True (default) draws a horizontal+vertical grid
            to separate the tiles.

    Returns:
        list of int: the graphical objects that were created for the pieces
            (grid excluded).
    """
    if not self.root:
        raise InterruptedError("window killed")
    # erase everything for a start
    self.delete(tk.ALL)
    # gap is used by draw_tile to fill the inside of a tile (including
    # borders, or not
    if grid:
        for i in range(self.size[0]+self._gap):
            self.create_line(1, i*self.pixels+1,
                             self.size[1]*self.pixels+1, i*self.pixels+1,
                             width=1)
        for i in range(self.size[1]+self._gap):
            self.create_line(i*self.pixels+1, 1,
                             i*self.pixels+1,
                             self.size[0]*self.pixels+1+self._gap,
                             width=1)
    # draw the pieces
    lobj = []
    if matrix is not None:
        for i in range(self.size[0]):
            for j in range(self.size[1]):
                if matrix[i][j] is not None:
                    lobj.append(self.draw_piece((i, j), matrix[i][j],
                                refresh=False))
    # update just once at the end, for performance
    self.update()
    return lobj
def draw_line(self, x_1, x_2, color='black', thickness=1, refresh=True)

Draw a line between x_1=(l1, c1) and x_2=(l2, c2) (excluded).

Args

x_1 : [int, int]
pixel-wise positions (line, column) of the first point. (0,0) = top-left position.
x_2 : [int, int]
pixel-wise positions (line, column) of the second point, excluded.
color : str, optional
line color (default: "black")
thickness : int, optional
thickness of the line (default: 1)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

int
the ID of the graphical object that was created.
Expand source code
def draw_line(
    self, x_1, x_2, color="black", thickness=1, refresh=True
):
    """Draw a line between x_1=(l1, c1) and x_2=(l2, c2) (excluded).

    Args:
        x_1 ([int, int]): pixel-wise positions (line, column) of the first
            point. (0,0) = top-left position.
        x_2 ([int, int]): pixel-wise positions (line, column) of the second
            point, excluded.
        color (str, optional): line color (default: "black")
        thickness (int, optional): thickness of the line (default: 1)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        int: the ID of the graphical object that was created.
    """
    if not self.root:
        raise InterruptedError("window killed")

    obj = self.create_line(x_1[1]+1, x_1[0]+1, x_2[1], x_2[0],
                           width=thickness, fill=color)
    if refresh:
        self.update()
    return obj
def draw_piece(self, pos, player=0, color=None, refresh=True)

Draw a piece in position pos=(line, column), player-colored.

Usage notice: don't use this function if your tiles are too small, you won't see the piece.

Args

pos : [int, int]
a couple (line, column) specifying the grid position. (0, 0) = top-left position.
player : int, optional
player number (default: 0) player is used only if color is NOT given.
color : str, optional
fill color of the piece. If a color is given, player is ignored.
refresh : bool
refresh the window after drawing (default: True)

Returns

int
the ID of the graphical object (circle) that was created.
Expand source code
def draw_piece(
    self, pos, player=0,
    color=None,
    refresh=True
):
    """Draw a piece in position pos=(line, column), player-colored.

    Usage notice: don't use this function if your tiles are too small, you
    won't see the piece.

    Args:
        pos ([int, int]): a couple (line, column) specifying the grid
            position. (0, 0) = top-left position.
        player (int, optional): player number (default: 0)
            player is used only if color is NOT given.
        color (str, optional): fill color of the piece. If a color is
            given, player is ignored.
        refresh (bool): refresh the window after drawing (default: True)

    Returns:
        int: the ID of the graphical object (circle) that was created.
    """
    if not self.root:
        raise InterruptedError("window killed")
    if color is None:
        if isinstance(player, int):
            color = self.DEFAULT_COLOR[player % len(self.DEFAULT_COLOR)]
        else:
            color = self.DEFAULT_COLOR[0]

    bord = self.pixels//10+1
    i, j = pos
    if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
        raise ValueError("trying to draw outside the window!")

    obj = self.create_oval(j*self.pixels+bord+1,
                           i*self.pixels+bord+1,
                           (j+1)*self.pixels-bord+1,
                           (i+1)*self.pixels-bord+1,
                           width=1, fill=color)
    if refresh:
        self.update()
    return obj
def draw_text(self, position, text, color='black', fontname='Purisa', fontsize=11, refresh=True)

Draw a text centered at a given position=(line, column).

Args

position : [int, int]
pixel-wise position (line, column). (0,0) = top-left position.
text : str
text to print
color : str, optional
text color (default: "black")
fontname : str, optional
font name (default: "Purisa")
fontsize : int, optional
font size (default: 11pt)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

int
the ID of the graphical object that was created.
Expand source code
def draw_text(
    self, position, text,
    color="black", fontname="Purisa", fontsize=11, refresh=True
):
    """Draw a text centered at a given position=(line, column).

    Args:
        position ([int, int]): pixel-wise position (line, column).
            (0,0) = top-left position.
        text (str): text to print
        color (str, optional): text color (default: "black")
        fontname (str, optional): font name (default: "Purisa")
        fontsize (int, optional): font size (default: 11pt)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        int: the ID of the graphical object that was created.
    """
    if not self.root:
        raise InterruptedError("window killed")

    obj = self.create_text(position[1]+1, position[0]+1,
                           text=text,
                           font=(fontname, fontsize),
                           fill=color)
    if refresh:
        self.update()
    return obj
def draw_tile(self, pos, color='black', border=0, refresh=True)

Fill a tile in position pos=(line, column) with a color.

Args

pos : [int, int]
grid position (line, column)
color : str, optional
color of the tile (default: "black")
border : int, optional
border thickness (default: 0) - borders may overlap over neighboring tiles
refresh : bool
refresh the window after drawing (default: True)

Returns

int
the ID of the colored tile
Expand source code
def draw_tile(
    self, pos,
    color="black",
    border=0,
    refresh=True
):
    """Fill a tile in position pos=(line, column) with a color.

    Args:
        pos ([int, int]): grid position (line, column)
        color (str, optional): color of the tile (default: "black")
        border (int, optional): border thickness (default: 0) - borders may
            overlap over neighboring tiles
        refresh (bool): refresh the window after drawing (default: True)

    Returns:
        int: the ID of the colored tile
    """
    if not self.root:
        raise InterruptedError("window killed")

    i, j = pos
    if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
        raise ValueError("trying to fill a tile outside the window!")

    obj = self.create_rectangle(j*self.pixels+1+self._gap,
                                i*self.pixels+1+self._gap,
                                (j+1)*self.pixels+1,
                                (i+1)*self.pixels+1,
                                width=border, fill=color)
    if refresh:
        self.update()
    return obj
def erase(self)

Erase everything from the window.

All graphical objects that were created are deleted.

Args

None

Returns

None

Expand source code
def erase(
    self
):
    """Erase everything from the window.

    All graphical objects that were created are deleted.

    Args:
        None

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")
    self.delete(tk.ALL)
    # redraw the grid if it was there:
    if self._gap == 1:
        self.draw_grid()
def fg(self, obj, after=None, refresh=True)

Send a graphical object (ID) to the foreground.

Args

obj : int
object ID returned by an object creation method
after : int, optional
the object after which to show up (default: all)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

None

Expand source code
def fg(
    self, obj, after=None, refresh=True
):
    """Send a graphical object (ID) to the foreground.

    Args:
        obj (int): object ID returned by an object creation method
        after (int, optional): the object after which to show up (default:
            all)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")

    if after:
        self.tag_raise(obj, after)
    else:
        self.tag_raise(obj)

    if refresh:
        self.update()
def message(self, message)

Display a message in a box and wait for the user to click somewhere.

Args

message : str
the string to print

Returns

bool
True if the user triggered a normal event, False if the user closed the window abruptly.
Expand source code
def message(self, message):
    """Display a message in a box and wait for the user to click somewhere.

    Args:
        message (str): the string to print

    Returns:
        bool: True if the user triggered a normal event, False if the user
            closed the window abruptly.
    """
    # private: the user clicked in the box
    # pylint: disable=unused-argument
    # it's an event, don't care which one.
    def _c(event):
        self._eventq.put("ok")
        self.root.quit()

    if not self.root:
        raise InterruptedError("window killed")
    msg = tk.Message(
            self.root, text=message,
            padx=20, pady=20,
            relief=tk.RAISED, borderwidth=5,
            )
    # mouse click:
    msg.bind("<Button-1>", _c)
    # m.grid(row=0, column=0)
    msg.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
    while True:
        evt = self.wait_event()
        if evt[0] == "END":
            # the window has been closed
            # put the end msg back in the queue and return False
            self._eventq.put(evt)
            return False
        # all the other events just close the message box and return True:
        msg.destroy()
        return True
def mouse_position(self)

Return the mouse position (pixel-wise).

Note

The returned coordinates can be negative or greater than the window size, when the mouse is out of the window but the window still selected.

Args

None

Returns

a couple ([int, int]): pixel-wise position (line, column) relative to (0,0) = top-left position in the window.

Expand source code
def mouse_position(self):
    """Return the mouse position (pixel-wise).

    Note:
        The returned coordinates can be negative or greater than the window
            size, when the mouse is out of the window but the window still
            selected.

    Args:
        None

    Returns:
        a couple ([int, int]): pixel-wise position (line, column) relative
    to (0,0) = top-left position in the window.
    """
    # private: the mouse moved
    def _motion(event):
        self._souris = (event.y, event.x)

    if not self.root:
        raise InterruptedError("window killed")
    self.refresh()
    try:
        return self._souris
    except AttributeError:
        self.bind("<Motion>", _motion)
        return self._souris
def move_piece(self, obj, pos, refresh=True)

Move a piece (obj ID) in grid position pos=(line, column).

Args

obj : int
a previously created piece ID
pos : [int, int]
new grid position, a couple (line, column)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

None

Expand source code
def move_piece(
    self, obj, pos, refresh=True
):
    """Move a piece (obj ID) in grid position pos=(line, column).

    Args:
        obj (int): a previously created piece ID
        pos ([int, int]): new grid position, a couple (line, column)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")

    i, j = pos
    if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
        raise ValueError("trying to move a piece outside the window!")
    bord = self.pixels//10+1
    self.coords(
        obj,
        j*self.pixels+bord+1,
        i*self.pixels+bord+1,
        (j+1)*self.pixels-bord+1,
        (i+1)*self.pixels-bord+1
        )
    if refresh:
        self.update()
def move_tile(self, obj, pos, refresh=True)

Move a colored tile to another grid position.

Args

obj : int
a previously created tile ID
pos : [int, int]
new position in the grid (line, column)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

None

Expand source code
def move_tile(
    self, obj, pos, refresh=True
):
    """Move a colored tile to another grid position.

    Args:
        obj (int): a previously created tile ID
        pos ([int, int]): new position in the grid (line, column)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")

    i, j = pos
    if not (0 <= i < self.size[0] and 0 <= j < self.size[1]):
        raise ValueError("trying to move a tile outside the window!")
    self.coords(
        obj,
        j*self.pixels+1++self._gap,
        i*self.pixels+1++self._gap,
        (j+1)*self.pixels+1,
        (i+1)*self.pixels+1
        )
    if refresh:
        self.update()
def refresh(self)

Refresh all objects previously drawn in the window.

Note

You can create several successive objects without refreshing by passing the extra argument "refresh=False" to the drawing functions, and then refresh only once using this function. Good for speed, especially if you draw many things.

Args

None

Returns

None

Expand source code
def refresh(
    self
):
    """Refresh all objects previously drawn in the window.

    Note:
        You can create several successive objects without refreshing by
            passing the extra argument "refresh=False" to the drawing
            functions, and then refresh only once using this function.
            Good for speed, especially if you draw many things.

    Args:
        None

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")

    self.update()
def rm(self, obj, refresh=True)

Delete a graphical object.

Args

obj : int
an object ID (returned by an object creation method)
refresh : bool, optional
refresh the window after drawing (default: True)

Returns

None

Expand source code
def rm(
    self, obj, refresh=True
):
    """Delete a graphical object.

    Args:
        obj (int): an object ID (returned by an object creation method)
        refresh (bool, optional): refresh the window after drawing
            (default: True)

    Returns:
        None
    """
    if not self.root:
        raise InterruptedError("window killed")

    self.delete(obj)
    if refresh:
        self.update()
def wait_event(self, delay=None)

Wait for the user to interact with the window.

The tracked events are only keypress and mouse click (left button).

Args

delay : int, optional
waiting time in ms (default: wait forever)

Returns

If the delay expires (when given)

  • None

Else, returns a couple, the first element being always a string:

  • ("click", (line, column))

    if the user clicks on a tile of the board, where (line, column) are the integer coordinates of the clicked tile

  • ("key", letter)

    if the user hits a key, where letter is a string containing a description of the key. If you're looking for a specific key, you can give a try to the demo - running 'python3 -m tkdraw' will print all occuring events in the console

  • ("END", None)

    if the user closes the window

Expand source code
def wait_event(self, delay=None):
    """Wait for the user to interact with the window.

    The tracked events are only keypress and mouse click (left button).

    Args:
        delay (int, optional): waiting time in ms (default: wait forever)

    Returns:
        If the delay expires (when given)

        - `None`

        Else, returns a couple, the first element being always a string:

        - `("click", (line, column))`

            if the user clicks on a tile of the board, where (line, column)
                are the integer coordinates of the clicked tile

        - `("key", letter)`

            if the user hits a key, where letter is a string containing a
                description of the key. If you're looking for a specific
                key, you can give a try to the demo - running
                `'python3 -m tkdraw'` will print all occuring events in the
                console

        - `("END", None)`

            if the user closes the window
    """
    # private: called when the timer expires
    def _delay_expire():
        self._eventq.put(None)
        self._idd = None
        self.root.quit()

    # trigger the timer
    self._idd = None
    if delay is not None:
        if not self.root:
            raise InterruptedError("window killed")
        self._idd = self.root.after(delay, _delay_expire)
    # This is Tk's main loop
    # checks the events and get out if something happens
    while True:
        ####################
        try:
            # get something from the event queue
            ret = self._eventq.get(False)
            # cancels the timer if I got something
            if self._idd is not None and self.root is not None:
                self.root.after_cancel(self._idd)
            return ret
        except queue.Empty:
            # no event in the queue, continue waiting (go to mainloop)
            pass
        ####################
        if not self.root:
            raise InterruptedError("window killed")
        # back to the mainloop, it will stop if I get an event
        self.root.mainloop()
        ####################