Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Hallo,
zu meiner FIBU-Programierung hab ich ja schon einen Beitrag reingesetzt.
Die Fibu läuft.
Sie kostet nichts, hat keine Lizenzgebühren, und produziert auf den 1/10 Cent genaue Auswertungen, wenn man berücksichtigt, daß die Amis bei 0.05 Cent abrunden, die Europäer aber bei 0.05 Cent aufrunden, und das abfängt.
Es läuft alles bestens.
Es ist nur nicht so schöööööööööööööön.
Daher gehe ich jetzt aus Spaß an der Freud mal daran, eine Konsolen-Anwendung auch optisch anspruchsvoll darzustellen (aufzumotzen).
Man will ja, wenn man programmiert, immer schnell Ergebnisse erzielen. Darunter leidet nicht nur die Optik, sondern ganz allgemein ist der Fehler, daß man im Vorfeld nicht die Geduld hat, die Komponenten des Programms genügend zu ABSTRAHIEREN und zu KAPSELN.
Der Mangel an Abstraktion Kapselung wird bei jeder Programmänderung zum immer größeren Problem.
Anders gesagt, wenn man bei der Programmierung zu sehr ERGEBNISORIENTIERT denkt und die STRUKTURIERUNG vernachlässigt, wird jede noch so kleine Programmänderung zum Problem und führt zu Instabilitäten. Einer der Hauptgründe für diese Problematik ist der, daß man sich am Anfang über das Thema nicht klar ist und daher die Programmierung immer wieder nachträglich an bislang nicht bekannte Umstände anpassen muß. Irgenwann ist dann der Punkt erreicht, wo man Änderungen besser nicht mehr vornimmt, weil die Stabilität des Programms in Gefahr gerät.
Daher:
Da das Ding nun läuft, und die grundlegenden Fragen der Programmierung geklärt sind, lasse ich es unverändert. Um es zu verbessern, wird es nicht mehr geändert, sondern es soll der Braut ein völlig neues und viel schöneres Kleid geschneidert werden.
Mein Beitrag wird sich mit folgenden Dingen befassen:
1.) Reine ANSI-C Programmierung. Kein Mischmasch mit C++.
2.) Schwerpunkt vernünftige Präsentation auf der KONSOLE.
3.) Schwerpunkt sicherer IO-Dialog mit dem Anwender.
4.) Schwerpunkt Management von Dateien, Binär- und Textformat, einige Tricks dazu, die man nicht überall liest (oder nirgends).
5.) Schwerpunkt Sortierung von Daten und der Umgang mit dynnamische Listen bzw. dynamischer Speicherverwaltung,
6.) Schwerpunkt Außendarstellung durch Dateiexport und Druckvorlagen
C is an all purpose language. Eine Sprache, die für alles taugt.
Man kann damit eine FIBU schreiben, eine CNC-Maschine programmieren oder zum Mond fliegen.
Ich beschränke mich hier auf die oben dargestellten Schwerpunkte.
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Ein Bild sagt mehr als tausend Worte.
Daher mal als Appetizer ein Blick auf einen leeren Bildschirm (Dateianhang).
Wir sehen da das, was ich schon immer (als alter DOS-Liebhaber) bevorzugt habe:
4 Fenster auf einem Bildschirm nebeneinander, die getrennt verwaltet werden. Keine überlappenden Fenster, sondern immer alles KONTEXT-SENSITIV auf einen Blick.
Also ein gutes altes Konsolen-Konzept.
Was wesentlich ergonomischer ist als dutzende von Windows-Fenstern, die sich überlagern, der Konzentration förderlich ist und das Auge erfreut.
Jedenfalls, wenn es darum geht, bei einer Finanzbuchhaltung Zahlen zu erfassen und zu verwalten. Das haben die FUGGER am Schreibpult per Tinte gemacht mit T-Bilanzen und ich wüßte nicht, daß die Fugger damit keinen Erfolg gehabt hätten.
Schwerpunkt also: klösterliche Abstinenz von allem, was aufgemotzt ist, karge (klösterliche) Darstellung, aber Übersicht, ERGONOMIE.
AUF EINEN BLICK ALLES SEHEN, nicht klicken und verschieben und
KEIN WECHSEL ZWISCHEN TASTATUR und MAUS.
Das alles gefällt mir persönlich nicht, und ich möchte das als unergonomisch bezeichnen.
Wir sehen auf der Abbildung ein Konsolen-Fenster.
Dieses Fenster ist zur besseren Darstellung in 4 Farbbereiche aufgeteilt.
Tatsächlich handelt es sich um 4 Fenster nebeneinander, die separat verwaltet werden (sollen).
In dem Fenster oben läuft das Menü, die Anwenderführung.
Links darunter findet der Dialog mit dem Anwender statt.
Rechts daneben ist ein Bildschirmbereich vorgesehen für die Auflistung von Daten, welche z. B. bei der EIngabe benötigt werden (hier hauptsächlich die verfügbaren Konten des KOntenrahmens SKR03, woraus man bei der Buchung eine Auswahl trifft).
Darunter ist ein kleiner Bereich für Kurzmitteilungen, z. B. daß der Anwender eine ungültige Auswahl getroffen hat oder Abfrage, was er machen will, z. B. Speichern oder verwerfen.
Man muß das farblich nicht trennen, ist nur mal so zur Demo. Und auch keine schreienden Farben verwenden, unter denen das Auge leidet.
Wie programmiert man sowas?
Ich zeige hier nurmal die Anweisungen aus dem Hauptprogramm, welche diesen Bildschirm auslösen, und erkläre das anschließend.
Programmauszug: int main(int argc, char *argv[]) { int i; init(); gotomenu();
printf("Der Bereich Menue, was der Anwender waehlen kann");
gotohelp(); printf("Hilfetexte und Auswahllisten ");
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Es sind 4 Anweisungen zu sehen, entsprechend den 4 Bildschirmbereichen.
goto prog, menu hilfe info
Man hätte auch schreiben können:
goto Fenster 1, Fenster 2 Fenster 3 Fenster 4
Nur daß da der Sinnzusammenhang fehlt. Denn was ist Fenster 2??
Schreibt man: goto prog ist man im Fensterbereich, der den IO-Dialog mit dem Anwender regelt.
Woher weiß daß Programm nun, was goto prog ist?
Es muß ja dafür einige Routinen erledigen:
1.) den korrekten Bilschirmbereich aufsuchen
2.) an der richtigen Stelle die printf(.. Anweisung, also die Zeichen ausgeben
3.) den Bilschirmbereich, wenn er mit alten Daten gefüllt ist, ggflls. löschen
4.) Ganz nebenbei eine andere Farbe für Text und Hintergrund einschalten
5.) verwalten, wenn die Ausgabe über die Fenstergrenzen hinausgehen sollte
Das alles soll der Anwender von MAIN aus nicht wissen, und er will es nicht wissen.
Er will einen Text schreiben in das Fenster seiner Wahl, und das Programm (bzw. die aufrufenden Routinen, bzw. Funktionen) sollen sich um die Details kümmern.
Das nennt man Kapselung.
Die Anweisung, was zu tun ist, wird nicht direkt eingegeben, sondern man ruft die dafür zuständige Funktion auf.
Will man schnell und einfach programmieren, nutzt man den ganzen Bildschirm und schmiert irgendwelche Zeichen drauf.
Will man das strukturiert erhalten, muß man kapseln, die Ausgabe an Funktionen übergeben, die den Bildschirm verwalten.
Zum Stichwort Kapselung mal folgende Abbildung:
Man hat sich überlegt, die Fenstergrößen zu verändern.
Was ist nun mit der Ausgabe der Zeichen? Hängen die im falschen Bereich?
Nein, hängen sie nicht, wie man hier sieht (Abb).
Die Aufteilung des Bildschirms wurde verändert, aber die ausgegebenen Daten "sitzen" an der richtigen Stelle, nämlich in ihrem Fensterbereich.
Weil die Ausgabe auf eine Weise gekapselt ist, die sicherstellt, daß man die Daten in dem vorgegebenen Bereich wiederfindet, auch wenn die BIldschirmaufteilung zwischenzeitlich geändert wurde.
Anders gesagt also, die Anweisungen im Programmbereich "main" wurden nicht verändert, sie erscheinen nur anders auf dem Bilschirm.
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Das macht natürlich keinen Spaß.
Oder doch?
Also man will Zahlen eingeben und saldieren, und muß sich mit so einem Sch... wie der Bildschirmdarstellung beschäftigen.
Das ist genau der Punkt, wo die Windows-Programmierer drauf herumreiten, alles kapseln und besser machen und so weiter.
Sch***! Als wenn es das unter DOS nicht längst gegeben hätte und man das Rad immer neu erfinden müßte.
Programmierung ist zielorientiert, wenn ich eine Anwendung programmieren müßte, wo der letzte ZULU in KIMBABWE noch mit der Spracheinstellung klarkommen müßte ...
hab ich aber nicht.
Also, mal grundsätzlich:
Man muß, wenn man sich komfortabel bewegen will im Programm, Angestellte haben, einen Mundschenk, einen Rittmeister, einen Chef de Cuisine, die sich selbstständig um die Aufgaben kümmern.
Die Programmierung kann nicht sein:
Liste die Zutaten auf Sellerie, Petersilie, Suppengewürz, Zeige die Zeiten Trocknen, Einlegen, Vorkochen, Zubereitung usf.
sondern das Programm muß so strukturiert sein, daß die Anweisung:
koche_gemuesesuppe()
zu dem gewünschten Ergebnis führt, egal, wie sich die Zutaten in der Zwischenzeit geändert haben.
Das ist später besonders interessant beim Management der Dateien.
Ich will jetzt mal kurz skizieren, wie man ein Programm so abstrahiert, daß die Gemüsesuppe auch das ergibt, was man wünscht:
(Programmauszug:)
#define BLACK 0 #define BLUE 1 #define GREEN 2 #define CYAN 3 #define RED 4 #define MAGENTA 5 #define BROWN 6 #define LIGHTGREY 7 #define DARKGREY 8 #define LIGHTBLUE 9 #define LIGHTGREEN 10 #define LIGHTCYAN 11 #define LIGHTRED 12 #define LIGHTMAGENTA 13 #define YELLOW 14 #define WHITE 15 #define BLINK 128
Da wird also ein Haufen von konstanten Werten festgelegt.
Diese Werte sind erstmal nicht unbedingt einsichtig, und müssen es auch nicht sein, weil sie dann wieder durch gekapselte Funktionen verwaltet werden. Z. B. zur Aufteilung der Fenster dient die Konstante
MITTE 50
Dann wird der rechte Bildschirmbereich ab Spalte 50 genutzt, weil MITTE 50
Ändert man das, z. B. auf 60, rückt der gesamte rechte Bildschirmbereich auf 60, und das linke Fenster wird größer. Alle Funktionen, die im rechten Bereich stattfinden, greifen aber nicht auf die Zahl 50 zu, sondern auf die Konstante MITTE, und wird diese geändert, paßt sich die Bildschirmausgabe aller Funktionen an diese Änderung an.
Struktuiert man das so, wird nur ein Parameter geändert, und der Rest des Programms folgt.
Jeder Bildschirmbereich hat eine andere Farbe für Text und Hintergrund (muß nicht sein). Dann muß die verwaltet werden.
Wir sehen aber vom Hauptprogramm main() keinen Zugriff auf die Farben, die sind auch gekapselt, nämlich im Aufruf der Bereiche menu, prog, help usf. schon enthalten:
Bevor diese Funktionen also einen Text ausgeben, ändern sie die Bildschirmfarben. Wünscht man das später nicht mehr, bleibt der AUfruf in der aufrundenden Funktion unverändert, die Änderung findet in der gekapselten Funktion statt.
Mit anderen Worten:
Was immer man später auch verändert, die eigentliche Programmierung bleibt UNVERÄNDERT, egal wie die Darstellung auf dem Bildschirm stattfindet.
Um einzelne Bilschirmbereich zu löschen, bevor neue Daten ausgegeben werden können, dienen diese Funktionen:
So, und woher wissen die STRINGS, die ja aus Leerzeichen bestehen oder die Linien, wie lang sie sein müssen?
Sind sie zu kurz, löschen sie den Bereich nicht vollständig, sind die Linien zu lang, überlappen sie auf den Anfang der nächsten Zeile und zerstören den Bildschirmaufbau.
Nun könnte man sagen: um den linken Bereich zu löschen, gib 40 Leerzeichen aus.
ABER:
Was ist, wenn der linke Bereich auf 60 Zeichen erweitert wird? Dann müßte man händisch nach der 40 suchen, die aber auch in anderen Bereichen vertreten sein könnte. Mit anderen Worten: dann ginge das los, mit dem Chaos.
Man kann die Länge der STrings schon beim Programmaufruf variabel definieren, nämlich:
Dieses +1 ist bei C immer im Mittelpunkt Hier bedeutet es, +1 Zeichen an den String für die STringendemarkierung.
Der Sinn dieser Deklaration ist also dieser, daß die Stringlänge schon beim Programmaufruf immer passend zur Verfügung steht, und wenn man irgendwelche Änderungen durchführt, der String nicht im Programm nachträglich verändert werden muß, sondern originär, per deklaration, paßt.
Das ist hier technisch dur die PRÄPROZESSOR-Direktive gelöst, Ergebnis, daß die Länge der Strings schon bei der Kompilierung so geetzt wird, daß sie paßt und nicht nachgträglich verändert werden muß.
Tja, das ist das eben.
Bisher ist noch keine einzige Zeile Programm geschrieben (das ist ja auch längst fertig )
Aber nur so kommt man an strukturierte PRogramme, DASS MAN DIE UNGEDULD ZUEGELT:
Danke fürs Zuhören demnächst mehr mal davon.
Gruß Sharky
Der Beitrag wurde von sharky bearbeitet: 31.01.2012, 20:58 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Ich fasse mal zusammen, was bisher erreicht wurde:
Wir haben 4 definierte Bildschirmbereiche, die wie Fenster getrennt voneinander verwaltet werden. Nur daß die eben nicht überlappen, sondern nebeneinander angeordnet sind = das ist die jahrhundertealte Tradition, sich mit einem Blick ohne Klick eine ÜBersicht zu verschaffen -> für die meisten ja doch recht banalen Anwendungen halte ich die BUNTE WINDOWS CLICK & PICK- Welt für völlig übertrieben und nicht zielführend.
Jetzt will ich aber doch einen Sündenfall begehen und zu dem Bildschirmbereich ein überlappendes Fenster programmieren.
Nur für den Fall, daß man es mal brauchen könnte. Das könnte der Fall sein, wenn man längere Infos, die man nicht unten in das kleine Infofenstesr quetschen kann, zusammenhängend am Bildschirm einblenden will, und zwar während der Bearbeitung, mittendrin eben.
Dann schreibt man einen Bildschirmbereich obendrauf, welcher die anderen Bereiche überdeckt.
Abb1: OVERLAPPING WINDOW
So weit, so schön. Das Problem ist jetzt: wie kriegt man das Fenster wieder weg, so daß der Programmablauf ungestört weiterlaufen kann?
Die eleganteste Lösung wäre physikalisch: vor dem Aufruf des OLW den Bildschirminhalt in einen BUffer kopieren, und um das OLW wieder wegzukriegen, den gespeicherten Inhalt in den Bildschirmspeicher zurückschreiben.
Mit DOS wäre das eine Lösung, unter Windows habe ich dazu nichts gefunden, höchstens die allgemeine Tendenz, Finger weg davon. Da hängen das Betriebssystem und die Grafiktreiber dran, und wenn man sich da nicht genauestens auskennt ... scheidet also aus.
Nun kann man die Sache natürlich auch softwareseitig lösen. Ist ´ne Kleinigkeit. Ein bißchen mehr formaler Aufwand, aber nur im Anfang. Das Programmieren wird dadurch insgesamt nicht aufwendiger oder unübersichtlicher, eher im Gegenteil.
Für die 4 Bildschirmbereiche MENU PROG HELP INFO bestehen separate Routinen, bislang solche, die den Bereich mit Leerzeichen üb erschreib en (d.i. löschen), allerdings in der passenden Textfarbe, so daß das Fenster kenntlich bleibt.
Die dazu erforderlichen Leerzeichen-Strings sind exakt auf den Bildschirmbereich angepaßt, weil sie mit Präprozessor-Direktiven "entstehen", das heißt die Maße werden nicht nachträglich über Konstanten eingelesen, sondern schon beim Programmstart via Compiler festgelegt. Die Präprozessor Direktiven
#define ... irgendwas
gelten heute als unmodern. Nun, hüstel, was ist nicht unmodern heute. Ich bin auch unmodern.
Ich wills mal kurz reinkopieren, wie das aufgebaut ist:
(Programmauszug):
#define EOS '\0' // END OF STRING-MARKIERUNG #define EOL '\n' // END OF LINE = NEWLINE (daneben gibt es noch '\f' = FORMFEED, Seitenumbruch, kommt später rein #define DINA4QUER 120 #define KON_BREITE DINA4QUER // DinA4 quer ist die perfekte Wahl, weil so auch die DATEV-BWAs gedruckt werden WYSIWYG #define KON_HOEHE 36 // ANzahl Zeilen Bildschirm-KOnsole #define OBEN 5 #define MITTE 80 // AUfteilung der Bereiche. #define INFOZ 5 #define UNTEN KON_HOEHE-INFOZ
#define MENUX 0 // xund y Positionen der Bereiche Punkt jeweils oben links #define MENUY 0 #define PROGX 0 #define PROGY OBEN #define HELPX MITTE #define HELPY OBEN #define INFOX MITTE #define INFOY UNTEN #define OLWX (KON_BREITE-OLWS)/2 // das OLW zentriert sich automatisch mittig, auch wenn die KOnsolenbreite oder das Fenster geändert wird #define OLWY (KON_HOEHE-OLWZ)/2
Die Funktionen, mit denen die Bildschirmbereiche getrennt voneinander mit Leerzeichen gefüllt werden :
Und so entstehen die Strings mit den Leerzeichen in der passenden Länge.
Nirgends ist eine Zahl zu sehen, das sollte man auch tunlichst vermeiden, die Formatierung geschieht immer
RELATIV ZU DEN #define Präprozessor-Definitionen, weil man so das Maximum an Flexibilität bei Programmänderungen sowie die größte Stabilität bei jeder Art von Programmänderung erreicht.
void init_formatstrings() { int i; for (i=0;i<=KON_BREITE;i++) { scrblank[i]=BLANK; } scrblank[KON_BREITE+1]=EOS;
Man sieht, bei strncpy bleibt es nicht (kopiert n Zeichen vom zweiten in den ersten STring), sondern es wird die Stringende-Markierung EOS explizit gesetzt.
Das empfiehlt sich bei C IMMER!!!!!!!!!!
Schlimmstensfalls ist es Hosenträger mit Gürtel. Besser als ohne Hose dastehen.
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Wie man das OLW wegbekommt, IM PRINZIP, sieht man in Abb. 2 und 3.
Erstmal überschreiben wir unseren PROG Bereich wieder mit Leerzeichen (andere Funktion derzeit noch nicht verfügbar), ABB1, und man sieht, das OLW ist im linken Bereich verschwunden, d.i. überschrieben.
Dazu reichen zwei Funktionsaufrufe: gotoprog(); // setze Cursor in den Bereich PROG, wo immer der auch sein möge clrprog(); // überschreibe den Bereich mit Leerzeichen
Da das OLW auch noch den Bereich HELP überdeckt, muß dieser ebenfalls ge"clear"t werden: gotohelp(); clrhelp();
Wie die Funktionen funktionieren (alles ziemlich simpel) hab ich oben schon reinkopiert.
Ergebnis vom ÜBerschreiben des HELP Bereichs zeigt Abb 2: das OLW wurde pulversiert
Gut und schön,weil, es fehlt natürlich was.
Denn mit dem CLEAR SCREENBEREICH sind natürlich auch die Daten aus den Fensterbereichen verloren.
Das ist nicht das, was wir wollen. Wir wollen natürlich an derselben Stelle weitermachen, wo wir beim Aufruf des OLW gestanden haben.
Da der Programmierer nicht zu jedem Zeitpunkt wissen kann, was der ANwender in den verfügbaren Fenster-Bereichen aufgerufen hat, und bei der Programmentwicklung auch nicht absehbar ist, wann das OLW erlaubt sein darf, oder ob vielleicht noch andere OLW dazukommen, gibt es softwaremäßig eigentlich nur eine vernünftige Lösung für das PRoblem.
WIR MÜSSEN ALLE AUSGABEN STÄNDIG IM ZWISCHENSPEICHER VORHALTEN, damit wir sie zu jedem Augenblick des PRogramms wiederherstellen können.
Das löst man ganz einfach so, daß man
NIEMALS DIREKT AUF DEN BILDSCHIRM SCHREIBT, SONDERN IN EINEN BUFFER (ZWISCHENSPEICHER)
statt also:
FALL A
1. gotoxy(zeile, spalte)
2. gib irgendwas auf den Bildschirm
muß es heißen:
FALL B
1. Nimm eine Information
2. Schreibe sie in den Zwischenspeicher
Im Fall B ist dann von der Info am Bildschirm noch nichts zu sehen. Sondern wir füllen erst den Buffer mit Infos, und wenn wir fertig sind sagen wir:
3. Schreibe Buffer auf den Bildschirm (automatisch in den dafür vorgesehenen Bereich)
Das klingt etwas umständlich, ist es aber gar nicht.
Der Vorteil, wenn wir Infos stets zwischenspeichern, ist z. B., daß wir diese auch jederzeit in eine Datei speichern und ausdrucken können.
Hier allerdings ist es erstmal angedacht, um den Bildschirminhalt jederzeit wiederherstellen zu können.
Ob es sich dabei um eine grafische Oberfläche handelt mit Pixeln oder eine Textausgabe, ist dabei wurst.
n Spalten und Zeilen, also eine zweidimensionale Tabelle.
Das Problem ist eher, daß wir nicht wissen, von welchem Datentyp.
C ist ja geradezu extrem typenbezogen.
Eine Funktion zu schreiben, die verschiedene Datentypen behandeln kann, nennt man ÜBERLADEN.
OVERLOAD
Man benutzt sie so, daß die Funktion impliizit den Datentypen erkennt und danach handelt.
Mag ja gut sein und modern, ist mir aber zu modern und ich rieche bei solchen Konstrukten immer INSTABILITÄTEN. Sobald da der kleinste Fehler drin ist, spielt das Programm Mickeymouse.
BLeibt die Frage, wie wir an eine einzige Funktion z., B. solche häufigen Datentypen wie float, integer, char, stringarrays etc. etc. übergeben können.
Die Anwort ist ganz einfach:
Wir übergeben sie im Textformat, weil ja die Ausgabe auch im Textformat erfolgt.
Für das Umwandeln belieber Datentypen in formatierter Form bietet C die hervorragende Funktion sprintf()
char info[255];
(float) f= -136.1 sprintf(info,"%10.2f",f) ergibt einen Zeichenstring der Form: " -136.10"
(char) c='#" sprintf(info,"%c",c) ergibt "#", wobei "#" nicht gleich '#' ist, das eine ist ein char, das andere ein String
Um stringarrays in stringarrays umzukopieren:
(char) text[50]="HALLO"; sprintf(info,"%20s",text) ergibt einen rechtsbündigen String etwa " HALLO";
linksbündig:
sprintf(info,"%-20s",text) ergibt "HALLO "
Vorzeichen erzwingen:
für f= 5.1 (vielleicht Prozent) sprintf(info,"%+10.1f",f) kommt heraus: " +5.1";
Und so weiter.
Sprintf() ermöglicht uns, jeden Datentyp in formatierter Form als Textstring darzustellen (nichts anderes ist ja Drucker- und Bildschirmausgabe) und bietet alles, was das Herz braucht.
Die Funktion, die nun den Buffer für den Fensterbereich verwaltet, muß folgendes leisten:
1.) Prüfen, ob der WUnsch des AUfrufers realisierbar ist oder zu Fehlern führt, Länge der Nachricht, Position, Breite des Fensters etc. etc. 2.) Wenn ok, dann den Wert dort unterbringen, in Zeile und Spalte, wie gewünscht 3.) Die Daten auf dem Bildschirmbereich ausgeben, für den der Buffer zuständig ist.
Die Größe eines solchen Buffers muß mindestens der Größe (Zeilen/Spalten) des Fensterbereichs entsprechen. Es liegt aber ja auf der Hand, daß man besser viel mehr Kapazität wählt, mit dem Vorteil, daß man dann den Buffer in dem Fenster nach oben und unten scrollen kann.
Also sagen wir mal: für einen Bildschirmbereich von 80 Spalten 20 Zeilen könnten wir einen Buffer vorsehen, der 80 Spalten und 200 Zeilen aufnimmt, und hätten dann zum Scrollen und Blättern 10 Seiten zur Verfügung. Oder 100. Je nachdem, wie man es braucht.
Das wäre dann die nächste allgemeine Routine, die zu schreiben wäre.
Bisher noch keine einzige Programmzeile, alles nur Vorbereitung.
Alles kommt zu dem, der warten kann (lao tse).
Angehängte Datei(en)
olw2.jpg ( 63.94KB )
Anzahl der Downloads: 10 olw3.jpg ( 60.57KB )
Anzahl der Downloads: 9
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Aufgabe also wie oben beschrieben. Wer benötigen für alle Fenster Buffer, d.s. Zwischenspeicher, um den Bildschirminhalt dieser Fenster, wenn er durch überlappende Fenster zerstört wurde, wiederherzustellen.
Sowas zu programmieren, als PROTOTYP, ist schnell gemacht. Die Funktion dann in allen EInzelheiten anzupassen und zu verfeinern, verlangt mehr AUfwand. Allerdings man hat entweder Spaß an der Freud oder geht abends ein Bierchen trinken. Das eine wie das andere kann nützen.
Ich stelle jetzt mal den PROTOTYP vor und beschreibe dann, wie man dahin kommt.
Wir sehen in Abb. 1, daß das Hauptfenster unten links, was dem Dialog mit dem Anwender dient, Daten enthält, und zwar pro Forma mal 3 Zeilen mit Text und Zahlen.
Dann überlappt in Abb. 2 unser Overlapping Window OLW, so daß der Text zeilweise zerstört ist.
Beim nächsten Druck auf die Taste wird das Originalfenster wiederhiergestellt, das OLW verschwindet (Abb. 3)
Das ist das, was wir haben wollen.
Angehängte Datei(en)
DIALbuf1.jpg ( 72.33KB )
Anzahl der Downloads: 7 DIALbuf2.jpg ( 78.54KB )
Anzahl der Downloads: 6 DIALbuf3.jpg ( 71.16KB )
Anzahl der Downloads: 8
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Um das zu verdeutlichen, wie das verschwindet, damit man nicht denkt, Abb.3 sei eine Kopie von Abb.1, hier der entscheidende Zwischenschritt:
1. Schritt: stelle Bildschirminhalt vom ersten überschriebenen Bereich wieder her.
Man sieht, wie das OLW hier teilweise zerstört wird, der Rest hängt rechts noch auf dem Bildschirm herum, aber auf einem anderen Bildschirmbereich. Der Bereich Programmdialog links ist wiederhergestellt, und hat mit dem Bereich rechts nicht zu tun. Den wiederherzustellen, verlangt den Aufruf der dazu passenden Funktion für diesen Bereich.
Das erfolgt dann im 2. Schritt.
Überlappt das OLW mehr als zwei Bereiche, geht das sinngemäß so, daß man eben alles wiederherstellen muß, was überlappt wurde.
Aus dem Ganzen, wenn man nicht weiß, was alles überlappt wurde (nach Programmänderungen), könnte man natürlich eine übergeordnete Funktion zusammenstellen namens
SaveScreen ()
bzw.
RestoreScreen()
die dann eben alle Bereiche sichert und anschließend wiederherstellt.
Der Beitrag wurde von sharky bearbeitet: 02.02.2012, 20:18 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Die dazu gehörigen (selbstgeschriebenen) Funktionsaufrufe sind diese:
zeigeDIAL(); // zeige den Inhalt des DIALogfensters
... drücke Taste
clrOLW(); // überblende das OLW im Zustand Leerzeichen
... drücke Taste
zeigeDIAL(); // zeige den Inhalt des DIALogfensters nochmal neu
Mit dem zweiten Aufruf des DIALogfensters wird der darüber liegende Teil des OLW zerstört, oder anders gesagt, der ursprüngliche Inhalt des Bereichs wiederhergestellt wird. Nochmal: es ist von der Organisation her egal, ob es sich um einen Textbildschirm oder um eine grafische Ausgabe (im Grafik-Modus) handelt. Das Prinzip ist exakt dasselbe. Wir haben im Grafik-Modus nur mehr Pixel, d. i. Spalten und Zeilen.
Wie man sowas organisiert, ist auch Geschmackssache. Ich bevorzuge bei der PRogrammentwicklung FLACHE HIERARCHIEN, meide also extreme Verschachtelungstiefen, jede Art von eierlegenden Wollmilchsäuen mit einer endlosen Parameterliste, mit anderen Worten alles, was das PRogramm schwer lesbar macht und zu seltsamen Seiteneffekten führen kann.
Daher wird für jeden Bildschirmbereich eine eigene Routine geschrieben, die man separat aufrufen kann/muß. Der Nachteil ist, daß man ZUNÄCHST MAL identischen Code an verschiedenen Stellen ablegt, der Nachteil muß aber kein Nachteil BLEIBEN, wenn man später die einzelnen Bereiche eben nicht mehr identisch organisieren will, sondern spezifizieren. Würde man eine einzige Funktion für die Organisation aller bestehenden und zukünftigen Programmbereiche schreiben wollen, wäre das Ergebnis die besagte eierlegende Wollmilchsau, alias ein sehr schwer lesbarer und fehleranfälliger Code.
Hier ist eine einfache Konstruktion:
int putsDIAL(char info[255],int spalte, int zeile)
Was sie macht, sieht man direkt: sie bekommt eine Information, die sie in Spalte x und Zeile y unterbringen soll,, und zwar in dem Bildschirmbereich DIAL(og). Da diese Routine nur für diesen Bereich zuständig ist, kennt sie auch die verfügbaren Spalten und Zeilen sowie die Bildschirmkoordinaten. Das kann gekapselt bleiben, wir müssen uns darum nicht kümmern.
Wir schreiben also in diesen DIAL Bereich des Bildschirms etwas hinein, nämlich Daten und Zahlen, und zwar im Textformat.
Damit ist jetzt zunächst noch nichts SICHTBARES erreicht, die Info liegt in dem Speicherbereich, der für den Bildschirmbereich zuständig ist.
Damit man das sehen kann, ruft man die Funktion auf, die den Buffer auf dem Bildschirm ausgibt, nämlich:
zeigeDIAL();
und schwupps, erscheinen die Daten auf dem Bildschirm. Sie sind aber eben nicht flüchtig, sondern wenn sie überlappt werden durch andere Fenster, kann man den ganzen Bereich durch die Wiederholung des Befehls wiederherstellen:
zeigeDIAL();
Wie sieht die Funktion aus, die die Daten in den Buffer schreibt?
Das sind die 4 Buffer für die 4 Bildschirmbereiche. Sie enthalten soviele Zeilen und Spalten, wie dem Bereich entspricht. Wieviele, ist durch #define-Präprozessor-Anweisungen festgelegt, man sollte feste Zahlen vermeiden.
Bitte das +1 beachten! Viele Programmierer sind zu C++ geflüchtet, weil sie mit den C-arrays nicht länger zu tun haben wollten. Und schreiben dann C ind C++ Dialekt. Ich hab den anderen Weg genomen und bin zu reinem ANSI-C zurück. Zu dem Problem CHAR-ARRAYS in ANSI C gleich noch ein kleines Schmankerl.
Nun die Funktion, welche sozusagen
gotoxy(Zeile,Spalte) Printf(Inhalt)
simuliert, indem sie dasselbe tut, aber nicht auf den Bildschirm schreibt,, sondern in den Zwischenspeicher:
int putsDIAL(char info[255],int spalte, int zeile) { char c; int s; // s=SPalte int pruef = OK; // 1.: Prüfung, ob Anforderung realisierbar oder nicht: if (strlen(info)+spalte>DIALS) pruef=NOTOK; // Paßt nicht in die Zeile rein if (zeile>DIALZ) pruef =NOTOK; // DIALS und DIALZ sind die max verfügbaren Spalten/Zeilen im Bereich DIALog // ... und so weiter, was nötig ist if (pruef==NOTOK) return NOTOK; // Funktion bricht hier ab, keine else-SChleife erforderlich for (s=0;s<strlen(info);s++) DIALbuf[zeile][spalte+s]=info[s]; // zeichenweises Kopieren DIALbuf[zeile][DIALS]=EOS; return OK;
}
Sie prüft also erstmal, passen die übergebenen Zeichenketten überhaupt rein, und wenn nicht, macht sie gar nichts, bricht ab.
Dann werden die Zeichen aus dem übergebenen Text zeichenweise kopiert. Und zwar so:
Text: "Hallo"
ANgefordert ist Hallo soll in Spalte 3, Zeile 5 stehen.
Dann wird das 'H' um die Position int spalte korrigiert kopiert, also
Quelle: "Hallo" Ziel:__"_____Hallo"
Eigentlich selbsterklärend (die Unterstriche mache ich, damit der Browser hier im Forum mir das nicht linksbündig setzt, was er leider tut):
Bevor wir die Zeichen reinkopieren in diese Matrix aus Spalten und Zeilen, müssen wir natürlich eine Funktion haben, die die vorhandenen Zeichen löscht.
Das ist diese:
void resetDIAL() { int z; for (z=0;z<DIALZ;z++) sprintf(DIALbuf[z],DIALblank); }
Das ist vielleicht etwas unübersichtlich, wenn man sich mit C-Char-Arrays nicht auskennt.
Es gibt verschiedene Möglichkeiten, char-Arrays zu organisieren.
Die hier verwendet wurde, ist diese:
char meine_matrix [ZEILEN][SPALTEN]
Die kann man so ansprechen:
printf(meine_matrix[0]);
gibt die Zeile 0 aus, also die erste.
Übersichtlicher oder sagen wir mal überdeutlicher wäre folgende Organisation (die ich nicht gewählt habe, weil mir zu umständlich):
Nach dem Prinzip des Ockhamschen Rasiermessers mache ich es so flach und einfach wie möglich. Kein Schwulst, daher:
char textmatrix[ZEILEN][SPALTEN] und nichts sonst.
Natürlich, wenn man Zeilen und Spalten verwechselt im Programm, gibt´s Mist, nur die Sprache C wurde nicht entwickelt, damit der Programmierer das DENKEN EINSTELLEN KANN.
Das ist gut so, denn sonst würde es ja keinen Spaß machen.
Der Beitrag wurde von sharky bearbeitet: 02.02.2012, 21:14 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Zum Thema Char-Arrays in C und die Fluchtmöglichkeit STrings in C++
Die C-char-arrays haben es natürlich in sich, wo Rauch ist, ist auch Feuer.
Das schlimmste, was man machen kann, ist Bereichsüberschreitung. Dann schaufelt das Programm MÜLL IN DEN SPEICHER,
ohne daß der Compiler oder zur Laufzeit irgendeine Fehlermeldung kommt.
Das ist eben der Charme von C.
Daß der Programmierer seinen Kopf nicht nur zum Haareschneiden hat.
Mal so eine Tücke vorgestellt, mit der man ein PRogramm regelrecht abschießen kann:
Das Datum "xx.xx.xxxx" hat 10 Zeichen. Nämlich 8 Zahlenwerte und zwei Punkte =10.
Wir sagen:
char datum[10]="01.01.2012";
Und sagen:
printf(datum);
Alles bestens!
Und fragen:
printf("%i", strlen(datum)); mit der Ausgabe : 10, weil 10 Zeichen lang
Jetzt lassen wir den Compiler mal selbst entscheiden, wieviele Stellen er für das char-array haben will:
char datum2[]="01.01.2012"; // der Compiler reserviert soviel Speicher, wie er für richtig hält, daher ist in den [] keine Zahl genannt
und fragen:
printf("%i",strlen(datum2)); mit der Ausgabe 10, weil 10 Zeichen lang.
Alles klar? Alles bestens?
Mit solchen Konstruktionen kann man wie gesagt ein ganzes Programm regelrecht abschießen.
Die Funktion strlen sagt nämlich überhaupt nichts über den reservierten Speicherbereich aus, sondern nur über die Anzahl ZUFÄLLIG VERSAMMELTER ZEICHEN.
Wenn wir wissen wollen, was da wirklich los ist, müssen wir den
OPERATOR sizeof() bemühen.
Machen wir das mal:
printf("%i",sizeof(datum)); // mit der Ausgabe 10
printf("%i",sizeof(datum2)); // mit der Ausgabe 11
HUCH?
Ja, das sind die C char arrays.
Wir brauchen 10 für den Inhalt und Nr. 11 für den END OF STRING '\0'
Nehmen wir char datum[10] ist das PRogramm schon im A...., weil bei jedem Aufruf der Funktion die Stringende-Markierung ins Nirwana des Rechenspeichers geschaufelt wird.
SCHLIMM, SCHLIMM,SCHLIMM
Weils weder der Compiler merkt, noch zur Laufzeit Fehlermeldung erscheint und dreifach SCHLIMM, weil das Programm durch diese einsamen Zeichen nicht gleich abstürzen wird.
Es macht nur seltsame Ergebnisse, es arbeitet nicht zuverlässig, ist nicht stabil.
Man kann ja mal versuchen, die Fehlerursache, ein nicht druckbares Zeichen '\0' zu finden!
Daher also immer +1
[size="4"][/size]
Und wenn man zwar weiß, was geschehen soll, aber nicht abschätzen kann, was geschehen wird, z. B.:
Wir haben ein Fenster von Breite 80 Zeichen.
Und kopieren da Infos im Textformat rein, von denen wir annehmen, die seien immer kleiner als 80 Zeichen, sagen wir:
sprintf(textbuffer,"%10.2f",floatx);
Dann haben wir spätestens dann verloren, wenn wir Zuweisungen machen wie:
sprintf(textbuffer,andererbuffer);
Oder strcpy(textbuffer,quellbuffersowieso);
Wenn der textbuffer 80 Zeichen aufnimmt, und der Quellbuffer 120 Zeichen lang ist, schaufeln wir 40 Zeichen Müll in den PRogrammspeicher, und überschreiben damit unkontrolliert reservierte Speicherbereiche.
Absoluter Horror!
Daher sollte man für solche Primitiv-Formate wie char info[] immer üppig Platz vorhalten, z. B. 255 Zeichen vorsehen. Ist zwar keine Garantie, daß da keine logischen Fehler erfolgen, aber das Programm läuft dann wenigstens stabil.
Wenn man zeichenweise kopiert
for (i=0;i<strlen(irgendwas),i++) ziel[i]=quelle[i];
sollte man immer daran denken, die END OF STRING Markierung '\0' explizit zu setzen, nämlich:
ziel[strlen(quelle)+1) = '\0';
Dann brennt da nichts an.
Und wenn man die char-arrays (was besonders bei Dateien natürlich nötig bzw. sinnvoll ist) in der Länge knapp am Nötigen kalkuliert, sollte man an die +1 denken.
Also wie gesagt, zu den char-arrays in C sind die Meinungen geteilt.
Meine Meinung habe ich geäußert. Diese Dialekte, C++ PRogrammierung, wo dann doch keine OOP drin ist, sondern nur Dialekt, ein paar Funktionen übernommen werden, MISCHMASCH-PROGRAMMIERUNG, ist nicht mein DIng. Das muß aber jeder für sich selbst entscheiden.
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Zurück zu der Funktion, die unseren Bildschirmbereich puffert.
Um die Kommentare bereinigt, bleibt davon:
int putsDIAL(char info[255],int spalte, int zeile) { int s;int pruef = OK; if (strlen(info)+spalte>DIALS||zeile>DIALZ) pruef=NOTOK; if (pruef==NOTOK) return NOTOK; for (s=0;s<strlen(info);s++) DIALbuf[zeile][spalte+s]=info[s]; DIALbuf[zeile][DIALS]=EOS; return OK; }
Die können wir natürlich nun per copy&paste sowie suchen und ersetzen für alle anderen Bildschirmbereiche übernehmen.
Bezeichner für die BIldschirmbereiche (ich hab die Namensgebung etwas geändert) sind jetzt:
MENU // für Menüführung DIAL // für Dialog mit dem Anwender bzw. die eigentliche Programmausgabe INFO // für zusätzliche Informationen und Auswahlfelder, hier der Kontenrahmen SKR03, der eingeblendet wird, damit wir die richtige Konto-Nr wählen können MBOX// Message-Box für HInweise und Meldungen im Programmverlauf
Das Format des Bildschirms ist 120 Spalten = DINA4QUER.
Wollen wir WYSIWYG (what you see is what you get) die Druckausgabe am Bildschirm vorher sehen, müssen wir umschalten auf ein DIAL_QUER von 120 Zeichen. Das kommt dann später und ist auch mit Copy&paste zu haben.
Wir kopieren jetzt die Funktion aus dem DIAL für den nächsten Bereich INFO:
int putsINFO(char info[255],int spalte, int zeile) { int s;int pruef = OK; if (strlen(info)+spalte>INFOS||zeile>INFOZ) pruef=NOTOK; if (pruef==NOTOK) return NOTOK; for (s=0;s<strlen(info);s++)INFObuf[zeile][spalte+s]=info[s]; INFObuf[zeile][DIALS]=EOS; return OK; }
Keine große Nummer.
Weil die Variablen alle standardisierte Bezeichner haben, wo man nur PROG gegen INFO austauschen muß und fertig.
Der nächste Schritt wäre dann, daß man die Fenster scrollt.
Und da hätten wir dann schon die Spezifizierung, nämlich die Message-Box MBOX zu scrollen wird wenig Sinn machen.
Ob man das Hauptfenster scrollen soll, nun, weiß ich noch nicht.
Das Scrollen funktioniert im Prinzip so:
Wir haben einen Buffer, der länger ist als der sichtbare Bereich.
Sagen wir, der sichtbare Bereich sei 20 Zeilen (20 darf man natürlich NIEMALS reinsetzen, sondern entsprechend der Präprozessor-Direktive die Konstante dafür, z. b. ZEILEN_BEREICH1), und der Buffer 200 (=ZEILEN_BEREICH*10), hätten wir 10 Seiten zum scrollen (ob das sinnvoll oder nötig ist, wie gesagt ...). Und könnten auch überlegen, soll man seitenweise scrollen oder zeilenweise.
Wir könnten dann die Taste Bildunten verknüpfen mit der Anweisung, daß die nächsten ZEILEN_BEREICH1 Zeilen angezeigt werden, und Bildoben mit der Anweisung, ZEILEN_BEREICH1 Zeilen zurückzuspringen bzw. zur ersten Zeile (wenn wir oben anstoßen), und Pfeiloben und Pfeilunten für zeilenweises Scrollen.
Ob das nun Sinn macht oder nicht, stellt sich oft erst später heraus. Daher werde ich unabhängig davon, ob es Sinn macht, nur aus formalen Gründen, für die Bereiche die Funktionen programmieren, damit man, wenn man aus der FORMALEN PROGRAMMIERUNG rausgeht und die VERARBEITENDE PROGRAMMIERUNG angeht, sich um solche Details nicht mehr kümmern muß und die Funktionen bei Bedarf verfügbar hat.
Das Datenformat für solche kleinen Datenbestände ist sinnvollerweise ein Index. Hätten wir es zu tun mit möglicherweise sehr großen Datenbeständen, wäre es sinnvoll, da mit dynamischen Listen zu lösen. Brauchen wir hier nicht.
Der Umgang mit dynamischen Listen folgt aber noch, nämlich in dem Zusammenhang, daß man Dateien, welche auf dem Speichermedium (Festplatte, USB-STick oder sonstwas abgelegt sind), einlesen und sortieren muß. Und das muß man, bei einer FIBU sollte das chronologisch sortiert sein, weil ja die Buchungen auch mit zurückliegenden Datumsangaben eingegeben werden können, das beim Sichten der Daten aber sehr irritierend ist, wenn man sie in der Reihenfolge der Eingabe sieht.
Kommt alles noch. Hab ich längst programmiert und ist schon in Betrieb, hier im Beitrag aber ist das noch zu früh. Erstmal kommen die Formalien der Darstellung auf dem Bildschirm, dann geht es an die eigentliche Datenverarbeitung.
Der Beitrag wurde von sharky bearbeitet: 02.02.2012, 22:29 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Hält man die Hierarchie flach, entstehen die Funktionen durch Kopieren und Auswechseln der MÖGLICHST STANDARDISIERTEN BEZEICHNER, wie hier an der Farbgebung sichtbar gemacht.
Funktion für Bildschirmbereich 1 (DIAL), d. i. Dialog, der eigentliche PRogrammbereich:
int putsDIAL(char info[255],int spalte, int zeile) { int s; int pruef = OK; if (strlen(info)+spalte>DIALS|| (zeile>DIALZ) pruef =NOTOK; if (pruef==NOTOK) return NOTOK; for (s=0;s<strlen(info);s++) DIALbuf[zeile][spalte+s]=info[s]; DIALbuf[zeile][DIALS]=EOS; return OK; }
Durch Suchen/Ersetzen entsteht fehlerfrei, da standardisiert, die Funktion für den nächsten PRogrammbereich INFO
int putsINFO(char info[255],int spalte, int zeile) { int s; int pruef = OK; if (strlen(info)+spalte>INFOS|| (zeile>INFOZ) pruef =NOTOK; if (pruef==NOTOK) return NOTOK; for (s=0;s<strlen(info);s++)INFObuf[zeile][spalte+s]=info[s]; INFObuf[zeile][INFOS]=EOS; return OK; }
Die Nachteile wurden schon genannt, würde man die erste Funktion ändern, und die Änderungen für alle folgenden übernehmen wollen, würde man die Clons mit der Hand ändern müssen, was immer so eine Sache ist. Sie würden sich nicht automatisch ändern.
Man kann aber, bei flachen Hierarchien, diese durchaus löschen und durch neue Kopien ersetzen.
Die in 20facher Verschachtelungstiefe angelegte eierlegende Wollmilchsau ist das andere Konzept, nun, wer´s mag.
Was ich überhaupt nicht mag, sind weniger die Verschachtelungstiefen als ellenlange Parameterlisten beim Aufruf einer Funktion, und je komplexer die ist, umso länger muß die Parameterliste sein, oder man verdeckt das, indem diese Funktion nicht sichtbar auf bestimmte globale Speicherbereiche Zugriff hat.
Das ist auf gut Deutsch gesagt für die Hobbyprogrammierung alles MIst, weil die Transparenz verloren geht und das in den kryptischen Bereich rückt.
Ist aber wie gesagt Ansichtssache. Es gibt bestimmt Leute, die sind voll vom Gegenteil überzeugt.
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Kurze Zusammenfassung, was bisher erreicht wurde.
Die Funktionen wurden für das Fenster DIALog geschrieben und getestet.
Danach werden sie per Copy&Paste und Suchen/Ersetzen für die anderen Bildschirmbereiche umkopiert. Damit können wir beliebig viele Bildschirmbereiche und beliebig viele Überblend-Fenster verwalten, ohne eine Zeile zusätzlichen Code zu schreiben.
Die Buffer werden nun zunächst mit Daten gefüllt.
Wir sagen, anstatt gotoxy(s,z) printf(irgendwas):
putsDIAL, schreibe den String in den Buffer DIAL, oder INFO, oder OLW
Das sieht dann so aus:
int main(int argc, char *argv[]) { char loc_info[255]; char c; init(); // die Initialisierung aller Variablen
resetOLW(); putsOLW("Hinweis: ",5,1); putsOLW("Vermutlich logisch falsche Buchung",5,3); putsOLW("Konto 8200 war bisher Einnahmenkonto, wird aber belastet",5,4); putsOLW("Buchungssatz Nr. 364 vom 18.01.2012",5,5);
Man erkennt, die Funktion sprintf() als zusätzliche Zeile benötigen wir nur, wenn wir andere Datentypen (z.B. Zahlen) in Text formatieren wollen. Haben wir es sowieso mit Text zu tun, können wir
anstelle von putsINFO(loc_info,zeile,spalte);
auch gleich den Text eingeben: putsINFO("Mein Text",zeile,spalte);
Somit sind wir beim Programmieren sogar wesentlich ökonomischer als mit dem Begriffspaar gotoxy() und printf()
Wenn wir mit den Daten soweit fertig sind, zeigen wir sie auf dem Bildschirm, indem wir die Ausgabefunktion zeige... aufrufen.
Wie gesagt, wir müssen uns nicht kümmern, wie die Funktion das macht oder wo der Bereich ist, einfach nur aufrufen:
zeigeDIAL(); zeigeINFO();
c=getch(); // Tastendruck
(Abbildung 1).
Wir warten auf den Tastendruck und überblenden dann das OLW:
zeigeOLW(); // das OLW überblenden
Den zugehörigen Bildschirm zeigt Abb. 2
Wenn wir dann erneut eine Taste drücken, wird der ursprüngliche Zustand wiederhergestellt (wie in Abb. 1) durch den erneuten Aufruf der Funktionen der beiden Fenster, die überblendet worden sind:
zeigeDIAL(); zeigeINFO();
Das ist schon alles.
Die Philosophie dahinter kann man so beschreiben:
Halte die Funktionen einfach und übersichtlich und strapaziere dich selbst (als Programmierer) nicht mit schwer durchschaubaren Referenzen und endlosen Parameterlisten.
Später mal könnte man das Design straffen und komprimieren, anstelle also die Funktionen per copy und paste zu vervielfältigen, wie hier realisiert:
int putsDIAL(char info[255],int spalte, int zeile) { int s; int pruef = OK; if (strlen(info)+spalte>DIALS||zeile>DIALZ) return NOTOK; for (s=0;s<strlen(info);s++) DIALbuf[zeile][spalte+s]=info[s]; DIALbuf[zeile][DIALS]=EOS; return OK; }
int putsINFO(char info[255],int spalte, int zeile) { int s; int pruef = OK; if (strlen(info)+spalte>INFOS||zeile>INFOZ) return NOTOK; for (s=0;s<strlen(info);s++) INFObuf[zeile][spalte+s]=info[s]; INFObuf[zeile][INFOS]=EOS; return OK; }
... könnte man diese Funktionen zu einer einzigen zusammenfassen per
switch (window) { case DIAL: .... break; case INFO: .... break; }
Nicht nur die Fenster separieren, sondern alle Befehle, die sich für alle wiederholen, ausklammern.
Das ist aber zum gegenwärtigen Zeitpunkt weder sinnvoll noch erforderlich.
Abb. 3 spare ich mir, sie ist ja identisch mit Abb. 1
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Scrollen
Das Scrollen ist die Anwort auf den Überhang von verfügbarem und sichtbarem Text. Ist der verfügbare Text größer, als er in den Ausschnitt paßt, bewegen wir den Ausschnitt im Text vor oder zurück.Wir können blättern oder zeilenweise verschieben. Oder zum Anfang und zum Ende springen.
Prototyp einer Funktion SCROLL:
setze den DATENZEIGER AUF EINEN WERT // WElchen, das wird noch die Frage sein
Endlosschleife
AUSGABE text ab DATENZEIGER
Tastaturabfrage ergibt:
option 1 DATENZEIGER -1 (1 Zeile zurück) option 2 DATENZEIGER +1 option 3 DATENZEIGER +1Seite option 4 DATENZEIGER 1 Seite zurück option 5 DATENZEIGER springe an den Anfang option 6 DATENZEIGER springe ans Ende option 7 Schleife abbrechen
Ende der Endlosschleife
Die in der Endlos-Schleife zwangsläufig befindliche Funktion AUSGABE muß natürlich eine Bereichsüberprüfung machen, ob die Anforderung so machbar ist. Bereichsüberschreitungen in C sind IMMER der sichere Griff in die Kloschüssel.
Das geht alles seinen Gang, bis der Prozeß abgebrochen wird zugunsten eines neuen. In dem Augenblick, in dem während des Scrollens ein neuer Prozeß aufgerufen wird, ist der Wert für DATENZEIGER zerstört. Denn beim Verlassen einer Funktion sind die WERTE ALLER LOKALEN VARIABLEN nicht länger definiert. Daher wird es uns mit LOKALEN Variablen nicht gelingen, den Bildschirm wiederherzustellen, um das OLW zu überschreiben. Diese Daten müssen während des Funktionsaufrufes irgendwo so gespeichert werden, daß man sie auch nach dem Verlassen der Funktion wieder auslesen kann. Und sie müssen nicht nur einmal gespeichert werden, sondern während des Prozesses ständig aktualisiert werden. Das kann man ziemlich einfach erreichen, indem man sie als GLOBAL deklariert.
GLOBAL hat Nachteile und Vorteile. Z. B. daß globale Variable mit lokalen Variablen konkurrieren und so Seiteneffekte entstehen können. .
Unbeabsichtigt, bei Allerweltsnamen, kann z. B. auf diese Weise Chaos entstehen:
int info; // Global, Allerweltsname
void MacheIrgendwas() {
int info // LOkale Variable
info = irgendwas ...
Dann greift die Funktion auf die lokale Variable info zurück.
Geht allerdings durch Editieren die lokale Deklaration verloren, versehentlich:
void MacheIrgendwas() {
// int info gibt es nicht mehr, wurde wegeditiert
info = irgendwas ...
dann gibt es keine Fehlermeldung, sondern die FUnktion greift auf die Globale Variable info zurück. Nun, vielleicht etwas theoretisch. Oder eben auch nicht. Shit happens.
Mehr praxisnah ist folgender Programmabschuß:
Die Funktion
void MacheIrgendwasAnderes() {
INFO = irgendwas ...
greift völlig beabsichtigt und völlig korrekt auf die nunmehr durch GROSSBUCHSTABEN als GLOBAL gekennzeichnete Variable INFO zu.
So weit, so gut.
Nur wenn diese Funktion einen Fehler enthält, und zwar an einer völlig anderen Stelle, die mit dem Aufruf der globalen Variable überhaupt nichts zu tun hat, welche zu einem BEREICHSÜBERLAUF führt, und eine derart fehlerhafte Funktion nun auf globale Variablen zugreift, BREITET SICH DIE SEUCHE AUS, wir haben dann CHAOS auch im Speicherbereich der Globalen Variablen. Grundsätzlich ist es also nicht gut, WENN JEDE XBELIEBIGE FUNKTION einfach auf GLOBALE VARIABLEN zugreifen darf. Man sollte im Idealfall alle globalen Variablen so kapseln, daß der Zugriff auf sie nur über eine Schnittstelle möglich ist. Da eine Schnittstelle wieder zusätzlichen Organisatiionsaufwand und Parameterlisten mit sich bringt, muß man aber auch fragen, ob der Aufwand lohnt.
Bis dahin wäre die Frage, welche der bisher vorhandenen bzw geplanten Funktionen nun die GLOBALEN Variablen verändern darf und welche nicht. Die Funktion SCROLL fragt die Tastatur ab und meldet WÜnsche an, den Text ab Zeile ... zu lesen. Die Funktion AUSGABE prüft, ob das realistisch ist und gibt den Text aus oder verweigert das. Die Funktion SCROLL darf den Wert für STARTZEILE nicht verändern, weil ihre Anforderungen ja noch von der der Funktion AUSGABE überprüft und evtl. verworfen werden. Also macht das (zunächst mal) die Funktion Ausgabe, was mir nicht wirklich gefällt, weil AUSGABE keine datenverarbeitende, sondern eine reine OUTPUT, eine Bildschirm- Funktion ist.
Die Organisation der globalen Variablen erfolgt sinnvollerweise NIEMALS als eine Anhäufung von irgendwelchen Daten, die irgendwo im PRogramm zu finden sind, sondern strukturiert, d. i. auf einem Platz zu finden. Nennen wir diesen Platz mal REGISTER, im REGISTER finden sich alle GLOBALEN Variablen.
Wir haben zunächst die Anforderung, theoretisch, für n Bildschirmbereiche n INTEGER verfügbar zu machen für die aktuelle Startzeile. Aktuell ist, wenn der BIldschirm überlagert wird. Was in diesem Moment anliegt, muß anschließend wiederhergestgellt werden.
int bildschirmbereiche [n] ist genau die Art von Programmierung, die man vermeiden sollte, denn was ist bildschirmbereich[0]?
Nehmen wir mal den Datentyp enum:
enum screens = {DIAL, MENU, MBOX,INFO, OLW}
wird das schon sehr viel übersichtlicher.
Jetzt brauchen wir noch einen Speicherplatz, um die Startzeilen der bislang 5 Bildschirmbereiche zu speichern:
int zeile_aktuell[OLW];
Schräg, was? Das sieht sehr wackelig aus, funktioniert aber nicht nur, sondern ist solide Programmierung.
Ist aber übertrieben, nicht zielführend und nicht ausreichend, denn wir können ziemlich sicher sein, daß die GLOBALEN VARIABLEN nicht nur von einem Typ sein werden, daher:
struct REGISTER { int datenzeiger[OLW]; // zunächst mal nur die aktuellen Buf-Zeilen für die Bildschirmbereiche }
struct REGISTER REG;
Dann können wir den Datenzeiger für den Bildschirmbereich OLW so ansprechen:
int buffer_pos;
buffer_pos= REG.datenzeiger[OLW];
... liefert den aktuellen Wert für den Buffer OLW, also ab welcher Zeile ausgegeben wird.
Sinn macht das Scrollen sicher NICHT beim Dialogfenster mit dem Anwender. Da werden Daten abgefragt und ausgegeben, um eine EIngabe zu erzielen.
Wo das SInn machen könnte, das wären die Info-Fenster oder das OLW.
Man könnte dann, im OLW, während der Bearbeitung z. B: einen Text von Charles Dickens lesen.
Der Beitrag wurde von sharky bearbeitet: 03.02.2012, 16:21 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Implementation der Scrolling-Funktion
Um ein Scrolling zu realisieren, benötigen wir zunächst mal einen SPeicher für den Bildschirmausschnitt, welcher größer ist als der Bildschirmbereicht. Wäre das nicht so, müßte man nicht scrollen.
Die Buffer-Länge (=spätere Anzahl der Textzeilen) wird als Präprozessoranweisung festgelegt. OLWBUFLEN bezieht sich auf den Bereich OLW, das überlappende Fenster bekommt also 200 Zeilen spendiert, ebenso das Fenster rechts, MTLG = MItteilungen. Die Buffer für die anderen Bereiche bleiben identisch mit der sichtbaren Zeilenhöhe, also DIALZ = Zeilen im Dialogfenster, identisch mit dem Speicher für dieses Fenster.
(MTLG statt INFO weil ich INFO info komplett rausgenommen habe wg. Seiteneffekten mit Windows-Systemaufrufen)
Als nächstes muß man die Initialisierungsroutinen anpassen:
Die Textfelder werden mit BLANKS überschrieben, und für OLW und MTLG benötigt man nun den größeren Zähler für den höheren Wert 200. Die anderen Fenster bleiben wie sie sind, Anzahl der Textzeilen=Bildschirmausschnitt. Macht man das nicht, hat man es mit großen Mengen nichtinitialisierten Textfeldern zu tun, wovon DRINGEND abzuraten ist.
Als nächstes werden die PUT-STRING Funktionen angepaßt, welche die Infos nach SPalten und Zeilen formatiert in diese Buffer schreiben. Die sind ja noch auf ANZAHL-ZEILEN = BILDSCHIRMBEREICH gestellt und würden sonst den längeren Text ignorieren.
Für Zeilenüberlauf wurde festgelegt, daß der Text trotzdem genommen wird, aber linksbündig, und mit einer Bestrafungs-Markierung ASCII 219 versehen wird.
int putsMTLG(char nachricht[255],int spalte, int zeile) { int fehler=NEIN; int s; int pruef = OK; int sp=spalte; // lokale Kopie int len=strlen(nachricht); // strlen nur einmal aufrufen und Ergebnis ablegen if (len>MTLGS||zeile>MTLGBUFLEN) return NOTOK; // zu breit btw. Bereichsüberschreitung if (sp+len>MTLGS) { sp=0; fehler=JA; }// zu weit rechts platziert for (s=0;s<len;s++) MTLGbuf[zeile][sp+s]=nachricht[s]; MTLGbuf[zeile][MTLGS]=EOS; // Stringendemarkierung explizit if (fehler==JA) { MTLGbuf[zeile][0]=219; // Die BEstrafung: ASCII CODE 219 return NOTOK; } // ASCII CODE 219 zur STrafe return OK; }
Eine Anmerkung dazu: Die Funktion strlen in einer lokalen Variable sp zu kopieren, scheint umständlich, ist aber guter Programmierstil. Wenn man strlen mehrfach benötigt, würde diese Funktion immer wieder neu aufgerufen, was für die Performance nachteilig ist. Ist die Info insgesamt zu lang für die Zeile, erfolgt keine Ersetzung sondern die Funktion bricht ab mit return NOTOK.
Um unseren Buffer sehen zu können, brauchen wir die Funktion zeigeOLW bzw. zeigeMTLG
Diese Funktionen, die bisher von 0 bis Bildschirmbereich Ende angezeigt haben, müssen auch angepaßt werden. Und zwar so, daß man ihnenn mitteilt, ab welcher Zeile sie auszugeben haben.
Statt: void zeigeDIAL() {
muß es jetzt heißen:
void zeigeMTLG(int bufzeiger)
Wir begeben uns nun in das Gefahrenfeld der Bereichsüberschreitung = PROGRAMMKILLER. Man fängt Bereichsgrenzen am besten am ENDE DER KETTE ab, also hier, bei der Ausgabe. Weil man sich nicht darauf verlassen kann, daß die vorangegangenen Funktionen durchgängig den Bereich abfangen.
Die Begrenzer sind nach vorn der Datensatz 0. Wenn also bufzeiger<0 wird er auf 0 gesetzt oder eine Fehlerroutine aufgerufen.
Der Begrenzer nach hinten ist die Anzahl Zeilen der Buffer, z.B. OLWBUFLEN. Der größte sinnvolle Wert ist hier nicht die letzte Zeile (weil dann der Bildschirm nicht gefüllt wäre), sondern OLWBUFLEN-OLWZ, also Dateigröße - Bildschirmausschnitt. Insofern also:
if (bufzeiger>OLWBUFLEN-OLWZ)bufzeiger=OLWBUFLEN-OLWZ; // überlauf verhindern if (bufzeiger<0) bufzeiger=0; // Bereichsüberschreitung nach vorn abfangen
Wir könnten also, um an den Dateianfang zu springen, den Wert -1 übergeben, die Funktion fängt das ab und macht eine 0 draus, ebenso zur anderen Seite z. B. 10000 aufrufen, um an das Dateiende zu springen (bzw. das Ende des Textbereichs).
Nachdem die Komponenten soweit bereit stehen, kommen wir zur eigentlichen SCROLLING-Funktion.
Die Steuerung erfolgt sinnvollerweise mit den dafür standardisierten Tasten: PFEILOBEN = gehe 1 zurück, BILDOBEN gehe 1 Seite zurück und so fort. Die ASCII-Werte der Tastatur finden sich als Präprozessor-Anweisung:
so daß wir sie in der Funktion wie folgt aufrufen können:
switch ( (int) taste) { case ESCAPE: return; break; case BILDOBEN: bufzeiger-=maxzeilen; // SEITE ZURÜCK if (bufzeiger<=0)bufzeiger=0; break; case BILDUNTEN: bufzeiger+=maxzeilen; // SEITE VOR if (bufzeiger>max-maxzeilen)bufzeiger=max-maxzeilen; break; case PFEILUNTEN: bufzeiger++; // Zeile vor if (bufzeiger>max-maxzeilen)bufzeiger=max-maxzeilen; break;
Wenn wir die SCROLL-Funktion für alle Bildschirmbereiche schreiben, müssen wir mitteilen, für welchen Bildschirmbereich der Aufruf gelten soll.
Dazu dient diese ENUM-Definition:
enum screens{MENU,DIAL,MTLG,MBOX,OLW};
int screen[OLW];
Der Kopf der SCROLL-Funktion sieht dann so aus:
void scroll(int screen,int bufzeiger) {
Und die Sortierung der betr. Fensterdaten erfolgt so:
char taste; int maxzeilen,max; if (screen==OLW){maxzeilen=OLWZ;max=OLWBUFLEN;} else if(screen==MTLG){maxzeilen=MTLGZ;max=MTLGBUFLEN;} else return; // keine anderen Optionen vorgesehen
do { if (screen==OLW) zeigeOLW(bufzeiger); else if (screen=MTLG) zeigeMTLG(bufzeiger); else return;
Die zweite else if - Anweisung erscheint überflüssig. Ist sie auch, WENN der Programmcode so BLEIBT. Wenn man aber später mal was ändert und was übersieht, ist sie eben NICHT überflüssig. Bei der Bereichsprüfung kann man gar nicht sorgfältig genug sein,
Nun hatte ich schon bemerkt, die Bereichsüberprüfung der Ausgabedatei (bzw. Textspeicher) erfolgt am Ende der Kette. Man sieht hier aber, daß die Funktion SCROLL ebenfalls eine Bereichsprüfung durchführt, und die könnte man ja nun wirklich weglassen, oder?
Nein, kann man nicht. Zwar brennt bei der Textausgabe nichts an, weil die Funktionen den Bereich abfangen, aber unser Tastenwert würde sich beim Scrollen "auskuppeln". Z. B. wenn man 5x BILDOBEN drückt, steht der Bufzeiger bei -100, Drückt man nun Pfeilunten, um eine Zeile nach unten zu gehen, müßte man 100x drücken, bevor was passiert. Daher also ...
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Nachdem das alles soweit parat steht, sehen wir mal auf die Anweisungen vom Hauptprogramm (von wo das vorläufig noch aufgerufen wird, später natürlich hat alles seinen Platz).
Erstmal füllen wir die Bildschirmspeicher mit Daten (nur pro Forma):
resetMTLG(); int su=0; for (i=0;i<MTLGBUFLEN;i++) { sprintf(loc_nachricht,"Mitteilung Nr. %4i",i); putsMTLG(loc_nachricht,su,i); su++; if (su>15)su=0; } resetOLW(); for (i=0;i<OLWBUFLEN;i++) { sprintf(loc_nachricht,"Ueberblendfenster - nachricht Nr. %4i",i+1); putsOLW(loc_nachricht,i,i); }
Die Nachrichten rücken (pro Forma) pro Zeile 1 Stelle nach rechts und müssen dann natürlich irgendwann am rechten Bildschirmrand anstoßen. Geschieht dies, erfolgt unsere "Strafaktion" mit dem ASCII-Zeichen 219. Bei OLW lassen wir es drauf ankommen, bei MTLG sorgen wir selbst dafür, daß es nicht geschieht.
Um die Fenster zu zeigen, rufen wir sie erstmal vom Dateianfang an auf, also mit dem Parameter 0 (null).
zeigeDIAL(); zeigeMTLG(0);
c=getch(); // Tastendruck
scroll(OLW,0); // Scrollen im Überblendfenster zeigeDIAL(); // Bildschirm links wiederherstellen scroll(MTLG,0); // Bildschirm rechts wiederherstellen und SCROLLEN im Fenster MITTEILUNGEN
zeigeDIAL(); //die Inhalte der beiden Fenster erneut ausgeben zeigeMTLG(0); // damit ist der Bildschirm wiederhergestellt
Auf Tastendruck also überblendet das OLW beginnend mit der Zeile 0 (Abb. scroll0)
Beim Scrollen entdecken wir irgendwann den ÜBergang, wo der Text rechts angeschlagen wäree und korrigiert wurde (Abb scroll1) Der schwarze Balken ASCII 219 macht darauf unmißverständlich aufmerksam, daß hier was nicht gestimmt hat.
Nach dem Verlassen des Scrolling kommen wir zum Fenster MTLG (rechts), wo wir ebenfalls scrollen können (Abb scroll2)
Damit steht die Scrolling-ROutine für 2 Fenster zur Verfügung und kann später bei der eigentlichen Datenverarbeitung genutzt werden.
Um zum Bsp. den Kontenrahmen SKR03 zu sehen, kann man ihn in den Buffer kopieren und in einem Fenster seiner Wahl anzeigen und zum Scrollen freigeben, denn der SKR03 ist so ein Typ, der nicht auf den Bilschirm paßt.
Oder man kopiert sich in das stark erweiterte OLW ein Buch seiner Wahl und macht noch eine BOSS-TASTE, damit der Chef einen nicht erwischt.
Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 10:43 Uhr
Angehängte Datei(en)
scroll0.jpg ( 227.27KB )
Anzahl der Downloads: 9 scroll1.jpg ( 228.68KB )
Anzahl der Downloads: 5 scroll2.jpg ( 140.03KB )
Anzahl der Downloads: 6
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Die Implementation der Dateiverwaltung
Die gesamte Dateiverwaltung kann man vor der eigentlichen Programmierung als PROTOTYP schon erledigen. Man muß dann nachher, wenn die endgültige Struktur der Daten feststeht, nur anpassen. Das kann mit Suchen und Ersetzen geschehen und ist schnell erledigt. Vorher möchte ich einige allgemeine Hinweise zum Arbeiten mit Dateien in ANSI-C geben.
1. Text oder binär?
Das Textformat, also ein char-array aus druckbaren Zeichen, exisistiert nur unter MSDOS/WINDOWS. In UNIX/LINUX gibt´s das gar nicht.
Weil man es eigentlich schlicht nicht benötigt.
Es ist für die Bearbeitung von bestimmten Datentypen wie z.B. allen Zahlen ein sehr sperriges Format, weil die Typen zunächst in Text umgewandelt und zur Bearbeitung wieder zurückgewandelt werden müssen. Hat man es mit reinem Text zu tun, ist das Format auch nicht wirklich handlich, weil es die \r\n Zeilenendemarkierungen benötigt. Für einen späteren Textumbruch auf eine andere Zeilenlänge muß man diese Markierungen allesamt entfernen und neu setzen. Der Nutzen ist meiner Meinung nach für die Datenverarbeitung eigentlich NULL. Nützlich wird es erst, wenn wir uns mit anderen Programmen austauschen wollen oder müssen. Jede Textdatei kann mit den gängigen Editoren direkt gelesen und bearbeitet werden und in andere Datenformate (Tabellen) importiert werden. Die Textdateien sind also eine Schnittstelle.
Binärdateien kann man mit Standard-Programmen nicht auslesen. Das heißt aber nicht, daß man eine unbekannte Binärdatei gar nicht auslesen könnte, wenn man es denn wollte (siehe weiter unten dazu).
2. Der Dateizeiger FILE
Wir finden ihn in <stdio.h>:
typedef struct _iobuf { char* _ptr; int _cnt; char* _base; int _flag; int _file; int _charbuf; int _bufsiz; char* _tmpfname; } FILE;
Daß da einiges mehr gespeichert sein muß als nur die physikalische Adresse des Dateianfangs, resultiert schon aus der Tatsache, daß die Standardfunktion fseek mit SEEK_END, SEEK_CUR nach dem Aufruf unmittelbar zur Verfügung steht.
Wir sehen, daß zwei File-Zeiger implementiert sind, char *_ptr und char *_base. Einer davon muß die Adresse Dateianfang sein, der andere der Laufzeiger, auf den sich SEEK_CUR bezieht. Das int _cnt kann sich nur auf die Anzahl der Datensätze beziehen, _bufsiz auf die Größe des Datensatzes, char *_tmpfname auf den Dateinamen, _flag vermute ich als Markierung für Schreib/Lesezugriff usw. Wenn man wollte, könnte man sich ganz schlau machen und den Datentyp zur Laufzeit auslesen, aber so neugierig sind wir gar nicht. Es soll nur ein Hinweis sein, daß sich hinter FILE mehr verbirgt als ein einfacher Datentyp Zeiger.
Woher die Dateiende-Markierung SEEK_END resultiert, ist einfach:
Startadresse der Datei + Anz-Datensätze*Datensatzgröße = Dateiende.
Das sollte auch gelten, wenn die Datei auf dem Massespeicher fragmentiert gespeichert wird. Ich vermute, daß das Betriebssystem die Organisation so vornimmt, daß die Datei virtuell mit fortlaufenden Adressen angesprochen werden kann. Auch das ist aber nicht so wichtig, weil man sich als Programmierer darum definitiv nicht kümmern muß.
In Vorwegnahme dessen, was später noch erläutert wird, will ich einmal mit dem Dateizeiger ein bißchen herumspielen anhand eines kleinen praktischen Beispiels. Ein Beispiel sagt mehr als tausend Worte.
Die Funktion: #include <stdio.h> #include <stdlib.h> #include <stdio.h>
char taste; struct ma{ int nr; char name[30]; }; struct ma m;
void init(){sprintf(m.name,"Mueller");}
void schreib() { int i; FILE *bdp; bdp=fopen("test.bin","wb"); if (bdp==NULL) return; for (i=0;i<3;i++) { m.nr=i+1; fwrite(&m,sizeof(struct ma),1,bdp);
} fclose(bdp); }
Initialisiert einen Datensatz namens Müller, fügt eine fortlaufende Nr hinzu und schreibt den dreimal in die Datei test.bin, bin weil binär.
Jetzt tun wir mal so, als wenn wir die Struktur ma mit ma.nr und ma.name nicht kennen, sondern lesen diese Datei einfach mal zeichenweise aus. Und zwar mit dieser Funktion:
void lies2() { char c; int pos; FILE *bdp; bdp=fopen("test.bin","rb"); if (bdp==NULL) return; do { c=fgetc(bdp); printf("%4i",(int)c); }while ((int)c!=-1); fclose(bdp); }
Da klar ist, daß ein Gutteil der Zeichen nicht druckbar sein wird, geben wir nicht die Zeichen als solche aus, sondern den ASCII-Wert der Zeichen. Dazu casten wir den char in einen integer-Wert.
Bei diesem cast-operator kann man übrigens unterscheiden:
steht dort (int) c ist es ANSI-C steht dort int ( c ) ist es C++
Ich bleibe bei reinem ANSI C. Nichts geht über das Original.
HInweis: um solche Konsolenausgaben in einen Text zu kopieren: Rechte Maustaste über dem - Symbol oben rechts am Fensterrand, Edit, Mark, dann Copy mit ENTER, und dann in den TExt einfügen.
Wir sehen also eine 1, gefolgt von 3 NUllen. Dann kommt die ASCII-Zeichenfolge für Mueller, dann folgen wieder jede Menge Nullen, dann kommt die 2, und wieder Mueller usf.
Am Ende steht -1. Das ist die Markierung für EOF bei zeichenweisem Auslesen. Anstelle !EOF könnten wir auch schreiben !=-1 (ungleich minus 1). ABER NUR, wenn wir mit GETC() auslesen! Lesen wir blockweise aus, schießen wir damit das Programm ins NIRWANA!
Wir ändern jetzt mal unsere STRUCT ma (ma soll heißen MItarbeiter) folgendermaßen:
Statt: struct ma{ int nr; char name[30]; }; struct ma m;
heißt es nun: struct ma{ short nr; char name[15]; }; struct ma m;
Name und das Format nr. wurden in ein kleineres FOrmat gebracht.
Wir sehen, anstelle von 4 Speicherplätzen für nr. sind es nunmehr nur noch 2 (short statt int), und die Nullen hinter der ASCII Folge Mueller haben abgenommen, weil wir die Speichergröße für ma.name von 30 auf 15 verkleinert haben.
Insgesamt ist die Datei natürlich auch kleiner geworden.
Dies zur Demonstration, daß man BINÄR-Dateien unbekannten Formates sehr wohl auslesen und interpretieren kann. In diesem Falle wäre es recht einfach, die passende Datenstruktur dafür zu finden:
Wir brauchen ein short für den vorausgehenden Zahlenwert, sowie einen Text von 15 Zeichen für den nachfolgendewn ASCII-Namen.
Will man Binärcode so verschlüsseln, daß er für andere nicht lesbar sein soll, muß man also schon wesentlich mehr Aufwand betreiben als einfach binär zu codieren.
Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 15:52 Uhr
Dies zur Demonstration, daß man BINÄR-Dateien unbekannten Formates sehr wohl auslesen und interpretieren kann. In diesem Falle wäre es recht einfach, die passende Datenstruktur dafür zu finden:
Will man Binärcode so verschlüsseln, daß er für andere nicht lesbar sein soll, muß man also schon wesentlich mehr Aufwand betreiben als einfach binär zu codieren.
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Spielen wir mal ein bißchen weiter herum, um die BASICS der Dateiverwaltung unter ANSI-C zu verdeutlichen. Was genau da abläuft, werde ich weiter unten noch erklären. Wir nehmen statt unserer Spionage-Routine fgetc() jetzt die passende Funktion:
void lies() { int pos; FILE *bdp; bdp=fopen("test.bin","rb"); if (bdp==NULL) return; do { if (fread(&m,sizeof(struct ma),1,bdp)==1) printf("%4i%s\n",m.nr,m.name); }while (!feof(bdp)); fclose(bdp); }
Und erhalten als Ausgabe:
1Mueller 2Mueller 3Mueller Press any key to continue . . .
Jetzt öffnen wir die Datei im Lese- und Schreibmodus, und versuchen, bei den Müllers einen Schulze einzufügen.
1Mueller 2Mueller 3Mueller Press any key to continue . . .
Das heißt, das hat nicht geklappt mit unserem Schulze.
Warum das nicht klappt: es ist im Internet vieles OBERFLÄCHLICHES zu lesen. Im Prinzip geht das so, heißt es. Aber:
Um Programmierung zu lernen, muß man nicht lesen, sondern SELBST PROGRAMMIEREN. So allgemeine Kenntnisse führen da nicht wirklich weiter.
Es gibt eine Verwirrung um das Makro EOF, sowie die Funktion fflush(). Von der letzteren wird behauptet, daß sie die Schreibzugriff erzwingt. Tatsächlich?
1Mueller 0->Schulze 3Mueller Press any key to continue . . .
Was macht fseek(bdp,0,SEEK_CUR)?
Eigentlich gar nichts. Der Dateizeiger bdp steht auf SEEK_CUR, und die angeforderte Verschiebung ist 0, es ändert sich überhaupt nichts bis auf die Tatsache, daß wir einen leeren Funktionsaufruf haben.
Tatsächlich aber schaltet der leere Funktionsaufruf zwischen Lesen und Schreiben um. Der Aufruf von fflush() macht das nicht.
Insgesamt ist es überhaupt verwunderlich, weil fwrite() ja an Deutlichkeit nichts zu wünschen übrig läßt. Aber: ohne Umschaltung wird fwrite() ignoriert. Tatsache.
Der leere Funktionsaufruf fseek(*FILE,0,SEEK_CUR) ist also der Schalter, der zwischen Lese- und Schreibzugriff umschaltet.
Der Schreibzugriff erfolgt, wie man sieht, mitten in die Datei hinein. Er kann aber genausogut am Ende erfolgen. Wichtig nur, daß er überhaupt erfolgt.
Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 16:31 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Weiter geht es systematisch, ohne Schmankerl.
Das ist zwar langweilig, aber nötig. Ohne systematische Kenntnisse kommt man in C sehr leicht ins Schwimmen und dann geht nichts mehr.
Der Dateizugriff mit fopen() benötigt als Parameter die Angaben:
w = write = schreiben
r = read = lesen
a = append = an die Datei anhängen
bzw. die erweiterten Parameter:
w+ = schreiben und lesen
r+ = lesen und schreiben
a+ = an das Ende schreiben und lesen
Der Parameter a verhindert, daß man mitten in die Datei hineinschreibt. Eigentlich ist der aber völlig überflüssig, denn wenn wir mit
fseek(FILE,0,SEEK_END) den Laufzeiger an das Dateiende setzen, können wir auch mit r+ an das Ende der Datei anhängen.
Also Modus a ist eigentlich Quatsch mit Soße oder etwas für Denkfaule.
Nun sehen wir: w+ kann schreiben und lesen, r+ aber auch. Wo ist der Unterschied?
Wird im w Modus geöffnet, wird die vorhandene Datei beim ersten Zugriff zerstört.
Wird im r-Modus geöffnet, bleibt sie unbeschädigt.
Um eine Datei aber neu anzulegen, muß man im w-Modus öffnen.
Um festzustellen, ob eine Datei vorhanden ist, nimmt man besser den r-Modus.
Kurz zusammengefaßt:
Will man eine Datei neu anlegen oder von vorn bis hinten schreiben, nimmt man w.
Für vorhandene Dateien nimmt man r.
Der Modus a ist überflüssig.
Das Editieren, also die Veränderung des Dateiinhalts, macht man mit r+. Das heißt, Lesen und Schreiben.
Dazu muß man den Laufzeiger der Datei mit fseek an die richtige Stelle setzen.
Daß man zwischen Lesen und Schreiben umschalten muß, wurde erwähnt, die Umschaltung erfolgt zuverlässig mit fseek(). Will man an der Stelle schreiben, wo man schon steht, ruft man fseek() mit dem Parameter 0 auf, macht also einen leeren Funktionsaufruf (schwer zu begreifen, ist aber so, Tatsache).
fseek verstellt NORMALERWEISE den Laufzeiger der Datei. Dafür sind in ANSI-C folgende Konstanten vorgesehen:
SEEK_SET = Anfang der Datei
SEEK_CUR= Current, also Aktuelle Dateizeiger-Position.
SEEK_END=Dateiende.
Die Syntax von fseek() ist etwas tückisch.
Sie lautet:
int fseek ( FILE * stream, long int offset, int origin );
Dabei handelt es sich um den schon vorgestellten Dateizeiger FILE.
Gefolgt von einem long Wert, den man in Ansi-C mit (long) casten muß. Dieser offset beschreibt, wieviele Datensätze wir vor- oder zurückspringen wollen von dem nun folgenden Wert, origin.
Ich kehr das mal um:
Für Origin gibt es die Werte SEEK_END (Dateiende), SEEK_CUR (current=aktuelle Position des Dateizeigers, hat nichts mit CURSOR zu tun ), sowie SEEK_SET =Dateianfang.
Wir sagen also: wir wollen (long) Datensätze von SEEK_END wegbleiben, also den 5.-letzten Datensatz haben.
So geht es aber nicht!
Weil nämlich der Dateizeiger FILE in Bytes rechnet, wir rechnen aber in Datensätzen.
Wir müssen den offset multiplizhieren mit der Größe des Datensatzes. Wollen wir umgekehrt die Position des Laufzeigers feststellen, müssen wir durch die Größe des Datensatzes dividieren.
Der ist je nach Betriebssystem unterschiedlich, und daher holen wir uns den mit dem Operator sizeof()
Handelt es sich um Ganzzahlen int, sagen wir:
sizeof(int)
Wenn wir 5 zurück wollen, sagen wir:
-5*sizeof(int)
Haben wir eine Struktur, sagen wir:
-5*sizeof(struct MeineStruktur)
bzwl, wenn MeineStruktur mit typedef definiert wurde:
-5*sizeof(MeineStruktur)
Dann rutschen wir 5 zurück. Was ist aber, wenn damit eine Bereichsüberschreitung stattfindet, von der Pos 3 FÜNF Sätze zurück geht nicht, weil der Zeiger dann im Nirwana stehen würde?
Nun, das kann man ja testen:
void lies2() { int pos; FILE *bdp; bdp=fopen("test.bin","rb"); if (bdp==NULL) return; fseek(bdp,(long) -2*sizeof(struct ma),SEEK_CUR); // muß Mist ergeben, Frage wer den Fehler abfängt
if (fread(&m,sizeof(struct ma),1,bdp)==1) printf("%4i%s\n",m.nr,m.name); else printf("Geht nicht"); fclose(bdp); }
Ergebnis:
1Mueller Press any key to continue . . .
Das heißt, die Funktion fseek() macht es so, wie weiter oben im SROLLING, sie fängt die Fehler ab und setzt -2 auf 0.
DARAUF WÜRDE ICH MICH ABER NIEMALS VERLASSEN!
IN C NICHT und NIEMALS!
Solche Schweinereien kann man sehr wohl in der aufrufenden Routine selbst abfangen und muß nicht die letzte Hoffnung auf den Papst setzen.
Und wenn es hier mal gut geht, solchen Mist zu programmieren, ist das weder gut noch eine Garantie, daß es beim nächsten Mal ebenfalls gut geht.
Wäre noch hinzuzufügen, weil WICHTIG:
Wenn wir den Datensatz mitten in der Datei gefunden haben, dessen Daten wir verändern wollen im MODUS r+ mit:
fread()
Und ändern die Daten und wollen ihn an seine Stelle zurückschreiben und machen das mit
fwrite()
Dann haben wir Chaos angerichtet, weil:
Mit fread() rutscht der Dateizeiger an das Ende des gelesenen Datensatzes.
Der erneute Zugriff (Zurückspeichern) mit fwrite() schreibt die Daten nicht an die Stelle, von wo gelesen wurde, sondern auf den nächstfolgenden Datensatz. Ist der Nächstfolgende nicht vorhanden, wird hinter EOF angehängt, die Datei wird größer.
In jedem Falle ist das ERgebnis MIST, weil der geänderte Datensatz ZUSÄTZLICH zu dem fehlerhaften vorhanden ist.
Wir müssen also, nach dem Einlesen und Editieren, vor dem Zurückspeichern (Nochmal: geht nur im Modus r+) den Dateizeiger zurücksetzen:
fseek(FILE,(long) -1*sizeof(MEIN_DATENSATZ),1);
um die ursprüngliche Position zu überschreiben.
Dann ist alles wieder in Butter.
Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 18:23 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Für die Dateiorganisation gibt es eigentlich nur 3 Zustände:
1.) Wir müssen Datei neu anlegen
2.) Wir müssen Datei erweitern (neue Daten)
3.) Wir müssen Dateiinhalt korrigieren.
Das trifft aber nicht auf alle Dateien zu.
Bleiben wir beim BEispiel einer FIBU, Finanzbuchhaltung.
Wir haben dann folgende Gruppen von Dateien:
Gruppe 1: Kontenrahmen (Mitglieder: Kontenrahmen, Anlagekonten)
Die Datei KONTENRAHMEN (hier: SKR03). Die ist nur in Maßen veränderlich. Wir dürfen daraus nichts löschen, sobald ein Konto bebucht wurde. Wir dürfen höchstens neue Konten hinzufügen, und auch, wenn man ein altes KOnto ändert, also alle Buchungen auf ein anderes Konto macht, muß das alte KOnto aus historischen Gründen (Dokumentation) noch erhalten bleiben.
Gruppe 2: Buchungssätze
Hier finden alle laufenden Geschäftsvorgänge ihren Niederschlag. Hier wird also ständig hinzugefügt, gelöscht oder geändert. Das Löschen, wie das früher mal üblich war, zu verhindern (ich glaube nach der EInführung der EDV lag das weniger an den Gesetzen als an der Faulheit der damaligen Programmierer) ist weder gesetzlich vorgeschrieben noch sinnvoll. Wenn die Tusse (Tschuldigung) in die Primanota einen Buchungssatz 12mal falsch eingibt und anschließend 12 mal storniert, ist auch ein Betriebsprüfer nicht erfreut, so einen Mist lesen zu müssen. Die Kaufleute im Mittelalter haben solche "Bleistiftbuchungen" aus anderen Gründen verhindert. Die waren ja nicht doof.
Gruppe 3: Abschlußkonten
Werden gesondert behandelt.
Gruppe 4: alle weiteren Dateien, Infos, Hilfestellungen und so weiter.
Die kann man ganz frei halten. Es geht was nötig ist.
Das wäre nun der EInstieg in die Datenverarbeitung, die logische Programmierung, wo man Algorithmen finden muß für logische PRobleme.
Ich will aber den Einstieg noch nicht machen, bevor die FORMALITÄTEN erledigt sind.
Und als solche stehen noch auf der TO_DO_LIST:
Sortiere die Dateien.
Dazu brauchen wir dynamische Listen.
Es geht also weiter mit der Implementation der Datei-Prototypen sowie der Verwaltung der dynamischen Listen. Da wird es dann ein bißchen mehr C-like, Pointer und dergleichen, während es bislang eher pascal-like abegegangen ist (Arrays).
Wenn aber eine Aufgabe genausogut oder besser mit Arrays zu lösen ist, gibt es auch in C keine Notwendigkeit, da mit Pointern herumzuswitchen.
Die kommen schon noch.
Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 18:56 Uhr
Gruppe: CNC-Arena-Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.620
Behandeln wir eine Datei im Textmodus, können wir schreiben : fopen("Dateiname","rt"), für Textmodus. Der Compiler ignoriert das allerdings, weil t=Textmodus der default ist (die Vorgabe). Man sollte es trotzdem schreiben, damit die Anweisung klar ist.
Um Dateien im Binärmodus zu schreiben und zu lesen, lautet die Formulierung fopen("Dateiname","rb"), also ein b wird hinzugefügt für BINÄR, es ist egal, ob man w+b oder wb+ bzw. r+b oder rb+ schreibt, jedenfalls das b unterschlägt der Compiler nicht, sondern das ist lebenswichtig, damit die Datei auch im Binärmodus bearbeitet wird.
Unter UNIX/LINUX ist das wiederum unwichtig, da sowieso alle Dateien im Binärmodus behandelt werden.
Um eine Datei im Binärmodus zu behandeln, können wir die Daten so lassen wie sie sind, reinschreiben und zurückschreiben. Die Funktionen sind:
fwrite()
und
fread()
Um die Daten im Textmodus zu speichern, müssen sie umgewandelt werden in Text. Danach werden sie gespeichert und gelesen mit
(zeilenweise:)
fputs()
fgets()
Die Möglichkeit, Dateien zeichenweise zu speichern und zu lesen, steht so in der Mitte zwischen beiden, wie oben schon beschrieben, die Funktionen sind:
fputc()
fgetc()
Beim Auslesen der Dateien ist natürlich die EOF (END OF FILE) Markierung zu beachten, bzw. zu finden, damit das PRogramm sich nicht ins Nirwana schaufelt. Man soll sich aber an dem Begriff nicht festbeißen, der funktioniert in C nicht so, wie erwartet.
Bei Binärdateien können wir das implementieren, ohne es zu erwähnen, mit der Anweisung:
while (fread(&buffer, (long) sizeof(Datensatz),1,pFILE)==1) // pFILE ist pointer auf FILE, definiert als FILE *pFILE
Das heißt, solange der Lesezugriff mit dem angeforderten Wert 1 (=1 Datensatz) das ERgebnis 1 (=1Datensatz) zurückliefert, lies weiter. Geht das nämlich nicht weiter, liefert der Lesezugriff einen Wert UNGLEICH 1, und das ist die schamhafte Umschreibung für EOF.
Was auch geht, ist do { lies die Datei } while (!feof(pFILE)}
Diese Schleifen mit Fußbedingung müssen allerdings je nachdem anders gemanagt werden, damit der Abbruch nicht später erfolgt, als über das Dateieende hinausgelesen wurde. Man muß innen mit if() arbeiten, sonst hat man den letzten Datensatz doppelt. Hier geht es nur um das Prinzip.
Handelt es sich um Textdateien, gilt analog:
LESEZUGRIFF:
while (fgets (Datensatz, ,sizeof(Datensatz),pFILE ) != NULL )
Eigentlich alles ganz banal.
Um im Textformat zu speichern, müssen allerdings strukturierte Datentypen zuvor allesamt in ein Textzeile umgewandelt werden.
Das Prinzip wurde schon erwähnt, nämlich sprintf() wandelt ALLE Datentypen in formatierten Text um.
Und wenn wir den Text wieder auslesen, um mit den Datentypen arbeiten zu können, müssen wir eine Rückumwandlung machen
Diese geschieht bei Zahlenwerten mit
atoi() = Alphanumeric to Integer , also Zeichenfolge in Integer umwandeln oder
atof() = Alphanumeric to Float, Zeichenfolge in Fließkommazahl umwandeln,
und dazu muß der Text exakt zerschnitten werden, sonst gibt das Müll.
So ein Fall tritt aber gar nicht auf, weil kein Mensch Daten in Text verwandelt und rückumwandelt, mit denen er rechnen will.
Die Textdateien sind lediglich die Kopie der im Binärformat vorliegenden Daten und dienen als Schnittstelle, werden sonst aber nicht gebraucht.
Daher sind die wesentlichen Routinen der Dateiverwaltung in wenigen Zeilen beschrieben:
fwrite(&buffer,(long) sizeof(Datentyp),1,pFILE) schreibt einen Buffer von dem Typ Datensatz in die Datei
und
fread(&buffer,(long) sizeof(Datentyp),1,pFILE) liest ihn wieder aus.
solange gilt:
while(fread(&buffer,(long) sizeof(Datentyp),1,pFILE)==1), also !feof(pFILE)
Die Funktion fread arbeitet CALL BY REFERENCE, weil sie direkt in den Speicherbereich buffer hineinschreibt und keine Kopie übergibt, Hinweis darauf ist der
Adreßoperator &
Damit wird der Speicherbereich buffer überschrieben.
Obwohl die Funktion fwrite() auch den Adreßoperator benutzt, arbeitet sie aber nicht CALL BY REFERENCE, sondern im PRINZIP CALL BY VALUE, weil sie ja diesen Bereich nicht verändert, sondern nur als Kopie in die Datei ablegt.
Wenn man allerdings die Adresse einer Variablen & an eine Funktion übergibt (die keine Standard-Funktion ist), muß man sich nicht wundern, wenn irgendwann nach der 25. Programmüberarbeitung plötzlich seltsame Effekte auftreten. Gibst du deine EC-CARD und deine GEHEIMZAHL heraus, mußt du damit rechnen, daß auch UNANGENEHME DINGE geschehen. Nichts anderes ist CALL BY REFERENCE.
WEil die Dateiorganisation eigentlich so banal ist wie beschrieben (mit Ausnahme des Vorlaufs, daß man sich eben auskennt), werd ich das hier nicht mehr weiter ausführen sondern fortfahren mit der Frage der
SORTIERUNG VON DATEN und DATEIEN.
Der nächste Beitrag also wird sich mit der Organisationn dynamischer Listen zum Zwecke der Sortierung von Daten beschäftigen.
Wenn das erledigt ist, wäre die formale Seite der Programmierung abgeschlossen und man kann endlich zur eigentlichen Datenverarbeitung übergehen.
Als da wäre, wie man eine Finanzbuchhaltung programmiert.
Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 19:51 Uhr