Farbe eines Knopfes dynamisch ändern

Fragen zu Tkinter.
Antworten
MrOdin
User
Beiträge: 11
Registriert: Montag 13. August 2018, 11:28

Sonntag 16. September 2018, 16:37

Ich hätte das eine Frage... Ich schreibe momentan ein Programm welches (16) Servos steuert. Momentan sitze ich an der Optimierung und würde gerne die Aktivität der Servos grafisch ausgeben, wofür ich einfach ein paar in einem Grid geordnete Labels nutzen möchte die die Farbe ändern. Wenn ich jetzt aber eine Funktion schreibe die anstatt die Servos zu drehen die Farbe der Labels ändert
(über liste-mit-allen-Labels[n]["bg"]="Farbe") wird die Farbe der Labels erst nach beenden der Funktion aktualisiert, allerdings werden die Servos in einen durchlauf mehrmals bewegt. Gibt es irgendeine Möglichkeit die Farbe während die Funktion durchläuft zu aktualisieren?
Benutzeravatar
__blackjack__
User
Beiträge: 1585
Registriert: Samstag 2. Juni 2018, 10:21

Sonntag 16. September 2018, 17:02

@MrOdin: Du versuchst hier linearen Programmablauf mit ereignisbasierter GUI-Programmierung zu mischen. Das funktioniert so nicht. Bei einer GUI muss immer die Hauptschleife der GUI laufen. Die kann dann bei bestimmten Ereignissen Funktionen/Methoden von Dir aufrufen, die dann ganz kurz was machen können und dann wieder zur GUI-Hauptschleife zurückkehren müssen. Denn solange Dein Rückrufcode läuft, ist die GUI eingefroren, weil die von der GUI-Hauptschleife aktuell gehalten wird und die auch auf Ereignisse wie Benutzereingaben reagiert.

Du kannst also immer nur kurz etwas machen. Wenn Du etwas brauchst was längerfristig nebenher läuft, brauchst Du das `threading`-Modul. Das nächste Problem ist dann, das GUI-Rahmenwerke in der Regel nicht thread-safe sind, man die GUI also nur aus dem Hauptthread aus, in dem die GUI-Hauptschleife läuft, manipulieren darf. Man braucht also eine sichere Kommunikation zwischen den beiden Threads, was man bei Tkinter üblicherweise mit einer Queue löst, die von der GUI regelmässig abgefragt wird. Dafür gibt es die `after()`-Methode auf Widgets.

Code: Alles auswählen

    **** COMMODORE 64 BASIC V2 ****
 64K RAM SYSTEM  38911 BASIC BYTES FREE
   CYBERPUNX RETRO REPLAY 64KB - 3.8P
READY.
█
MrOdin
User
Beiträge: 11
Registriert: Montag 13. August 2018, 11:28

Montag 17. September 2018, 12:37

Ich hab jetzt mal (versucht) mich in die after() - Methode einzulesen aber nicht wirklich viel gefunden was wir weiterhilft, da ich sie nicht nach einer bestimmten Zeit sondern mit einen Knopf aufrufen will. Kennt da irgendwer ein umfassendes Tutorial oder kann mir direkt weiterhelfen?
Benutzeravatar
__blackjack__
User
Beiträge: 1585
Registriert: Samstag 2. Juni 2018, 10:21

Montag 17. September 2018, 16:22

@MrOdin: Du willst mit dem Knopf etwas aufrufen was dann weitere Sachen zeitverzögert mit `after()` erledigt. Entweder kannst Du Deine Aufgabe ich kurze Schritte zerlegen, bei denen jeder durch `after()` aufgerufen wird, oder die Aufgabe läuft in einem eigenen Thread und kommuniziert mit dem Hauptthread über eine Queue. Die wird dann dort mit Hilfe von `after()` regelmässig abgefragt.

Code: Alles auswählen

    **** COMMODORE 64 BASIC V2 ****
 64K RAM SYSTEM  38911 BASIC BYTES FREE
   CYBERPUNX RETRO REPLAY 64KB - 3.8P
READY.
█
MrOdin
User
Beiträge: 11
Registriert: Montag 13. August 2018, 11:28

Dienstag 18. September 2018, 15:39

Ich hab jetzt ein einfaches Programm geschrieben. Wenn man auf den Knopf drückt soll sich die Farbevariable des Labels von rot nach grün und zurück ändern. Dies geschieht (glaube ich zumindest) in einen eigenen Thread. Gleichzeitig sollte in der durch after() aufgerufenen Funktion "update" die Variable abgefragt und an das Label übergeben werden. Wenn ich jetzt allerdings auf den Knopf klicke, wird die "update"-Funktion angehalten (sehe ich daran das für eine Sekunde kein "updating" ausgegeben wird)

Code: Alles auswählen

import tkinter as tk
import time
import _thread

main=tk.Tk()



color="#FF0000"
count=0

label=tk.Label(main, text="TEST", bg=color)
label.pack()

def update():
    global count
    print("updating"+str(count))
    label["bg"]=color
    main.after(500, update)
    count=count+1
    
    

def change():
    
    global color
    
    color="#00FF00"
    time.sleep(1)
    color="#FF0000"

def ausgeben():
    thread.start_new_thread(change)
    


button=tk.Button(main, command=change)
button.pack()

main.after(500, update)


main.mainloop()
Was mache ich falsch? :?: :shock:
Benutzeravatar
__blackjack__
User
Beiträge: 1585
Registriert: Samstag 2. Juni 2018, 10:21

Dienstag 18. September 2018, 16:14

@MrOdin: Warum glaubst Du das da irgendwas in einem eigenen Thread läuft? Du importierst `_thread` und verwendest das nirgends. Es gibt die `ausgeben()`-Funktion die so aussieht als könnte sie einen Thread starten, wenn da kein `NameError` kommen würde, aber auch diese Funktion wird nirgends aufgerufen.

Grundsätzlich ist das `_thread`-Modul schon der falsche Ansatz, denn der führende Unterstrich sagt, dass das nicht Teil der öffentlichen API ist. Das ist in Python 3 das `threading`-Modul. Und auch in Python 2 ist das `thread`-Modul schon seit Ewigkeiten „deprecated“ gewesen.

``global`` sollte man nicht verwenden. Auch nicht zum Spass, äh, in so kleinen Beispielen.

Code: Alles auswählen

    **** COMMODORE 64 BASIC V2 ****
 64K RAM SYSTEM  38911 BASIC BYTES FREE
   CYBERPUNX RETRO REPLAY 64KB - 3.8P
READY.
█
Benutzeravatar
__blackjack__
User
Beiträge: 1585
Registriert: Samstag 2. Juni 2018, 10:21

Dienstag 18. September 2018, 16:29

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
import time
from threading import Thread
from queue import Empty, Queue


def update(label, colors, count=0):
    try:
        color = colors.get_nowait()
    except Empty:
        pass
    else:
        print('updating', count)
        label['bg'] = color
        count += 1
    label.after(500, update, label, colors, count)
    

def change(colors):
    while True:
        colors.put('green')
        time.sleep(1)
        colors.put('red')


def main():
    root = tk.Tk()

    label = tk.Label(root, text='TEST', bg='red')
    label.pack()

    colors = Queue()
    thread = Thread(target=change, args=(colors,), daemon=True)
    
    tk.Button(root, command=thread.start).pack()

    root.after(500, update, label, colors)

    root.mainloop()


if __name__ == '__main__':
    main()

Code: Alles auswählen

    **** COMMODORE 64 BASIC V2 ****
 64K RAM SYSTEM  38911 BASIC BYTES FREE
   CYBERPUNX RETRO REPLAY 64KB - 3.8P
READY.
█
MrOdin
User
Beiträge: 11
Registriert: Montag 13. August 2018, 11:28

Dienstag 18. September 2018, 17:53

Vielen Dank das bring mich schon viel weiter, allerdings soll der Befehl (also das blinken) nicht dauerhaft sondern einmal je Betätigung des Button ausgeführt werden. Wenn ich jetzt einfach die Schleife entferne kommt nach dem zweiten Drücken des Buttons die Meldung, dass ich einen Thread nur einmal ausführen kann. Wie kann ich das umgehen? ( und eine Frage zu deinem Code habe ich noch. Was bewirkt das
if __name__ == '__main__':
main()
am Ende?)
( übrigens sorry das ich so unbeholfen bin, aber ich bringe mir Python selbst bei)
Benutzeravatar
__blackjack__
User
Beiträge: 1585
Registriert: Samstag 2. Juni 2018, 10:21

Dienstag 18. September 2018, 18:45

@MrOdin: Ja an der Stelle habe ich mir eine zusätzliche Funktion gespart die den Thread erstellt und startet. Das müsste man machen wenn das mehrfach startbar sein soll.

Der Code am Ende bewirkt, dass die `main()`-Funktion aufgerufen wird wenn man das Modul als Programm ausführt, aber *nicht* wenn man es in einem anderen Modul oder in einer Python-Shell importiert. Das ist sinnvoll zum testen von einzelnen Funktionen, sowohl manuell, als auch automatisiert, und es gibt auch noch ein paar andere Werkzeuge und Bibliotheken die erwarten das man ein Modul importieren kann, ohne das irgend etwas ”passiert”, ausser dem definieren des Modulinhalts.

Edit: Wenn die Farbe nur einmal geändert werden soll, dann ist die Frage was da noch so passiert, und ob das lange dauert, und falls ja warum, denn vielleicht kommt man auch ohne Thread aus. Man muss dann nur das was da ablaufen soll in genügend kurz laufende Einzelstücke zerlegen können. Dann reicht es dafür `after()` zu verwenden.

Und eventuell möchte man die Schaltfläche deaktivieren solange die Aktion läuft, falls der Benutzer nicht während der laufenden Aktion noch eine weitere auslösen können soll.

Code: Alles auswählen

    **** COMMODORE 64 BASIC V2 ****
 64K RAM SYSTEM  38911 BASIC BYTES FREE
   CYBERPUNX RETRO REPLAY 64KB - 3.8P
READY.
█
MrOdin
User
Beiträge: 11
Registriert: Montag 13. August 2018, 11:28

Donnerstag 20. September 2018, 19:37

Ich denke es ist wirklich einfacher das mit einer after() Funktion zu machen. Allerdings habe ich ein anderes Problem: Wenn ich in dieser

Code: Alles auswählen

	
	county=0
        print(colory)
        
        while county<8:
            
            impuls=display[countx][county]
            pwm.set_pwm(county, 0, impuls)
            if impuls==335:
                colory[county]="green" 
            
            county=county+1
        labely0["bg"]=colory0
Funktion die Farbe des Labels ändern möchte erscheint die Fehlermeldung
TypeError: 'NoneType' object does not support item assignment
auch wenn ich (böse) labely0 als global definiere, warum bin ich so unfähig?

Edit: Ich hab auch versucht die Variablen in den Klammern hinter der Funktion übergebe, aber das endet in einer Schleife von werten, die voreinander definiert werden müssen
Sirius3
User
Beiträge: 8805
Registriert: Sonntag 21. Oktober 2012, 17:20

Donnerstag 20. September 2018, 21:45

Mehr als zu sagen, dass die Variable den Wert None hat, kann man bei dem Code-Ausschnitt nicht sagen. Wahrscheinlich bindest Du den Rückgabewert der pack-Methode an diese Variable.
MrOdin
User
Beiträge: 11
Registriert: Montag 13. August 2018, 11:28

Freitag 21. September 2018, 14:22

Da hast Recht... auch wenn ich die Grid-Methode nutze. Jetzt klappt alles so wie es soll. Danke
Antworten