Position von Buttons im Grid

Fragen zu Tkinter.
Antworten
Caskuda
User
Beiträge: 26
Registriert: Sonntag 15. März 2020, 22:09

Servus,

ich möchte ein 2dimensionales Feld variabler Größe von Buttons erzeugen und beim Klick auf einen der Buttons die aktuelle Position
zurückgegeben bekommen.

Meine Versuche führen auf den Holzweg:
Dies wäre entweder der Index [row][col] der verschachtelten Liste self.buttons oder wie beim Beispiel self.placeholder per grid_info()
der Zugriff auf row und column des tkinter - Gridmanagers.
Wie der aktuelle Versuch per Zählvariablen row, col zeigt, sind diese bereits bei 2, 2, wenn die lambda-Funktionen aufgerufen werden.


Es hapert leider an der Idee, wie ich die Referenzen auf die Buttons an die jeweilige Lambda-Funktion binde.


Der Button self.placeholder zeigt, wie man per grid_info() an die Position kommen könnte.


Code: Alles auswählen

import tkinter as tk
from copy import copy

def main():
    app = ButtonGrid()
    app.mainloop()


class ButtonGrid(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__()
        self.rows = kwargs.get('rows', 3)
        self.cols = kwargs.get('cols', 3)

        self.buttons = [
            [
                tk.Button(
                    self,
                    text = '-',
                    padx=5,
                    pady=5,
                    command=lambda: self.show_position2(row, col)
                    #command=lambda:self.show_pos(self.buttons[row][col])
                )
                for col in range(self.cols)
            ]
            for row in range(self.rows)
        ]
        for row, row_of_btns in enumerate(self.buttons):
            for col, btn in enumerate(row_of_btns):
                btn.grid(row=row, column=col)


        self.placeholder = tk.Button(
            self,
            text="Placeholder",
            command=lambda: self.show_pos(self.placeholder)
        )
        self.placeholder.grid(row=3, column=0, columnspan=3)


    def show_pos(self, btn):
        print(btn.grid_info().get('row'), btn.grid_info().get('column'))


    def show_position2(self, row, col):
        print(row, col)


if __name__ == '__main__':
    main()



Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

copy wird importiert aber nicht benutzt. Was soll das denn mit den kwargs? Wenn row und col normale Argumente wären, wäre der Code nicht nur kürzer, sondern auch verständlicher. Wenn du erst mit einer komplizierten Listcomprehension die Buttons erzeugt, um dann gleich mit for-Schleifen mit den Elementen weiterarbeitest, wäre es einfacher, die Buttons gleich innerhalb der for-Schleife zu erzeugen. lamdas sind kein eigener Namensraum, daher hat row und col den Wert am Ende der Funktion. Deshalb sollte man immer functools.partial benutzen, wenn möglich.
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Caskuda: Dafür wurde `functools.partial()` erfunden.

`copy` wird importiert aber nirgends verwendet. Ist auch eher ungewöhnlich das für irgend etwas zu verwenden.

Warum verschleierst Du bei der `__init__()` die Argumente?

`row` und `col` in der „list comprehension“ werden nicht verwendet, dafür werden die gleichen Werte dann in den beiden folgenden verschachtelten Schleifen per `enumerate()` noch mal erzeugt. Wenn man sowieso zwei verschachtelte Schleifen hat, kann man auch dort alles erledigen.

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial


class ButtonGrid(tk.Tk):
    def __init__(self, row_count=3, column_count=3):
        tk.Tk.__init__(self)
        self.buttons = []
        for row_index in range(row_count):
            row = []
            for column_index in range(column_count):
                button = tk.Button(
                    self,
                    text="-",
                    padx=5,
                    pady=5,
                    command=partial(print, row_index, column_index),
                )
                button.grid(row=row_index, column=column_index)
                row.append(button)
            self.buttons.append(row)

    @property
    def row_count(self):
        return len(self.buttons)

    @property
    def column_count(self):
        return len(self.buttons[0])


def main():
    app = ButtonGrid()
    app.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Caskuda
User
Beiträge: 26
Registriert: Sonntag 15. März 2020, 22:09

@__blackjack__ Ich hatte es zwischenzeitlich mit einem dummen Ansatz row=copy(row) versucht. Allerdings führte das ebenfalls zum Holzweg.

Danke für den lieben Hinweis auf functools -> partials. Werde mich gleich einlesen.

Lieben Dank auch für den zweiten - von dir gar nicht erwähnten - Verbesserungsvorschlag: @property
Das muss ich mir an die Tür pinnen, bis es in Fleisch und Blut übergeht.
Gerade hinsichtlich sich verändernden Größen erspart das ja wesentlichen den Verwaltungsaufwand meiner sinnfreien Variablen: self.rows , self.cols.
Caskuda
User
Beiträge: 26
Registriert: Sonntag 15. März 2020, 22:09

@Sirius: Auch für Deine Antwort vielen Dank. Habe sie gestern mit müden Augen erst nach dem Antworten gesehen.

Bevor ich die Button-Grids für andere Spielereien missbrauche, habe ich bestimmt das tausendste TicTacToe hier
im Forum unten angefügt:

Code: Alles auswählen

import tkinter as tk
from tkinter import *
from functools import partial

class TicTacToe:
    def __init__(self):
        self.current_player = 1
        self.turn = 1
        self.grid = [['_' for _ in range(3)] for _ in range(3)]

    @property
    def row_count(self):
        return len(self.grid)

    @property
    def col_count(self):
        return len(self.grid[0])

    def place(self, y, x):
        if not 0 <= x < self.col_count:
            print('x position is outside of the board! choose again.')
            return -1
        if not 0 <= y < self.row_count:
            print('y position is outside of the board. choose again.')
            return -1
        if self.grid[y][x] != '_':
            print('position is already occupied! choose again.')
            return -1
        self.grid[y][x] = str(self.current_player)

    def show(self):
        print()
        for row in self.grid:
            print(*row)
        print()

    def next_player(self):
        self.current_player %=2
        self.current_player += 1

    def check(self):
        """
        Erinnerung an mich: sau hässlich! je nach Position reichen
        2-4 checks.
        Bei Vier-Gewinnt oder 5-in-einer-Reihe dringend überarbeiten!
        """
        to_check = set()
        for row in self.grid:
            to_check.add(''.join(row))
        for column in zip(*self.grid):
            to_check.add(''.join(column))
        to_check.add(''.join([
            self.grid[0][0],
            self.grid[1][1],
            self.grid[2][2]
        ]))
        to_check.add(''.join([
            self.grid[0][2],
            self.grid[1][1],
            self.grid[2][0]
        ]))
        if '111' in to_check:
            return 'player1'
        if '222' in to_check:
            return 'player2'

    def play_game(self):
        self.turn = 1
        while self.turn < 10:
            print('Board:')
            self.show()
            print(f'its the turn of player {self.current_player}')
            row_index = self.get_input("please select a row (0, 1, 2): ")
            column_index = self.get_input("please select a column (0, 1, 2): ")
            flag = self.place(row_index, column_index)
            if flag != -1:
                winner = self.check()
                if winner is not None:
                    print(f"The winner is: {winner}")
                    self.show()
                    break
                self.next_player()
                self.turn += 1

        print('there is no winner!')
        self.show()

    def get_input(self, text):
        while True:
            try:
                return int(input(text))
            except ValueError:
                print('Wrong value!')
                continue

    def reset(self):
        self.current_player = 1
        self.turn = 1
        self.grid = [['_' for _ in range(3)] for _ in range(3)]


class TicTacToeGui(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('80x150')

        self.tictactoe = TicTacToe()
        self.player = tk.Label(
            self,
            text=f'Player {str(self.current_player)}'
        )
        self.player.pack()


        self.turn = tk.Label(
            self,
            text=f"Turn {self._turn}"
        )
        self.turn.pack()
        self.game = GameFrame(self)
        self.game.pack()

    @property
    def current_player(self):
        return self.tictactoe.current_player

    @property
    def _turn(self):
        return self.tictactoe.turn

    def place(self, row_index, column_index):
        flag = self.tictactoe.place(row_index, column_index)
        if flag == -1:
            return flag
        state = self.tictactoe.check()
        self.tictactoe.turn += 1
        if state is not None:
            self.reset(state)
            return -1
        elif self.tictactoe.turn == 10:
            self.reset(state)
            return -1
        else:
            self.tictactoe.next_player()
            self.player.config(text=f'Player {str(self.current_player)}')
            self.turn.config(text=f'Turn {self._turn}')

    def reset(self, state):
        if state is not None:
            text = f"The winner is {state}"
        else:
            text = "there is no winner"
        Result(self, text)
        self.tictactoe = TicTacToe()
        self.player.config(text=f'Player {str(self.current_player)}')
        self.turn.config(text=f'Turn {self._turn}')
        self.game.reset()


class Result(tk.Toplevel):
    def __init__(self, master, text):
        super().__init__()
        self.master = master
        self.winner = tk.Label(
            self,
            text=text
        )
        self.winner.pack()
        self.ok = tk.Button(
            self,
            text='ok',
            command=self.destroy
        )
        self.ok.pack()


class GameFrame(tk.Frame):
    def __init__(self, master, row_count=3, column_count=3):
        super().__init__()
        self.buttons = list()
        for row_index in range(row_count):
            row = list()
            for column_index in range(column_count):
                button = tk.Button(
                    self,
                    fg='black',
                    text='-',
                    padx=5,
                    pady=5,
                    state=tk.NORMAL,
                    command=partial(self.place, row_index, column_index)
                )
                button.grid(row=row_index, column=column_index)
                row.append(button)
            self.buttons.append(row)

    @property
    def row_count(self):
        return len(self.buttons)

    @property
    def col_count(self):
        return len(self.buttons[0])

    def place(self, row_index, column_index):
        if self.master.current_player==2:
            color='GREEN'
        else:
            color='RED'
        flag = self.master.place(row_index, column_index)
        if flag is None:
            self.buttons[row_index][column_index].config(
                highlightbackground=color,
                state=tk.DISABLED
            )

    def reset(self):
        for row_index in range(self.row_count):
            for column_index in range(self.col_count):
                self.buttons[row_index][column_index].config(
                    highlightbackground="WHITE",
                    state=tk.NORMAL
                )

def main():
    app = TicTacToeGui()
    app.mainloop()


if __name__ == '__main__':
    main()



Wenn ihr Antipattern seht - und ich vermute, ich werde einige haben - , reißt mir bitte den Kopf ab.
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Caskuda: Warum ist denn der Sternchen-Import wieder da? Wird der für irgendetwas tatsächlich verwendet?

Der Inhalt von `TicTacToe.__init__()` und `TicTacToe.reset()` ist identisch. Der sollte nicht zweimal im Quelltext stehen. Methoden die einen Objektzustand wieder in den Ausgangszustand versetzen sind ein potentielle Fehlerquelle wenn die nicht auch als einziges in der `__init__()` aufgerufen werden, weil man bei Veränderungen wie der Zustand verwaltet wird, auch immer daran denken muss die `reset()`-Methode entsprechend anzupassen. Es ist in der Regel einfacher und sicherer einfach ein sauberes, neues Objekt zu erstellen, statt ein vorhandenes wieder zurückzusetzen. Das machst Du ja anscheinend sogar, denn die `TicTacToe.reset()`-Methode wird offenbar nirgends aufgerufen.

Statt der `TicTacToe.show()`-Methode oder zumindest einen Teil davon, würde ich persönlich in eine `__str__()`-Methode verschieben. Denn hier geht es ja um die Zeichenkettendarstellung eines `TicTacToe`-Objekts. Ist natürlich Geschmackssache.

`get_input()` ist keine Methode sondern einfach nur eine Funktion die in die Klasse gesteckt wurde. Das ``continue`` in der Funktion ist sinnlos. Ohne würde genau der gleiche Programmfluss entstehen.

Schräge APIs: `TicTacToe.place()` gibt entweder -1 oder implizit `None` zurück, und `TicTacToe.check()` gibt "player1", "player2", oder implizit `None` zurück. Das sollte nicht sein. Also erst einmal das entweder explizit etwas zurückgegeben wird oder implizit `None` — das sollte alles immer explizit sein, auch ein `None` wenn das ein gültiger Rückgabewert sein kann. Aber es sollte auch keine komischen besonderen Rückgabewerte wie -1 geben. Genau dafür wurden Ausnahmen erfunden.

Bei `TicTacToe.place()` sollte auch keine Benutzerinteraktion erfolgen, also kein `print()`. In einer GUI-Anwendung bekäme das dann ja auch niemand mit, beziehungsweise nur auf einer Konsole wenn es von einer gestartet wurde.

In der `TicTacToeGui.place()` macht ein Rückgabewert gar keinen Sinn, weil die GUI-Hauptschleife den komplett ignoriert.

Ich würde `x` und `y` in `TicTacToe.place()` auch in `row_index` und `column_index` umbenennen, oder die Reihenfolge der Argumente umkehren, denn `y`, `x` ist eine unerwartete Reihenfolge für den Leser.

In die Methode gehört IMHO auch das hochzählen der Spielzugnummer, statt das irgendwo von ausserhalb zu machen und einen inkonsistenten Zustand zu riskieren. Und auch das weitersetzen des aktuellen Spielers kann man dort unterbringen.

Die Spielzugnummer in `TicTacToe.play_game()` am Anfang auf 0 zu setzen ist ein Fehler. Das funktioniert nur wenn das Spielfeld leer ist. Ansonsten kann das ``while`` zu einer Endlosschleife werden.

Ein weiterer Fehler ist die Ausgabe das es keinen Gewinner gibt nach der ``while``-Schleife, denn das wird ja *immer* ausgegeben. Auch dann wenn es einen Gewinner gab und die Schleife mit ``break`` verlassen wurde. Genau für solche Fälle gibt es ``else`` für Schleifen.

`Result` ist eine einfache Funktion als Klasse geschrieben. Das muss man sich auch nicht unbedingt selbst schreiben, `tkinter.messagebox` hat da schon Funktionen die man nutzen könnte.

`GameFrame.__init__()` macht gar nichts mit dem `master`-Argument. Das sollte an `Frame.__init__()` weitergeleitet werden, sonst wird da automagisch das Hauptfenster für genommen. Das ist hier zufällig richtig, aber muss es ja nicht.

Selbst sollte man das nicht verwenden, denn `GameFrame` sollte möglichst nichts über das übergeordnete Widget wissen und da auch keine Methoden über `self.master` aufrufen. Das Objekt ist für die Darstellung des `TicTacToe`-Objekts verantwortlich, sollte darauf dann auch direkt Zugriff haben in dem es übergeben wird. Von dem Objekt sollten dann auch Zeilen- und Spaltenanzahl abgefragt werden, statt fest von 3 auszugehen.

Anstelle einer `GameFrame.reset()`-Methode würde ich eine `update_display()`-Methode schreiben, die das Spielobjekt benutzt um die Schaltflächen zu konfigurieren.

Die Attribute bei `TicTacToeGui` sind irreführend benannt, also mindestens `turn` und `_turn`. Das eine steht für ein `Label`, das andere für eine Zahl.

Auch bei dieser Klasse würde eine `update_display()`-Methode Sinn machen um die Wiederholungen aus dem Code zu bekommen.

Die `reset()`-Methode wäre besser eine `start_new_game()`-Methode ohne die Verkündigung eines Gewinners, dann könnte man die nämlich auch ganz am Anfang einmal aufrufen und kann damit gleichen Code aus der `__init__()` entfernen.

Die `place()`-Methode hat mit dem Platzieren nichts mehr zu tun wenn das im `GameFrame` passiert, sondern ist nur noch für das Prüfen der Endbedingung zuständig. Und da `GameFrame` nichts von der übergeordneten Klasse in der Widgethierarchie wissen sollte, löst man das über eine Rückruffunktion.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial
from tkinter.messagebox import showinfo


def get_input(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("Wrong value!")


class TicTacToe:
    def __init__(self):
        self.current_player = 1
        self.turn = 1
        self.grid = [["_" for _ in range(3)] for _ in range(3)]

    def __str__(self):
        return "\n".join(" ".join(row) for row in self.grid)

    @property
    def row_count(self):
        return len(self.grid)

    @property
    def column_count(self):
        return len(self.grid[0])

    @property
    def is_full(self):
        return self.turn > self.row_count * self.column_count

    def next_player(self):
        self.current_player = (self.current_player % 2) + 1

    def place(self, row_index, column_index):
        if not 0 <= column_index < self.column_count:
            raise ValueError("x position is outside of the board!")

        if not 0 <= row_index < self.row_count:
            raise ValueError("y position is outside of the board!")

        if self.grid[row_index][column_index] != "_":
            raise ValueError("position is already occupied!")

        self.grid[row_index][column_index] = str(self.current_player)
        self.turn += 1
        self.next_player()

    def check(self):
        """
        Erinnerung an mich: sau hässlich! je nach Position reichen
        2-4 checks.
        Bei Vier-Gewinnt oder 5-in-einer-Reihe dringend überarbeiten!
        """
        to_check = set()
        for row in self.grid:
            to_check.add("".join(row))
        for column in zip(*self.grid):
            to_check.add("".join(column))
        to_check.add(
            "".join([self.grid[0][0], self.grid[1][1], self.grid[2][2]])
        )
        to_check.add(
            "".join([self.grid[0][2], self.grid[1][1], self.grid[2][0]])
        )
        for player_number in [1, 2]:
            if str(player_number) * 3 in to_check:
                return player_number

        return None

    def play_game(self):
        while not self.is_full:
            print(f"Board:\n\n{self}\n")
            print(f"its the turn of player {self.current_player}")
            row_index = get_input("please select a row (0, 1, 2): ")
            column_index = get_input("please select a column (0, 1, 2): ")
            try:
                self.place(row_index, column_index)
            except ValueError as error:
                print(f"{error} choose again.")
            else:
                winner = self.check()
                if winner is not None:
                    print(f"The winner is: Player {winner}\n\n{self}\n")
                    break
        else:
            print(f"there is no winner!\n\n{self}\n")


class BoardUI(tk.Frame):
    def __init__(self, master, tictactoe, turn_callback):
        tk.Frame.__init__(self, master)
        self._tictactoe = None
        self.turn_callback = turn_callback

        self.buttons = list()
        for row_index in range(tictactoe.row_count):
            row = list()
            for column_index in range(tictactoe.column_count):
                button = tk.Button(
                    self,
                    fg="BLACK",
                    text="-",
                    padx=5,
                    pady=5,
                    command=partial(self.place, row_index, column_index),
                )
                button.grid(row=row_index, column=column_index)
                row.append(button)
            self.buttons.append(row)

        self.tictactoe = tictactoe

    @property
    def row_count(self):
        return len(self.buttons)

    @property
    def column_count(self):
        return len(self.buttons[0])

    @property
    def tictactoe(self):
        return self._tictactoe

    @tictactoe.setter
    def tictactoe(self, value):
        if (
            value.column_count != self.column_count
            or value.row_count != self.row_count
        ):
            raise ValueError(
                f"expected {self.column_count!r}x{self.row_count!r},"
                f" got {value.column_count!r}x{value.row_count!r}"
            )
        self._tictactoe = value
        self.update_display()

    def update_display(self):
        for grid_row, button_row in zip(self.tictactoe.grid, self.buttons):
            for cell_value, button in zip(grid_row, button_row):
                button.config(
                    **(
                        {
                            "_": {
                                "highlightbackground": "WHITE",
                                "state": tk.NORMAL,
                            },
                            "1": {
                                "highlightbackground": "RED",
                                "state": tk.DISABLED,
                            },
                            "2": {
                                "highlightbackground": "GREEN",
                                "state": tk.DISABLED,
                            },
                        }[cell_value]
                    )
                )

    def place(self, row_index, column_index):
        self.tictactoe.place(row_index, column_index)
        self.update_display()
        self.turn_callback()


class TicTacToeWindow(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.tictactoe = TicTacToe()

        self.player_label = tk.Label(self)
        self.player_label.pack()
        self.turn_label = tk.Label(self)
        self.turn_label.pack()
        self.board_ui = BoardUI(self, self.tictactoe, self.on_turn)
        self.board_ui.pack()
        self.start_new_game()

    def update_display(self):
        self.player_label["text"] = f"Player {self.tictactoe.current_player}"
        self.turn_label["text"] = f"Turn {self.tictactoe.turn}"

    def start_new_game(self):
        self.tictactoe = TicTacToe()
        self.board_ui.tictactoe = self.tictactoe
        self.update_display()

    def on_turn(self):
        winner = self.tictactoe.check()
        if winner is not None or self.tictactoe.is_full:
            showinfo(
                "Result",
                "There is no winner"
                if winner is None
                else f"The winner is player {winner}",
            )
            self.start_new_game()
        else:
            self.update_display()


def main():
    window = TicTacToeWindow()
    window.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Caskuda
User
Beiträge: 26
Registriert: Sonntag 15. März 2020, 22:09

@__blackjack__ Lieben Dank für das ausführliche Review und tut mir leid wegen späten Antwort darauf. War leider wegen privater Dinge die letzten Tage fern vom PC. Ich hoffe du bist gut ins neue Jahr gekommen.
Ich werde gleich dein Review gründlich Durchgehen und dazu Stellung nehmen sowie vermutlich alsbald mit den nächsten Fragen bohren.
Antworten