Konsolenausgabe in tkinter ausgeben

Fragen zu Tkinter.
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo zusammen

Ein Programm für die Konsole (Terminal), das ich vor längerem geschrieben habe, möchte ich jetzt gerne in einem Programm das mit tkinter läuft ablaufen lassen.
Dabei möchte ich gerne, das was bisher in der Konsole ausgegeben wurde, in tkinter direkt ausgeben lassen.

Welche Tipps könnt Ihr mir dazu geben?

Grüße Nobuddy
yipyip
User
Beiträge: 418
Registriert: Samstag 12. Juli 2008, 01:18

1. Die Frage etwas konkreter stellen? ;-)
2. Dass man Logik und Userinterface trennen sollte, ist dir ja bekannt.
Wenn man das richtig organisiert, kann man auch die Ausgabekomponenten austauschen. (Stichwort "Dependency Injection")
3. Man sollte sollte sich fuer ein einfaches, sequentiell ablaufendes Programm natuerlich auch die Frage stellen: Lohnt sich das ueberhaupt oder ist es sinnvoll?
4. Alles weitere sollte man am konkreten Problem festmachen.

:wink:
yipyip
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo yipyip

Danke für Deine Meldung, auch wenn mein Post kurz und nicht konkret genug war. :wink:

Dein Input, hat mir zu Denken gegeben, so daß ich Abstand von einer Konsolenausgabe in meinem tkinter-Programm genommen habe.

Aktuell ist, wie ich die Printausgabe von einem importierten Modul in tkinter ausgeben lassen kann?
Und das nicht zeit-versetzt, also nicht wenn das Modul komplett durchgelaufen ist erst die Ausgabe.

Gibt es da erste Tipps, oder ist das wieder nicht konkret genug?

Grüße Nobuddy
BlackJack

@Nobuddy: ``print`` verwendet `sys.stdout` zur Ausgabe. Das müsstest Du durch ein eigenes Objekt ersetzen was die Daten entgegennimmt statt sie auf der Konsole auszugeben, oder was auch immer die Daten von der Standardausgabe an der anderen Seite in Empfang nimmt. Wenn die Funktionen in dem anderen Modul nicht lange laufen, also die GUI nicht blockieren, kannst Du die Daten dann einfach in ein Text-Widget einfügen. Sonst müssten die Funktionen in einem anderen Thread laufen und Dein Objekt müsste die Daten in eine `Queue.Queue` schreiben von wo sie aus dem GUI-Code mittels der `after()`-Methode regelmässig abgeholt und in das Textfeld übertragen werden.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@Blackjack: besser hätt ichs auch nicht sagen können :twisted:
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi Nobuddy

Hier noch ein Link der für dich beim Studium interessant sein könnte:
Umleiten der print Ausgabe
Geschrieben in englisch aber die Python-Schnipsel sind gut lesbar.

Gruss wuf :wink:
Take it easy Mates!
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo zusammen

Danke für Eure Tipps, werde mich da mal an die Arbeit machen. :wink:

Melde mich wieder!

Grüße Nobuddy
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi Nobuddy

Hier etwas zum herum experimentieren. Skrip noch ohne Kosmetik:

Code: Alles auswählen

#!/usr/bin/env python
# coding: UTF-8

import sys

try:
    #~~ For Python 2.x
    import Queue as qu
    import Tkinter as tk
except ImportError:
    #~~ For Python 3.x
    import queue as qu
    import tkinter as tk

class TeeStd(object):
    def __init__(self, queue, type, name, mode):
        
        self.queue = queue
        self.file = open(name, mode)
        if type == 'stderr':
            self.out_obj = sys.stderr
            sys.stderr = self
        else:
            self.out_obj = sys.stdout
            sys.stdout = self
        
    def write(self, data):
        # Ausgabe in eine Datei
        #self.file.write(data)
        # Ausgabe auf das Standart-Terminal
        #self.out_obj.write(data)
        # Ausgabe auf Tk-Textbox
        self.queue.put(data)
        
APP_WIN_XPOS = 50
APP_WIN_YPOS = 50

class App(object):
    
    def __init__(self):
        self.win = tk.Tk()
        self.win.geometry('+{0}+{1}'.format(APP_WIN_XPOS, APP_WIN_YPOS))
        self.win.protocol("WM_DELETE_WINDOW", self.close)

        self.queue = qu.Queue()
        
        self.text = tk.Text(self.win, width=80, height=20, highlightthickness=0,
            bd=0, bg='white', relief='sunken', padx=5, pady=5)
        self.text.pack()

        print_button = tk.Button(self.win, text='Print',
            command=self.print_trigger)
        print_button.pack()
                
        err_button = tk.Button(self.win, text='Error',
            command=self.error_trigger)
        err_button.pack()
        
        self.sampler()
        
    def print_trigger(self):
        print('Good day mate! How are you doing!')
    
    def error_trigger(self):
        import NoExistModul
                
    def sampler(self):

        if self.queue.qsize():
            #~~ In der Queue sind Daten vorhanden!
            try:
                data = self.queue.get()
                self.text.insert('end', data)
                self.queue.task_done()
            except qu.Empty:
                pass

        self.win.after(50, self.sampler)
        
    def run(self):
        self.win.mainloop()
    
    def close(self):
        """Process before shutdown"""
        self.win.destroy()         

app = App()
app.win.title("New Terminal")

TeeStd(app.queue, 'stderr', "log.txt", "a")
TeeStd(app.queue, 'stdout', "log.txt", "a")

print ('Hello')

app.run()
Gruss wuf :wink:
Take it easy Mates!
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo wuf

Sorry, daß ich mich erst jetzt zurück melde, hatte noch ein paar andere Baustellen zuvor. :wink:

Danke für Deinen Code, funktioniert so wie ich es mir vorgestellt habe.
Ein paar Fragen habe ich noch dazu.
Die Funktion 'print_trigger(self)' in der Class 'App', übernimmt den Empfang für die Ausgabe im 'NewTerminal.
Die Funktion 'write(self, data)' in der Class 'TeeStd' ist für die Ausgabe zuständig, der Du noch weitere Optionen eingefügt hast.
Habe ich das so richtig verstanden?

Dein Code funktioniert als Modul, so daß ich die Printausgaben an die Funktion 'print_trigger(self)' in der Class 'App' übergeben muß.
Ist das soweit richtig?

Sollte ich da völlig falsch liegen, würde ich mich über eine kurze Erklärung darüber freuen.

Grüße Nobuddy
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi Nobuddy

a) Beim aktivieren des Buttons 'Print' wird mittels 'print_trigger(self)' ein Printbefehl ausgelöst. Ist nur für Testzwecke. Das gleiche gilt für den Button 'Error'. Hier wird 'error_trigger(self)' aufgerufen womit ein nicht existierendes Modul importiert wird. Dies löst eine Exception aus. Auch nur für Testzwecke. Das heisst diese Print- und Exception-Ausgaben werden für dessen Anzeige in die Textbox katapultiert.

b) 'write(self, data)' ist für die Ausgabe der Print- und Exception-Daten, die in eine Queue geschickt werden. Die Queue wird dann durch 'def sampler(self)' zyklisch nach Daten abgefragt und an die Textbox weitergeleitet.

c) Für die Ausgabe von Print- bzw. Exceptiondaten musst du 'print_trigger(self)' nicht mehr aufrufen. Diese werden von hier an:

Code: Alles auswählen

TeeStd(app.queue, 'stderr', "log.txt", "a")
TeeStd(app.queue, 'stdout', "log.txt", "a")
automatisch in die Textbox geschrieben.

Gruß wuf :wink:
Take it easy Mates!
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo wuf

Danke für Deine Antwort, da war ich völlig daneben.

Was mir noch nicht klar ist, wie ich von einem Modul die Printausgabe ausgeben lasse?

Beispiel update_vendor.py gibt folgende Printausgabe aus:
Es werden von 12 Lieferanten Produktdaten aktualisiert.

Lieferant 001 27000 Datensätze
Lieferant 002 7000 Datensätze
Lieferant 003 37000 Datensätze
...
..
.
Grüße Nobuddy
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

@Nobuddy: Ich kenne das Modul update_vendor.py nicht, verstehe aber wohl was damit gemeint ist. Hast du mehr Info zu diesem Modul?

Gruß wuf :wink:
Take it easy Mates!
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo wuf

Wie Du schon vermutest, ist 'update_vendor.py' ein eigenes Modul, das Lieferantendaten verarbeitet.
Dazu kommen weitere Module für bestimmte Aufgaben in Einsatz, die in 'update_vendor.py' aufgerufen werden.

Ich poste Dir mal den Code, wobei er für Dich nicht lauffähig sein wird, da Dir die restlichen Daten dazu fehlen.

Code: Alles auswählen

#!/usr/bin/python3
# coding: UTF-8

import os
from datetime import date
now = date.today()
heute = now.strftime('%Y.%m.%d-')

from z_list_definition import items_def
from __modul_bericht__ import info, leerzeile, trennzeile
from __modul_abfrage__ import abfrage


# {}liste.txt
base_daten_path = os.path.join(os.path.dirname(__file__),
                            'base_daten', '{}liste.txt'.format(heute))
# LISTE-HERSTELLER-EAN.txt
liste_hersteller_ean_path = os.path.join(os.path.dirname(__file__),
                                    'base', 'LISTE_HERSTELLER_EAN.txt')


### Datenverarbeitung von Lieferanten-Quelldatei
info('suppliers_list')
from suppliers_list import suppliers_data_load
result = suppliers_data_load()


### Nachbearbeitung
if result > 0:

    # Aktualisieren von Herstellernamen
    from producer_update import get_producer
    info('Aktualisierung von Herstellernamen - producer_update')
    info('Liste liste.txt')
    hersteller = items_def('base_daten_path', 'hersteller')
    get_producer(base_daten_path, hersteller, 'yes')
    #get_producer(quellliste_path, 4, 'yes')

    info('Liste LISTE-HERSTELLER-EAN.txt')
    leerzeile()  
    hersteller = items_def('liste_hersteller_ean_path', 'hersteller')
    get_producer(liste_hersteller_ean_path, hersteller, 'yes')


    # Aktualisieren von Herstellernamen über EAN-Code
    # die z.B. 'LIEFERANT' heißen.
    # producer_name('NAMEN') entsprechend anpassen
    info('Aktualisieren von Herstellernamen durch EAN-Code.')
    info('producer_ean')
    leerzeile()
    from producer_ean import get_producer_ean_name
    get_producer_ean_name('LIEFERANT')


    # Aktualisieren von Herstellernamen über Herstellerliste
    # die z.B. 'LIEFERANT' heißen.
    # producer_name('NAMEN') entsprechend anpassen
    info('Aktualisieren von Herstellernamen laut Bennenung.')
    info('producer_name')
    leerzeile()
    from producer_name import get_producer_name
    get_producer_name('LIEFERANT')

info('End of update_vendor')
Das Modul __modul_bericht__.py habe ich bisher für die Ausgabe und die Archivierung in eine Datei verwendet.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

import os
from datetime import date
now = date.today()
heute = now.strftime('%Y.%m.%d-')
import time
uhr = time.strftime('Time %H:%M:%S')

from __modul_write__ import write_tmp, writeappend_tmp


# {}bericht.txt
base_bericht_path = (os.path.join(os.path.dirname(__file__),
                        'base_daten', '{}bericht.txt'.format(heute)))

def infoneu():
    write_tmp(base_bericht_path, uhr)
    leerzeile()

def info(string):
    print(string)
    writeappend_tmp(base_bericht_path, uhr)
    leerzeile()
    writeappend_tmp(base_bericht_path, string)

def leerzeile():
    print('')
    writeappend_tmp(base_bericht_path, '')

def trennzeile():
    print('')
    print(35 * '-')
    print('')
    leerzeile()
    writeappend_tmp(base_bericht_path, 35 * '-')
    leerzeile()
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi Nobuddy

Danke für deine Modul-Schnippsel. Aber wie du richtig vermutet hast werde ich diese nicht verwenden, da ich schlicht zu wenig Zeit habe diese so zurechtzustutzen um für meine folgende Demo einzusetzen. Mein obiges Schnippsel, welches du schon kennst ist als reiner Test-Prototyp zum experimentieren und nicht als fertiges Modul gedacht. Da braucht es natürlich noch ein wenig Arbeitsaufwand. Zum Beispiel die Textbox in einem Toplevelfenster zu platzieren.

Um zu zeigen wie die Umleitung der 'print'-Standard-Ausgabe auf deine Tk-Textbox-Ausgabe funktioniert habe ich ein eigenes kleines Modul zusammengebraut. Sein Dateiname ist vendors.py und wird wie folgt importiert:

Code: Alles auswählen

from vendors import MyVendors
Der Inhalt von vendors.py ist:

Code: Alles auswählen

#!/usr/bin/env python
# coding: UTF-8

class MyVendors(object):
    
    def __init__(self):
        
        self.vendors = list()      
    
    def update_vendors(self, new_vendor=None):
        
        if new_vendor != None:
            self.vendors.append('Vendor-{0}: {1}'.format(len(self.vendors)+1,
                new_vendor))
            
        for vendor in self.vendors:
            print(vendor)
        print()
Das Demoskript ist:

Code: Alles auswählen

#!/usr/bin/env python
# coding: UTF-8

import sys
from vendors import MyVendors

try:
    #~~ For Python 2.x
    import Queue as qu
    import Tkinter as tk
except ImportError:
    #~~ For Python 3.x
    import queue as qu
    import tkinter as tk
        
class TeeStd(object):
    def __init__(self, queue, type):
       
        self.queue = queue

        if type == 'stderr':
            self.std_obj = sys.stderr
            sys.stderr = self
        else:
            self.std_obj = sys.stdout
            sys.stdout = self
       
    def write(self, data):
        # Ausgabe auf Tk-Textbox
        self.queue.put(data)
       
APP_WIN_XPOS = 50
APP_WIN_YPOS = 50

class App(object):
   
    def __init__(self):
        self.win = tk.Tk()
        self.win.geometry('+{0}+{1}'.format(APP_WIN_XPOS, APP_WIN_YPOS))
        self.win.protocol("WM_DELETE_WINDOW", self.close)

        self.queue = qu.Queue()
       
        self.text = tk.Text(self.win, width=80, height=20, highlightthickness=0,
            bd=0, bg='white', relief='sunken', padx=5, pady=5)
        self.text.pack()

        vendor_button = tk.Button(self.win, text='Vendor hinzufügen',
            command=self.add_vendor)
        vendor_button.pack()
               
        self.win.title("My Stdout/Stderr Terminal")

        # Umleitung der Standard-Ausgabe auf meine Textbox
        TeeStd(self.queue, 'stderr')
        TeeStd(self.queue, 'stdout')

        self.my_vendors = MyVendors()
               
        self.sampler()
       
    def add_vendor(self):
        self.my_vendors.update_vendors('Nobuddy')
   
    def sampler(self):

        if self.queue.qsize():
            # In der Queue sind Daten vorhanden!
            try:
                data = self.queue.get()
                self.text.insert('end', data)
                self.queue.task_done()
            except qu.Empty:
                pass

        self.win.after(50, self.sampler)
       
    def run(self):
        self.win.mainloop()
   
    def close(self):
        """Process before shutdown"""
        self.win.destroy()        

App().run()
Gruß wuf :wink:
Take it easy Mates!
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo wuf

Keine Frage, den Code meiner Module muß ich schon selbst zurechtstutzen, ist eh noch Code vom Anfang meines Schaffens, den ich dringend überarbeiten muß. :wink:

Herzlichen Dank für Deine Mühe, werde mir das in Ruhe durchschauen und testen!

Grüße Nobuddy
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo wuf

Dein Demoscript funktioniert prächtig, nicht nur mit Deinem selbst zusammengebrauten Modul, sondern auch mit meinen Modulen! :D

Ich habe noch 2 kleiner Änderungen vorgenommen.
Ich habe der Class App, def __init__(self, check) einen Parameter spendiert, der den Funktionsaufruf beinhaltet.
So lässt sich dann von einem anderen Modul aus, Dein Demoscript ansprechen.

Code: Alles auswählen

from gui_terminal import App

App(MyVendors).run()
Den Button verwende ich jetzt zum Schließen des Fensters und benutze dazu die Funktion 'close'.
Die Funktion 'add_vendor' habe ich entfernt.
Den Status des Button habe deaktiviert, soll erst nach Beendigung der übergebenen Funktion, zum Schließen Aktiviert werden, damit kein versehentlicher Abbruch möglich ist.
Allerdings gestaltet sich das Positionieren der Button-Aktivierung

Code: Alles auswählen

close_button['state']="normal"
schwierig, habe dafür noch keine Lösung gefunden.

Poste mal Dein Demoscripft mit den besagten Änderungen:

Code: Alles auswählen

#!/usr/bin/env python
# coding: UTF-8

import sys

try:
    #~~ For Python 2.x
    import Queue as qu
    import Tkinter as tk
except ImportError:
    #~~ For Python 3.x
    import queue as qu
    import tkinter as tk
       
class TeeStd(object):
    def __init__(self, queue, type):
       
        self.queue = queue

        if type == 'stderr':
            self.std_obj = sys.stderr
            sys.stderr = self
        else:
            self.std_obj = sys.stdout
            sys.stdout = self
       
    def write(self, data):
        # Ausgabe auf Tk-Textbox
        self.queue.put(data)
       
APP_WIN_XPOS = 50
APP_WIN_YPOS = 50

class App(object):
   
    def __init__(self, check):
        self.win = tk.Tk()
        self.win.geometry('+{0}+{1}'.format(APP_WIN_XPOS, APP_WIN_YPOS))
        self.win.protocol("WM_DELETE_WINDOW", self.close)

        self.queue = qu.Queue()
       
        self.text = tk.Text(self.win, width=80, height=20, highlightthickness=0,
            bd=0, bg='white', relief='sunken', padx=5, pady=5)
        self.text.pack()

        close_button = tk.Button(self.win, text='Fenster schließen',
            command=self.close)
        close_button.pack()
        close_button['state']="disabled"
               
        self.win.title("My Stdout/Stderr Terminal")

        # Umleitung der Standard-Ausgabe auf meine Textbox
        TeeStd(self.queue, 'stderr')
        TeeStd(self.queue, 'stdout')

        self.check = check()
        #if ...:
            #close_button['state']="normal"
               
        self.sampler()
   
    def sampler(self):

        if self.queue.qsize():
            # In der Queue sind Daten vorhanden!
            try:
                data = self.queue.get()
                self.text.insert('end', data)
                self.queue.task_done()
            except qu.Empty:
                pass

        self.win.after(50, self.sampler)
       
    def run(self):
        self.win.mainloop()
   
    def close(self):
        """Process before shutdown"""
        self.win.destroy()     
Grüße Nobuddy
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Code: Alles auswählen

def sampler(self):
    if self.queue.qsize():
        # In der Queue sind Daten vorhanden!
        try:
            data = self.queue.get()
            self.text.insert('end', data)
            self.queue.task_done()
        except qu.Empty:
            pass
Hier hast du gleich mehrere Fehler eingebaut: Wenn du erst die Größe der Warteschlange abfragst und dann ein get machst, dann hast du dir eine prima Race Condition zusammengebaut. Angenommen du erweiterst dein Programm und an einer anderen Stelle werden noch Elemente aus der Queue abgefragt. Dann kann es passieren, dass die Bedingung im if wahr wird, der andere Thread holt sich das Element aus der Schlange und nun wird das get in sampler aufgerufen. Da get blockierend ist, bleibt deine gedamte GUI stehen. Wie lange hängt natürlich davon ab, wann das get in sampler mal wieder Glück hat ein Element zu erwischen. Wirf einen Blick in die Dokumentation von Queue und überlege dir, wie du es richtig machst.

Weiter kann es bei einem get ohne Parameter niemals vorkommen, dass eine Empty-Exception geworfen wird. Du solltest deine Programme so weit testen, dass auch alle Programmzweige mal abgelaufen werden. In diesem Fall hast du das offenbar nicht getan und setzt auf Hoffen und Beten. Oder ein wenig von beidem...
Das Leben ist wie ein Tennisball.
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

Hallo EyDu

Ich habe den Code, aus Wufś Demoscript genommen.
Ich bin etwas ratlos, da ich mit der Materie Queue noch nicht vertraut bin.
Ich muß wohl noch einige male die deutschsprachige Beschreibung von Galileo http://openbook.galileocomputing.de/pyt ... .htm#t2t34 lesen, vielleicht komme ich dann auf die Lösung des Problems, welches Du ansprichst.

Hoffen und Beten, sind nicht immer verkehrt, aber bestimmt nicht hier! :wink:

Grüße Nobuddy
Nobuddy
User
Beiträge: 996
Registriert: Montag 30. Januar 2012, 16:38

@EyDu, habe zigmale Queue in Galileo durchgelesen, muß aber hier passen. :K

Wie sollte dies nach Deiner Kenntnis aussehen?

Code: Alles auswählen

def sampler(self):
    if self.queue.qsize():
        # In der Queue sind Daten vorhanden!
        try:
            data = self.queue.get()
            self.text.insert('end', data)
            self.queue.task_done()
        except qu.Empty:
            pass
yipyip
User
Beiträge: 418
Registriert: Samstag 12. Juli 2008, 01:18

@Nobuddy
Nicht den Galileo lesen, sondern die Original Doku: http://docs.python.org/2/library/queue.html :wink:

Dort steht sinngemaess: Queue.get() === Queue.get(True) blockt, d.h. an dieser Stelle wartet das Programm solange, bis wieder etwas aus der Queue ausgelesen werden kann. Queue.get(False) blockt nicht, d.h. entweder wird etwas ausgelesen oder falls nicht, eine Empty Exception ausgeloest. In einer Multithreading Umgebung sollte mit

Code: Alles auswählen

data = self.queue.get(False)
das Problem, das EyDu ansprach, wohl behoben sein. (Wenn ich das richtig verstehe.)
Bei dir kommt aber (erstmal ?) gar kein Multithreading vor, vielleicht kommt man dann auch mit list/deque aus.
:wink:
yipyip
Antworten