Code Aufbau: Abfrage User Eingabe, Valedierung der Eingabe und entsprechende Reaktion

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1176
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

ich stehe mal wieder vor einem Problem. Ich erstelle ein GUI mit Qt.
Der Benutzer gibt im besten Fall positive Zahlen mit Punkt anstatt mit Komma ein. In allen anderen Fällen muss ich darauf reagieren und ich habe gerade Probleme damit Funktionen zu schreiben, die wirklich nur eine Sache mache.
Bevor ich in einer halben Seite erkläre, wie das Problem genau aussieht, wäre hier ein vereinfachtes Beispiel. Es sind in echt 8 Eingabefelder.
Klickt der Benutzer auf den "Calculate"-Button wird die Funktion `check_for_calculation`aufgerufen.
Der Name 'check_set_user_entry`sagt ja schon, dass die Funktion zwei Sachen macht, weil ich es für mich nicht sinnvoll aufteilen konnte. Wenn ich eine Funktion schreibe `check_user_entry`dann kann ich alle Eingaben abfragen und `True`oder `False`zurück geben und wenn das passt, kann ich eine Funktion `set_user_entry`schreiben, aber dann frage ich wieder alle Eingabefelder ab. Die Prüfung entfällt dann, aber ich frage zwei mal die gleichen Eingabefelder ab. Macht man das so? Oder wie macht man das?
Hier der (vereinfachte) Code wie ich ihn habe:

Code: Alles auswählen

def show_user_entry_error(self, value, entry):
    pop_up = QMessageBox()
    pop_up.setIcon(QMessageBox.Warning)
    pop_up.setText(f"Check your entry for <{value}>. '{entry}' is not valid!")
    pop_up.setWindowTitle("No valid user entry")
    pop_up.setStandardButtons(QMessageBox.Ok)
    pop_up.exec()


def convert_to_float(self, string, description):
    try:
        converted = float(string.replace(",", "."))
        if converted <= 0:
            self.show_user_entry_error(description, converted)
            return False
    except ValueError:
        self.show_user_entry_error(description, string)
        return False
    return converted


def check_set_user_entry(self):
    self.outer_diameter = self.convert_to_float(self.ui.outer_diameter.text(), "Outer diameter")
    if not self.outer_diameter:
        return False



def set_yield_strength(self):
    if self.ui.unlock_settings.isChecked():
        self.yield_strength = float(
            self.material_properties[self.material][self.temperature]
        )
        return True
    else:
        self.yield_strength = self.convert_to_float(self.ui.yield_strength.text(), "Alternative Rp0,2")
        return True


def check_for_calculation(self):
    if not self.check_set_user_entry():
        return
    if not self.set_yield_strength():
        return
    self.thickness = calculate_something(
        self.outer_diameter,
    )
    self.show_result()
Die beschriebene Alternative habe ich so gemeint:

Code: Alles auswählen

def show_user_entry_error(self, value, entry):
    pop_up = QMessageBox()
    pop_up.setIcon(QMessageBox.Warning)
    pop_up.setText(f"Check your entry for <{value}>. '{entry}' is not valid!")
    pop_up.setWindowTitle("No valid user entry")
    pop_up.setStandardButtons(QMessageBox.Ok)
    pop_up.exec()


def convert_to_float(self, string, description):
    try:
        converted = float(string.replace(",", "."))
        if converted <= 0:
            self.show_user_entry_error(description, converted)
            return False
    except ValueError:
        self.show_user_entry_error(description, string)
        return False
    return converted


def check_user_entry(self):
    if not self.convert_to_float(self.ui.outer_diameter.text(), "Outer diameter"):
        return False
    return True

def set_user_entry(self):
    self.outer_diameter = float(self.ui.outer_diameter.text().replace(",", "."))



def check_for_calculation(self):
    if not self.check_user_entry():
        return
    self.set_user_entry()
    self.thickness = calculate_something(
        self.outer_diameter,
    )
    self.show_result()
Sieht für mich auch irgendwie falsch aus. Könnt ihr mir bitte sagen, wie ihr solche Abfragen gestalltet?

Vielen Dank!

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13158
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: `convert_to_float()` macht zu viel. Das sollte nur in eine Gleitkommazahl umwandeln, und nicht mal eine Zahl und mal einen Wahrheitswert liefern, der zudem auch noch nur `False` sein kann, aber nicht `True`. (Und `True` und `False` sind ja eigentlich auch Zahlen, weil `bool` von `int` abgeleitet ist.)

Qt hat etwas für die Validierung von Eingaben: `QValidator` & Co. Und auch eigene Eingabefelder für Zahlen.

Ansonsten würde ich in der Rechnung erst die Werte abfragen/umwandeln, und wenn das nicht klappt, also Ausnahmen auslöst, darauf reagieren. Und nicht mit `True`/`False` erst umständlich testen ob umgewandelt werden kann, um danach dann noch mal umzuwandeln.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Sirius3
User
Beiträge: 17778
Registriert: Sonntag 21. Oktober 2012, 17:20

Eine Function convert_to_float sollte von der GUI nichts wissen müssen. Fehler gibt man per Exception zurück, nicht als Boolean. Die Funktion `set_user_entry` wandelt die Zahl ja nochmal um, das ist überflüssig, weil die Umwandlung nur einmal stattfinden sollte.
Eine Funktion, die `check_for_calculation` heißt, sollte nichts rechnen.
Ich würde eine Funktion schreiben, die die Form-Validierung und -Übernahme der Werte übernimmt, und eine zweite, die etwas berechnet.

Code: Alles auswählen

def convert_positive_float(string):
    converted = float(string.replace(",", "."))
    if converted <= 0:
        raise ValueError("not positive")

...

    def process_user_data(self):
        outer_diameter_text = self.ui.outer_diameter.text()
        try:
            outer_diameter = convert_to_float(outer_diameter_text)
        except ValueError:
            self.show_user_entry_error("Outer diameter", outer_diameter_text)
            return False

        if self.ui.unlock_settings.isChecked():
            yield_strength_text = self.material_properties[self.material][self.temperature]
        else:
            yield_strength_text = self.ui.yield_strength.text()
        try:
            yield_strength = convert_to_float(yield_strength_text)
        except ValueError:
            self.show_user_entry_error("Alternative Rp0,2", yield_strength_text)
            return False
        
        self.outer_diameter = outer_diameter
        self.yield_strength = yield_strength
        return True

    def calculate(self):
        if self.process_user_data():
            self.thickness = calculate_something(
                self.outer_diameter,
            )
            self.show_result()
Benutzeravatar
Dennis89
User
Beiträge: 1176
Registriert: Freitag 11. Dezember 2020, 15:13

Folgender Text entstand vor Sirius' Antwort.

@__blackjack__
Vielen Dank für die schnelle Antwort.

Dass `convert_to_float`False zurückgibt ist mir auch schon auf die Füße gefallen, weil bei zwei Feldern auch die Eingabe von 0 zulässig sind. Hat etwas gedauert, bis ich den Fehler hatte.

Ich habe jetzt `QDoubleValidator`genutzt, das ist ja sehr cool, da kann der Benutzer ja gar keine falschen Eingaben mehr machen und wenn ich nichts übersehe, kann ich die Eingabe "blind" in `float`wandeln. (Also ink. `replace`wegen des möglichen Kommas). Dann fällt einiges weg und ich muss auch keine Fehlermeldung mehr ausgeben. Verkürzt sieht das jetzt so aus:

Code: Alles auswählen

class UserInterface(QWidget):
    def __init__(self, parent=None):
        self.validator = QDoubleValidator(
            0.0, 9999.9, 2, notation=QDoubleValidator.StandardNotation
        )
        self.set_input_rules()

    def set_input_rules(self):
        self.ui.outer_diameter.setValidator(self.validator)

    def convert_to_float(self, string):
        return float(string.replace(",", "."))

    def set_user_entry(self):
        self.outer_diameter = self.convert_to_float(self.ui.outer_diameter.text())

    def check_for_calculation(self):
        self.set_user_entry()
        self.set_yield_strength()
        self.hickness = calculate_something(
            self.outer_diameter,
        )
        self.show_result()
Die Eingabefelder, die nur Zahlen zulassen, habe ich nicht gefunden.

Ich habe eigentlich bewusst die Eingaben nicht in der Rechnung abgefragt, weil ich die nicht abhängig von der Grafik haben will. Jetzt kann ich die Funktion nehmen und ohne GUI testen. War dass nicht immer das Ziel? Oder habe ich dein letzten Satz vielleicht nicht richtig verstanden?

@Sirius3
Dir auch vielen Dank für die Antwort.

Dass `convert_to_float` nicht in die Klasse gehört, macht Sinn, habe ich geändert.

Das zwei mal umgewandelt wird, wollte ich ursprünglich vermeiden, dafür hatte ich nur keine Alternative gefunden, bzw. nur die im ersten Code gezeigte, die auch Mist ist.

Durch den `QDoubleValidator` ist meine `convert_to_float' etwas schlanker geworden, da gar keine negativen Zahlen mehr eingegeben werden können.
`check_for_calculation` rechnet, weil nach meiner Vorstellung ist alles irgendwie falsch außer ich finde noch heraus, wie durch den Klick auf den `calculate`- Button deine Funktion `process_user_data` und danach `calculate` aufgerufen wird, ohne das `calculate` in der Funktion `process_user_data`aufgerufen wird.
Soll die Funktion an ein Event gebunden werden oder wie wird die aufgerufen?


Grüße
Dennis

Edit: Ich muss natürlich prüfen, ob überhaupt etwas im Eingabefeld steht.
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13158
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Wenn die Berechnung nicht in der GUI stehen soll, dann ist das noch mal was eigenes. Also in der GUI dann Feldinhalte abfragen und validiere, gegebenenfalls eine Fehlermeldung, dann mit den Werten die Berechnung aufrufen, und das Ergebnis in der GUI darstellen.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1176
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, genau so habe ich es, dann lasse ich das so, vielen Dank.

Dann muss ich jetzt "nur" noch schauen, wie ich das mache das, dass die zwei Funktionen die nach dem drücken des Buttons aufgerufen werden sollen auch aufgerufen werden. Bzw. `calculate` nur, wenn `process_user_data` erfolgreich war. Lege ich dafür eine weitere Funktion an `calculate_button_pressed`, das ist doch auch komisch.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Noe, das ist schon ok, das man eine Funktion schreibt, die diese prinzipielle Logik abbildet. Man kann da natuerlich etwas "crazy" werden, und sich zB ein komplexes Validierungs-System ausdenken, wo zB mittels eines Dekorators eine Validierung vor dem eigentliche Aufruf durchgefuehrt wird. Aber im Alter kommt man mehr zu KISS zurueck :D
Benutzeravatar
Dennis89
User
Beiträge: 1176
Registriert: Freitag 11. Dezember 2020, 15:13

Super, vielen Dank.

Das Alter (also je nach dem wie man das auslegt) habe ich noch nicht, da dass Programm aber von Kollegen genutzt werden soll, bleibe ich bei KISS. Wenn ich nachher nicht mehr durchblicke, ist das auch nicht vorteilhaft :D

ABER, vielleicht kann ich das im Hinterkopf behalten, vor ein paar Wochen hatte ich mich mal etwas mit Dekorators beschäftigt, vielleicht wäre das mal ein guter Anwendungsfall, an der Stelle weiter zu machen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1176
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,
__deets__ hat geschrieben: Freitag 8. März 2024, 13:01 Man kann da natuerlich etwas "crazy" werden, und sich zB ein komplexes Validierungs-System ausdenken, wo zB mittels eines Dekorators eine Validierung vor dem eigentliche Aufruf durchgefuehrt wird.
ich habe hier gerade ein GUI mit `tkinter` erstellt, ist eigentlich auch nur ein Test, aber mir kam gerade die Dekorator-Idee wieder in den Kopf und dachte mir, wenn ich schon am testen bin, könnte ich mir dazu auch mal Gedanken machen.
Aktuell habe ich es so, dass ich an jedes Eingabefeld `<FocusOut>` gebunden habe und da wird dann eine Funktion aufgerufen, die Eingabe geprüft. Finde ich eigentlich ziemlich cool.
Wenn ich jetzt einen eigenen Dekorator schreibe, dann würde den die Funktion bekommen, die aufgerufen wird, wenn zum Beispiel ein Button gedrückt wird und zum Beispiel eine Berechnung gestartet wird? Wenn ja, dann sehe ich da den Vorteil noch nicht, weil ich einfach eine Validierungsfunktion aufrufen könnte.

Der Code macht jetzt nicht wirklich Sinn, aber ich habe zur Zeit so etwas, nur mit mehr Eingabefelder und es passiert auch etwas mehr, wenn man den Button drückt:

Code: Alles auswählen

#!/usr/bin/env python

import tkinter as tk
from tkinter import messagebox
from functools import partial

WARNING_TITLE = "Falsche EIngabe"
ENTRY_TEXTS = ["Muss positiv sein", "Darf auch negativ sein"]


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.entry_to_state = {}
        self.description_to_user_entry = {
            description: tk.StringVar(value="0") for description in ENTRY_TEXTS
        }
        for row_index, (text, entry) in enumerate(
            self.description_to_user_entry.items()
        ):
            tk.Label(self, text=text).grid(row=row_index, column=0)
            entry_field = tk.Entry(self, textvariable=entry)
            entry_field.grid(row=row_index, column=1)
            entry_field.bind("<FocusOut>", partial(self.validate_input, entry, text))
        tk.Button(self, text="Fertig", command=self.process_user_data).grid(
            row=len(self.description_to_user_entry) + 1, column=0
        )

    def validate_input(self, value, description, _):
        try:
            value = float(value.get().replace(",", "."))
        except ValueError:
            messagebox.showwarning(WARNING_TITLE, "Nur Zahlen eingeben!")
            self.entry_to_state[description] = False
        else:
            if description == "Muss positiv sein" and value < 0:
                messagebox.showwarning(WARNING_TITLE, "Eingabe muss größer 0 sein")
                self.entry_to_state[description] = False
            else:
                self.entry_to_state[description] = True

    def process_user_data(self):
        if all(self.entry_to_state.values()):
            messagebox.showinfo(message="Juhu!")


def main():
    root = tk.Tk()
    root.title("Test")
    app = App(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Jetzt ist es schön, dass gleich nach der Eingabe überprüft wird. Nicht schön ist, dass man danach weiter machen kann ohne die Eingabe zu korrigieren und am "Schluss" noch mal prüfen muss.
Ich könnte in `process_user_data` auch eine Funktion aufrufen die durch `description_to_user_entry` iteriert und alle Eingaben prüft. Aber wie bzw. wo würde man denn mit der Dekorator-Lösung ansetzen?
Vielleicht auch mal unabhängig, ob das jetzt viel Sinn macht und eher im Hinblick auf den Lerneffekt.

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten