Problem mit Programm; Speicher überlauf

Fragen zu Tkinter.
Antworten
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Hallo.
Ich bin neu hier und muss sagen, dass hier ist ein super Forum.
Hab schon einiges hier gelesen. In Pyhton bin ich so ziemlich ein Anfänger.

Nun zu meinem Problem:
Ich habe ein kleines Programm geschrieben, welches Daten aus einer Datei ausliest, und die Daten als Diagramm zeichnet. Außerdem werden zwischendurch immer wieder Bilder übereinander gelegt.
Und ich denke, dass die ganzen Linien und Bilder, die ich da zeichne im Speicher als Objekte bestehen bleiben. Und da liegt mein Problem:
Nach einer gewissen Zeit wird der PC langsamer und dann beendet sich das Programm mit einer Fehlermeldung. Gibt es da eine Möglichkeit, dass schneller zu machen und den Speicher freizugeben? Die CPU-Auslastung ist auch extrem hoch.

Und dann wollte ich noch einen Schließen-Button einbauen. Aber ich schaff es irgendwie nicht, den im gleichem Fenster zu plazieren. Kann mir da einer einen Denkanstoß geben?

Ich hab das Programm und die Dateien mal hochgeladen:
http://www.fh-augsburg.de/~tmq/FH/Proje ... chfeld.rar


Für jede Hilfe wäre ich sehr dankbar.

Gruß, Tommy.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

Hi Tommy und willkommen im Forum!

Ohne dein Programm jetzt gesehen zu haben, kannst du nicht zwischendurch mal das Canvas( du zeichnest doch auf nem Canvas, oder) einfach leeren? Du kannst auch die Sachen (bzw. dessen IDs), die du nach einiger Zeit wieder Löschen kannst in eine Liste speichern und dann in einer Schleife über die Liste dann löschen.

Zum Beendenbutton guck dir das mal an:http://www.python-forum.de/topic-10498.html

Gruß, jj
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Hallo.

Danke für die Tips. Jetzt hatte ich einige Zeit zum rumprobieren gehabt (und musste mein Programm auch ganz schön umschreiben), aber ich komm bei einigen Punkten nicht so richtig weiter:

1. Den "Quit"-Button hab ich jetzt hinbekommen. Problem ist jetzt aber nur, dass mein Programm fortlaufend die ganze Zeit zeichnen soll und es auch tut. Und wenn man dann den Quit-Button betätigt, bringt er eine Fehlermeldung, dass er nicht mehr zeichnen kann.
Zum beenden habe ich folgendes verwendet:

Code: Alles auswählen

    
def Quit(self):
      self.cv.delete(*self.cv.find_all())  #Löschen des Canvas
      self.root.destroy()                       #beenden
2. Und dann hab ich noch ein Geschwindigkeitsproblem, aber ich denk da wird man nichts machen können.
Nach dem Start oder dem Löschen des canvas zeichnet das Programm die Linien gleichmäßig schnell und nach einiger Zeit geht die Geschwindigkeit deutlich runter. Da es sich aber um 4 Spannungs/Stromverläufe handelt, ist das nicht gerade so toll, wenn die Geschwindigkeit nach einer gewissen Zeit langsamer ist. Ich zeichne die Verläufe von einem Pixel zum nächsten als Linie (wodurch eine Menge Linien gezeichnet werden müssen), da bei Punkten manchmal unschöne Verläufe gezeichnet wurden. Gibt es vielleicht da schon eine fertige oder bessere Funktion?


Gruß, Thomas. :D :D
BlackJack

Das Problem ist, dass das `Canvas` Vektorgrafik ist. Jedesmal wenn Du eine Linie hinzufügst werden alle alten Linien nochmal gezeichnet plus die neue Linie. Das wird natürlich im Laufe der Zeit dann mit jeder neuen Linie immer langsamer.

Vielleicht kannst Du auf Pixelgrafik umsteigen, zum Beispiel mit der "Python Imaging Library" (PIL) und dort immer eine neue Linie zeichnen und das Pixelbild anzeigen.
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Hallo BlackJack.

Ich hab mal ein bischen im Internet über die Python Imaging Library gelesen.
Sehe ich das richtig, dass wenn ich es mit der "Python Imaging Library" (PIL) mache, ich die Linien ins Bitmap zeichnen und speichern muss, um es dann im Canvas anzuzeigen?
Das wäre nicht so gut, weil ich in ein Koordinatensystem zeichne, welches als Gif vorliegt. Und nachdem der Zeichenvorgang abgeschlossen ist, soll alles wieder von vorne beginnen und darum sollten die Bitmaps unverändert bleiben (das ganze soll dann in der Art wie ein Oszilloskop durchlaufen).
Kann man im Canvas nichts umstellen, damit die alten Objekte nicht nochmals gezeichnet werden?

Gruß, Tommy.
BlackJack

Abspeichern musst Du das Bild nicht. PIL kommt mit einer Klasse um ein `Image` in eine Bildobjekt für Tk umzuwandeln. Das kann man alles im Speicher machen.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

BlackJack hat geschrieben:Abspeichern musst Du das Bild nicht.
Davon abgesehen müsstest du - selbst wenn du es speichern müsstest - das Ursprungsbild nicht überschreiben. Somit könntest du immer das Ursprungsbild laden. Aber klar, im Speicher ist das viel eleganter.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Okay, wenn ich das richtig verstanden habe, zeichne ich die Punkte in das Bild und lade jedesmal das Bild neu, oder?
Ich hab das mal mit folgendem Beispielprogramm probiert:

Code: Alles auswählen

import Image, ImageDraw, ImageTk
import Tkinter as tk

class Zeichnen():
    def __init__(self):
        self.root = tk.Tk()
        self.zeichnelinie=tk.Button(self.root,bg="grey",text="GO",command = self.linien)
        self.zeichnelinie.grid(pady=1.5, sticky="nw")
        self.cv = tk.Canvas(self.root, width = 1000, height=700, background="white")
        self.cv.grid()
        self.oeffnen()

    def oeffnen(self):
        self.im = Image.open("GIF/diagramm.gif")
        self.draw = ImageDraw.Draw(self.im)
        self.image = ImageTk.PhotoImage(self.im)
        self.cv.create_image(737,340,image=self.image,tags="image1")

        
    def linien(self):
        for x in range(200):
            self.draw.point((x,x), fill=128)
            self.cv.delete("image1")
            self.image = ImageTk.PhotoImage(self.im)
            self.cv.create_image(737,340,image=self.image,tags="image1")
            self.cv.update()

app = Zeichnen()
app.root.mainloop()
      
Bei diesem Programm, läuft das zeichnen noch um einiges langsamer ab :(


Gruß, Tommy.
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Tommy hat geschrieben:Bei diesem Programm, läuft das zeichnen noch um einiges langsamer ab
Hallo Tommy!

Je nach Verwendungszweck:

Code: Alles auswählen

    def linien(self):
        for x in range(200):
            self.draw.point((x,x), fill=128)
            if x % 10 == 0:
                self.cv.delete("image1")
                self.image = ImageTk.PhotoImage(self.im)
                self.cv.create_image(737,340,image=self.image,tags="image1")
                self.cv.update()
oder

Code: Alles auswählen

    def linien(self):
        for x in range(200):
            self.draw.point((x,x), fill=128)
        self.cv.delete("image1")
        self.image = ImageTk.PhotoImage(self.im)
        self.cv.create_image(737,340,image=self.image,tags="image1")
        self.cv.update()
Das Anzeigen braucht die meiste Zeit. Je öfter du das Bild neu anzeigst, desto langsamer wird das Programm.

Edit:
Ich habe mir jetzt dein Beispiel (rar) angesehen. Ich glaube, du bist besser dran, wenn du bei deiner alten Vorgehensweise bleibst, dich aber zusätzlich darum kümmerst, dass nach jedem Durchlauf alle Objekte (bis auf das Hintergrundbild) wieder vom Canvas gelöscht werden. Vielleicht wird damit auch der verbrauchte Speicher wieder frei gegeben. Das müsstest du ausprobieren. Ich habe deinen Code auch nicht durchgelesen, sondern nur kurz drübergescannt. Vielleicht tust du das ja schon.

Mit "delete()" löscht du ein Objekt und mit "find_all()" findest du die IDs aller Objekte.

mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Hallo.
Ja, im Moment zeichne ich das Diagramm fertig und lösche danach alle Objekte wieder. Aber das Problem ist halt im Moment, dass er, während er noch zeichnet, immer langsamer wird (was mir jetzt dank BlackJack auch klar ist warum).
Aber es wird wohl nicht anders gehen. Ich werde dann wahrscheinlich ein bischen rumtricksen (zu beginn das zeichnen verlangsamen) damit es gleichmäßig ausschaut.

Danke noch mals für die Tips.

Gruß, Tommy.
Benutzeravatar
Michael Schneider
User
Beiträge: 569
Registriert: Samstag 8. April 2006, 12:31
Wohnort: Brandenburg

Hi Tommy,

ich bin mir noch nicht ganz sicher, was Du eigentlich erreichen möchtest, vielleicht könntest Du das noch einmal genauer erklären (soll der ganze Graph wandern, wie etwa bei der Überwachung der Down-/Uploads - oder soll die Update-Position wandern wie beim EKG)?

Hier mal ein Beispiel, wie man einen kompletten Graphen mit nur einem Item einfügen kann:

Code: Alles auswählen

import Tkinter as Tk
import random

def ValueGenerator(iInitValue, iWidth):
    iValue = iInitValue
    for i in range(iWidth+1):
        yield (i, iValue)
        iValue += random.randint(-5, 5)
        
class GraphCanvas(Tk.Canvas):
    def __init__(self, wMaster):
        ##  Klasse initialisieren
        self._init_base_class(wMaster)

        ##  Demographen generieren und zeichnen
        self.draw_new_graph()

    def _init_base_class(self, wMaster):
        """Initialisiert die Klasse"""
        Tk.Canvas.__init__(self, wMaster)
        self.config(width=400, height=300)
        self.iGraphItem = 0

    def draw_new_graph(self):
        ##  Testwerte erzeugen
        lValues = self._create_point_list(iInitValue=150, iWidth=400)

        ##  Graphen zeichnen
        self._draw_graph(lValues)

    def _draw_graph(self, lValues):
        """Loescht den Graphen und zeichnet ihn neu"""
        if self.iGraphItem:
            self.delete(self.iGraphItem)
        self.iGraphItem = self.create_line(*lValues)
        
    def _create_point_list(self, iInitValue, iWidth):
        """Erzeugt eine Liste fuer einen Testgraphen"""
        lValues = []
        for tPoint in ValueGenerator(iInitValue, iWidth=400):
            lValues.extend(tPoint)
        return lValues

class MainWindow(Tk.Tk):
    def __init__(self):
        self._init_base_class()
        self._create_widgets()

    def _init_base_class(self):
        """Klasse initialisieren"""
        Tk.Tk.__init__(self)
        self.title("Hauptfenster")

    def _create_widgets(self):
        """Widgets des Hauptfensters erzeugen"""
        self.wGraphCanvas = GraphCanvas(self)
        self.wGraphCanvas.grid(row=0, column=0)
        self._create_menu()

    def _create_menu(self):
        """Menuezeile erzeugen"""
        wMenuLine = Tk.Menu(self)
        wProgramMenu = Tk.Menu(wMenuLine, tearoff=False)
        wGraphMenu = Tk.Menu(wMenuLine, tearoff=False)

        self.config(menu=wMenuLine)
        wMenuLine.add_cascade(label="Programm", menu=wProgramMenu)
        wMenuLine.add_cascade(label="Graph", menu=wGraphMenu)
        wProgramMenu.add_command(label="Ende", command=self._exit_program)
        wGraphMenu.add_command(label="neu", command=self.wGraphCanvas.draw_new_graph)

    def _exit_program(self):
        """Sicherheitsabfrage und Programmende"""
        import tkMessageBox
        Answer = tkMessageBox.askyesno(title="Programm beenden", message="Wirklich?")
        if Answer == True:
            self.quit()
            self.destroy()


Main = MainWindow()
Main.mainloop()
Das geht bei mir recht fix. Du brauchst hier nur die Liste manipulieren, also entweder zwischendrin überschreiben oder an einem Ende entfernen und am anderen Ende anfügen, und dann den Graphen (direkt) neu zu zeichnen.

Grüße,
Michel
Diese Nachricht zersört sich in 5 Sekunden selbst ...
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Hallo Michael.

Ich muss mir heute noch mal deinen Code anschauen und ihn ausprobieren. Bin leider gestern nicht mehr dazu gekommen. Aber danke schon mal.

Also Funktion soll folgende sein:
Ich habe eine Schaltung die zwei Zustände annehmen kann. Jeden Zustand habe ich in ein GIF-Bild gezeichnet.
Das Programm soll nun aus einer TXT-Datei die x-Werte und die dazugehörigen vier y-Werte (entsprechen Spannungen) auslesen. Mit diesen Werten sollen die 4 Spannungen parallel und synchron in 4 Koordinatensysteme langsam, wie beim EKG, gezeichnet werden. Zusätzlich soll bei jedem 0-Durchgang einer bestimmten y-Funktion der andere Zustand der Schaltung angezeigt werden.
Wenn, nach 3 Perioden, das Ende erreicht ist, soll alles wieder von vorne beginnen (also eine Dauerschleife).

Probleme hatte ich eben, wie oben bereits beschrieben, beim zeichnen, dass zu beginn alles schnell gezeichnet wird und danach die Geschwindigkeit runter geht.
Ich hab auch im allerersten Beitrag einen Link mit eingefügt, wo man es mal ausprobieren und anschauen kann, allerdings eine frühe Version die sehr unsauber programmiert ist, aber zum anschauen funktioniert es.


Gruß, Tommy.
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

wenn ich dich jetzt richtig verstanden habe, wandert die linie immer etwas weiter und verschwindet dann am Rand. Du musst doch nur die Punkte, die mann nicht mehr sieht, löschen, dann hast du, sobald die linien einmal über das ganze Canvas gehen, ein konstantes und nicht allzulangsames Tempo.
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Also die x-Achsen sind 420 Pixel lang. Und ich zeichne die vier Linien auch bis zu den 420 Pixel und lösche dann alles um von neuem zu beginnen. Aber: Wenn er bei Null beginnt zu zeichnen, dann zeichnet er eben ziemlich schnell und wenn er z.B. bei Pixel 300 angekommen ist dann zeichnet er deutlich langsamer. Das zeichnen sollte in etwa zeitsynchron sein.
Benutzeravatar
Michael Schneider
User
Beiträge: 569
Registriert: Samstag 8. April 2006, 12:31
Wohnort: Brandenburg

Hi Tommy,

ich sehe darin kein Problem. Es gibt zwei Möglichkeiten, das mit einem Canvas und Vektorgrafik einfach und elegant umzusetzen umzusetzen.

speicherorientiert:
Du nimmst mein obiges Beispiel und modifizierst einfach nur einmal pro Periode die Liste mit den Punktwerten. Ihr Aufbau ist [x0, y0, x1, y1, x2, ..., y418, x419, y419]. Dann rufst Du einfach nur die Methode _draw_graph mit der manipulierten Liste als Argument auf. Dabei gibt es evtl. einen etwas unschönen Übergang zwischen vorigem Durchlauf und Aktuellem, aber das kann man evtl. vernachlässigen. Wenn Du vorher mit einer 420-Punkte Nulliste initialisierst, hast Du eine konstante Geschwindigkeit.

performanceorientiert:
Erzeuge ein Dictionary für die 420 einzelnen Linien. Dann zähle den x-Counter hoch und ersetze die Dictionary-Linie der Stelle x (wenn eine existiert, lösche sie vorher) durch eine neue Linie. Das ist die Variante vom Schlangenbeschwörer. So musst Du pro Periode nur eine Linie neu zeichnen, brauchst aber x-max Linien im Canvas. Wenn Du nicht jeden Schleifendurchgang alle Linien löschst und neuzeichnen, sollte die Geschwindigkeit konstant bleiben.

Aber wenn ich das richtig verstehe und Du wirklich nur 5 Zustände (mit liniearen Übergängen) darstellen möchtest, warum zeichnest Du dann nicht einfach nur eine Linie zwischen zwei Zuständen? Entweder Du verlängerst sie mit Canvas.itemconfig oder ersetzt sie einfach.

Übrigens wird beim EKG die vorige Linie auch nicht gelöscht. Warum musst Du das machen? Übrigens wird der Bildaufbau Deines Beispielprogramms auf meinem PC auch nach 6-7 Durchgängen nicht merklich langsamer - es ist aber auch nicht der schlechteste Rechner. :-)

Ich habe Dein Programm jetzt zum Laufen bekommen und sehe es mir nochmal genauer an. Bis denn,

Michael
Diese Nachricht zersört sich in 5 Sekunden selbst ...
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

Wenn du die Anfangsgeschwindigkeit einfach so runterregeln möchtest, das die Linie immer gleich schnell weitergezeichnet wird, kannst du bem Programmstart ja bereits eine Linie in weiß (oder wie auch immer dein Hintergrund aussieht) zeichnen und die dann langsam weiterrücken, bis sie verschwunden, also gelöscht, ist, und durch die richtige ersetzt wurde.
Wenn du ein Bild als Hintergrund hast, kannst du für die Anfangslinie auch andere Koordinaten nehmen, die außerhalb des sichtbaren Bereichs liegen.
Benutzeravatar
Michael Schneider
User
Beiträge: 569
Registriert: Samstag 8. April 2006, 12:31
Wohnort: Brandenburg

Tommy hat geschrieben:Aber: Wenn er bei Null beginnt zu zeichnen, dann zeichnet er eben ziemlich schnell und wenn er z.B. bei Pixel 300 angekommen ist dann zeichnet er deutlich langsamer. Das zeichnen sollte in etwa zeitsynchron sein.
Hi Tommy,

hier ein kleiner Trick für gerade, vorhersagbare Graphen wie Deine; sehr performance- und speicherfreundlich: zeichne den Graphen komplett und verdecke nur den rechten, noch nicht sichtbaren Bereich mit einem hintergrundfarbenen Rechteck. Etwa so:

Code: Alles auswählen

import Tkinter as TK

iOffsetY = 150
dData = {"x":0, "item1":0}

#   Liste der T-Werte initialisieren -> TODO: aus Datei importieren
lT =  [  0, 80, 80, 160, 160, 240, 240, 320, 320, 400]

#   Liste der Y-Werte initialisieren -> TODO: aus Datei importieren
lY1 = [100, 100, -100, -100, 100, 100, -100, -100, 100, 100]

#   einfache Additionsfunktion definieren
add = lambda a, b: a+b

#   Offset einrechnen
lY1 = map(lambda a: a+iOffsetY, lY1)

#   Listenelemente mit zip paarweise und dann mit reduce zu einer Liste zusammenfuegen
lGraph1 = reduce(add, zip(lT, lY1))

#   grafische Oberflaeche aufbauen
wTk = TK.Tk()
wCanvas = TK.Canvas(wTk, width=400, height=300)
wCanvas.grid(row=0, column=0)
wCanvas.create_line(lGraph1)

def update_mask(iX, iPrevItem=0):
	#	voriges Rechteck loeschen, wenn vorhanden
	if iPrevItem:
		wCanvas.delete(iPrevItem)
	iRectItem = wCanvas.create_rectangle(iX, 0, 400, 300, fill="SystemButtonFace", outline="SystemButtonFace")
	return iRectItem

def update():
	"""Update aller Graphen alle 1/25 Sekunden"""
	if wTk.winfo_exists():
		dData["item1"] = update_mask(iX=dData["x"], iPrevItem=dData["item1"])
		dData["x"] = (dData["x"]+1)%400
		wCanvas.after(40, func=update)

update()
wTk.mainloop()
Ich denke der Code ist gut kommentiert. Noch Fragen? :D

Grüße,
der Michel
Diese Nachricht zersört sich in 5 Sekunden selbst ...
Tommy
User
Beiträge: 8
Registriert: Donnerstag 17. Mai 2007, 09:46

Hallo Michael.
Also das ist wirklich eine super Lösung für mein Problem, an so was hab ich noch gar nicht gedacht.
Das die Verläufe jetzt nur triviale Rechteckspannungen sind, hab ich jetzt nur so mal zum testen angenommen, die richtigen Graphen muss ich erst noch rausmessen, weil das Ding gibt es auch real 8)
Aber das ändert ja nichts daran, das ich es so machen kann wie du beschrieben hast.

Ich bin dir - und den anderen natürlich auch - überaus dankbar das ihr mir geholfen und Zeit investiert habt.
Ein großes DANKESCHÖN.

Gruß, Tommy.
Antworten