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: 1156
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: 13116
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.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17754
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: 1156
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: 13116
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.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
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: 14544
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: 1156
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]
Antworten