Fragen zu Notenprogramm

Fragen zu Tkinter.
Antworten
Kleonx
User
Beiträge: 14
Registriert: Dienstag 31. Mai 2016, 17:38

Code: Alles auswählen

from tkinter import *
from tkinter import messagebox


def action_get_info_dialog():
    m_text = "\
************************\n\
Autor: xxxx\n\
************************"
    messagebox.showinfo(message=m_text, title="Infos")


def action_get_help_dialog():
    m_text = "\
************************\n\
1.Nachdem sie die Anzahl Noten eingegeben haben, drücken sie auf Weiter und füllen sie danach die Felder aus.\n\
************************"
    messagebox.showinfo(message=m_text, title="Infos")

fenster = Tk()
fenster.configure(background='lightblue')
fenster.geometry('400x300')
fenster.title("Notenprogramm")

menuleiste = Menu(fenster)

datei_menu = Menu(menuleiste, tearoff=0)
help_menu = Menu(menuleiste, tearoff=0)

eingabefeld = Entry(fenster, bd=5, width=100)
welcome_label = Label(fenster, background = "lightblue")
anzahlFenster = eingabefeld.get()


xcord = 200
ycord = 50

def button_action():
    global ycord, liste, welcome_button, eingabefeld, welcome_label, xyz
    entry_text = eingabefeld.get()
    if (entry_text == ""):
        welcome_label.config(text="Gib an wie viele Noten du hast.")

    else:

        def notenschnitt():
            button2.place_forget()
            eingabefelder_label.place_forget()
            for eingabefeldx in liste:
                eingabefeldx.place_forget()

            eingabefeld2 = Entry(fenster, bd=5, width=100)
            welcome2_label = Label(fenster, background = "lightblue")
            xyz = eingabefeld2.get()
            global xyz
            button3 = Button(fenster,text="Ausrechnen!", command=zusammenrechnen)
            welcome2_label.config(text="Welchen Notenschnitt willst du haben?")
            eingabefeld2.place(x=0, y=10, width=200, height=30)
            welcome2_label.place(x=-150, y=35, width=500, height=30)
            button3.place(x=50, y=70, width=90, height=50)

        liste = []
        a = int(entry_text)
        xyo = sum(liste)
        global a, liste, xyo
        for i in range(a):
            ycord += 20
            liste.append(Entry(fenster))
            liste[-1].place(x = xcord,y=ycord)
        eingabefelder_label = Label(fenster, background = "lightblue")
        button2 = Button(fenster,text="OK!", command=notenschnitt)
        eingabefelder_label.config(text="Gib deine bisherigen Noten ein.")
        button2.place(x=200, y=200,width=125, height=20)
        eingabefelder_label.place(x=10, y=30, width=500, height=30)
        welcome_label.place_forget()
        eingabefeld.place_forget()
        welcome_button.place_forget()
global xyz, a, xyo
def zusammenrechnen():
    benötigteNote = int(xyz)*a-xyo
    notenschnitt_label = Label(fenster, background = "lightblue")
    notenschnitt_label.config(text="Du brauchst eine um deinen Wunschnitt zu erreichen")
    notenschnitt_label.place(x=10, y=30, width=500, height=30)






welcome_button = Button(fenster, text="Weiter", command=button_action)


datei_menu.add_command(label="Beenden",command=fenster.quit)

help_menu.add_command(label="Info", command=action_get_info_dialog)
help_menu.add_command(label="Hilfe", command=action_get_help_dialog)

menuleiste.add_cascade(label="Datei", menu=datei_menu)
menuleiste.add_cascade(label="Hilfe", menu=help_menu)

welcome_label.place(x=-150, y=30, width=500, height=50)
welcome_button.place(x=70, y=70, width=50, height=50)
eingabefeld.place(x=0, y=10, width=200, height=30)
fenster.config(menu=menuleiste)

fenster.mainloop()
Als fehlermeldung kommt bei mir : File "C:\Miniconda3\lib\tkinter\__init__.py", line 1487, in __call__
return self.func(*args)
File "P:/Configuration/PycharmProjects/Projekt/Projekt.py", line 80, in zusammenrechnen
benötigteNote = int(xyz)*a-xyo
ValueError: invalid literal for int() with base 10: ''

Ich finde den Fehler leider nicht, darum benötige ich Hilfe, geht um eine Schularbeit, darum bin ich noch ein rechter Anfänger.
BlackJack

@Kleonx: Ich würde dringend dazu raten die GUI weg zu lassen. Das ist ein bisschen viel für den Anfang denn um das sauber hinzubekommen brauchst Du objektorientierte Programmierung. Davor sollte man aber erst einmal Funktionen richtig drauf haben, also zum Beispiel auf keinen Fall ``global`` verwenden. Alles was eine Funktion oder Methode verwendet, ausser Konstanten, sollte als Argument übergeben werden und falls es ein Ergebnis gibt, bindet man das nicht an Namen ausserhalb der Funktion/Methode sondern gibt es an den Aufrufer als Rückgabewert. Methoden könnten alternativ noch den Zustand des Objekts verändern, aber man verändert keinen globalen Zustand. Das führt zu unübersichtlichen, schwer bis gar nicht nachvollziehbaren Programmen. Verschachtelte Funktionsdefinitionen bei denen innere Funktionen komplexere Dinge tun, sind ebenfalls nicht gut, weil man die beispielsweise nicht einzeln Testen kann.

Wenn ich das richtig sehe hast Du keine einzige Funktion verwendet. Du missbrauchst die Funktionsdefinition um Codeabschnitten Namen zu geben die man mit einem Aufruf ”anspringen” kann.

Wenn GUI, dann nicht `place()` verwenden. Das sieht vielleicht auf dem Rechner wo Du es programmierst brauchbar aus, kann aber auf anderen Systemen mit anderen Bildschirmgrössen, -auflösungen, und Einstellungen schlecht aussehen, oder wenn Du Pech hast sogar unbenutzbar sein.

In dem Programm sind einige *sehr* schlechte Namen wie a, xyz, xyo, liste, eingabefeldx, button2, … die alle nicht wirklich verraten was der Wert der dahinter steckt im Kontext des Programms bedeutet. Das ist aber gerade der Sinn von Namen, dem Leser diese Information zu vermitteln.

Die Ausnahme die Du bekommst kommt zustande wenn `xyz` an eine leere Zeichenkette gebunden ist:

Code: Alles auswählen

In [1]: int('')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-1-5ab1b188288a> in <module>()
----> 1 int('')

ValueError: invalid literal for int() with base 10: ''
BlackJack

Um das mit dem `place()` mal zu illustrieren, so sieht das bei mir aus:
Bild
Das die Ok-Schaltfläche über den Eingabefeldern ist, macht es fast unbenutzbar. Man kann die halben Zahlen der beiden betroffenen Eingabefelder nur schwer entziffern.
Bild
Und hier fehlt ein Teil des Textes.

Insgesamt ist da zu viel freie Fläche in der GUI.
BlackJack

@Kleonx: Noch ein paar Anmerkungen zum Quelltext:

Sternchen-Importe machen Programme unübersichtlicher und bergen zudem die Gefahr von Namenskollisionen. Aus dem `tkinter`-Modul holst Du Dir ca. 190 Namen in das Modul, von denen Du nur einen ganz kleinen Bruchteil tatsächlich verwendest. Üblicherweise wird das `tkinter`-Modul mit ``import tkinter as tk`` an den Namen `tk` gebunden und über diesen Namen dann auf die Objekte aus diesem Modul zugegriffen. Das ist nur wenig mehr Tipparbeit, aber man sieht beim Lesen sofort wo der jeweilige Name her kommt.

Auf Modulebene gehören nur Definitionen von Konstanten, Funktionen, und Klassen. Das Hauptprogramm gehört auch in eine Funktion und auf keinen Fall auf Modulebene zwischen Funktionsdefinitionen verteilt, wo man sich das als Leser mühsam zusammen suchen muss.

Es gibt Konventionen bei Namensschreibweisen, zum Beispiel das man Konstanten komplett in Grossbuchstaben schreibt, die meisten anderen Namen in kleinbuchstaben_mit_unterstrichen, ausser Klassennamen, die MixedCase geschrieben werden. Du mischst kleinbuchstaben_mit_unterstrichen und camelCase scheinbar willkürlich. Genau so wie Deutsch und Englisch bei den Namen. Das sollte man einheitlicher lösen. Bei der Schreibweise hält man sich am besten an den Style Guide for Python Code.

Ich hatte in einem früheren Beitrag ja schon mal einige schlechte Namen aufgezählt, weil die dem Leser nicht verraten was der Wert im Programm bedeutet. Du hast auch Namen die das fast tun, bei denen man aber im Zweifelsfall doch raten muss, weil sie abgekürzt sind. Abkürzungen sollte man meiden solange sie nicht allgemein bekannt sind. Man macht damit sonst nur das Programm unverständlicher ohne einen wirklichen Mehrwert. Weder ist Speicherplatz knapp, noch kann man sich mit Tipparbeit herausreden, weil heute jeder vernünftige Editor eine Autovervollständigung bietet. Selbst wenn die einfach nur ihre Vorschläge aus den geöffneten Textdateien bezieht, reicht das schon aus.

Die ``global``-Anweisung hat auf Modulebene keinen Effekt!

Funktionen sind üblicherweise nach Tätigkeiten benannt. Und zwar am besten nach den Tätigkeiten die sie vollziehen. Bei `action_get_info_dialog()` und `action_get_help_dialog()` hat das `action_`-Präfix jeweils nichts zu suchen. Und das `get_` ist falsch weil so ziemlich jeder Programmierer aus Erfahrung erwartet das eine so benannte Funktion einen Wert von irgendwo holt und als Ergebnis liefert. Was diese Funktionen tun ist Dialoge anzeigen. `show_info()` oder `show_info_dialog()` währen passende Namen für so eine Funktion. Wobei die Funktionen fast das gleiche machen und sich eigentlich nur durch den Text unterscheiden. Da könnte man die Texte als Konstanten definieren und eine gemeinsame Funktion definieren die dann mit dem entsprechenden Text als Argument aufgerufen wird.

Was soll das Willkommen in `welcome_label` und `welcome2_label` bedeuten?

Als Programmierer versucht man Code- und Datenwiederholungen zu vermeiden. Das macht Programme schwerer wart- und änderbar. Überleg zum Beispiel mal wieviel Arbeit das machen würde Hellblau als Hintergrundfarbe in etwas anderes zu ändern. Du musst überall nach der Zeichenkette suchen und an jeder Stelle schauen ob es sich um die Angabe einer Hintergrundfarbe handelt oder etwas anderes damit bezweckt wird. Wenn man am Anfang des Programms einmal eine Konstante für die Hintergrundfarbe definiert und die dann überall verwendet, kann man die Hintergrundfarbe an dieser einen Stelle verändern, ohne das gesamte Programm durchgehen zu müssen.

Das erstellen des Menüs wird unnötigerweise durch das erstellen von anderen Widgets unterbrochen. Das ist unübersichtlich. Genau wie in Aufsätzen sollte man erst einen Gedanken zuende ausformulieren, bevor man etwas anderes anfängt.

`anzahlFenster` ist ein falscher Name weil der nicht an die Anzahl von Fenstern gebunden wird sondern an eine Zeichenkette die die Anzahl der Noten bedeutet. Allerdings noch nicht zu dem Zeitpunkt wo diese Zuweisung stattfindet, weil der Benutzer da noch gar keine Chance hatte irgend etwas einzugeben. Ausserdem wird der Name nach der Zuweisung nirgendwo verwendet.

Die Anweisung das man die Anzahl der Noten in das Eingabefeld eingeben soll, sollte *sofort* dort stehen. Das weiss sonst kein Mensch der das Programm das erste mal startet oder es sich nicht vom letzten mal her gemerkt hat.

Wenn man das ganze ein bisschen aufräumt und tatsächlich Funktionen so benutzt wie sie gedacht sind, dann kommt man ungefähr bei so etwas heraus:

Code: Alles auswählen

# coding: utf8
from functools import partial
import tkinter as tk
from tkinter import messagebox

BACKGROUND_COLOR = 'lightblue'
INFO_MESSAGE = (
    '************************\n'
    'Autor: xxxx\n'
    '************************'
)
HELP_MESSAGE = (
    '************************\n'
    '1. Nachdem sie die Anzahl Noten eingegeben haben, drücken sie auf'
    ' Weiter und füllen sie danach die Felder aus.\n'
    '************************'
)


show_info_dialog = partial(messagebox.showinfo, 'Infos')


def calculate_needed_mark(marks, desired_mark):
    pass  # TODO


def on_marks_entered(window, entry, info_label, button, mark_entries):
    try:
        desired_mark = float(entry.get())
    except ValueError:
        info_label['text'] = 'Bitte den gewünschten Schnitt oben eingeben.'
        entry.select_range(0, tk.END)
        entry.focus()
    else:
        marks = list()
        for mark_entry in mark_entries:
            try:
                mark = float(mark_entry.get())
            except ValueError:
                info_label['text'] = 'Bitte alle Noten unten eingeben.'
                mark_entry.select_range(0, tk.END)
                mark_entry.focus()
                return
            else:
                marks.append(mark)
        info_label['text'] = ''
        needed_mark = calculate_needed_mark(marks, desired_mark)
        result_text = 'Ergebnis ...'  # TODO
        tk.Label(
            window, text=result_text, background=BACKGROUND_COLOR
        ).pack(side=tk.TOP)
        button.config(text='Beenden', command=window.quit)


def on_mark_count_entered(window, entry, info_label, button):
    try:
        mark_count = int(entry.get())
    except ValueError:
        entry.select_range(0, tk.END)
        entry.focus()
    else:
        mark_entries = list()
        for _ in range(mark_count):
            mark_entry = tk.Entry(window)
            mark_entry.pack(side=tk.TOP)
            mark_entries.append(mark_entry)

        entry.delete(0, tk.END)
        info_label['text'] = (
            'Wunschschnitt (oben) und vorhandene Noten (unten) eingeben'
        )
        button['command'] = partial(
            on_marks_entered, window, entry, info_label, button, mark_entries
        )

 
def main():
    root = tk.Tk()
    root.configure(background=BACKGROUND_COLOR)
    root.title('Notenprogramm')
     
    main_menu = tk.Menu(root)
    
    datei_menu = tk.Menu(main_menu)
    datei_menu.add_command(label='Beenden', command=root.quit)
    main_menu.add_cascade(label='Datei', menu=datei_menu)
     
    help_menu = tk.Menu(main_menu)
    help_menu.add_command(
        label='Info', command=partial(show_info_dialog, INFO_MESSAGE)
    )
    help_menu.add_command(
        label='Hilfe', command=partial(show_info_dialog, HELP_MESSAGE)
    )
    main_menu.add_cascade(label='Hilfe', menu=help_menu)
     
    root.config(menu=main_menu)
     
    entry = tk.Entry(root, borderwidth=5)
    entry.pack(side=tk.TOP)
    info_label = tk.Label(
        root, text='Gib die Anzahl der Noten ein.', background=BACKGROUND_COLOR
    )
    info_label.pack(side=tk.TOP)
    button = tk.Button(root, text='Weiter')
    button.pack(side=tk.TOP)
    button['command'] = partial(
        on_mark_count_entered, root, entry, info_label, button
    )
    root.mainloop()


if __name__ == '__main__':
    main()
Hier muss man `functools.partial()` verwenden um die benötigten Werte in die Funktionen transportieren zu können. Wenn man das verstanden hat, kann man aber auch gleich objektorientiert programmieren (OOP), denn das kommt von der Komplexität im Grunde auf das gleiche hinaus, nur dass das in Python eigentlich der offensichtlichere Weg ist.

Man könnte bei der GUI natürlich noch nachbessern. Beispielsweise bei der Eingabe der Anzahl unsinnige Werte (0, negative, zu grosse) und bei der Eingabe von Noten die Grenzen der üblichen Noten (1-6 oder 0 bis 15) erzwingen. Bei einer OOP-Lösung würde es sich anbieten entsprechende Eingabeelemente mit Bereichsprüfung zu implementieren und die Noteneingaben noch einmal in einem Objekt zu kapseln das alle Eingaben validieren kann.

Die eigentliche Programmlogik fehlt auch noch. Also die im Moment noch leere Funktion, die aus den gegebenen Noten und dem gewünschten Schnitt ausrechnet welche Note man noch benötigt. Das ist eigentlich die Funktion mit der Ich angefangen hätte. Erst einmal muss das was ein Programm leisten soll funktionieren. Dann kann man da eine GUI drauf setzen. Eine GUI mit der man am Ende das gestellte Problem nicht lösen kann, nützt nicht viel. Und normalerweise zählt bei Hausaufgaben eine durchdachte und getestete, und damit funktionierende Programmlogik mehr als eine GUI. Das was Du da bei `benötigteNote` als Rechnung stehen hast ist nicht ganz korrekt (sofern die Variablen die entsprechenden Werte hätten). An der Stelle ist es besonders bei Hausaufgaben nützlich Herleitung der verwendeten Formel zu dokumentieren, damit der Lehrer sehen kann was man sich dabei gedacht hat, und falls es falsch ist, auch eine Idee davon bekommt an welcher Stelle man sich geirrt hat oder ”falsch abgebogen” ist.
Kleonx
User
Beiträge: 14
Registriert: Dienstag 31. Mai 2016, 17:38

Erst einmal vielen Dank für deine Hilfe, das schätze ich sehr. Ich hätte noch eine Frage, ich hätte die "Notenausrechnungs" Definition so gestaltet, dass es die Anzahl Noten die man hat + 1*Den Wunschschnitt- Die Summierung der Liste rechnet, wie jedoch mache ich das am besten, bin ja auch schon bei meinem ersten Programm daran gescheitert, wäre sehr gut, wenn du mir noch bei dem Helfen könntest. (Kannst es in deine Programmverbesserung einfügen dann könnte ich das übernehmen.)
Lg Kleonx
BlackJack

@Kleonx: Das macht man eigentlich genau so wie Du das in Worten beschreibst. Du bist ja nicht wirklich daran gescheitert, es fehlte halt das + 1, ein bisschen Klammerung, und vernünftige Namen in Deiner Formel. Und die Berechnung würde man in eine eigene Funktion schreiben, damit man die interaktiv oder auch automatisiert mal testen kann. Tatsächlich gescheitert ist das dann daran, dass Du das Entry gleich nach dem Erstellen abgefragt hast, also dann wenn der Benutzer da noch gar keine Chance hatte irgend etwas einzutragen. Du darfst das erst abfragen wenn der Benutzer etwas eingetragen hat und das irgendwie mitteilt, zum Beispiel durch klicken einer Schaltfläche.
Kleonx
User
Beiträge: 14
Registriert: Dienstag 31. Mai 2016, 17:38

Kleonx hat geschrieben:

Code: Alles auswählen

             def calculate_needed_mark(marks, desired_mark):
                             notensummierung = sum(marks)
                             Ausgabe = (mark_count+1)*desired_mark-notensummierung


Ich habe nun diese definition geschrieben, wo sollte ich sie einfügen egal wo ich sie hinschreibe sie funktioniert nicht.
Kleonx
User
Beiträge: 14
Registriert: Dienstag 31. Mai 2016, 17:38

Code: Alles auswählen

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

BACKGROUND_COLOR = 'lightblue'
INFO_MESSAGE = (
    '************************\n'
    'Autor: xxxx\n'
    '************************'
)
HELP_MESSAGE = (
    '************************\n'
    '1. Nachdem sie die Anzahl Noten eingegeben haben, drücken sie auf'
    ' Weiter und füllen sie danach die Felder aus.\n'
    '************************'
)

show_info_dialog = partial(messagebox.showinfo, 'Infos')

def calculate_needed_mark(marks, desired_mark):
    global Ausgabe
    notensummierung = sum(marks)
    Ausgabe = (len(marks)+1)*desired_mark-notensummierung

def on_marks_entered(window, entry, info_label, button, mark_entries):
    try:
        desired_mark = float(entry.get())
    except ValueError:
        info_label['text'] = 'Bitte den gewünschten Schnitt oben eingeben.'
        entry.select_range(0, tk.END)
        entry.focus()
    else:
        marks = list()
        for mark_entry in mark_entries:
            try:
                mark = float(mark_entry.get())
            except ValueError:
                info_label['text'] = 'Bitte alle Noten unten eingeben.'
                mark_entry.select_range(0, tk.END)
                mark_entry.focus()
                return
            else:
                marks.append(mark)

        info_label['text'] = ''
        result_text = "Diese Noten ", Ausgabe , "brauchst du um deinen Schnitt zu erreichen"
        tk.Label(window, text=result_text, background=BACKGROUND_COLOR).pack(side=tk.TOP)
        button.config(text='Beenden', command=window.quit)


def on_mark_count_entered(window, entry, info_label, button):
    try:
        mark_count = int(entry.get())

    except ValueError:
        entry.select_range(0, tk.END)
        entry.focus()
    else:
        mark_entries = list()
        for _ in range(mark_count):
            mark_entry = tk.Entry(window)
            mark_entry.pack(side=tk.TOP)
            mark_entries.append(mark_entry)

        entry.delete(0, tk.END)
        info_label['text'] = (
            'Wunschschnitt (oben) und vorhandene Noten (unten) eingeben'
        )
        button['command'] = partial(
            on_marks_entered, window, entry, info_label, button, mark_entries
        )





def main():
    root = tk.Tk()
    root.configure(background=BACKGROUND_COLOR)
    root.title('Notenprogramm')

    main_menu = tk.Menu(root)

    datei_menu = tk.Menu(main_menu)
    datei_menu.add_command(label='Beenden', command=root.quit)
    main_menu.add_cascade(label='Datei', menu=datei_menu)

    help_menu = tk.Menu(main_menu)
    help_menu.add_command(
        label='Info', command=partial(show_info_dialog, INFO_MESSAGE)
    )
    help_menu.add_command(
        label='Hilfe', command=partial(show_info_dialog, HELP_MESSAGE)
    )
    main_menu.add_cascade(label='Hilfe', menu=help_menu)

    root.config(menu=main_menu)

    entry = tk.Entry(root, borderwidth=5)
    entry.pack(side=tk.TOP)
    info_label = tk.Label(
        root, text='Gib die Anzahl der Noten ein.', background=BACKGROUND_COLOR
    )
    info_label.pack(side=tk.TOP)
    button = tk.Button(root, text='Weiter')
    button.pack(side=tk.TOP)
    button['command'] = partial(
        on_mark_count_entered, root, entry, info_label, button
    )
    root.mainloop()


if __name__ == '__main__':
    main()

Ich habe das jetzt so umgeschrieben, aber da steht dass "Ausgabe" nicht definiert ist.
BlackJack

@Kleonx: Ist es ja auch nicht. Und bitte vergiss das es ``global`` überhaupt gibt. Damit löst man keine Probleme sondern schafft sich welche. Schau Dir in Deinen Unterlagen aus der Schule oder anderen Quellen an wie Funktionen funktionieren. Du musst wissen was lokale Namen sind, und das die *gut* sind und globale Namen sind *böse*, und wie man Rückgabwerte aus einer Funktion zum Aufrufer zurück gibt. Das ist Grundlagenwissen das in der Schule vermittelt werden sollte, und in jedem Anfängertutorial. So ein ganz kleines bisschen musst Du schon noch selber machen. ;-) Und am besten auch verstehen was Du da übernommen hast. So für den Fall das Dein Lehrer Dir Fragen zu der Lösung stellen sollte. :-)
BlackJack

Das ganze mal komplett in Ruby. Ist das erste mal das ich etwas in Ruby programmiert habe.
[codebox=ruby file=Unbenannt.rb]# :title: Mark Calculator
#
# The Program lets the user enter a number of marks, a desired mark,
# and then calculates the one mark needed to reach the desired mark.
# Iff possible.
#
require "tk"

## Background color of window and labels.
BACKGROUND_COLOR = 'lightblue'

## Info message for the help menu. See #show_info.
IMFO_MESSAGE = "************************\n"
"Autor: xxxxx\n"
"************************"

## Help message for the help menu. See #show_info.
HELP_MESSAGE = "************************\n"
"1. Nachdem sie die Anzahl Noten eingegeben haben, drücken"
"sie auf Weiter und füllen sie danach die Felder aus.\n"
"************************"

## Raised when a mark isn't a valid value for a mark.
class MarkError < RuntimeError
## The mark that isn't one.
attr_reader :mark

def initialize mark, *args
super *args
@mark = mark
end
end

## Numeric extended by constants and methods to test for valid mark values.
##
## 0.mark? # -> false
## 5.mark? # -> true
## 42.check_mark # -> raises MarkError
## 3.check_mark # -> 3
##
class Numeric
## Lowest achievable mark.
MIN_MARK = 1
## Highest achievable mark.
MAX_MARK = 6
## Range of all marks from MIN_MARK to MAX_MARK.
MARK_RANGE = MIN_MARK..MAX_MARK

## Return true if this Numeric value is a valid mark.
def mark?
MARK_RANGE.include? self
end

## Check receiver and raise MarkError if it is not a valid mark.
def check_mark
raise MarkError.new self, "#{self} not in #{MARK_RANGE}" unless mark?
self
end
end


## Calculate the needed mark given the marks so far and the desired mark.
##
## Raise MarkError if the result isn't a valid mark.
##
## See Numeric#check_mark and Numeric::MARK_RANGE.
##
def calculate_needed_mark marks, desired_mark
(desired_mark * (marks.length + 1) - marks.reduce(:+)).check_mark
end


## Show a Tk info dialog with the given message.
##
def show_info message
Tk.messageBox :title => 'Infos', :message => message
end


## Extends TkEntry by methods
## * to select the complete text and set the focus to the instance,
## * to get a positive, non-zero integer value,
## * and to get a mark value.
##
class TkEntry

## Select the whole content and set the focus on this entry.
##
def select_and_focus
selection_range 0, :end
focus
end

## Get a positive, non-zero integer value from the entry.
##
## Raise ArgumentError if the entry content can't be converted to
## an integer value.
## Raise RangeError if the integer is smaller than 1.
##
## In case of an exception the content is selected and the entry
## gets the focus.
##
def get_count
result = Integer get
rescue ArgumentError
select_and_focus
# This was a bad surprise → Tk widgets have a `raise` method!
Kernel.raise
else
Kernel.raise RangeError unless result > 0
result
end

## Get a valid mark value from the entry.
##
## Raise ArgumentError if the content can't be converted to a number.
## Raise MarkError if the numeric value isn't a valid mark.
##
## In case of an exception the content is selected and the entry
## gets the focus.
##
## See Numeric#mark? and Numeric#check_mark.
##
def get_mark
(Float get).check_mark
rescue ArgumentError, MarkError
select_and_focus
Kernel.raise
end
end


## Tk user interface to use the Object#calculate_needed_mark method.
class MarkCalculatorUI < TkRoot

## Create the user interface with menu, an entry, a label to
## inform the user of the expected inputs, and a button.
##
## The user is expected to input the number of marks already
## handed out. After pressing the button, the private
## on_mark_count_entered method is called.
##
def initialize options
super options
## test
@mark_entries = []
@result_label = nil

main_menu = TkMenu.new self

file_menu = TkMenu.new main_menu
file_menu.add_command :label => 'Beenden',
:command => method(:destroy)
main_menu.add_cascade :label => 'Datei', :menu => file_menu

help_menu = TkMenu.new main_menu
help_menu.add_command :label => 'Info',
:command => proc { show_info INFO_MESSAGE }
help_menu.add_command :label => 'Hilfe',
:command => proc { show_info HELP_MESSAGE }
main_menu.add_cascade :label => 'Hilfe', :menu => help_menu

self[:menu] = main_menu

@entry = TkEntry.new self, :borderwidth => 5
@entry.pack :side => :top

@info_label = TkLabel.new self,
:text => 'Gib die Anzahl der Noten ein.',
:background => BACKGROUND_COLOR
@info_label.pack :side => :top

@button = TkButton.new self, :text => 'Weiter'
@button.pack :side => :top
@button[:command] = method(:on_mark_count_entered)
end

private

## If the user entered a valid mark count in the text entry, thus
## many entries for the marks are added to the UI. And a label
## for the result.
##
## The top entry is cleared and the user prompted to enter the
## desired average mark there. The button now triggers the
## validation of the entered marks and calculating the needed mark
## to reach the desired one in #on_marks_entered.
##
def on_mark_count_entered
mark_count = @entry.get_count
rescue ArgumentError, RangeError
@entry.select_and_focus
else
mark_count.times do
entry = TkEntry.new self
entry.pack :side => :top
@mark_entries << entry
end

@result_label = TkLabel.new self, :background => BACKGROUND_COLOR
@result_label.pack :side => :top

@entry.delete 0, :end
@info_label[:text] =
'Wunschschnitt (oben) und vorhandene Noten (unten) eingeben'
@button.configure :text => 'Berechnen',
:command => method(:on_marks_entered)
end

## Validate the entered marks, call Object#calculate_needed_mark
## and display the result.
##
def on_marks_entered
desired_mark = @entry.get_mark
rescue ArgumentError, MarkError
@info_label[:text] = 'Bitte den gewünschten Schnitt oben eingeben.'
@entry.select_and_focus
else
marks = @mark_entries.map do |entry|
begin
mark = entry.get_mark
rescue ArgumentError, MarkError
@info_label[:text] = 'Bitte alle Noten unten eingeben.'
entry.select_and_focus
return
end
end
@info_label[:text] = ''
begin
needed_mark = calculate_needed_mark marks, desired_mark
rescue MarkError
result_text = 'Das ist nicht möglich.'
else
result_text = "Du benötigst eine #{needed_mark}."
end
@result_label[:text] = result_text
end
end

## Create the UI and run Tk's mainloop.
def main
ui = MarkCalculatorUI.new :title => 'Notenprogramm',
:background => BACKGROUND_COLOR
Tk.mainloop
end


main if __FILE__ == $PROGRAM_NAME[/code]
Antworten