errechnete Werte in Canvas anzeigen

Fragen zu Tkinter.
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hallo, habe das Skript von BlackJack aus Beitrag "Analoguhr" als Basis für meine "Spielerei" genommen.
Ich will Werte aus einer Programmschleife nacheinander in einem Diagramm anzeigen. Das funktioniert mit plot() nicht, weil das Plotfenster vom User geschlossen werden muß. Nachdem ich QT für einen Anfänger für zu umfangreich empfinde, wollte ich das mal in Tkinter versuchen. Leider bekomme ich entweder das komplette Diagramm am Ende der Schleife, was ja fast dem Verhalten von plot() entspricht oder es wird ein leeres Diagramm ausgegeben. Versuche mit "after" haben auch kein Ergebnis gebracht. Kann mir jemand sagen, was ich falsch mache oder geht das mit einem Tkinter-Canvas wie in plot() auch nicht?
Vielen Dank und Gruß, Rainer

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sun Sep 17 17:04:15 2017

@author: Rainer
"""

from __future__ import absolute_import, division, print_function
import tkinter as tk
from math import cos, pi as PI, sin
import time
 
CANVAS_WIDTH = 1000
CANVAS_HEIGHT = 800
#DIAG=Diagramm
DIAG_X_OFFSET_L = 50
DIAG_X_OFFSET_R = 50
DIAG_Y_OFFSET_O = 20
DIAG_Y_OFFSET_U = 100
 
DIAG_WIDTH = CANVAS_WIDTH - DIAG_X_OFFSET_L - DIAG_X_OFFSET_R
DIAG_HEIGHT = CANVAS_HEIGHT - DIAG_Y_OFFSET_O - DIAG_Y_OFFSET_U

#grid festlegen, anz_x=Anz. der vertikalen, anz_y=Anz. der vertikalen
#def make_grid(anz_x, anz_y):
#    x = int((DIAG_WIDTH)/anz_x)
#    y = int((DIAG_HEIGHT)/anz_y)
#    return x, y
 
def y_achse_txt(canvas, null_p, max_p):
    #Y- Beschriftung berechnen von 0_p bis max_p
    #gibt String-Liste zurück!
    y_txt=[]
    for i in range(null_p, (max_p * 10) + 1):
        i=i/10
        a='{0:.2f}'.format(i)
        y_txt.append(a)
    return y_txt

def zeichne_kreis(canvas, x, y, farbe="red"):
    #kleiner Kreis an Pos. x, y
    return canvas.create_oval(x - 2, y - 2, x + 2, y + 2, fill=farbe)
 
def zeichne_diagramm(canvas, x_1, y_1, x_2, y_2, y_txt):
    x = int((y_2 - y_1)/10)
    index=0
    canvas.create_rectangle(x_1, y_1, x_2, y_2, fill="white",width=3)
    for i in range(y_2, y_1 - x, -x):
        canvas.create_line(x_1, i, x_2, i, fill="black", width=1)
        
        canvas.create_text(x_1 - 20, i, text=y_txt[index])
        index += 1    
 
                
def main():
    
    root = tk.Tk()
    root.title( "Diagramm" )
    canvas = tk.Canvas(root, width=CANVAS_WIDTH, height=CANVAS_HEIGHT)
    canvas.pack()
    a = y_achse_txt(canvas, 0, 1)
    zeichne_diagramm(canvas, DIAG_X_OFFSET_L, DIAG_Y_OFFSET_O,
                     CANVAS_WIDTH - DIAG_X_OFFSET_R,
                     CANVAS_HEIGHT - DIAG_Y_OFFSET_U, a)
    for i in range(5):
        zeichne_kreis(canvas, 100 + (i * 10), 100 + (i * 10))
        time.sleep(1)

    root.mainloop()
  
if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ohne after geht es nicht. Wenn du es nicht hinbekommen hast, zeig uns deinen Versuch.

Generell gilt, das man GUIs nicht durch lange arbeitende schleifen blockieren darf. Sie müssen beständige Ereignisse ve arbeiten können.
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hallo @__deets__, danke für die Antwort. Beschäftige mich, wie in einer anderen Anfrage von mir schon gesagt, mit Neuronalen-Netzwerken und da kommen die Werte einer Iteration schon mal im Stundentakt! Und gerade auch deshalb wäre es schön, die Tendenz kontinuierlich im Diagramm sehen zu können.
Bin erst vor einiger Zeit von VB6 auf Py umgestiegen und kenne von dort so eine Einschränkung, wie du sie beschreibst, nicht. Du definierst einfach eine Picture-Box und schreibst was rein, wann und woher auch immer. Aber egal...vielleicht ist es für mich doch schlauer, die Visualisierung zu lassen und die einzelnen Werte - wie bisher - einfach auf der Konsole auszugeben.
Oder dann doch in QT einarbeiten?! Ist mir aber wirklich noch zu heavy! Im NN gibts genug Probleme...
Gruß Rainer
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die gibt es auch ganz bestimmt in VB. Du magst das nicht bemerkt haben, weil du unbewusst ein Pattern benutzt hast, welches dir da geholfen hat. Dazu müsste man jetzt Code sehen.

Hier ein VB Diskussion dazu: https://stackoverflow.com/questions/336 ... -in-vb-net

qt hat gewisse Vorteile für nebenläufigige Programmierung, aber so gravierend sind die nun auch nicht.

Ich würde dir raten, erstmal ein ganz simples Beispiel mit After zu programmieren. Der nächste Schritt wäre ein Hintergrundthread, der mittels einer queue updates an eben diese after-Funktionalität weitergibt. Dann du da sein.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

"Solltest du da sein". Das doofe iPad verschluckt Wörter :evil:
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hallo @__deets__, danke, ich nehme erst mal mit, dass es in Tkinter vielleicht doch geht! Muß dann ganz sicher noch etwas an "After" rumknabbern.
In VB habe ich die Zwischenergebnisse sowohl in einen File geschrieben, als auch in einer Box visualisiert...ohne Problem, auch bei tagelangen Berechnungen...aber egal, dass ist Schnee von gestern. Ich melde mich.
Gruß Rainer
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

pyzip hat geschrieben:ich nehme erst mal mit, dass es in Tkinter vielleicht doch geht!
Nicht vielleicht, ganz bestimmt, denke ich mir zumindest. Ich nehme mal nur Deine "def zeichne_kreis" mit "create_oval" als Beispiel. Wie man dabei mit Canvas etwas anderes als einen fertigen Kreis erhalten könnte, weiß ich nicht. Doch mit PIL und "after" kannst Du zuschauen, wie ein Kreis sich bildet. Du musst nur Deine eigenen "Pinsel" oder sonstige zum Zeichnen geeignete Utensilien anlegen. Es gibt noch eine andere Methode, mit der Du einzelne Pixel setzen kannst, die soll aber weniger geeignet sein.

Code: Alles auswählen

from tkinter import Tk,Frame, Label, Button
from PIL import Image, ImageTk
from math import pi, sin, cos


class Animation:

    def __init__(self):
        self.fenster = Tk()
        self.fenster.title("Leinwand mit Kreis")
        self.label_image = Label()
        self.leinwand = "leinwand.png"            # Größe für Test 500 x 400 Pixel
        self.pinsel = "pinsel-orange.png"         # Größe für Test 4 x 4 Pixel

    def layout(self):
        self.leinwand_image = Image.open(self.leinwand)
        self.hintergrung = ImageTk.PhotoImage(self.leinwand_image)
        self.label_image = Label(
            self.fenster,
            image = self.hintergrung,
            bd = 0)
        self.label_image.pack()
        Button(
            self.fenster,
            text = "Start",
            command = self.zeichne_kreis).pack()

    def zeichne_kreis(self, n=1):
        if n <= 360:
            punkt_x = round(250 + 180 * sin(n*pi/180))
            punkt_y = round(200 + 180 * cos(n*pi/180))

            pinselpunkt = Image.open(self.pinsel)
            self.leinwand_image.paste(pinselpunkt, (punkt_x, punkt_y))
            self.hintergrung = ImageTk.PhotoImage(self.leinwand_image)
            self.label_image.config(image = self.hintergrung)

            self.fenster.after(40, self.zeichne_kreis, n + 1)

    def main(self):
        self.layout()
        self.fenster.mainloop()

if __name__ == "__main__":
    Animation().main()
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hallo Melewo, danke für das Beispiel. Habe selbst diverse Beispiele gefunden, die sowas machen. Die analoge Uhr z.B. Das Problem ist aber, dass ich in einem Programm Werte ausrechne, die ich jeweils sofort ins Diagramm eintragen will. Das habe ich vereinfacht mit der Schleife und Timer gemacht. Irgendwie habe ich noch nicht verstanden, warum das Diagramm immer erst nach Beenden der Schleife angezeigt wird. Die Punkte werden ja wohl gesetzt. Vielleicht muß man innerhalb der Schleife noch einen Event oder sowas erzeugen. Bin da aber als Anfänger leicht überfordert. Es kam auch schon die Anregung, das mit "threading" zu versuchen. Mal sehen...
Gruß Rainer
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

pyzip hat geschrieben:warum das Diagramm immer erst nach Beenden der Schleife angezeigt wird.
Und das wird in meinem Beispiel gerade nicht gemacht, da wird jeder neu hinzukommende Punkt angezeigt, so wie er gesetzt wird und nicht erst, wenn after die letzte Runde gedreht hat. Und ja, da gehört für alles eine Berechnung zu, gleich ob ein Kreis Punkt für Punkt sich vervollständigen soll oder ein Balken sich verlängern oder verkürzen. Du aktualisierst entsprechend der Einstellung von after in Millisekunden das angezeigte Image, nicht das Fenster. Und wenn die Werte sich innerhalb der eingestellten Zeit geändert haben, dann übergibst Du diese halt. Ich habe nur im Beispiel feste Werte eingefügt.
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@pyzip: Du mußt das machen, was __deets__ schon gesagt hat: Berechnungen werden in einem Hintergrundthread gemacht. Wenn es Daten gibt, die angezeigt werden sollen, werden diese in eine Queue gesteckt und im GUI-Hauptthread wird regelmäßig die Queue verarbeitet und die Daten angezeigt.
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hi, @__deets__ ja, mal schaun, ob ich das hinkriege....und @Melewo, habe eben mal versucht, dein Script laufen zu lassen. Funzt natürlich nicht. Nachdem ich PIL installiert habe, scheitert es daran, dass es bei mir natürlich kein "leinwand.png" gibt. Leider ist es mir bisher nicht gelungen, diesen File zu erzeugen. Ist wahrscheinlich sooo trivial...aber ich habs - noch - nicht geschafft.
Gruß Rainer
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

pyzip hat geschrieben:Leider ist es mir bisher nicht gelungen, diesen File zu erzeugen.
Eine Leinwand ist doch nur eine helle Fläche, auf der etwas projiziert wird. Im Beispiel ein PNG-Image von 500 x 400 Pixel. Ob Du das weiß wie eine Leinwand lässt oder einen hellen Farbton verleihst, bleibt gleich. Nur der sich bewegengende Punkt sollte sich deutlicher abheben. Ein zweites Beispiel mit einem Balken, dessen Höhe sich durch Zufallszahlen ändert, hätte ich dann auch noch hier zu liegen, als Ansatz für ein Balkendiagramm. Das würde aber nichts werden und nicht funktionieren, wenn Du nicht weißt, was ein weißes image.png ist.
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

ja gut, aber das Script steigt mit der Fehlermeldung aus, dass es kein *.pgn" gibt.
Rainer
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ein bisschen mitdenken und den Code versuchen zu verstehen darfs schon sein. Du musst ein PNG entsprechenden namens selbst an die entsprechende Stelle liegen. Kann auch ein Bild deiner Katze sein, Hauptsache es wird gefunden. Dazu muss der Pfad passen, gegebenenfalls einfach den absoluten Pfad angeben, oder im Verzeichnis in dem du Python startest. Die Fehlermeldung die du bekommst gibt da spätestens nachdem man sie in google geworfen hat auch ausreichend Hinweise zu.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

@pyzip: Du könntest Dir z.B. später ein Hintergrund-Image mit matplotlib fertigen. Wenn Du aus diesem den restlichen Inhalt entfernen würdest und nur die beschrifteten x und y Achsen stehen ließest, hättest Du eine brauchbare Vorlage als Hintergrund. Hier noch ein Beispiel mit einem Balken. Und ob Du das nun leinwand.png nennst oder background-image.png oder falls es etwas mehr sein darf, matrix.png, das ist letztendlich Jacke wie Hose.
Die einzigen Grenzen, auf die ich bisher stieß, wurden nicht von Python vorgegeben, sondern durch meine eigenen Wissenslücken. Das mit einem Hintergrundthread und Queue, da habe ich auch noch eine Glatze. Bei mehr als einer Berechnung hatte ich die zusammen mit after in einer zweiten Klasse "Werkzeuge" ausgelagert, von der die Klasse mit Tkinter dann erbte, doch irgendwie kam mir das dann stümperhaft vor.

Code: Alles auswählen

from tkinter import Tk, Label, Button
from PIL import Image, ImageTk
from copy import deepcopy
from random import randrange     # Nur für einen Test

class Balkentest:

    def __init__(self):
        self.fenster = Tk()
        self.fenster.title("Leinwand mit Balken")
        self.leinwand = "images/leinwand.png"   # Größe im Test 500 x 400 Pixel
        # Höhe des Balkens gleich Abstand y1 von oben, als veränderlicher Wert
        self.hoehe = 100  

    def layout(self):
        self.leinwand_image = Image.open(self.leinwand)
        self.hintergrung = ImageTk.PhotoImage(self.leinwand_image)
        self.label_image = Label(
            self.fenster,
            image = self.hintergrung,
            bd = 0)
        self.label_image.pack()
        Button(
            self.fenster,
            text = "Start",
            command = self.zeichne_balken).pack()

    def zeichne_balken(self):
        self.hoehe = randrange(10, 300, 2)      # Nur für einen Test

        # Hier mit Kopie arbeiten, sonst wird der Balken nur höher, geht aber
        # nicht mehr herunter.
        temp_image = deepcopy(self.leinwand_image)
        # RGB Farbwert als 3er Tupel, 
        # Position, Breite und Höhe des Balkens als 4er Tupel x1, y1, x2, y2
        temp_image.paste((255, 138, 1),(220, self.hoehe, 300, 400))
        self.hintergrung = ImageTk.PhotoImage(temp_image)
        self.label_image.config(image = self.hintergrung)

        self.fenster.after(500, self.zeichne_balken)

    def main(self):
        self.layout()
        self.fenster.mainloop()

if __name__ == "__main__":
    Balkentest().main()
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hi, ich bin nicht so faul...wollte nur "rasch" einen PNG-File im selben Script erzeugen...was aber nicht so recht funktioniert hat.
@ Sirius3 Hintergrundthread ist leicht gesagt, aber man muß es auch verstehen...ich gebe mir etwas Zeit!
Danke Allen! Gruß Rainer
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

Erzeugen ist eigentlich auch kein Problem mit Image.new. Das hätte dann statt so

Code: Alles auswählen

        self.leinwand_image = Image.open(self.leinwand)
        self.hintergrung = ImageTk.PhotoImage(self.leinwand_image)
so ausgesehen:

Code: Alles auswählen

        self.leinwand_image = Image.new("RGB", (500, 400), (246, 230, 190))
        self.hintergrung = ImageTk.PhotoImage(self.leinwand_image)
Bei dem kleinen 4 x 4 halt ähnlich, so dass nicht unbedingt echte Images geladen werden müssen. Hatte nur kein Beispiel zu liegen, weil ich nur mit Animationen für Videos experimentiert hatte und da dann, falls es einmal fertig werden sollte, echte Images als Hintergrundmotive verwendet werden sollen und keine leeren Flächen.
pyzip
User
Beiträge: 89
Registriert: Freitag 16. Juni 2017, 19:36

Hallo Melewo, habe dein Beispiel jetzt nachvollziehen können, danke. Dir ist aber schon klar, dass das nicht mein Problem löst...
@__deets__, habe mal versucht, rauszukriegen, wie man die berechneten Werte aus einem zweiten Thread zum Eventhandler in der "root.mainloop()" rüberschaffen könnte. Im Tkinter-Tutorial empfehlen sie, die Threads über globale Variable zu verbinden. Leider wird das aber in nur wenigen Zeilen abgehandelt und es ist ja überall zu lesen, dass globale Variablen schlechter Programmierstil ist....weiterhin habe ich versucht zu verstehen, wie die Pipes funktionieren und da bin ich noch nicht durchgestiegen. Was mir jedoch klar wird, ist, dass ich in der "after-Schleife" auf "irgendwas" pollen kann/muß. D.h. also, der Eventhandler lauert auf seine Events und meine Schleife lauert auf "meinen Event". Beschreibt das in etwa die Situation, auf der ich dann aufbauen kann??
Vielen Dank, Rainer
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Eine Queue ist erstmal nichts anderes als eine Liste, an der man am einen Ende was reinstopft, und am anderen Ende was rausholt.

Und in tkinter schreibt man sich nun eine after-Methode, welche

- prueft, ob etwas in der Queue ist.
- wenn ja, es rausholt und dann ein GUI-update anstoesst.

Die muss halt im gewuenschten Interval aufgerufen werden.

Ein anderer Thread kann die Queue dann mit Daten bestuecken.

Ein Beispiel findet sich natuerlich auch hier im Forum, zB hier: viewtopic.php?t=40146 - letzten Post anschauen.

Streng genommen ist die Queue auch dort globaler Zustand, aber das muss nicht so sein - deine Quellen haben in dieser Beziehung nicht recht. Und der Post zeigt auch, wie es gemacht wird: man muss halt die Instanz der Queue sowohl an die GUI-Klassen uebergeben, als auch an den Thread. Danach braucht niemand anderes darauf eine Referenz.
Melewo
User
Beiträge: 320
Registriert: Mittwoch 3. Mai 2017, 16:30

@pyzip: Und mit queue hast Du Dich noch nicht befasst?
Liest sich für mich zumindest so, als ob Du Dir das wirklich einmal näher betrachten solltest.
The queue module implements multi-producer, multi-consumer queues. It is especially useful in threaded programming when information must be exchanged safely between multiple threads.
https://docs.python.org/3/library/queue.html
Antworten