Widget in Grid verschieben klappt nicht

Fragen zu Tkinter.
Antworten
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

Hallo,

habe versucht ein Minispiel zu erstellen, bei dem zwei Toggle-Buttons, vertauscht werden, wenn sie beide gedrueckt sind. Leiter klappt das nicht. Es tritt folgender Fehler auf:

Klickt man auf Button A und dann auf Button B, dann vertauschen sie wie gewollt. Klickt man danach erneut auf Button A und Button B oder umgekehrt, dann ist der zweite Button immer reaktionslos??? Von Vertauschen auch keine Spur.

Habe schon viel probiert (mit copy falls Referenzen Problemen machen, Button.update(); Button.update_idletasks(), ...) - irgendwie komme ich nicht auf die richtige Lösung.

Habt ihr eine Idee?

Hier der Code:

Code: Alles auswählen


import logging
import ttk

tk = ttk.Tkinter


Logger = logging.getLogger("stundenplan.gui.toggle")


class ToggleError(Exception): pass


class ToggleButton(tk.Label):

    MINWIDTH = 10
    MINHEIGHT = 1

    UP = 1
    DOWN = -1

    def __init__(self, master, text=""):
        """
        :param master: {Tkinter-parent}
        :param text: {str|unicode}
        """
        Logger.debug("ToggleButton init starts with text='%s'", text)

        self.stati = {self.UP: {}, self.DOWN: {}}
        self.order = (self.UP, self.DOWN)

        tk.Label.__init__(self, master)

        self.__status__ = self.order[0]

        self.__setup__(self.UP, text, "raised")
        self.__setup__(self.DOWN, text, "sunken")

        # Hooks, die ausgefuehrt werden, wenn der Button gedrueckt wird.
        # Dabei ist der Zustand relevant.

        self.hook_up = None
        self.hook_down = None

        self.bind("<Button-1>", self.toggle)

        Logger.debug("ToggleButton init done")

    # privates

    def __setup__(self, status, text, relief):
        self.stati[status].update({"relief": relief,
                                  "text": text,
                                  "width": self.MINWIDTH,
                                  "height": self.MINHEIGHT,
                                  "fg": self.cget("fg"),
                                  "bg": self.cget("bg"),
                                  "font": self.cget("font")})

    def __run_hooks__(self, event=None):
        """Fuehrt den Hook aus, bevor der Zustand gewechselt wird, dh.
        ist der Status UP, dann wird beim Klicken der ``hook_up``
        ausgefuehrt.
        """
        Logger.debug("__run_hooks__ starts")
        if self.is_up():
            if callable(self.hook_up):
                Logger.debug("up-hook starts")
                self.hook_up(event)
        elif self.is_down():
            if callable(self.hook_down):
                Logger.debug("down-hook starts")
                self.hook_down(event)
        Logger.debug("__run_hooks__ done")

    # ui

    def reverse_order(self):
        """Vertauscht die Reihenfolge der Stati
        """
        Logger.debug("reverse_order starts")
        try:
            self.order = (self.order[1], self.order[0])
        except ToggleError:
            raise
        Logger.debug("change_order done")

    def __config__(self):
        Logger.debug("config starts")
        d = {}

        # in self.stati[x] werden die aktuellen Einstellungen
        # gehalten. Darin koennen sich ein paar mit nicht
        # gesetztem Wert befinden, die in Tkinter einen
        # Fehler ausloesen wuerden => diese werden extrahiert.

        for k, v in self.stati[self.__status__].items():
            if v:
                d.update({k: v})

        Logger.debug("real config options are %s", d)
        tk.Label.config(self, **d)
        Logger.debug("config done")

    def config(self):
        """Das Label hat nun zwei Zustaende => config nicht eindeutig!
        :return:
        """
        raise NotImplementedError()

    configure = config

    # Konfigurationen einstellen

    def config_up(self, **configs):
        """Die Einstellungen des ToggleButtons sollen nur ueber die
        beiden Methoden ``config_up`` und ``config_down`` vorgenommen
        werden.
        """
        Logger.debug("config_up starts with configs=%s", configs)
        try:
            if "hook" in configs:
                self.hook_up = configs.pop("hook")

            self.stati[self.UP].update(configs)
        except ToggleError:
            raise

        self.__config__()
        Logger.debug("config_up done")

    def config_down(self, **configs):
        """Die Einstellungen des ToggleButtons sollen nur ueber die
        beiden Methoden ``config_up`` und ``config_down`` vorgenommen
        werden.
        """
        Logger.debug("config_down starts with configs=%s", configs)
        try:
            if "hook" in configs:
                self.hook_down = configs.pop("hook")

            self.stati[self.DOWN].update(configs)
        except ToggleError:
            raise

        self.__config__()
        Logger.debug("config_down done")

    def config_all(self, **configs):
        """
        Alle gemeinsamen Optionen koennen hier in einem Schritt
        eingestellt werden.
        """
        self.config_up(**configs)
        self.config_down(**configs)

    def toggle(self, event=None):
        """Holt naechsten Status und stellt das Toggle darauf ein.
        """
        Logger.debug("toggle starts")

        self.__run_hooks__(event)

        if self.__status__ == self.order[0]:
            self.__status__ = self.order[1]
        else:
            self.__status__ = self.order[0]

        self.__config__()  # tk.Label wird angewiesen neues anzuzeigen

        Logger.debug("toggle done")

    def is_up(self):
        return self.__status__ == self.UP

    def is_down(self):
        return self.__status__ == self.DOWN


def test():

    Logger.debug("####################################")

    window = tk.Tk()
    window.title("Toggle")
    window.geometry("200x200")

    class SpielButton(ToggleButton):
        def __init__(self, master, **kwargs):
            ToggleButton.__init__(self, master, **kwargs)
            self.pos = None, None


    class Spiel(tk.Frame):
        def __init__(self, master, **kwargs):
            tk.Frame.__init__(self, master, **kwargs)

            t1 = ToggleButton(window)
            t1.config_all(text="A", height=2)
            t1.config_up(bg="#666")
            t1.config_down(bg="#888")
            t1.grid(column=0, row=0)
            t1.pos = 0, 0
            t1.bind("<Button-1>", self.add_to_list, add='+')

            t2 = ToggleButton(window)
            t2.config_all(text="B", bg="#CF11AA", fg="#CCC", height=2)
            t2.grid(column=1, row=0)
            t2.pos = 1, 0
            t2.bind("<Button-1>", self.add_to_list, add='+')

            t3 = ToggleButton(window)
            t3.config_all(text="C", bg="#00FFAA", fg="#CCC", height=2)
            t3.grid(column=0, row=1)
            t3.pos = 0, 1
            t3.bind("<Button-1>", self.add_to_list, add='+')

            t4 = ToggleButton(window)
            t4.config_all(text="D", bg="#0011AA", fg="#CCC", height=2)
            t4.grid(column=1, row=1)
            t4.pos = 1, 1
            t4.bind("<Button-1>", self.add_to_list, add='+')

            self.grid()

            self.to_move = []

        def add_to_list(self, event):
            if len(self.to_move) < 2:
                Logger.debug("liste ist %s", self.to_move)
                self.to_move.append(event.widget)

            if len(self.to_move) == 2:
                Logger.debug("liste ist %s", self.to_move)

                self.to_move[0].toggle()
                spalte0, zeile0 = self.to_move[0].pos
                #spalte0, zeile0 = copy(self.to_move[0].pos)
                #self.to_move[0].grid_forget()

                self.to_move[1].toggle()
                spalte1, zeile1 = self.to_move[1].pos
                #spalte1, zeile1 = copy(self.to_move[1].pos)
                #self.to_move[1].grid_forget()

                # neu setzen

                self.to_move[0].grid(column=spalte1, row=zeile1)
                #self.to_move[0].update_idletasks()

                self.to_move[1].grid(column=spalte0, row=zeile0)
                #self.to_move[1].update_idletasks()

                self.to_move = []

    spiel = Spiel(window)
    window.mainloop()


LOGFMT = '[%(lineno)4d,Logger=%(name)s,Module=%(module)s,Func=%(funcName)s]\n'
LOGFMT += '%(message)s'

Logger = logging.getLogger("")
logging.basicConfig(format=LOGFMT)
Logger.setLevel(10)
test()

CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
BlackJack

@sedi: Vielleicht solltest Du wenn Du die Schalflächen woanders hin verschiebst auch deren `pos`-Attribute auf die neue Position setzen, sonst ”denken” die dauerhaft sie wären an der Stelle wo sie gestartet sind. ;-)

Ich würde vor einem erneuten `grid()` auch `grid_forget()` aufrufen um sie an der bisherigen stelle aus dem Layout heraus zu nehmen. Sonst passieren eventuell komische Sachen.

Sonstiges: Namen die mit zwei führenden Unterstrichen anfangen und enden sind eigentlich ”magischen” Methoden von Python vorbehalten, dass heisst ein normaler Programmierer sollte die nicht erfinden. Manchmal machen das Rahmenwerke die mit Metaklassenmagie hantieren, wie beispielsweise SQLAlchemy beim ORM (z.B. `__tablename__`). Andere verwenden da einen führenden und folgenden Unterstricht für. Zum Beispiel `ctypes` aus der Standardbibliothek (z.B. `_fields_`). Für ”private” Attribute, wie der Kommentar in Deinem Quelltext nahelegt, verwenden man *einen* führenden Unterstrich. Also beispielsweise `_status`.

Normalwerweise reagieren Schaltflächen erst wenn man die Maustaste wieder loslässt, und auch nur wenn sich der Mauszeiger zu dem Zeitpunkt noch über der Schaltfläche befindet. Viele Benutzer erwarten das so.

`__config__()` ist ziemlich umständlich. Wenn man `dict.update()` ausschliesslich mit einem Wörterbuch aufruft das grundsätzlich nur *ein* Schlüssel/Wert-Paar enthält das ausschliesslich zu diesem Zweck *erstellt* wird, dann ist das die falsche Methode. Da ist ``d[k] = v`` deutlich passender und direkter als ``d.update({k: v})``. Wenn man `update hier sinnvoll einsetzen möchte, kann man das zum Beispiel so tun (ungetestet):

Code: Alles auswählen

        d.update((k, v) for k, v in self.stati[self._status].iteritems() if v)
Der `Frame` den das `Spiel`-Objekt darstellt wird überhaupt nicht wirklich verwendet. Kann es sein, dass Du *dort* die `ToggleButton`-Exemplare eigentlich darstellen wolltest, statt in der Klasse auf das irgendwo aussen lebende `window` zuzugreifen, was keine gute Idee ist.

Widgets sollten sich nicht selber layouten. Die wissen letztendlich ja gar nicht wo sie später wie eingebaut werden. Diese Freiheit sollte man dem Code überlassen der das Widget erstellt.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

Hallo @BlackJack, Danke für die umfangreiche Antwort...

@sedi: Vielleicht solltest Du wenn Du die Schalflächen woanders
hin verschiebst auch deren `pos`-Attribute auf die neue Position
setzen, sonst ”denken” die dauerhaft sie wären an der Stelle wo
sie gestartet sind. ;-)

Oh Mann... - kann ich ja mit der etwas fortgeschrittenen Stunde erklären :) Wohl auch dass der Spielframe gar nicht ``master`` war - tztztz

Zu ``grid()`` bzw. ``grid_forget()``:
OK - umgesetzt, zeigte aber keinen Unterschied :o

Zu den Unterstrichen:
Hieß es nicht mal, dass Attribute der Form ``_attname`` als protected und die Form ``__attname`` als private anzusehen sind?
Dass nachfolgende Unterstriche anderweitige Verwendung implizieren wusste ich nicht.

Zu ``__config__()``:
??? Das is mir nich klar...
Das Woerterbuch ``self.stati[self._status]`` hat doch aber mehrere Eintraege - wieso sprichst Du von "... grundsätzlich nur *ein* Schlüssel/Wert-Paar..."

Code: Alles auswählen

self.stati[status].update({"relief": relief,
                          "text": text,
                          "width": self.MINWIDTH,
                          "height": self.MINHEIGHT,
                          "fg": self.cget("fg"),
                          "bg": self.cget("bg"),
                          "font": self.cget("font")})
Den vorgeschlagenen Generator (so nennt man das doch oder) umzusetzen leuchtet mir dagegen ein - bringt das einen Geschwindigkeitsvorteil?


Zum Layout
??? - uups - don't check this:
Widgets sollten sich nicht selber layouten. Die wissen letztendlich ja gar nicht wo sie später wie eingebaut werden. Diese Freiheit sollte man dem Code überlassen der das Widget erstellt.
Das Widget ist doch eigentlich der Toggle - das Spiel braucht aber eine Positionsinfo zum Widget, also eine abgeleitete Klasse erstellen (``SpielButton``), welche die Info hält. Gesetzt wird das Attribut dann aber vom Hauptakteur Spielobjekt. Was ist daran falsch?


Hier der neue, überarbeitete und funktionstüchtige (!!!) Code:
Lag an der falschen Positionsinfo - denke ich...

Code: Alles auswählen

class ToggleError(Exception): pass


class ToggleButton(tk.Label):
    MINWIDTH = 10
    MINHEIGHT = 1

    UP = 1
    DOWN = -1

    def __init__(self, master, text=""):
        """
        :param master: {Tkinter-parent}
        :param text: {str|unicode}
        """
        Logger.debug("ToggleButton init starts with text='%s'", text)

        self.stati = {self.UP: {}, self.DOWN: {}}
        self.order = (self.UP, self.DOWN)

        tk.Label.__init__(self, master)

        self._status = self.order[0]

        self.__setup__(self.UP, text, "raised")
        self.__setup__(self.DOWN, text, "sunken")

        # Hooks, die ausgefuehrt werden, wenn der Button gedrueckt wird.
        # Dabei ist der Zustand relevant.

        self.hook_up = None
        self.hook_down = None

        self.bind("<Button-1>", self.toggle)

        Logger.debug("ToggleButton init done")

    # privates

    def __setup__(self, status, text, relief):
        self.stati[status].update({"relief": relief,
                                  "text": text,
                                  "width": self.MINWIDTH,
                                  "height": self.MINHEIGHT,
                                  "fg": self.cget("fg"),
                                  "bg": self.cget("bg"),
                                  "font": self.cget("font")})

    def __run_hooks__(self, event=None):
        """Fuehrt den Hook aus, bevor der Zustand gewechselt wird, dh.
        ist der Status UP, dann wird beim Klicken der ``hook_up``
        ausgefuehrt.
        """
        Logger.debug("__run_hooks__ starts")
        if self.is_up():
            if callable(self.hook_up):
                Logger.debug("up-hook starts")
                self.hook_up(event)
        elif self.is_down():
            if callable(self.hook_down):
                Logger.debug("down-hook starts")
                self.hook_down(event)
        Logger.debug("__run_hooks__ done")

    # ui

    def reverse_order(self):
        """Vertauscht die Reihenfolge der Stati
        """
        Logger.debug("reverse_order starts")
        try:
            self.order = (self.order[1], self.order[0])
        except ToggleError:
            raise
        Logger.debug("change_order done")

    def __config__(self):
        Logger.debug("config starts")

        # in self.stati[x] werden die aktuellen Einstellungen
        # gehalten. Darin koennen sich ein paar mit nicht
        # gesetztem Wert befinden, die in Tkinter einen
        # Fehler ausloesen wuerden => diese werden extrahiert.

        d = dict((k, v) for k, v in self.stati[self._status].iteritems() if v)
        Logger.debug("real config options are %s", d)

        tk.Label.config(self, **d)
        Logger.debug("config done")

    def config(self):
        """Das Label hat nun zwei Zustaende => config nicht eindeutig!
        :return:
        """
        raise NotImplementedError()

    configure = config

    # Konfigurationen einstellen

    def config_up(self, **configs):
        """Die Einstellungen des ToggleButtons sollen nur ueber die
        beiden Methoden ``config_up`` und ``config_down`` vorgenommen
        werden.
        """
        Logger.debug("config_up starts with configs=%s", configs)
        try:
            if "hook" in configs:
                self.hook_up = configs.pop("hook")

            self.stati[self.UP].update(configs)
        except ToggleError:
            raise

        self.__config__()
        Logger.debug("config_up done")

    def config_down(self, **configs):
        """Die Einstellungen des ToggleButtons sollen nur ueber die
        beiden Methoden ``config_up`` und ``config_down`` vorgenommen
        werden.
        """
        Logger.debug("config_down starts with configs=%s", configs)
        try:
            if "hook" in configs:
                self.hook_down = configs.pop("hook")

            self.stati[self.DOWN].update(configs)
        except ToggleError:
            raise

        self.__config__()
        Logger.debug("config_down done")

    def config_all(self, **configs):
        """
        Alle gemeinsamen Optionen koennen hier in einem Schritt
        eingestellt werden.
        """
        self.config_up(**configs)
        self.config_down(**configs)

    def toggle(self, event=None):
        """Holt naechsten Status und stellt das Toggle darauf ein.
        """
        Logger.debug("toggle starts")

        self.__run_hooks__(event)

        if self._status == self.order[0]:
            self._status = self.order[1]
        else:
            self._status = self.order[0]

        self.__config__()  # tk.Label wird angewiesen neues anzuzeigen

        Logger.debug("toggle done")

    def is_up(self):
        return self._status == self.UP

    def is_down(self):
        return self._status == self.DOWN


def test():
    Logger.debug("####################################")

    window = tk.Tk()
    window.title("Toggle")
    window.geometry("200x200")


    class SpielButton(ToggleButton):
        def __init__(self, master, **kwargs):
            ToggleButton.__init__(self, master, **kwargs)
            self.pos = None, None


    class Spiel(tk.Frame):
        def __init__(self, master, **kwargs):
            tk.Frame.__init__(self, master, **kwargs)

            t1 = ToggleButton(self)
            t1.config_all(text="A", height=2)
            t1.config_up(bg="#666")
            t1.config_down(bg="#888")
            t1.grid(column=0, row=0)
            t1.pos = 0, 0
            t1.bind("<Button-1>", self.move, add='+')

            t2 = ToggleButton(self)
            t2.config_all(text="B", bg="#CF11AA", fg="#CCC", height=2)
            t2.grid(column=1, row=0)
            t2.pos = 1, 0
            t2.bind("<Button-1>", self.move, add='+')

            t3 = ToggleButton(self)
            t3.config_all(text="C", bg="#00FFAA", fg="#CCC", height=2)
            t3.grid(column=0, row=1)
            t3.pos = 0, 1
            t3.bind("<Button-1>", self.move, add='+')

            t4 = ToggleButton(self)
            t4.config_all(text="D", bg="#0011AA", fg="#CCC", height=2)
            t4.grid(column=1, row=1)
            t4.pos = 1, 1
            t4.bind("<Button-1>", self.move, add='+')

            self.grid()

            self.to_move = []

        def move(self, event):
            if len(self.to_move) < 2:
                Logger.debug("liste ist %s", self.to_move)
                self.to_move.append(event.widget)

            if len(self.to_move) == 2:
                Logger.debug("liste ist %s", self.to_move)

                self.to_move[0].toggle()
                spalte0, zeile0 = self.to_move[0].pos
                #spalte0, zeile0 = copy(self.to_move[0].pos)
                #self.to_move[0].grid_forget()

                self.to_move[1].toggle()
                spalte1, zeile1 = self.to_move[1].pos
                #spalte1, zeile1 = copy(self.to_move[1].pos)
                #self.to_move[1].grid_forget()

                # neu setzen

                self.to_move[0].grid_forget()
                self.to_move[1].grid_forget()

                self.to_move[0].grid(column=spalte1, row=zeile1)
                self.to_move[0].pos = spalte1, zeile1
                #self.to_move[0].update_idletasks()

                self.to_move[1].grid(column=spalte0, row=zeile0)
                self.to_move[1].pos = spalte0, zeile0
                #self.to_move[1].update_idletasks()

                self.to_move = []

    spiel = Spiel(window)
    window.mainloop()


LOGFMT = '[%(lineno)4d,Logger=%(name)s,Module=%(module)s,Func=%(funcName)s]\n'
LOGFMT += '%(message)s'

Logger = logging.getLogger("")
logging.basicConfig(format=LOGFMT, level=10)
test()
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
BlackJack

@sedi: Ja es gibt Leute die Sachen von anderen Programmiersprachen unbedingt auf Python übertragen wollen und wenn es dort so etwas wie ``public``, ``protected``, und ``private`` gibt, wollen sie das mit aller Gewalt irgendwie 1:1 auf Python abbilden. Letztlich sind in Python alle Attribute ”public” im dem Sinne das man von aussen darauf zugreifen kann. Ausgenommen vielleicht Interna von Datentypen die als ”native”, meistens in C geschriebene, Implementierung vorliegen. Ein führender Unterstrich kennzeichnet nicht-öffentliche API. Zwei führende Unterstriche sind ein Sprachmittel um bei Mehrfachvererbung oder tiefen Vererbungshierarchien Namenskollisionen zu vermeiden. Beides wird in Python in der Praxis aber nur sehr selten gemacht, also braucht man in der Regel die beiden führenden Unterstriche auch nicht.

Zu `__config__()`: Du rufst im vorherigen Quelltext `update()` in der Schleife dort mehrfach mit einem Wörterbuch als Argument auf das grundsätzlich nur *ein* Schlüssel/Wert-Paar hat. Darum sprach ich davon. Und nicht von der `__status__()`-Methode.

Mit dem „nicht selbst layouten” ist `Spiel` gemeint. Das ruft in seiner eigenen `__init__()` die `grid()`-Methode auf sich selbst auf.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

Danke @BlackJack

wieder was gelernt - und funktionieren tut es nun auch! :)

ps. ``grid()`` aus ``Spiel.__init___`` entfernt!
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
Antworten