StringVar in Listen/Arrays -> Radiobuttons

Fragen zu Tkinter.
Antworten
Incident
User
Beiträge: 3
Registriert: Dienstag 31. Oktober 2017, 17:23

Hallo zusammen,

ich habe eine Problem und ich habe jetzt Stunden im Netz verbracht um eine Lösung zu finden. Entweder habe ich die falschen Suchbegriffe oder ich bin einfach festgefahren.
Ob die Überschrift 100% richtig ist, weiß ich auch nicht.

Was möchte ich?
-> Ich habe eine Liste von Sensoren (in einem 2D Array), die ich in einem Fenster auflisten möchte und über Radiobuttons eine Zuordnung an Positionen durchführen möchte. Pro Sensor soll nur eine Position möglich sein, also eine von 1 bis 4.
Das sieht wie folgt aus:

Code: Alles auswählen

Sensorname:	Pos 1	Pos 2	Pos 3	  Pos4
Sensor 1		[ ]		[ ]		[ ]		[ ]
Sensor 2		[ ]		[ ]		[ ]		[ ]    
Sensor 3		[ ]		[ ]		[ ]		[ ]
Sensor 4		[ ]		[ ]		[ ]		[ ]
...
Die Liste der Sensoren ist dynamisch, es können also 2 Sensoren sein oder 10 oder mehr sein. Meine grafische Anzeige läuft als Thread, da ich im Hauptprogramm Sensordaten vom System abgreife und in eine Liste verpacke und diese auch aktualisiere. Das funktioniert auch sehr gut.
Wenn ich also die Radiobuttons des Sensors 1 bündeln möchte mache ich das wie folgt:

Code: Alles auswählen

 
 sensor1 = StringVar()
 ...
 rb1 = tk.Radiobutton(self.posfenster, text="0", variable=sensor1 value="pos0")
 rb2 = tk.Radiobutton(self.posfenster, text="1", variable=sensor1 value="pos1")
 rb3 = tk.Radiobutton(self.posfenster, text="2", variable=sensor1 value="pos2")
 rb4 = tk.Radiobutton(self.posfenster, text="3", variable=sensor1 value="pos3")
 ...
Um nun für jeden Sensor eine Zeile zu spendieren, erzeuge ich den Variablennamen dynamisch, was vielleicht nicht sehr schick ist, aber mir ist nichts besseres eingefallen. Dazu läuft eine Schleife, bis die Liste in meinem Fenster vollständig abgebildet ist.
Die Empfehlung in verschiedenen Foren war, dass man die StringVar-Variablen in ein Array packt.

Code: Alles auswählen

   ...
        self.elementzaehler = 1                      # bewusst bei 1, denn an Position 0 stehen andere Infos
        while self.elementzaehler <= int(beispiel.get_count()):			# Anzahl der Sensoren wird in Klasse "beispiel" bestimmt
            # - Holt alle Elemente aus der Sensorliste und platziert den Sensornamen
            self.gui_sensordaten = beispiel.lesen(self.elementzaehler)	# die Sensordaten sind in einem 2D Array gespeichert
            self.t = tk.Label(self.posfenster, text=self.gui_sensordaten[0])	# die Sensordaten sind ebenfalls in einem Array, Element 0 ist der Name
            self.t.grid(row=self.elementzaehler+2, column=0) 			#  Offset für Platzierung 
            self.test = StringVar()
            self.pos.append(self.test)    							# Packt "StringVar" in eine Liste
            self.rb1 = tk.Radiobutton(self.posfenster, text="0", variable=self.pos[-1], value="pos0", command=self.funktion))  
            self.rb1.grid(row=self.elementzaehler+2, column=2)
            self.rb2 = tk.Radiobutton(self.posfenster, text="1", variable=self.pos[-1], value="pos1", command=self.funktion))
            self.rb2.grid(row=self.elementzaehler+2, column=3)
            self.rb3 = tk.Radiobutton(self.posfenster, text="2", variable=self.pos[-1], value="pos2", command=self.funktion))
            self.rb3.grid(row=self.elementzaehler+2, column=4)
            self.rb4 = tk.Radiobutton(self.posfenster, text="3", variable=self.pos[-1], value="pos3", command=self.funktion))
            self.rb4.grid(row=self.elementzaehler+2, column=5)
            self.elementzaehler += 1
...
Was der Code macht:
Ich kann die nun die Radiobuttons nun einer Zeile zuordnen -> Klappt, ich kann pro Zeile nur eine Position anklicken.
Was der Code nicht macht:
Wenn ich einen Radiobutton klicke, soll eine Funktion aufgerufen werden und dort der Wert der Zeile erfasst werden.
Und da liegt das Problem. Ich kann den Wert eines Radiobuttons nicht auslesen.
Was der Code machen soll:
Ich möchte immer wenn ein Radiobutton geklickt wird, den Wert von "value" und den Indexwert von "self.pos" haben und in der Funktion entsprechend verarbeiten.

Ich habe im Netz eine Lösung gefunden, die allerdings nur bei der letzten Zeile geht. Das liegtwahrschienlich daran, dass mit dem Parameter "-1" immer das letzte Element des Arrays ausgegeben wird.

Code: Alles auswählen

self.rb1 = tk.Radiobutton(self.posfenster, text="0", variable=self.pos[-1], value="pos0", command=lambda:self.funktion(self.pos[-1].get())) 
Mein Problem ist, dass ich nicht auf alle Zeilen und den Wert zugreifen kann. Vielleicht ist es auch ganz einfach, aber ich komme einfach nicht drauf. Man muss dazu sagen, dass ich Python seit ca. 3 Wochen erlerne und mein "Können" aus Büchern und dem Internet beziehe.
Sollte mein Programmierstil grottig sein, dann könnt Ihr mir das gerne sagen, ich möchte ja besser werden.

Könnt Ihr mir vielleicht einen Schubs geben, damit ich in die richtige Richtung weiter machen kann?

Danke und Gruß
Malte
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Incident: statt lambda sollte man functools.partial verwenden, da lambdas keinen eigenen Namensraum haben und damit wie Du schon festgestellt hast, nur das letzte gebundene Argument an die Funktion übergeben wird.

Ansonsten solltest Du nicht alles als Argument an `self` binden. Index-Variablen gehören da garantiert nicht hin, aber auch alle anderen Attribute überschreibst Du ja in jedem Schleifendurchgang wieder, das sind also auch keine Attribute. Statt immer pos[-1] solltest Du gleich `test` verwenden, da das ja das selbe Objekt ist und einfacher zu verstehen ist.

Deine Variablennamen sind zwar lang, aber auch nicht sehr aussagekräftig. elementzaehler oder beispiel, pos sagt auch nicht wirklich, was da in dieser Liste steht.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ein paar Anmerkungen:

- du schreibst, zeigst aber nicht genau was du mit Threads machst. Threads und GUIs zu mischen ist riskant bzw. funktioniert nicht ohne gewisse Vorkehrungen. Ich bezweifele das du die getroffen hast, und darum solltest du auch das mal zeigen.
- statt muehselig eine while-Schleife mit Zaehler und Abbruchbedingung zu definieren benutzt man einfach for

Code: Alles auswählen

for wert in ding_das_aufzaehlbar_ist:
...
Im haeufigen Fall von Zaehlern konkret

Code: Alles auswählen

for wert in range(obere_grenze):
...
Das steht so in jedem Python Tutorial, das kann nicht schaden, so etwas mal anzuschauen. Die range-Funktion bzw Konstruktor kann noch etwas mehr (verschiedene Start, Stop, Schrittweiten-Werte).

- ein absolutes anti-Pattern in Python ist es allerdings auf Listenelemente mit einem Index zuzugreifen. Das ist nahezu immer falsch. Wenn man doch einen Index braucht, weil zB wie bei dir danach noch ein wahlfreier Zugriff erfolgen soll, dann benutzt man enumerate~

Code: Alles auswählen

for i, sensor in enumerate(sensors):
      ...
- es ist unnoetig und sogar tendenziell falsch, alle moeglichen Variablen die eigentlich fluechtig sind als Instanzattribute anzulegen. Man macht nicht "for self.zaehler in ...". Sondern laesst zaehler ohne das self. davor einfach so erscheinen.
- das gleiche gilt fuer deine ganzen Buttons, die du ebenfalls an self bindest, aber dabei natuerlich immer nur den letzten ueberschreibst. Wenn du die spaeter wirklich brauchst, dann speichere sie in einer Liste.
- apropos: das was du array nennst ist eine Liste in Python. Es gibt Arrays, aber die sind anders und man sollte das nicht verwuerfeln.
- self.pos ist ein ganz schlechter weil fehlleitender Name. pos klingt nach Position. Du hast aber keine Position. Das ist eine Liste von Variablen. Also nenn das auch so, am besten basierend auf deren eigentlicher Funktion. ZB self.value_vars, self.active_vars, self.sensors, self.sensor_row_buttons etc.
- mit self.pos[-1] muehselig auf etwas zuzugreifen das du unter self.test zur Verfuengung hast ist umstaendlich (und natuerlich sollte es nicht self.test, sondern einfach "value_var" oder so heissen)
- last but not least: ja, dein command greift natuerlich immer nur auf das letzte Element zu. Stattdessen musst du den aktuellen Zaehlerwert verwenden. Dazu musst du ihn im lambda "capturen" wie es so schoen denglischt.

Code: Alles auswählen

command=lambda i=i, radiobutton=radiobutton: self.funktion(self.vars[i], radiobutton)
- ganz generell sind deine Namen gruetze. Das ist wirklich nicht irrelevant, in zwei Wochen ist Zukunfts-Malte schwerstens verwirrt, was Vergangenheits-Malte da zusammengedengelt hat.

Edit statt dem lambda ist es besser wie von Sirius beschrieben partial zu benutzen. Ich habe das gelassen weil mir so war das Tkinter damit nicht umgehen kann. Gerade getestet, scheint aber doch zu gehen, ist also vorzuziehen.
Incident
User
Beiträge: 3
Registriert: Dienstag 31. Oktober 2017, 17:23

Hallo zusammen,

vielen Dank für die Rückmeldungen. Ich führe mir das jetzt in Ruhe zu Gemüte und schaue, dass ich mein Codeverhau etwas aufräume und verbessere.
Mit Euren Rückmeldungen habe ich verstanden, dass mein erster Versuch nicht ganz sauber war. Den Rest zeige ich dann erst einmal nicht, da er sehr ähnlich aufgezogen ist.
Ich passe alles erst mal an und zeige dann das Ergebnis. Leider komme ich nicht so oft dazu, kann also etwas dauern.

Eure Kritik ist super, vielen Dank.

Gruß Malte
Incident
User
Beiträge: 3
Registriert: Dienstag 31. Oktober 2017, 17:23

Hallo zusammen,

nach doch einer, längerer Zeit wollte ich Rückmeldung geben, dass ich mit den Hinweisen das Problem lösen konnte.
Mein Code sieht nun so aus:

Code: Alles auswählen

 ...
      for zeilennr, zeileninhalt in enumerate(satz1.sensorliste):   
            if not zeileninhalt:
                return
            sensorname = tk.Label(self.zuordnungsfenster, text=zeileninhalt[0])
            sensorname.grid(row=zeilennr+2, column=0)
            sensor_positionsauswahl_1 = tk.Radiobutton(self.zuordnungsfenster, text="VL", variable=zeilennr, value=0, \
                                                       command=partial(self.position_zuordnen, zeileninhalt[0],"VL"))
            sensor_positionsauswahl_1.grid(row=zeilennr+2, column=2)
            sensor_positionsauswahl_2 = tk.Radiobutton(self.zuordnungsfenster, text="VR", variable=zeilennr, value=1, \
                                                       command=partial(self.position_zuordnen, zeileninhalt[0],"VR"))
            sensor_positionsauswahl_2.grid(row=zeilennr+2, column=3)
            sensor_positionsauswahl_3 = tk.Radiobutton(self.zuordnungsfenster, text="HL", variable=zeilennr, value=2, \
                                                       command=partial(self.position_zuordnen, zeileninhalt[0],"HL"))
            sensor_positionsauswahl_3.grid(row=zeilennr+2, column=4)
            sensor_positionsauswahl_4 = tk.Radiobutton(self.zuordnungsfenster, text="HR", variable=zeilennr, value=3, \
                                                       command=partial(self.position_zuordnen, zeileninhalt[0],"HR"))
            sensor_positionsauswahl_4.grid(row=zeilennr+2, column=5)
...
In einer Klasse wird die Sensorliste erstellt und geführt. Aus der Instanz der Klasse (satz1.sensorliste) lese ich alles aus und platziere es dann im Fenster. Ist die Formulierung so richtig?
Die Zuordnung der Zeilennummer als Variable des Radionbuttons ist vielleicht nicht sinnvoll, da nicht aussagekräftig, wichtig ist aber, dass die Radiobuttons als Gruppe funktionieren und das funktioniert so. Auf die Variable des Radiobutton muss ich auch nicht zugreifen, wichtig ist mir der Aufruf der Funktion, die dann alles regelt.
Als Kommando wird die Funktion "position_zuordnen" über "functools.partial" aufgerufen und der entsprechende Text übergeben.

Den Hinweis nicht alle Variablen mit "self." zu erweitern habe umgesetzt und nur bei Variablen, bei denen ich das benötige so gelassen. Auch die Schleifen und den Zugriff auf Listen habe ich entsprechend angepasst und verbessert.

Danke noch mal für die Hilfe
Gruß Malte
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Incident: dieses `if not zeileninhalt: return` ist etwas seltsam. `return` sollte `break` heißen. Warum kann dieser Fall überhaupt auftreten? Sollte man da nicht `sensortliste` schon entsprechend ändern? Backslashes sollte man, wenn es geht vermeiden, hier braucht man sie nicht, weil der Python-Compiler die Zeilenfortsetzung an den Klammern erkennt. `variable` sollte wirklich eine Tk-Variable sein, nicht dass es zufällig doch eine Tk-Variable mit dem Namen gibt und es zu komischen Effekten kommt. Die Radiobuttons kann man in einer Schleife erstellen:

Code: Alles auswählen

from itertools import takewhile

    for zeilennr, zeileninhalt in enumerate(takewhile(bool, satz1.sensorliste), 2):
        sensorname = tk.Label(self.zuordnungsfenster, text=zeileninhalt[0])
        sensorname.grid(row=zeilennr, column=0)
        var = tk.StrVar()
        for column, value in enumerate(["VL", "VR", "HL", "HR"], 2):
            tk.Radiobutton(self.zuordnungsfenster, text=value, variable=var,
				value=value, command=partial(self.position_zuordnen, zeileninhalt[0], value)
			).grid(row=zeilennr, column=column)
Antworten