Anhang: Das Formular Form2, voreingerichtet.
Die Funktionselemente:
listBoxKunden
listBoxTeile
Zu den beiden Listboxes gibt es jeweils zwei Radio-Buttons, welche die Sortierung nach Nummer oder Name steuern,
Alle radiobuttons innerhalb einer Form verhalten sich (dankenswerterweise) so, daß immer nur einer aktiv sein kann.
Hier wollen wir aber zwei voneinander unabhängige Gruppen von jeweils zwei Elementen.
Dazu muß man die Radio Buttons nach Gruppen trennen, sie in ein Steuerelement setzen namens GroupBox. Dann ist innerhalb der Box jeweils ein Button
aktiv, ohne auf die Buttons in anderen Boxen zurückzuwirken.
Eine weitere groupBox ist erforderlich für die radioButtons MWSt 19%, 7%, Differenzbesteuerung und ohne MWST.
Die Werte-Ausgabe erfolgt in den Textboxen. Da wir die Werte durch Clicks bzw. interene Berechnung erzeugen, sind die Textboxen auf readonly gesetzt (Eigenschaftsfeld).
Nur eine einzige ist vorgesehen für eine Eingabe mit der Tastatur: Preisnachlaß. Das dient hier nur, um den Umgang mit solchen Feldern zu demonstrieren.
Die Textboxen mit den Werten benennt man zweckmäßigerweise um, z. B. tbBrutto, tbNetto usf.
Um dem Anwender zu zeigen, was die Textbox darstellt, sind Labels aufgeführt. Das sind die Feldbezeichner. Die Labels braucht man nicht umzubennen,
sondern nur an die passende Stelle vor der Textbox zu platzieren.
Der DateTimePicker wird ausgelesen mit dem Typ value vom Typ DateTime. Das ausgewählte Datum erscheint in einer Textbox.
Vom Umgang mit DateTime siehe unten.
Es gibt 4 Buttons:
Neue Rechnung, Eingabe prüfen, Speichern, Ende.
Die Ansicht ist eine Designer-Ansicht. Starten wir das Programm, verschwindet der Button speichern, warum, weil erst gespeichert werden darf, wenn
die Eingabe fertig und überprüft ist. Man erreicht das mit der Anweisung:
this.btSpeichern.Visible = false;
Und um den zu zeigen, wird die Eigenschaft true gesetzt.
Wenn wir die Vorbereitungen abgeschlossen haben, solange sind wir sozuagen noch prozedural, gelangen wir in die
WINDOWS WELT DER EREIGNISSE
Das heißt, das Programm macht dann nur überhaupt etwas, wenn mit der Maus, Tastur oder sonstirgendetwas irgendein Ereignis ausgelöst wird. Die ganze
übrige Zeit macht das Programm gar nichts. Es sei denn, wir hätten einen Timer aktiviert, der in regelmäßigen Abständen selbst ein Ereignis auslöst.
Das macht bei diesem kaufmännischen Programm keinen Sinn.
Es tut mir leid, ich muß noch mit etwas Theorie weiternerven.
Betrachten wir den Code der Form1:
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1() // der Konstruktor
{
InitializeComponent();
/* Was in den Konstruktor an Anweisungen eingefügt wird, wird beim Aufruf der Form1 nur einmalig ausgeführt
Und zu einem Zeitpunkt, zu dem das Formular am Bildschirm noch nicht sichtbar ist
*/
}
}
}
Der Einsprung des Programms ist die Stelle innerhalb der Klasse Program, wo die Funktion form1() steht. Diese beginnt mit der Initialisierung der
benötigten Komponenten. Der Raum für mögliche eigene Anweisungen ist gekennzeichnet.
Um Anweisungen für die Ereignishandler der
Element zu erzeugen, können wir mit der IDE die Funktionsrümpfe anlegen, indem wir das Element doppelclicken. Dabei sollten wir eine sinnvolle Reihenfolge beachten, damit nicht alles wild durcheinander steht.
Wir doppelklicken also (z.B.) nacheinander erstmal die Listboxen,
dann die ganzen RadioButtons
danach die Textboxen, damit das nach Gruppen geordnet hintereinander steht.
Das sieht dann ungefähr so aus:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
}
private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
}
private void radioButton3_CheckedChanged(object sender, EventArgs e)
{
}
// und so weiter für alle Elemente
}
Die Funktionsrümpfe finden sich innerhalb der Klasse Form1, aber außerhalb des Kontruktors Form1().
Es juckt geradezu in den Fingern, nun in diese Funktionsrümpfe mal eben was reinzuschreiben, z. B. rbMWST19 soll dann in das Feld .MWSt des
Datensatzes Rechnung 19.0 eintragen, der DateTimeChecker trägt das Datum ein, und so weiter. Und fertig ist die Rechnung, oder?
Also: Organisation ist alles.
Daher zu Anfang einige wichtige Überlegungen, wie man so eine Programmierung strukturiert.
Wir können unterscheiden zwischen Anweisungen, die in der Form eine Ausgabe zur Folge haben, z. B. textBox1.Text="Hallo", und solchen, die zur Programmlogik gehören, zur eigentlichen Datenverarbeitung.
Ganz abstrakt steht es damit folgendermaßen:
Wenn wir Programmlogik in die Ereignishandler einfügen, ist die erste und zwangsläufige Folge:
1.) Schlechte und unübersichtliche Strukturierung,
weil der Code wie ein Flickenteppich über das Formular verstreut wird. Die nächste Folge ist
2.) Redundanz,
weil Elemente mit ähnlicher Funktion dieselbe Logik benutzen (müssen). Daher haben wir hier identischen Code an verschiedenen Stellen, was zur Folge
hat
3.) Fehleranfälligkeit entweder von vornherein oder bei Programmänderungen. Wir müssen dann alle Stellen finden, die von der Änderung betroffen sind, und das ist
4.) Erhöhter Aufwand bei jeder ÄnderungZudem wird unser Formular durch die Logik und die Redundanz ungehemmt aufgebläht, so daß wir einen
5.) Berg von unnötigem Code mit vielen Wiederholungenanhäufen.
Ein Beispiel, der Mehrwertsteuersatz:
rbuttonMWSt19 setzt den gewählten Satz auf 19 Prozent. Will der radioButtonEreignishandler dies in den Datensatz Rechnung eintragen, müßte er erstmal
überprüfen, ob die Voraussetzungen gegeben sind, z. B. innergemeinschaftlicher Verkehr in der EU, jedoch nicht Inlandsverkehr, oder ob der Kunde aus
irgendeinem Grund nicht mit 19%, sondern differenzbesteuert wird. Womöglich müßte er auch prüfen, ob dieser Kunde bis zu einem gewissen Stichtag so,
und danach anders besteuert wird, oder das nur für bestimmte Warengruppen gilt, und woher die Daten kommen sollen, wäre auch noch eine Frage.
Irgendeine Datenbank käme dafür infrage.
Ein bißchen viel Holz für einen kleinen Radiobutton, oder?Nicht nur das. Derselbe Code müßte von den 3 anderen Radiobuttons ebenfalls aufgerufen werden, und zum Schluß müßte die Überprüfung der Eingabe das
zum 5. Male aufrufen.
Man sieht, was für diese Art von Programmierung spricht: gar nichts.
Und was dagegen spricht: Alles.
Man sollte bei der Programmierung (meiner Meinung nach) folgendes anstreben, ob OOP oder nicht OOP:
ÄNDERUNGEN AN DATENFELDERN EINER KLASSE SOLLEN ZENTRAL VON EINER EINZIGEN METHODE DURCHGEFÜHRT WERDEN
PROGRAMMLOGIK UND BILDSCHIRMSTEUERUNG (Anwenderdialog) SOLLTEN STRENG VONEINANDER GETRENNT WERDEN
Was heißt das praktisch für das kleine Demo-Programm?
1. Die Eventhandler der Eingabesteuerung dürfen nur Anzeigeanweisungen enthalten. Sie dürfen keine Programmlogik enthalten.
2. Die gesamte Programmlogik wird in einer (oder je nachdem auch mehreren) Klassen zusammengefaßt.
3. Die Klasse befindet sich nicht in dem Form1-Formular, sondern wird ausgelagert, so daß das Formular schön "schlank" bleibt.
„OOP bedeutet für mich nur Messaging, lokales Beibehalten, Schützen und Verbergen des Prozesszustands und spätestmögliche Bindung aller Dinge.“
– Alan Kay-
Wenn wir die späte Bindung mal zum Extrem treiben, könnte das bedeuten, daß wir in ALLE EREIGNISHANDLER ÜBERHAUPT KEINEN CODE schreiben.
Wir lassen den Anwender frei herumclicken, und die WinForms Laufzeitumgebung switcht die Buttons oder die Listenfelder, scrollen kann man ja auch
nach Belieben.
BIS AUF EINEN BUTTON: Prüfe Eingabe.
Jetzt wird die Programmlogik aktiv und überprüft das gesamte Formular auf Zulässigkeit, Fehlern bei der Eingabe etc. etc. und kann dann darauf reagieren, etwa mit Fehlerhinweisen und Anweisungen.
Der Nachteil dabei ist natürlich, daß der Anwender erst dann etwas bemerkt, wenn er diesen einen Button drückt. Dann sieht er seine gemachten Fehler und ärgert sich natürlich, daß das Eingabefeld nicht sofort die Fehlermeldung gebracht hat. Bei größeren Formularen mit voneinander abhängigen Feldern ärgert er sich zu Recht.
Das heißt, die Programmlogik soll bei jedem Click- oder sonstigem Ereignis sofort reagieren, dafür muß sie im Hintergrund ständig aktiviert werden.
Sowas kann man durchaus mit einem Timer machen. Man stellt den TimeSpan auf sagen wir 350ms, und das Ereignis ist dann der Aufruf der methode pruefeEingabe(). Wenn der Anwender allerdings das Formular in der Mittagspause geöffnet läßt, und der Timer alle 50ms seine Überprüfung durchführt, haben wir eine Menge sinnlosen Betrieb im Speicher.
Meine Empfehlung, um sowas sauber zu programmieren, wäre die folgende:
Alle Ereingishandler der sie auslösenden Ereignisse enthalten nur eine einzige Anweisung, und zwar alle dieselbe:
private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe();
}
private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe();
}
Egal was der Anwender macht, aber IMMER wenn er etwas macht, wird EINE EINZIGE METHODE aufgerufen, die das gesamte Formular auf formale und logischwe
Fehler überprüft, und zwar nicht erst am Ende, wenn die ganzen Eingabefehler schon drin sind, sondern ständig.
Den Zustand der Buttons und Felder kann die Methode sehr leicht abfragen. Daher ist es völlig egal, ob der Button MWST19 oder der Button
Warengutschrift aktiviert wurde, also von welchem Sender das Ereignis ausgelöst wurde.
Die Zustandsabfrage sieht dann etwa so aus:
if (rbMWSt19.Checked) ... tu was
else if (rbMWSt7.Checked) ... mach was anderes
else if ...
if (ListBox1.SelectedItem == irgendwas) mach irgendwas
else if (Listbox1.SelectedItem == was anderes) mach was anderes
Und so weiter.
Stehen die Parameter insgesamt fest, können auch komplexere Anweisungen folgen, z. B. könnte man einen Lagerbestand abfragen, ob noch genügend von den angeforderten Artikeln vorhanden sind oder ob ein anderer Lieferant mit dieser Schraubengruppe aushelfen kann. Diese Abfragen erfolgen vielleicht über ein Netzwerk, und wenn sich das ändert, muß die Abfragekommunikation vielleicht geändert werden etc. etc.
Man erkennt, all dieses hat innerhalb der Ereignishandler der Click- oder sonstigen Ereignisse wirklich NICHTS ZU SUCHEN.
Ich weiß, es juckt in den Fingern, sofort was in die Ereignishandler einzutippen.
Es kostet einige Überwindung, darauf zu verzichten.
Behandeln wir im Anschluß noch einige Besonderheiten von Eingabefeldern.
So ein Kandidat, der mit Systemfunktionen schlecht zu behandeln ist, ist ein Textfeld, das bestimmte Zeichen erwartet. Es wurde schon gesagt, wir lassen den Anwender freie Eingabe, und prüfen anschließend. Nur daß wir dafür keinen Ereignisauslöser haben. Wenn wir nämlich:
private void textBox1_TextChanged(object sender, EventArgs e)
{
// ueberpruefeNumerischeEingabe() // so nicht
CRegister.textBox1 = true; // besser
}
formulieren, dann wartet das Ereignis TextChanged nicht, bis der Anwender seine Eingabe fertig eingegeben hat, sondern wird schon mit dem ersten Zeichen aktiv, so daß der Anwender seine Eingabe nicht beenden kann.
Wir könnten das Problem mit einem Flag lösen, also einem Schalter in einem Register, der auf true gesetzt wird, wenn hier eine Eingabe erfolgt ist.
Die Methode pruefeEingabe() stellt dann fest, ok, hier wurde was geändert, und behandelt dieses Textfeld. Aber eben nicht sofort, sondern erst, wenn sie von anderer Stelle aufgerufen wurde. Sind alle Eingaben schon ausgeführt, und dieses Textfeld an letzter Stelle, dann passiert nichts, bis "prüfe Eingabe" gedrückt wird. Dazu muß man sich dann was passendes überlegen.
DateTimePicker
Man kann, was am ehesten gewünscht ist, das Datumsformat in den Eigenschaften auf "short" setzen. Dann ist das Format nach der Stringumwandlung
"tt.mm.yyy"; Leider hängt da aber noch die Uhrzeit dran, die in der Regel nicht gewünscht wird.
Um den string zeitstring z1 "26.04.2011 ... Uhrzeit) abzuschneiden, kann man den Substring bilden:
z1 = z1.Substring(0,10), was dann Umwandlung in "26.04.2011" zur Folge hat.
Umwandlung von Fließkommazahlen in string und umgekehrt
Erwartet das Textfeld eine Zahlenangabe, könnte man die masked.TextBox benutzen. Die mag ich aber gar nicht, weil die keine Fließkommazahlen kann.
Die Eingabe ist daher stets sehr hakelig. Wir lassen dem Anwender freie Wahl.
Und den String wandeln wir um mit double d=Convert.ToDouble(zahlstring). Das setzen wir in einen try/catch Block. Bei Umwandlungsfehler wird der
Betrag 0.00 zurückgegeben.
Es gibt ein Problem mit dem Dezimalkennzeichen. Convert.ToDouble erwartet in dem String ein Komma als Trennzeichen. Steht dort ein Punkt, ignoriert die Methode dies, und aus 47.11 wird 4711.
Abhilfe: Wir überprüfen vor der Umwandlung den String auf PUNKT, und wenn wir PUNKT finden, ersetzen wir mit KOMMA.
const char Punkt='.';
const char Komma=',';
if (zahlstring != null ...
if (zahlstring.Length> 0 ... usf.
for (int i=0;i<zahlstring.Length;i++)if(zahlstring[i]==PUNKT) zahlstring[i]=KOMMA;
Danach mit try/Catch die Convert Funktion.
Insgesamt sind alle diese Umwandlungen nicht sicher. In einem anderen Kulturkreis (auf den der Compiler versehentlich eingestellt wurde oder aus Spaß), kann die ganze Programmlogik, die sich auf Strings aufbaut, zerschossen werden. Es würde auch schon ausreichen, wenn das DateTime Format nicht auf short gestellt wurde.
Man sollte daher
INTERN
nur mit "sicheren" Datentypen arbeiten, DateTime, int, double etc., und die Strings nur bilden zu reinen Ausgabefunktionen, also
1.) Sicherer Typ in String umwandeln zu Ausgabezwecken = OK
2.) String in Zahlentypen zurückverwandeln = NOTOK, davon ist dringend abzuraten.
Bei 1.) geschieht im schlechtesten Falle, daß die Ausgabe Unsinn anzeigt, intern jedoch läuft das Progamm stabil.
bei 2.) wird das ganze Programm zerschossen, sobald der kleinste Formatierungsfehler auftritt oder die Formatierung geändert wurde.
Bündig formatierte Strings aus zusammengesetzten Datentypen:
Wir müssen zur Ausgabe von Datenfeldern strings benutzen. Mit der Umwandlung string s = double d.ToString() erzeugt man zwar den gewünschten Betrag, aber abhängig von der Zahlengröße ist der String unterschiedlich lang. Fügt man das nun so zusammen:
string zeile = data.Name + data.Nr.ToString() + data.betrag.ToString() und gibt das in einer Liste aus, habes wir ein ziemlich wüstes Schriftbild, so
etwa:
"Müller AG Remscheid183411,36"
"Schnell GmbH58,10"
"
Schmid Rainer Privat6011,00"
Um da saubere Verhältnisse zu schaffen, brauchen wir für jedes Feld eine bestimmte Länge, die auch am Rand noch Leerzeichen stehen läßt, damit die Daten nicht zusammenklumpen. Hierfür bietet die Methode ToString() die Möglichkeit, in den runden Klammern Formatierungszeichen einzustellen, die man nachschlagen kann, führe ich hier nicht weiter aus. Diese müssen dafür sorgen, daß immer zwei Nachkommastellen erzeugt werden, sowie eine führende Null bei Double wäre gewünscht. Ist das erfolgt, kann man die Strings folgendermaßen auf eine bestimmte Länge rechts- oder linksbündig setzen:
data.Name = data.Name.Trim() (lösche alle Leerzeichen vorn und hinten). TrimStart löscht die Leerzeichen nur vorn, TrimEnd nur hinten.
Rechtsbündig machen:
data.Name=data.Name.Padleft(50). Das erzeugt eine Feldlänge von 50, in der der Text rechtsbündig steht. Bei Text ist das natürlich Quatsch, der soll
linksbündig stehen. Also:
data.Name = data.Name.PadRight(50)
Dagegen sollen alle Fließkommazahlen gewöhnlich rechtsbündig stehen:
data.betrag=data.betrag.ToString(gewünschte Formatierungsmerkmale);
data.betrag= data.betrag.PadLeft(15);
Und als Lohn der Mühe?
Wir sehen, wir haben immer noch ein wüstes Schriftbild. Nichts von dem, daß die Spalten links- oder rechtsbündig fluchten.
Wieso?
Wir müssen eine Schriftart wählen, die nichtproportional ist. 99 Prozent der Schriftarten sind proportional, und so helfen die Pad-Anweisungen wenig.
Eine gut geeignete
NON-Porportionalschrift ist Courier bzw. Courier New. In den Ausgabeelementen listBox stellen wir diese Schriftart unter font ein.
Dann haben wir alles schön bündig.
Das soll jetzt erstmal reichen als Bemerkungen zur Einrichtung unseres Formulars form2, also Rechnungen.
Was ist als nächstes zu erledigen?
Die Klasse CRechnungen ist zu erstellen.
Die Klasse CStart muß komplettiert werden
Die Klasse CControl muß erstellt werden (Control: überprüft die Eingabe unserer Rechnung formal und logisch)
In der Klasse CRechnungen brauchen wir auch Methoden, welche die Daten zu einer Rechnung der gewünschten Form zusammenstellen.
In den Klassen CKunden und CTeile fehlen noch Methoden, um aus der Struktur einen bündigen String zu erzeugen
In der Klasse CSort fehlen noch Methoden für die neuen Klassen
Also eine ganze Menge Kleinkram.
Ohne daß das erledigt ist, kann man die Form2 aber nicht wie gewünscht in Betrieb nehmen.
Der Beitrag wurde von sharky2014 bearbeitet: 10.04.2014, 15:46 Uhr
Angehängte Datei(en)
DemoForm4.jpg ( 324.37KB )
Anzahl der Downloads: 9