Programmieren in Pure Pascal, Teil 4

Nachdem wir in der letzten Folge eigene Unit-Module hergestellt haben, wollen wir heute einen näheren Blick darauf werfen, welche Module die Profis von Pure Software dem Compiler beilegen. Diese richten sich übrigens weitgehend nach denen des Turbo-Pascal-Compilers unter DOS.

Einen guten Überblick über die Elemente dieser Units erhalten Sie jederzeit über die Online-Hilfefunktion von Pure Pascal oder die Interface-Quelltexte im Verzeichnis »INTRFACE«. Fast schon idiotensicher ist die Printer-Unit: Sie deklariert die Textdateivariable »Lst«, die eine Umleitung der Ausgaben auf den Drucker steuert:

WRITELN(Lst,'Druckerausgabe!');

Die GEMDOS-, BIOS-, XBIOS-, GEM-AES und GEM-VDI-Bibliotheken in den Units »Tos« und »Gern« entsprechen weitgehend dem Atari-üblichen Standard. Die einschlägige Fachliteratur wie das Atari-Profibuch oder Programmierleitfäden geben dazu genügend Auskunft. Deshalb wollen wir die TOS-Unit gar nicht näher untersuchen, ich verweise auf das kleine Beispielprogramm »DEMO4A.PAS« der TOS-Diskette. Auch die GEM-Funktionen lassen sich konform zum Standard programmieren, was Sie am Programm »DEM04B.PAS« anhand der »form_alert«- und »fsel_input«-Funktionen sehen.

Da Pure Pascal ein Turbo-Pascal-Clone ist, sind die DOS-orientierten Units »DOS«, »Crt« und »Graph« ein Muß. Neben interessanten neuen Prozeduren bringen uns diese einen weiteren Vorteil: Möchte man doch seinem Atari untreu werden und ein Programm nach DOS portieren, so ist eine Verwendung der DOS-Funktionen statt der äquivalenten GEMDOS-, BIOS-oder XBIOS-Elemente von Vorteil. Auch der umgekehrte Weg ist interessant: DOS-Programme unter Turbo Pascal sind auch der Atari-Gemeinde zugänglich.

Werfen wir zunächst einen Blick auf die DOS-Unit, die »Konkurrenz« zur TOS-Unit: Systemnahe Prozeduren wie das Erfragen und Setzen der Uhrzeit mit den Prozeduren »GetDate«, »GetTime«, »SetDate« und »SetTime« oder verschiedene Dateioperationen sind ohne Probleme möglich. Im Beispielprogramm »DEMO4C.PAS« sehen Sie die Anwendung von »GetDate« und »GetTime«: In zu übergebenden Variablen erhalten Sie das Datum inklusive der Nummer des Wochentages (z.B. 1 für Montag) bzw. die Uhrzeit zurück.

GetDate(Jahr,Monat,Tag,Wochentag);
GetTime(Stunde,Minute,Sekunde,Sek100);
(* Mit 1/100 sec *)

Das Stellen der Zeit mittels »SetTime« bzw. des Datums mit »SetDate« erfolgt analog, beim Datum ist nicht die Nummer des Wochentags anzugeben:

SetDate(1990,9,22); (* Setzt den 22.9.1990 *)
SetTime(20,10,15,0); (* Setzt Zeit 20:10h 15s *)

Nützlich ist auch die GetFTime-Prozedur, mit der wir das letzte Modifikationsdatum einer Datei erfragen. Wir öffnen zunächst mit den Standardoperationen »Assign« und »Reset« die Datei und wenden einen »GetFTime(Datei,Zeitpaket)«-Aufruf an, worauf wir in der LONGINT-Variablen »Zeitpaket« die verschlüsselte Zeitinformation erhalten. Damit eventuelle Laufzeitfehler zu keinem Programmabbruch führen, setzen wir zuvor {I-} in den Quelltext. Über die Variable »DOSError« testen wir nun stets, ob ein Fehler beim Zugriff auf die Datei auftrat, was durch einen Wert ungleich null signalisiert wird. Mit einem »UnpackTime(Zeitpaket,Zeitdaten)«-Aufruf übertragen wir die Zeitinformation in die »Zeitdaten«-Struktur vom Typ »DateTime«, die sich nun leicht weiterverarbeiten läßt:

WITH Zeitdaten DO
BEGIN
    WRITELN('Tag: ',day,'.',month, ’.',year);
    WRITELN('Zeit: ',hour,':',min,’:’,sec);
END;

Den umgekehrten Weg gehen wir beim Setzen einer Zeit: Zunächst packen wir unsere Zeit per »PackTime-(Zeitdaten,Zeitpaket)«-Aufruf und stellen die Atari-Uhr über einen »SetFTime(Datei, Zeitpaket)«-Aufruf. Dateiattribute wie Schreibschutz, Archivbit usw. erfragen wir mit »GetFAttrfDatei,Attribute)«, worauf die BYTE-Variable »Attribute« die bitweise verschlüsselte Attributkombination angibt. Mit Hilfe der vordefinierten Konstanten »ReadOnly« (Schreibschutz), »Hidden« (versteckte Datei), »Sysfile« (Systemdatei), »VolumelD« (Volumenangabe), »Directory« (Unterverzeichnis) und »Archive« (Archivbit gesetzt) ermitteln wir über eine logische UND-Verknüpfung die Eigenschaften der Datei:

IF (Attribute AND ReadOnly) <> 0
THEN WRITELN(’schreibgeschützt’)
ELSE WRITELN(’ungeschützt’);

Auch hier gibt es das Gegenstück in Form des »SetFAttr(Datei,Attribute)«-Aufrufs, der neue Dateiattribute setzt.

Bei der Behandlung von Dateinamen bietet die DOS-Unit wirklich Neues: Aus einem unvollständigen Namen einer Datei des aktuellen Verzeichnisses läßt sich per »Pfadname:=FExpand('TEST.DAT')«-Aufruf der komplette Pfadname ermitteln, so daß wir beispielsweise »C:\TESTS\TEST.DAT« zurückerhalten. Mit der »FSplit«-Prozedur können wir einen solchen in Pfad, Dateinamen und Extension (».xxx«) aufspalten:

VAR Pfadname:PathStr;
    Dirname:DirStr;
    Filename:NameStr;
    Fileext:ExtStr;
... FSplit(Pfadname,Dirname,Filename, Fileext);

Möchten wir eine Datei in einer Reihe von Verzeichnissen suchen, so hilft ein »Pfadname — FSearch ('TEST.', 'A:\;C:\')«-Aufruf weiter. Als ersten Parameter geben wir die zu suchende Datei an, wobei die Wildcards »« und »?« erlaubt sind. Als zweiten Parameter geben wir eine durch Semikolon getrennte Liste der Verzeichnisse an. Bei erfolgloser Suche erhalten wir einen Leerstring zurück, andernfalls den kompletten Pfadnamen der gefundenen Datei. Die freie Kapazität eines Laufwerks erhalten wir als LON-GINT-Resultat der »DiskFree(Laufwerk)«-Funktion, wobei ein »Laufwerk«-Wert von 0 das aktuelle Laufwerk und eine 1 Laufwerk A: etc. bezeichnen. »DiskSize(Laufwerk)« informiert über die Gesamtkapazität. Auch Vektorverbieger kommen in der DOS-Unit auf ihre Kosten: Mit »GetIntVec(Nr,Zeiger)« erhalten wir die aktuelle Adresse des Interrupt-Vektors »Nr«, mit »SetIntVec(Nr,Zeiger)« modifizieren wir ihn. Zur Suche nach Dateien stehen ähnliche Funktionen wie in der TOS-Bibliothek zur Verfügung. Die »FindFirst(Suchname, SuchStatus,Eintrag)«-Prozedur sucht den ersten Eintrag im angegebenen Verzeichnis, wobei wir als »Suchname« einen Dateinamen, der Wildcards enthalten darf, und als »Suchstatus« eine Verknüpfung der gewünschten Attribute (z.B. »ReadOnly+Hidden«) bzw. die Konstante »AnyFile« angeben. War die Suche erfolgreich (DOSError=0), so finden wir in der »Ein-trag«-Struktur vom Typ »SearchRec« den Dateinamen (».Name«), die Größe (».Size«), die gepackte Zeit (».Time«) und die Attribute (».Attr«). Den nächsten Verzeichniseintrag erhalten wir mit einem »FindNext (Eintrag)«-Aufruf, was zu folgendem Mechanismus führt (»DEMO4D.PAS«):

FindFirst('*.*',AnyFile,Eintrag)
WHILE DosError =  DO BEGIN 
WRITELN(Eintrag.Name);
FindNext(Eintrag); END;

Die DOS-Unit bietet eine Reihe von Prozeduren und Funktionen zum Ermitteln verschiedener Informationen (siehe »DEM04E.PAS): Mit »Dosversion« erhalten wir die Versionsnummer des Betriebssystems, wobei wir mit HI(Dosversion) die Hauptversionsnummer (z.B. 1 bei TOS 1.4) und mit LO(Dosversion) die Nebenversionsnummer (4 bei TOS 1.4) erhalten. Mit »GetVerify(BooleanStatus)« erhalten wir den Schreib-verify-Status, mit »SetVerify(Status)« ändern wir ihn. »Set/GetCBreak(Status)« wurde nur aus Kompatibilitätsgründen übernommen und zeigt keine Wirkung.

Auch zur Prozeßsteuerung hat die DOS-Unit einiges zu bieten: Mit »Keep(Ergebnis)« beenden wir das aktuelle Programm mit dem Rückgabewert »Ergebnis«, worauf das Programm resident im Speicher erhalten bleibt. Mit »Exec(Pfadname,Parameter)« führen wir das angegebene Programm aus, wobei wir die Kommandozeilenparameter »Parameter« übergeben. Den Ergebniswert des Programmes erhalten wir über die Funktion »DosExitCode«. Im sogenannten Environment des Betriebssystem sind verschiedene Umgebungsvariablen vermerkt, z.B. »PATH=A: \«. Die Funktion »EnvCount« informiert uns über die Zahl der gesetzten Variablen, »EnvStr(n)« liefert den n-ten Eintrag:

FOR i:=1 TO EnvCount DO WRITELN(EnvStr (i));

Den Wert einer Umgebungsvariablen erhalten wir mit der »GetEnv(Name)«-Funktion: WRITELN(GetEnv-('PATH')).

Eine nützliche Novität ist die Crt-Unit von Pure Pascal: Sie erlaubt DOS-kompatible Textausgaben auf dem Atari ohne Kopfschmerzen. Hierzu verwenden wir die üblichen Prozeduren wie WRITELN usw.. Mit »ClrScr« löschen wir den gesamten Bildschirm, mit »ClrEol« den Rest der Zeile ab der Cursorposition. Letztere können wir mit »GotoXY(x,y):< setzen, wobei im 80-Zeichen-Modus x-Werte von 1 bis 80 und y-Werte von 1 bis 25 möglich sind. Einen neuen Textmodus setzen wir mit »Textmode(modus)«, wobei »modus« den gewünschten Modus bestimmt. In dem Crt-Unit sind dazu eine Reihe von Konstanten (»CO80« für Farbe bei 80 Zeichen/Zeile, »BW40« für Schwarzweißdarstellung bei 40 Zeichen/Zeile etc.) deklariert, von denen jedoch nur wenige auf dem Atari korrekt arbeiten. Mit einem »Textmode(Lastmode)«-Aufruf gelangen wir in den vorigen Modus zurück. Mit »AssignCrt(Datei)« lenken wir die Bildschirmausgaben in die angegebene Textdatei um. Die Funktionen »WhereX« und »WhereY« liefern uns die aktuellen Bildschirmkoordinaten.

Mit »Textcolor(Farbe)« setzen wir eine neue Textfarbe, wobei für die Farbnummer eine Reihe von Werten vordefiniert sind: Black, Blue, Green, Cyan, Red, Magenta, Brown, LightGray, DarkGray, LightBlue, LightGreen, LightCyan, LightRed, LightMagenta, Yellow und White. Die Hintergrundfarbe der Zeichen legen wir mit »TextBackGround(Farbe)« fest. Die Prozeduren »HighVideo«, »NormVideo« und »LowVideo« wählen zwischen Fett-, Normal- und Dünndruck, wobei letzterer auf dem Atari keine Wirkung zeigt. Mit »DelLine« löschen wir die aktuelle Bildschirmzeile komplett, wobei der darunterliegende Bildschirmbereich hochscrollt, »InsLine« fügt eine Leerzeile ein.

Sehr brauchbar ist die »Window(,y1,x2,y2)«-Prozedur, die ein Zeichenfenster mit der linken oberen Ecke bei (,y1) und der rechten unteren Ecke bei (x2,y2) definiert. Alle Bildschirmausgaben erfolgen nun in diesem Fenster. Längere Ausgabezeilen werden in die nächste Fensterzeile umgebrochen, bei Platzbedarf scrollt der Fensterinhalt hoch. Alle Ausgabefunktionen, darunter auch ClrScr, beeinflussen nur das Fenster. Zum Deaktivieren des Fensters führen Sie einfach einen »Window(1,l,80,25)«-Aufruf (im 80x25-Zeichen-Modus) aus.

Etwas komplizierter ist da schon der Umgang mit der BGI-Unit (Borland Graphics Interface), die wir mit »USES Graph« einbinden: Diese Unit existiert in identischer Form auch unter MS-DOS-Turbo-Pascal, was eine problemlose Übernahme von Grafikprogrammen aus dem PC-Sektor gestattet. Doch keine Sorge: BGI greift nicht über irgendwelche ominösen Treiber auf den Atari-Bildschirm zu, vielmehr arbeitet es über einen Treiber mit Funktionen des GEM-VDI, weshalb Inkompatibilitäten nicht auftreten sollten. Dies bremst jedoch auch die Geschwindigkeit. Man sollte die BGI-Funktionen deshalb nur dann einsetzen, wenn Sie das Programm portabel zum PC halten bzw. es von diesem übernehmen wollen.

Den ersten Schritt der BGI-Programmierung bildet die Initialisierung der Bibliothek mit der »lnitgraph(Treiber,Modus,Pfad)«-Prozedur. Mit der Variablen »Treiber« legen wir den zu verwendenden Grafiktreiber fest. Dabei bietet sich die Verwendung der Konstanten »Detect« an, die automatisch den Treiber für den höchstmöglichen Grafikmodus verwendet, dessen Kennung wir in »Modus« zurückerhalten. »Pfad« bestimmt den Laufwerkspfad der BGI-Treiber (Endung ».BGI«) und läßt sich als Leerstring übergeben, wenn sich der Treiber im Programmverzeichnis befindet. Bitte ändern Sie die Beispielprogramme der TOS entsprechend Ihrer Installation ab!

Den Erfolg der Initialisierung testen wir über die Funktion »GraphResult«, die im Erfolgsfall den Integer-Wert »GrOk« zurückliefert. Trat ein Fehler auf, so können wir die Fehlernummer mit der »Graph ErrorMsg(Nr)«-Funktion in einen String umwandeln:

WRITELN(GraphErrorMsg(Fehler)).

Nach getaner Arbeit sollten wir mit einem »Close-Graph«-Aufruf die BGI-Bibliothek schließen. Einen Auflösungswechsel erreichen wir mit »Setgraphmode(modus)«, wobei auf dem Atari jedoch nur der Modus »Vdimode« verfügbar ist. Der Ursprung des VDI-Koordinatensystems befindet sich in der linken oberen Ecke, die maximalen x- und y-Koordinaten und Farbwerte erhalten wir über die »GetMaxX«-, »GetMaxY«- und »GetMaxColor«-Funktionen.

Die zur Verfügung stehende Farbpalette entspricht übrigens den 16 Farben der Crt-Unit. Mit »SetColor-(Farbe)« wählen wir eine Zeichenfarbe aus, mit »SetBk-Color(Farbe)« die Hintergrundfarbe, die bei Lösch-und Fülloperationen zum Einsatz kommt. Die Farbzuordnung läßt sich jedoch auch ändern. Durch einen »SetPalette(0,WHITE)«-Aufruf ordnen wir beispielsweise dem Paletteneintrag 0 die Farbe Weiß zu, so daß der Hintergrund weiß erscheint. Eine beliebige Zuordnung eines RGB-Farbwertes ist per »SetRGBPalette-(Eintrag,Rot,Gruen,Blau)«-Aufruf möglich. Zum Wiederherstellen der alten Palette verwenden wir einfach folgende Sequenz:

VAR Palette:PaletteType; . . .
GetDefaultPalette(Palette);
SetAllPalette(Palette);

Ähnlich wie die Crt-Unit erlaubt auch die Graph-Unit die Definition eines Zeichenfensters, wozu wir einen »SetViewport(,y1,x2,y2,ClipOn)«-Aufruf ausführen. Alle über dieses Fenster hinausgehenden Zeichenausgaben werden abgeschnitten. Setzen wir den letzten Parameter auf ClipOff, so wird das Fenster deaktiviert. Einzelne Bildpunkte der Farbe »f« setzen wir per »PutPixel(x,y,f)«-Aufruf (siehe »DEM04G.PAS«).

Keine Probleme bereitet das Darstellen der gängigen grafischen Grundobjekte: Mit »Line(,y1,x2,y2)« ziehen wir eine Linie von (;y1) nach (x2;y2). Am letzten Punkt der vorausgehenden Zeichenoperation positioniert BGI einen imaginären Cursor, von dem aus wir mit »LineTo(x,y)« eine Linie zum Punkt (x,y) zeichnen. Mit »LineRel(dx,dy)« stellen wir vom Cursorpunkt (cx,cy) eine Linie zum Punkt (cx+dx,cy+dy) dar. Ein rechteckiger Rahmen läßt sich per »Rectangle(,y1,x2,y2)«-Prozedur darstellen, ein Kreis mit Radius »r« und Mittelpunkt (xm,ym) per »Circle(xm,ym,r)«-Aufruf. Ein Kreissegment beschreiben wir zusätzlich zu den Anfangs- und Endwinkel des darzustellenden Segmentes »w 1« und »w2« im Gradmaß: Arc(xm,ym,w1,w2,r). Für ein Ellipsensegment sind getrennte Radien in x-(»rx«) und y-Richtung (»ry«) nötig: Ellipse(xm,ym, w1,w2,rx,ry). Für eine vollständige Ellipse geben wir einen Anfangswinkel von 0 und einen Endwinkel von 360 Grad an. Doch auch beliebige Vielecke sind kein Problem: In einem Feld aus »PointType«-Strukturen speichern wir die x- und y-Koordinaten jedes Eckpunkts. Bei geschlossenen Linienzügen ist der Anfangspunkt zusätzlich als Endpunkt ananzugeben. Mit einem »DrawPoly«-Aufruf stellen wir den Linienzug dar (siehe »TEST4H.PAS):

VAR e:ARRAYfl. .4] OF Point Type;
e[1].x:=10; e[1].y:=10; ...
e[4]:=e[1];
DrawPoly(4,e);

Mit der »SetLineStyle(Muster,Code,Dicke)«-Prozedur bestimmen wir das Aussehen der Linie. Als Musterkonstanten stehen uns »SolidLn« (normal), »DottedLn« (gepunktet), »CenterLn«, »DashedLn« (gestrichelt) und »UserbitLn« (benutzerdefiniert) zur Verfügung. Bei der benutzerdefinierten Form legen wir mit dem Bitmuster der WORD-Variablen »Code« das Aussehen fest. Es sind sowohl normale »NormWidth« als auch dicke Linien »ThickWidth« möglich. Mit der »SetWriteMode(Modus)«-Prozedur legen wir den Schreibmodus fest, wobei wir die Wahl zwischen »CopyPut/Normal-Put« (überschreiben), »XORPut« (Exklusiv oder), »OrPut« (Oder), »AndPut« (Und) sowie »NotPut« (Nicht) haben.

Ein gefülltes Rechteck zeichnen wir mit »Bar(,y1,x2,y2)«, eine gefüllte Ellipse mit »FillEllipse(xm,ym,rx,ry)«, ein gefülltes Kreissegment mit »PieSlice(xm,ym,w1,w2,r)«, ein gefülltes Ellipsensegment per »Sector(xm,ym,w1,w2,rx,ry)«-Aufruf und ein gefülltes Polygon mit »FillPoly(zahl,ecken)«. Mit »Flood-Fill(x,y,Endfarbe)« wird der Bildschirmbereich ab der Position (x,y) bis zum Erreichen einer Umgrenzung der Farbe »Endfarbe« gefüllt.

Als Sonderfunktion für beeindruckende Geschäftsgrafiken bietet sich die »Bar3D(,y1,x2,y2,tiefe,oben)«-Prozedur an, mit der wir einen Balken mit 3D-Effekt zeichnen. Die Variable »tiefe« bestimmt die Tiefe des 3D-Balkens, der BOOLEAN-Parameter »oben« legt fest, ob die Oberseite des Balkens dargestellt wird. Füllmuster und -farbe lassen sich mit der »SetFillStyle-(Muster,Farbe)«-Prozedur einstellen, wobei verschiedene Konstanten für leere (EmptyFill), komplett gefüllte (SolidFill), schraffierte, gerasterte (... DotFill) und benutzerdefinierte Muster zur Verfügung stehen. Benutzerdefinierte Füllmuster legen wir in einem acht BYTE-Elemente umfassenden Feld vom Typ »FillPatternType« fest, das wir anschließend der SetFillPattern-(Muster,Farbe)-Prozedur übergeben:

CONST muster : FillPatternType = ( 8,8,28,28,42,42,73,0 );
SetFillPattern(muster,Red);
SetFillStyle(UserFill,Red);

Möchten Sie Ausschnitte des Bildschirms kopieren, so stellen Sie zunächst mit einem »bytes:=Imagesize (,y1 ,x2,y2)« die Größe des zu übertragenden Blocks in Bytes fest. Per »GetMem(Zeiger,Bytes)«-Prozedur reservieren Sie den benötigten Speicher. Anschließend lesen Sie durch einen »GetImage(,y1,x2,y2,Zeiger^)«-Aufruf den Block in den Speicher ein, und kopieren ihn nun nach Bedarf per »PutImage(x,y,Zeiger^, Modus)« auf den Bildschirm. »Modus« bestimmt dabei den Verknüpfungsmodus.

Die BGI-Möglichkeiten bei der Textausgabe können sich ebenfalls sehen lassen: Neben dem Systemzeichensatz »DefaultFont« haben wir die Wahl zwischen den frei skalierbaren Schriften »TriplexFont«, »Small-Font«, »SansSerifFont« und »GothicFont«, die jedoch nicht in verschiedenen Stilarten wie fett oder kursiv zur Verfügung stehen. Hierzu müssen Sie die Zeichensatzdateien (Endung ».CHR«) in das Programmverzeichnis kopieren. Die Textausgabe erfolgt stets mit den »OutText(Text)«- und »OutTextXY(x,y,Text)«-Prozeduren, wobei erstere den Text ab der aktuellen Position des imaginären Grafikcursors darstellt.

Mit »SetTextJustify(HorOrnt,VerOrnt)« bestimmen wir die Orientierung des Textes. »HorOrnt« bestimmt die horizontale Ausrichtung, wobei die Konstanten »Left-Text«, »CenterText« und »RightText« eine linksbündige, zentrierte und rechtsbündige Ausrichtung erlauben. Die vertikale Ausrichtung ist an der Textbasislinie (»BottomText«) und der Textoberkante (»TopText«) möglich. Die »SetTextStyle(Font,Richtung,Groesse)«-Prozedur erlaubt neben der Wahl des Zeichensatzes auch die Festlegung der Ausgaberichtung, wobei uns die Wahl zwischen der horizontalen und der vertikalen Ausgabe von unten nach oben bleibt. Mit dem dritten Parameter geben wir die Vergrößerung der Zeichen in Stufen von 1 bis 9 an. Zum feineren Skalieren dient die Prozedur »SetUserCharSize(XZaehler, XNenner, YZaehler, YNenner). Die Brüche »X Zaehler/XNenner« und »YZaehler/YNenner« legen dabei die Vergrößerung in horizontaler und vertikaler Richtung fest. Parameter von 5, 4, 3, 10 bewirken Faktoren von 5/4=1,25 und 3/10=0,3.

Bei Textausgaben interessiert uns stets die effektive Größe des ausgegebenen Textes, damit wir wissen, um wieviel Punkte versetzt die nächsten Ausgabe erfolgen muß. Die Prozedur »Texthleight(Text)« liefert uns die Höhe des angegebenen Textes in Bi Id punkten, »Text-Width(Text)« die Breite des Textes, wobei alle Attribute berücksichtigt werden.

Damit haben wir unseren Streifzug durch die Units von Pure Pascal abgeschlossen, der Sie mit dem nötigen Handwerkszeug ausstatten sollte. (ah)

Literaturhinweise: Frank Mathy, BGI - Die portable Grafiklösung für Turbo C, TOS 10-11/1990.


Frank Mathy
Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]