CharEditor Tkinter Buttons + Labels aktualisieren

Fragen zu Tkinter.
Antworten
Lofy
User
Beiträge: 6
Registriert: Donnerstag 15. Juni 2017, 10:31

Hi ihr Python Künstler,

ich bin der/die das Lofy und dies ist meiner erster Post im Forum. Ich habe vor ca. einem Monat einen Kurs bei Udemy "belegt". Na ja, und nun begonnen mein aller erstes Programm selber zu schreiben.

Als Passionierter Rollenspieler (Pen&Paper - wem es was sagt Shadowrun, Exalted, Vampire) würde ich gerne einen Charakter Editor schreiben.
Ein "einfaches" Tool welches die Daten eines Charakters speichert. So weit so schlecht, nach nun 2 Wochen Arbeit merke ich das ich an meine Grenzen komme und mir einige Dinge fehlen oder nicht einfach mal eben mit einem bissel Tutorials im Internet Googeln zu lösen sind. Auch nicht mit noch mal drüber schlafen und neu angehen. Erstaunlich bei wie vielen Dingen das hilft.

Wie kriege ich Tkinter dazu das es Werte / Buttons Aktualisiert.

Bild

was ich gerne hätte ist das wenn ich auf den 3. der Buttons neben dem z.B. Dexterity Label klicke sich Button 1,2 und 3 Schwarz färben.
Wenn sich dann noch der Wert links Dodge ändern würde wäre das auch toll. Rein rechnerisch nimmt Dodge nämlich 2 an. Nur halt eben in der Gui nicht.

Den Code kann ich Posten, lass ich aber erstmal. Beide Files haben je 200 Zeilen und ich denke für einen Erfahrenen Programmierer, ist die Augenkrebs Gefahr recht hoch.
BlackJack

@Lofy: Meine erste Frage wäre ja ob Du an dieser Darstellung sehr hängst, denn für die „Skalen“ würde sich ein `Scale`-Widget, also so eine Art Schieberegler in der Darstellung vielleicht besser eignen. Zumindest müsste man weniger selber programmieren. Und bei den „Skills“ wären die Schaltflächen vor den Namen der Fertigkeiten vom UI-Standpunkt aus gesehen eigentlich `Checkbutton`\s. Vielleicht sind es ja sogar welche, denn die kann man auch als Schaltfläche statt Kästchen zum ankreuzen darstellen lassen.

Ansonsten wären die fünf Buttons in einer eigenen Klasse zusammengefasst, jeder würde die Zahl kennen für die er steht, und wenn man einen drückt, wird dessen Zahl als akueller Wert intern gespeichert (oder vieleicht in einer `IntVar`) und dann die Darstellung entsprechend aktualisiert in dem die Schaltflächen bis zu der Zahl schwarz und die anderenn grau gefärbt werden. Und das Ding hätte eine Möglichkeit eine Rückruffunktion zu registrieren, damit die Geschäftslogik über die Änderung informiert werden kann, und dann zum Beispiel den Dodge-Wert neu berechnen kann.
Lofy
User
Beiträge: 6
Registriert: Donnerstag 15. Juni 2017, 10:31

Am liebsten hätte ich die Buttons Rund :D

und ja es sind eigentlich Checkboxen die müssten dann aber auch nicht wie Checkboxen aussehen weil das ist - bäh.

Deswegen habe ich die Buttons genommen um sie eventuell später durch Bilder auszutauschen.

Im Moment habe ich die Frames als Klassen und die Buttons quasi nur als Eigenschaften dieser.

[codebox=python file=Unbenannt.txt]
class FrameDefinition:
def __init__(self, master, dictionary):
self.row = dictionary[ "row" ]
self.col = dictionary[ "col" ]
self.sticky = dictionary[ "sticky" ]
self.text = dictionary[ "text" ]
self.frame = LabelFrame(master, text=self.text)
self.frame.grid(row=self.row, column=self.col, sticky=self.sticky)
self.widgets = {}

def return_frame(self):
#no one cares

def registerentry(self, text, row, col, sticky, number):
# no one cares

def register_skillbutton(self, text="", row=0, col=0, sticky="w", skill="", favorite=False, value=0):
if favorite:
color = "black"
else:
color = "grey"
widgetsign = str(skill)
widgetsign = widgetsign + "F"
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
command=lambda: chardata.change_favoriteskill(skill))
self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)
col += 1
widgetsign = str(skill) + "L"
self.widgets[ widgetsign ] = Label(self.frame, text=skill).grid(row=row, column=col, sticky=sticky)
col += 1
widgetsign = str(skill) + "B"
if value >= 1:
color = "black"
else:
color = "grey"
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
command=lambda: chardata.set_skill(skill, 1))
self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)
col += 1
if value >= 2:
color = "black"
else:
color = "grey"
widgetsign = widgetsign + "B"
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
command=lambda: chardata.set_skill(skill, 2))
self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)
col += 1
if value >= 3:
color = "black"
else:
color = "grey"
widgetsign = widgetsign + "B"
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
command=lambda: chardata.set_skill(skill, 3))
self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)
col += 1
if value >= 4:
color = "black"
else:
color = "grey"
widgetsign = widgetsign + "B"
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
command=lambda: chardata.set_skill(skill, 4))
self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)
col += 1
if value >= 5:
color = "black"
else:
color = "grey"
widgetsign = widgetsign + "B"
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
command=lambda: chardata.set_skill(skill, 5))
self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)

def register_attributebutton(self, text="", row=0, col=0, sticky="w", attribute="", value=1):
# sieht ähnlich dem Skill Button aus nur ohne den Favored Teil
[/code]

Im Prinzip fehlt mir genau diese Rückruf Funktion.

Hm die Buttons mit einer Vorschleife zu erzeugen wäre wohl schöner.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Ich nehme mal an, die Buttons kennen sich nicht gegenseitig. Aber mit grid_slaves kannst Du herausbekommen, welche widgets mit grid in derselben Zeile sind. Und dann heißt es eben, mit grid_info die column zu vergleichen.
BlackJack

@Lofy: `FrameDefinition` ist eine komische Klasse. Die bekommt ein `master` ohne selbst ein Widget zu sein. Und die Klasse enthält viel zu viel. Man sollte gleiche ”Widgetgruppen” in der Benutzeroberfläche zu einem Objekt zusammenfassen. Eben beispielsweise diese fünf Schaltflächen for die Attribute und Fertigkeiten. Das selbe Muster wiederholt sich ja zigfach. Und ja, Schleifen statt x mal fast das gleiche zu schreiben wäre nicht schlecht. :-) Und wenn Du fünf Schaltflächen auf diese Weise zusammengefasst hast, kannst Du dieses kombinierte Widget 31 mal verwenden. Statt der 155 einzelnen `Button`\s die Du da im Moment im `widgets`-Wörterbuch sammelst.

Wo kommt `chardata` eigentlich her? Da wird einfach so magisch drauf zugeriffen und das ist keine Konstante. Also globaler Zustand. Das sollte nicht sein.

Wie gesagt, Du kannst `Checkbutton`\s auch wie normale Schaltflächen aussehen lassen, aber hast dann halt das Verhalten von `Checkbutton`. Also *ein* `*Var`-Exemplar das Du allen übergeben kannst, und das dann den entsprechenden Wert enthält.
Lofy
User
Beiträge: 6
Registriert: Donnerstag 15. Juni 2017, 10:31

@Black Jack
chardata ist ein Objekt was die ganze Charakter Logik enthält. Welche Attribute Fähigkeiten er hat usw...
Die Buttons Speichern alles im Objekt chardata. Will daraus irgendwann die Speicherung in einer Datei generieren. Die funktioniert auch in sich und macht recht wenig Probleme wenn ja sind die recht schnell gelöst. Die Frage dazu kann ich einer Methode eine function als Argument zu weisen? Dann könnte ich die Buttons besser wiederverwenden.

Kann ich dann das neue Button Widget dem Frame als Objekt übergeben? Habe das noch nicht gerafft wie das in Python ist und wie ich dann in der Frame Klasse intern darauf zugreife. :K
Am liebsten würde ich die ganze GUI ja als ein Objekt begreifen und ihr die Frames und Widgets als weitere Objekte zuweisen.

Das mit den For schleifen sollte ich mal machen, zu unübersichtlich.

@Alfons Mittelmeyer: Danke ich such mal was die beiden machen. Wie lernen sich die Buttons den kennen?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Lofy hat geschrieben:@Alfons Mittelmeyer: Danke ich such mal was die beiden machen. Wie lernen sich die Buttons den kennen?
Das wäre ewa, wenn Du die Buttons beim Erzeugen in eine Matrix eintragen würdest. Dann könntest Du in der Matrix nachsehen, welche Buttons davor wären. Bzw. da es nur darum geht, links davon die Farbe zu ändern, reicht auch eine Liste pro zusammengehörige Buttonzeile. Also den command jeweiils mit einer Liste verbinden, in welcher die betreffenden Buttons drin sind. Am einfachsten gleich nur die Liste der Buttons links davon übergeben, welche dann die andere Farbe bekommen sollen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Lofy: Du hast da beim Frame:

self.widgets = {}

Danach könntest Du eine Matrix machen, etwa:

self.widgets.row_column = [
[ None,None,None,...],
[ None,None,None,...],
...
]

Natürlich das generiert.

Dann hast Du bei Deinen Registermethoden:

self.widgets[ widgetsign ].grid(row=row, column=col, sticky=sticky)

Danach kann man ergänzen:
self.widgets.row_column[row][col] = self.widgets[ widgetsign ]

Bei Deinem Command fehlt auch etwas:

command=lambda: chardata.set_skill(skill, 5))

Weder kennt der comand den Button, noch wo er ist.

command=lambda: chardata.set_skill(skill, 5,row,col))

Das mit zu übergeben wäre sinnvoll, sowie die info, wieviel buttons links davon scharz werden sollen
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Du kannst Dir natürlich auch hinterher die Matrix generieren.
So bekommst Du die höchste Zeile und Spalte heraus:

Code: Alles auswählen

            min_row = 0
            min_col = 0
            children = self.grid_slaves()
            for child in children:
                grid_layout = child.grid_info()
                min_row = max(min_row,int(grid_layout['row']))
                min_col = max(min_col,int(grid_layout['column']))
Also +1 jeweils dazugezählt ergibt dann die Anzahl der Zeilen und Spalten

Und nach Erzeugung der Matrix kannst Du im zweiten Durchlauf die Widgets eintragen.
Dem command musst Du allerdings row und col übergeben.

Nö, auch nicht unbedingt, man könnte ja auch automatisch noch einen <ButtonRelease-1> draufsetzen, Bleibt dann nur die Frage, ob bei jeder Art von Button der und eine gewisse Anzahl links davon schwarz werden sollen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Das einfachste wird wohl sein, wenn Du den command so läßt, wie er ist und für das schwarz machen noch ein <ButtonRelease-1> event draufsetzt. Und bei dem dann auch die Zeile und die betroffenen Spalten angeben, oder das Widget und die betreffenden Spalten relativ dazu. Die Matrix braucht man nicht unbedingt, da es auch mit grid_slaves() und grid_info() geht.
BlackJack

@Lofy: Was das `chardata`-Objekt *ist* war mir schon klar, aber es kommt so magisch aus der Umgebung. Funktionen und Methoden sollten ausser Konstanten nur Sachen verwenden die als Argument übergeben wurden. Sonst wird ein Programm sehr schnell unübersichtlich und ist schlechter testbar.

Funktionen (und Methoden) sind Werte wie jeder andere. Die kann man als Argumente übergeben, als Attribute speichern, in Datenstrukturen stecken, und so weiter. Du machst das ja bereits bei den `command`-Argumenten von `Button`\s. (Die anonymen Funktionen dort würde ich persönlich übrigens mit `functools.partial()` umsetzen statt mit ``lambda``-Ausdrücken.)

Falls man von den Fertigkeiten übrigens nur *eine* auswählen darf, dann wäre da ein `Radiobutton` besser geeignet als ein `Checkbutton`.

Hier mal ein Beispiel, schnell aus der Hüfte geschossen, wie so ein `ButtonScale`-Widget umgesetzt werden könnte:

Code: Alles auswählen

#!/usr/bin/python
# coding: utf8
from __future__ import absolute_import, division, print_function
import Tkinter as tk
from itertools import chain, izip, repeat
from functools import partial


class ButtonScale(tk.Frame):

    def __init__(
        self,
        parent,
        max_value,
        on_change=None,
        value=0,
        color_on='black',
        color_off='grey',
    ):
        tk.Frame.__init__(self, parent)
        self.value = None
        self.on_change = None
        self.color_on = color_on
        self.color_off = color_off
        self.buttons = list()
        for i in xrange(1, max_value + 1):
            button = tk.Button(
                self, height=1, width=1, command=partial(self.set_value, i)
            )
            button.pack(side=tk.LEFT)
            self.buttons.append(button)
        self.set_value(value)
        self.on_change = on_change

    def set_value(self, value):
        if self.on_change and value != self.value:
            self.on_change(value)
        self.value = value
        colors = chain(
            repeat(self.color_on, self.value),
            repeat(self.color_off, len(self.buttons) - self.value)
        )
        for button, color in izip(self.buttons, colors):
            button['bg'] = color


def main():
    root = tk.Tk()
    ButtonScale(root, 5, on_change=partial(print, 'A')).pack()
    ButtonScale(root, 10, on_change=partial(print, 'B')).pack()
    root.mainloop()


if __name__ == '__main__':
    main()
Lofy
User
Beiträge: 6
Registriert: Donnerstag 15. Juni 2017, 10:31

Okay erstmal raff ich gar nichts, Aber Google ist mein Freund.
Mal die ganzen neuen Imports durchsehen was sie so machen.

und Matrix ansehen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Lofy hat geschrieben:Okay erstmal raff ich gar nichts, Aber Google ist mein Freund.
Mal die ganzen neuen Imports durchsehen was sie so machen.

und Matrix ansehen.
Google bringt Dir nichts und Matrix auch nichts. Du machst zuerst einfach folgendes: die Buttons, bei denen sich die Farbe und noch die Farbe der Buttons links ändern soll, belegst Du mit einem event:

Code: Alles auswählen

# nach diesem:
self.widgets[ widgetsign ] = Button(self.frame, text=text, height=1, width=2, bg=color,
                                            command=lambda: chardata.set_skill(skill, 3))

# machst Du noch:
self.widgets[ widgetsign ].bind('<ButtonRelease-1>',lambda event, me = self.widgets[ widgetsign ],cols = x_wievielevorher : make_black(event,me,cols)

# und damit es Dir nicht abstürzt, machst Du vorher zunächst ein Dummy:

def make_black(*args):
    pass
Wenn Du dieses gemacht hast, können wir weitermachen
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Lofy: ob der event funktioniert, kannst Du mal testen durch:

Code: Alles auswählen

def make_black(event,me,cols):
    me_row = int(me.grid_info()['row'])
    me_col = int(me.grid_info()['column'])
    print(me_row,me_col)
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Lofy: oder machen wir es gleich fertig:

Code: Alles auswählen

def make_black(event,me,cols):
    me_row = int(me.grid_info()['row'])
    me_col = int(me.grid_info()['column'])

    for col in range(me_col - cols,me_col):
        me.master.grid_slaves(row = me_row, column = col)[0]['bg'] = 'black'
Wenn der Button selber auch schwarz werden soll, schreibst Du: range(me_col - cols,me_col+1)
Lofy
User
Beiträge: 6
Registriert: Donnerstag 15. Juni 2017, 10:31

So danke erstmal, und willkommen Hirn Kapazität.

Gestern nach 4 Stunden Pneumatischen Steuerungen war diese definitiv aufgebraucht. Euren Beispielen zu folgen war schlicht weg unmöglich.

@Black Jack:
musste das izip durch zip ersetzen und das xrange durch range. (hängt glaube ich mit Python 2 und 3 zusammen)
Dann Funktioniert es und sieht auch echt so aus wie ich es haben will. ( Toller als ein Scale-Widget )
bin auch soweit durchgekommen und weiß nun was partial tut.
Ebenso was chain und repeat tun und gerade hat es auch klick gemacht wofür die Zeile 37 ist.
Das Funktioniert ja :)

Die Zeile 20 baust ein weil du die Buttons in einem Frame zusammen haben willst, damit das Button-Scale-Widget Räumlich immer zusammen hängt?
BlackJack

@Lofy: Zeile 20 ruft die `__init__()` von der Basisklasse auf weil ein `ButtonScale` ein `Frame` *ist*. Das erbt alles von `Frame` und erweitert den um die Funktionalität die in der `ButtonScale`-Klasse steht. Und ja, das ist um dieses ”Ding” als *ein* Widget behandeln zu können ohne von aussen wissen zu müssen wie genau das innen aufgebaut ist. Vererbung und Kapselung wären da die Stichworte.
Lofy
User
Beiträge: 6
Registriert: Donnerstag 15. Juni 2017, 10:31

Gracias :)
Danke euch beiden für die Mühe und für die Beispiele waren sehr hilfreich.

Denke nun komme ich erstmal weiter. Habe noch eingebaut das man wenn man auf 1 ein 2. mal klickt Value 0 annimmt.

heißt für mich eigentlich jetzt die Frameconfiguration Klasse auflösen.

Habe dann auch gleich dafür gesorgt das die Namen der Fähigkeiten in einem Objekt stecken und ich die nur da pflegen muss. Sollte ich ein ähnliches System haben. Brauch ich nur noch an einem Wert die Attribute und Fähigkeiten ändern. Macht das ganze zum einen Wartungsfreundlicher zum anderen Besser für andere Systeme Nutzbar.
Antworten