GUI Integration in Programm

Fragen zu Tkinter.
Antworten
TobiOrNotTobi
User
Beiträge: 3
Registriert: Mittwoch 26. Juni 2019, 20:44

Hallo beisammen,

ich bin ein Anfänger und hab ein kleines Programm zusammengezimmert und möchte nun eine GUI dafür zusammenbauen.

Das Programm ist für einen Barcode-Scanner gedacht, der die gescannten Daten als Keyboard-Eingabe zur Verfügung stellt.
Es wird auf eine Eingabe gewartet und je nach Input werden Werte gesetzt. Sind die Angaben für einen Eintrag vollständig, wird eine txt-Datei geöffnet und um den neuen Eintrag ergänzt. Grundsätzlich funktioniert dies.

Nun möchte ich erstmal eine GUI, die mir die aktuellen Eingaben darstellt. Später möchte ich die Richtung des Artikels (IN/OUT) auch mittels eines Buttons auf der GUI bestimmen können. Grade scheitert es jedoch am grundsätzlichen Einbinden der GUI in das vorhandene Programm.

Hat jemand eine gute Idee?
Bitte gerne auch Hinweise, wie man so etwas ordentlich aufbaut.

Code: Alles auswählen

import sys
import time
import datetime
import os

projekt = str("leer")
artikel = str("leer")
richtung = str("leer")
zeitstempel = str("leer")
#pfad = str("\\\Server\\02_Projekte\\Python_test\\")

kalenderwoche = datetime.date.today().isocalendar()[1]
kalenderjahr = time.strftime("%Y")
dateiname = str(kalenderjahr) + "KW" + str(kalenderwoche)

print (dateiname)

try:
    while True:
        code = input("Scan: ")
            
        if code == "IN" or code == "OUT":    
            richtung = code
            
        if code[0] == "P":
            projekt = code
            
        if code[0] == "A" and projekt != "leer" and richtung != "leer":
            artikel = code
            zeitstempel = time.strftime("%d.%m.%Y; %H:%M:%S")
            print ("Eingabe vollständig.")
            print (str(zeitstempel) + "; " + richtung + "; " + projekt + "; " + artikel + "; \n")
            
            datei = open(str(dateiname) + ".txt","a")
            
            datei.write(str(zeitstempel) + "; " + richtung + "; " + projekt + "; " + artikel + "; \n")

            datei.close()
            
        else:
            print("\n" "Eingabe unvollständig. \n""Erwartet werden in Reihenfolge: IN oder OUT, P xxxx und A xxxx")
            print ("Bereits vorhanden: " + str(zeitstempel) + "; " + richtung + "; " + projekt + "; " + artikel + "; \n")
            
except KeyboardInterrupt:
    print ("\nExit")

Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TobiOrNotTobi: Dein Programm ist doch etwas entfernt von dem was man für eine GUI braucht. Du hast nicht einmal eine einzige Funktion in Deinem Programm definiert. Mindestens eine hat jedes Programm, nämlich die Funktion in dem das Hauptprogramm steht. Das gehört genau so wenig wie Variablen auf Modulebene.

Auf Funktionen bauen dann Klassen auf, bei denen zusammengehörige Daten und Funktionen die auf diesen Daten operieren in Form einer Klasse mit Methoden zusammengeführt werden.

Klassen und objektorientierte Programmierung (OOP) braucht man dann für jede nicht-triviale GUI. Was dort dann neu dazu kommt, ist eine deutlich andere Ablaufsteuerung. Da kann man dann keine Schleifen mehr schreiben die ewig, oder auch nur lange laufen, denn das blockiert die GUI. Man schreibt bei GUIs keinen Code der am Stück linear abläuft, sondern Funktionen bzw. Methoden die von der GUI-Hauptschleife bei bestimmten Ereignissen aufgerufen werden, für die man diesen Code registriert. Also zum Beispiel gibt man eine Methode an die aufgerufen wird, wenn der Benutzer eine Schaltfläche anklickt, oder wenn er in einem Eingabefeld die Eingabetaste auf der Tastatur drückt. Dann darf diese Methode *kurz* etwas machen, und gibt dann die Kontrolle an die GUI-Hauptschleife zurück. Da man sich Zustand über solche Rückrufe hinweg merken muss, kommt man um OOP nicht herum.

Zum bisherigen Programm: `sys` und `os` werden importiert, aber nicht verwendet.

Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Die Zeichenkette 'leer' ist relativ willkürlich gewählt. Hier würde man eher `None` für Namen vewenden für die man an der Stelle noch keinen endgültigen Wert hat.

Sämtliche `str()`-Aufrufe sind letztendlich überflüssig. Nur an einer einzigen Stelle kann man jetzt schon alle einfach ersatzlos streichen. Und die Stelle wo tatsächlich etwas anderes als eine Zeichenkette übergeben wird, sollte man das gar nicht machen müssen, weil man das Datumsobjekt als ganzes als Zeichenkette formatieren kann.

Das ist auch eine komische Mischung zwischen `datetime` und `time`. Das `time`-Modul braucht man hier nicht, man kann das alles mit dem `datetime`-Modul erledigen.

Zeichenketten und Werte mit `str()` und ``+`` zusammenstückeln ist auch eher BASIC als Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode und ab Python 3.6 f-Zeichenkettenliteralen.

Den Dateinamen würde man komplett mit '.txt'-Endung aus *einem* `datetime.date`-Objekt erstellen, das man in eine Zeichenkette hineinformatiert.

Zwischen Funktion und öffnender Klammer der Argumentliste gehört kein Leerzeichen.

Die ganzen ``if``\s die den Inhalt von `code` prüfen sollten bis auf das erste ``elif``\s sein.

Das ``else`` ist dann falsch und gehört mit der Prüfung der Vollständigkeit kein in den Zweig der bei 'A…' genommen wird.

Auf 'IN' oder 'OUT' kann man kürzer prüfen wenn man eine Liste und den ``in``-Operator verwendet statt zwei Vergleiche zu schreiben.

Tests wie ``code[0] == 'P'`` fallen auf die Nase wenn `code` die leere Zeichenkette ist. Wenn also jemand aus versehen (oder absichtlich) einfach nur die Eingabetaste drückt, bricht das gesamte Programm mit einem `IndexError` ab. Um zu prüfen ob eine Zeichenkette mit einer anderen anfängt, gibt es die `startswith()`-Methode, die auch funktioniert wenn die Zeichenkette leer ist.

Wenn alle Daten vollständig sind wird die gleiche Zeichenkette zweimal zusammengesetzt. Solche Wiederholungen macht man nicht. Die setzt man einmal zusammen, bindet das Ergebnis an einen Namen und benutzt den dann zweimal. Wiederholungen von Code und Daten machen Mehrarbeit sind fehleranfällig wenn man Änderungen vornehmen will/muss, weil man alle Kopien auf die gleiche Art ändern muss.

In dem Zeitstempel kommt ein Semikolon vor – das ist *ein* Zeitstempel, den sollte man nicht auf zwei Felder aufteilen. Das macht eine spätere Weiterverarbeitung nur unnötig schwer.

Bei Textdateien sollte man immer explizit eine Kodierung angeben.

Mit der ``with``-Anweisung stellt man sicher, dass die Datei auch unter allen Umständen wieder geschlossen wird, also insbesondere auch wenn ein Fehler zwischen `open()` und `close()` auftritt.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import datetime


def main():
    dateiname = f'{datetime.date.today():%YKW%V}.txt'
    print(dateiname)

    zeitstempel = None
    richtung = None
    projekt = None
    artikel = None
    try:
        while True:
            code = input('Scan: ')
            
            if code in ['IN', 'OUT']:
                richtung = code
            elif code.startswith('P'):
                projekt = code
            elif code.startswith('A'):
                if projekt and richtung:
                    artikel = code
                    print('Eingabe vollständig.')
                    zeitstempel = f'{datetime.datetime.now():%d.%m.%Y %H:%M:%S}'
                    line = f'{zeitstempel};{richtung};{projekt};{artikel}\n'
                    print(line)
                    
                    with open(dateiname, 'a', encoding='utf-8') as datei:
                        datei.write(line)
                else:
                    print(
                        '\nEingabe unvollständig.\n'
                        'Erwartet werden in Reihenfolge: IN oder OUT, P xxxx'
                        ' und A xxxx'
                    )
                    print('Bereits vorhanden:')
                    print('zeitstempel:', zeitstempel)
                    print('   richtung:', richtung)
                    print('    projekt:', projekt)
                    print('    artikel:', artikel)
    except KeyboardInterrupt:
        print('\nExit')


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
TobiOrNotTobi
User
Beiträge: 3
Registriert: Mittwoch 26. Juni 2019, 20:44

Danke schön für die Hinweise.

Ich hab den Code nochmal etwas überarbeitet.
Nur steht bei mir immer noch grundsätzlich ein großes Fragezeichen, wie ich die GUI und das Programm zusammenbringe.
Dass das Warten auf Eingabe und GUI Ablauf sich gegenseitig behindern, so wie ich es mir vorgestellt habe, hab ich auch schon bemerkt. Die Frage die sich mir stellt, hab ich in der main() eine Funktion, die auf Tastatureingabe oder GUI Eingabe wartet und anschließend nach dem Abarbeiten der Eingabe wieder auf eine neue Eingabe wartet?
Ich bekomme grade gedanklich die beiden Schleifen logisch nicht zusammen.

Code: Alles auswählen

#!/usr/bin/env python3
import datetime
import tkinter as tk

def print_fehler(zeitstempel, richtung, projekt, artikel):
    print(
        '\nEingabe unvollständig.\n'
        'Erwartet werden in Reihenfolge: IN oder OUT, P xxxx'
        ' und A xxxx'
        )
    print('Bereits vorhanden:')
    print('Zeitstempel:', zeitstempel)
    print('   Richtung:', richtung)
    print('    Projekt:', projekt)
    print('    Artikel:', artikel)
    print('')

def code_scannen(zeitstempel, richtung, projekt, artikel, dateiname):
 
    code = input('Scan: ')
    
    if code in ['IN', 'OUT']:
        richtung = code
        print_fehler(zeitstempel, richtung, projekt, artikel)    
    elif code.startswith('P '):
        projekt = code
        print_fehler(zeitstempel, richtung, projekt, artikel)   
    elif code.startswith('A '):
        if projekt and richtung:
            artikel = code
            print('Eingabe vollständig.')
            zeitstempel = f'{datetime.datetime.now():%d.%m.%Y %H:%M:%S}'
            line = f'{zeitstempel};{richtung};{projekt};{artikel}\n'
            print(line)
                    
            with open(dateiname, 'a', encoding='utf-8') as datei:
                datei.write(line)
            artikel = None   
    else:
        print(
        '\nEingabe unbekannt.\n'
        'Erwartet werden in Reihenfolge: IN oder OUT, P xxxx'
        ' und A xxxx \n'
        )

    return(zeitstempel, richtung, projekt, artikel)
    
def main():
    dateiname = f'{datetime.date.today():%YKW%V}.txt'
    print(dateiname)
    
    zeitstempel = None
    richtung = None
    projekt = None
    artikel = None

    fenster = tk.Tk()
    fenster.title(dateiname)
    fenster.geometry('350x145')
    
    (zeitstempel, richtung, projekt, artikel) = code_scannen(zeitstempel, richtung, projekt, artikel, dateiname)
    w = tk.Label(fenster, text = f'{zeitstempel};{richtung};{projekt};{artikel}\n')

    w.pack() 
    fenster.mainloop()
    
if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@TobiOrNotTobi: Die Scheifen gehen auch logisch nicht zusammen. Du brauchst Eingabefelder in Deiner GUI, die dann entsprechend reagieren, wenn der Scanner scannt.

Muß man eigentlich immer alles Scannen oder reicht es, einmal IN/OUT und Projekt zu scannen und kann dann mehrere Artikel bearbeiten?
TobiOrNotTobi
User
Beiträge: 3
Registriert: Mittwoch 26. Juni 2019, 20:44

Als Eingabe habe ich erstmal nur den Scanner. Und es soll in der tatsächlichen Anwendung auch möglichst nur der Scanner genutzt werden.
Das Interface soll eigentlich nur nochmal abbilden, welche Werte für einen Eintrag bereits eingescannt wurden und welche letzten vollständigen Einträge bereits erfolgt sind.
Als Wunsch für eine spätere Optimierung wäre die Auswahl IN/OUT über die GUI und die Eingabe einer Stückzahl, wenn diese mit der Anzahl der Scannvorgänge nicht mehr sinnvoll abbildbar ist.

Konkret zur Frage: Beim ersten Mal alles scannen. Bei den folgenden Durchgängen reicht ein Artikel. Nur wenn Richtung oder Projekt sich ändern müssten diese neu gescannt werden.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Da der Scanner eine Tastatur ist, ist das einfachste erstmal, ein Eingabefeld für die Eingabe zu verwenden. GUI-Programme funktionieren komplett anders, als Dein Konsolenprogramm.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Im ersten Schritt könnte man für das Konsolenprogramm mal die Programmlogik, so winzig sie auch sein mag, von der Benutzerinteraktion trennen. Dann kann man den Teil nämlich leichter durch eine GUI ersetzen. Komplett ungetestet (verwendet das externe `attr`-Modul/`attrs`-Paket):

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import date as Date, datetime as DateTime

from attr import attrib, attrs


@attrs
class Warenbewegung:

    dateiname = attrib()
    richtung = attrib(default=None)
    projekt = attrib(default=None)
    artikel = attrib(default=None)

    @property
    def ist_komplett(self):
        return self.richtung and self.projekt and self.artikel

    def speichern(self):
        try:
            if self.ist_komplett:
                line = (
                    f'{DateTime.now():%d.%m.%Y %H:%M:%S};'
                    f'{self.richtung};{self.projekt};{self.artikel}\n'
                )
                with open(self.dateiname, 'a', encoding='utf-8') as file:
                    file.write(line)
                return line
            else:
                raise ValueError('Datensatz unvollständig')
        finally:
            self.artikel = None


def main():
    warenbewegung = Warenbewegung(f'{Date.today():%YKW%V}.txt')
    print(warenbewegung.dateiname)
    
    while True:
        code = input('Scan: ')
        if code in ['IN', 'OUT']:
            warenbewegung.richtung = code
        elif code.startswith('P '):
            warenbewegung.projekt = code
        elif code.startswith('A '):
            warenbewegung.artikel = code
            try:
                line = warenbewegung.speichern()
                print(line)
            except ValueError as error:
                print(error)
                print(warenbewegung)
        else:
            print(f'Unbekannte Eingabe: {code!r}')
            print(
                'Erwartet werden in Reihenfolge IN oder OUT, P xxxx,'
                ' und A xxxx.'
            )
        
        print(warenbewegung)


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Nahezu ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import csv
from functools import partial
import tkinter as tk
from tkinter.messagebox import showerror
from datetime import date as Date, datetime as DateTime

from attr import attrib, attrs


@attrs
class Warenbewegung:

    dateiname = attrib()
    richtung = attrib(default=None)
    projekt = attrib(default=None)
    artikel = attrib(default=None)

    @property
    def ist_komplett(self):
        return self.richtung and self.projekt and self.artikel

    def speichern(self):
        try:
            if self.ist_komplett:
                with open(self.dateiname, 'a', encoding='utf-8') as file:
                    csv.writer(file, delimiter=';').writerow(
                        [
                            format(DateTime.now(), '%d.%m.%Y %H:%M:%S'),
                            self.richtung,
                            self.projekt,
                            self.artikel,
                        ]
                    )
            else:
                raise ValueError('Datensatz unvollständig')
        finally:
            self.artikel = None


class ScannerFrame(tk.Frame):
    
    def __init__(self, parent, warenbewegung):
        tk.Frame.__init__(self, parent)
        self.warenbewegung = warenbewegung
        frame = tk.Frame(self)
        tk.Label(frame, text='Scan').pack(side=tk.LEFT)
        self.scan_entry = tk.Entry(frame)
        self.scan_entry.pack(side=tk.LEFT)
        self.scan_entry.bind('<Return>', self.on_scan)
        tk.Button(frame, text='Ok', command=self.on_scan).pack(side=tk.LEFT)
        frame.pack()
        
        self.richtung_var = tk.StringVar()
        frame = tk.LabelFrame(self, text='Richtung')
        for richtung in ['IN', 'OUT']:
            tk.Radiobutton(
                frame,
                text=richtung,
                value=richtung,
                variable=self.richtung_var,
                command=partial(self.on_change, 'richtung'),
            ).pack(side=tk.LEFT)
        frame.pack()
        
        frame = tk.Frame(self)
        for row, attribute_name in enumerate(['projekt', 'artikel']):
            self._create_text_entry(attribute_name, frame, row)
        frame.pack()
        self.scan_entry.focus()

    def _create_text_entry(self, attribute_name, parent, row):
        variable = tk.StringVar()
        setattr(self, attribute_name + '_var', variable)
        tk.Label(
            parent, text=attribute_name.title()
        ).grid(row=row, column=0, sticky=tk.E)
        entry = tk.Entry(parent, textvariable=variable)
        entry.grid(row=row, column=1, sticky=tk.W)
        entry.bind('FocusOut', partial(self.on_change, attribute_name))
        
    def update_warenbewegung_display(self):
        for attribute_name in ['richtung', 'projekt', 'artikel']:
            value = getattr(self.warenbewegung, attribute_name)
            getattr(self, attribute_name + '_var').set(value or '')

    def on_change(self, attribute_name, _event=None):
        value = getattr(self, attribute_name + '_var').get()
        setattr(self.warenbewegung, attribute_name, value)
        self.update_warenbewegung_display()

    def on_scan(self, _event=None):
        code = self.scan_entry.get()
        if code in ['IN', 'OUT']:
            self.warenbewegung.richtung = code
        elif code.startswith('P '):
            self.warenbewegung.projekt = code
        elif code.startswith('A '):
            self.warenbewegung.artikel = code
            try:
                line = self.warenbewegung.speichern()
                print(line)
            except ValueError as error:
                showerror('Kann nicht speichern', str(error))
        else:
            showerror(
                'Unbekannte Eingabe!',
                f'Unbekannte Eingabe: {code!r}\n\n'
                f'Erwartet werden in Reihenfolge IN oder OUT, P xxxx,'
                f' und A xxxx.'
            )
        
        self.update_warenbewegung_display()
        self.scan_entry.delete(0, tk.END)
        self.scan_entry.focus()


def main():
    warenbewegung = Warenbewegung(f'{Date.today():%YKW%V}.txt')
    root = tk.Tk()
    root.title(warenbewegung.dateiname)
    ScannerFrame(root, warenbewegung).pack()
    root.mainloop()


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten