9 Was man schwarz auf weiß besitzt...

10.1 Das Prinzip der geräte-unabhängigen Programmierung
10.2 Der erste Versuch
10.3 Die Skalierung muss flexibler werden!
10.4 Eine "richtige" Druckausgabe
10.5 Schlußwort des Angeklagten
Aufgaben

Zum vorigen Kapitel Zum Inhaltsverzeichnis



10.1 Das Prinzip der geräte-unabhängigen Programmierung

Die im vorigen Kapitel vorgestellte "Canvas-Grafik" stellt ein sehr mächtiges Programmierwerkzeug dar, weil es viele "Geräte" gibt, die eine "Leinwand" besitzen: nicht nur unsere PaintBox hat eine "Canvas", sondern fast jedes Gerät, auf dem eine Grafik ausgegeben werden kann, wie z.B. ein Drucker oder ein Plotter. Auch diese Geräte arbeiten auf einem rechteckigen Ausgabebereich, der in Pixelzeilen und -spalten organisiert ist. Dies ermöglicht es dem Programmierer, seine Ausgaben stets auf dieselbe Art und Weise durchzuführen, ohne sich (- all zu sehr -) um die technischen Details des jeweiligen Zielgeräts kümmern zu müssen. Dem Programmierer erscheinen die verschiedenen Ausgabegeräte "virtualisiert", d.h. er "sieht" trotz ihrer im Detail sehr unterschiedlichen Konstruktionen stets eine einheitliche Programmierschnittstelle. Somit wird eine "Geräte-Unabhängigkeit" der Programmierung erreicht - oder zumindest angestrebt.

In diesem Kapitel soll das Prinzip der geräte-unabhängigen Ausgabe am Beispiel der Druckausgabe einer Grafik genauer studiert werden.



10.2 Der erste Versuch

Wir wollen den Funktionen-Plotter aus dem vorigen Kapitel so erweitern, dass die jeweilige Zeichnung auch ausgedruckt werden kann. Völlig ohne geräte-spezifische Befehle wird das nicht gehen, aber im Prinzip geschieht die Ausgabe auf einen Drucker ganz analog zur Ausgabe in unsere Paintbox: die Anweisung
    with Paintbox1.Canvas do begin...
      {.....}
      end
ist zu ersetzen durch:
    with Printer.Canvas do begin...
      {.....}
      end
"Printer" ist dabei ein Objekt vom Typ TPrinter (-: was immer das auch sein mag ;-), das den aktuell gewählten Drucker Ihres Windows-Systems repräsentiert. Delphi macht dieses Objekt in unserem Programm verfügbar, wenn wir die "USES"-Liste am Kopf unseres Quelltextes um einen Eintrag "Printers" ergänzen. Dies ist eine Anweisung an den Compiler, dass er beim Zusammenbauen unserer EXE-Datei die für das Ausdrucken nötigen Teile mit einbinden soll.

Aufgrund der guten Vorarbeit im vorigen Kapitel brauchen wir nur noch wenige Änderungen an unserem Quelltext vorzunehmen, bis wir den ersten Test durchführen können. Es gibt im Grunde ja nur 2 Stellen, an denen die Paintbox1.Canvas auftaucht, nämlich in den Prozeduren "DrawCoordSys" und "DrawFunction". Diese beiden Prozeduren sollen nun so umgebaut werden, dass sie die Zeichnung einmal auf den Bildschirm und dann aber auch auf den Drucker ausgeben können. Dazu erweitern wir die Deklarationen dieser Prozeduren und übergeben die jeweils gemeinte "Ziel-Leinwand" als Parameter:
    procedure DrawCoordSys(TargetCanvas: TCanvas);
    procedure DrawFunction(TargetCanvas: TCanvas);
In den zugehörigen Implementierungen müssen wir dann nur noch "Paintbox1.Canvas" durch "TargetCanvas" ersetzen. Und bei allen Aufrufen muss jetzt zusätzlich die gemeinte "Ziel-Leinwand" angegeben werden. Um die bisherige Funktionalität zu erhalten, müssen wir also bei jedem Aufruf den Parameter "(Paintbox1.Canvas)" hinzufügen.

Bisher wurde alles nur komplizierter, ohne dass unser Programm einen sichtbaren Leistungszuwachs erfahren hätte. Dies soll sich nun ändern: wir erzeugen einen zweiten Knopf mit der Aufschrift "Drucken" und schreiben in dessen Klickprozedur die folgenden Anweisungen:
     procedure TForm1.Button2Click(Sender: TObject);
       begin
       Printer.BeginDoc;
       DrawCoordSys(Printer.Canvas);
       DrawFunction(Printer.Canvas);
       Printer.EndDoc;
       end;
Hübsch einfach, nicht wahr?
Die Anweisung "BeginDoc" startet einen neuen Druckauftrag. Es folgen die beiden Anweisungen, die unsere Zeichnung auf den Printer.Canvas ausgeben. Das abschließende "EndDoc" ist das Signal, dass der Druckauftrag nun zu Ende ist; erst dann beginnt die eigentliche Datenübermittlung zum Drucker! Wir kompilieren das Programm, starten es und...
....machen unseren ersten Ausdruck!!!

Das Ergebnis ist etwas zwiespältig. Zwar kann man sich darüber freuen, dass überhaupt etwas beim Drucker ankommt, das entfernt an unsere Zeichnung erinnert; andereseits ist aber dieses kleine Fleckchen Tintenverschwendung in der linken oberen Ecke des Blattes nicht gerade seligmachend. Die mickrige Größe des bedruckten Bereiches läßt jedoch darauf schließen, dass bei einem Drucker die Pixel sehr viel enger sitzen als bei einem Bildschirm. In der Tat: ein Bildschirm hat in der Regel weniger als 100 dpi (= "dots per inch", also Pixel pro 2,54 Zentimeter), ein moderner Tintenstrahl-Drucker bringt es locker auf 600 oder gar 1200 dpi. Kein Wunder also, dass der Ausdruck so klein ist!




10.3 Die Skalierung muss flexibler werden!

Das Ergebnis unseres ersten Druckversuches zeigt, dass wir unsere Zeichnung für die Ausgabe auf den Printer.Canvas umskalieren müssen. Für die Skalierung diente bisher schon die Prozedur "InitScale", die aber in der vorliegenden Form für die neue Aufgabe noch nicht taugt:
    procedure TForm1.InitScale;
      begin
      ppcm := 37.4;                    { Skalierung initialisieren }
      x0   := PaintBox1.Width Div 2;
      y0   := PaintBox1.Height Div 2;
      xmin := userx(0);                { Zeichenbereich ermitteln  }
      xmax := userx(PaintBox1.Width);
      ymin := usery(PaintBox1.Height);
      ymax := usery(0);
      end;
Die direkten Referenzen auf die "Paintbox1" müssen unbedingt verschwinden. Aber was sollen wir stattdessen nehmen? Woher bekommen wir die passenden Daten? Verschieben wir dieses Problem zunächst mal und statten wir auch "InitScale" mit einer passenden Parameterliste aus:
    procedure TForm1.InitScale(ippcm: Double; ix0, iy0: Integer; iTargetRect: TRect);
      begin
      ppcm := ippcm;                    { Skalierung initialisieren }
      x0   := ix0;
      y0   := iy0;
      With iTargetRect do begin
        xmin := userx(left);                { Zeichenbereich ermitteln  }
        xmax := userx(right);
        ymin := usery(bottom);
        ymax := usery(top);
        end;
     end;
"ippcm" ist der neue ("I"nitialisierungs-) Wert für ppcm, "ix0" und "iy0" beschreiben die neue Position des Ursprungs im Zeichenbereich in Pixel-Koordinaten. "iTargetRect" ist eine kurze Liste von 4 Integerzahlen (also: (x1, y1, x2, y2) ), die die Lage der Ecken des Ausgabe-Rechtecks auf dem aktuellen Canvas beschreiben, ebenfalls in Pixel-Koordinaten.

Für alle bisherigen parameterlosen Aufrufe von "InitScale" müssen wir nun schreiben:
    InitScale(Screen.PixelsPerInch / 2.54,
              PaintBox1.Width Div 2, PaintBox1.Height Div 2,
              PaintBox1.ClientRect,);
Hoppla, da müssen wir ja schon wieder einige neue Dinge lernen:



10.4 Eine "richtige" Druckausgabe

Es ist nun klar, wie wir im Falle eines anderen Ausgabe-Gerätes vorzugehen hat: wir müssen uns
  1. die Pixel-Anzahlen in x- und y-Richtung und
  2. die Pixeldichte
der Ausgabe-Leinwand besorgen. Der erste Punkt ist leicht erledigt: die Abmessungen des Druckbereiches sind in den Eigenschaften "PageWidth" und "PageHeight" des "Printer"-Objekts verfügbar, welche die Anzahlen der Pixel in horizontaler bzw. vertikaler Richtung liefern. Schwieriger wird es beim zweiten Punkt, denn leider hat das "Printer"-Objekt im Gegensatz zum "Screen"-Objekt keine Eigenschaft "PixelsPerInch", was ein schlimmes Versäumnis der Borland-Programmierer ist. Wir könnten die Anzahl der Pixel pro Zentimeter ja selbst berechnen, wenn wir wüßten, welche physikalischen Abmessungen der Druckbereich hat. Aber auch darüber schweigt sich das "Printer"-Objekt aus. Um diese Information zu bekommen, muss man eine Funktion aus dem Windows-API ("A"pplication "P"rogramming "I"nterface) bemühen:
    xSize := GetDeviceCaps(Printer.Canvas.Handle, HorzSize) / 10;
Der Funktionsaufruf von GetDeviceCaps mit den angegebenen Parametern liefert die Breite des Druckbereiches in Millimetern, weshalb das Funktionsergebnis erst noch durch 10 geteilt werden muss, ehe es der Double-Variablen "xSize" zugewiesen wird. Die Anzahl der Pixel pro Zentimeter ist dann also der Quotient von Printer.PageWidth und xSize!

Als Ausgabe-Rechteck für den Ausdruck könnten wir natürlich den gesamten druckbaren Bereich nehmen. Aber das würde sicher das Seitenverhältnis unserer Grafik verzerren und zu unschönen Ergebnissen führen: Bildschirme stehen meist im Querformat vor uns, Ausdrucke geschehen meist im Hochformat! Und selbst wenn wir den Ausdruck auf Querformat umgeschaltet hätten (- was wir derzeit noch nicht können -), würden die Seitenverhältnisse nur in seltenen Fällen zufälligerweise mal übereinstimmen. Ein anderes Problem kommt hinzu: meist wird man aus ästhetischen Gründen nicht den gesamten druckbaren Bereich des Blattes auch wirklich bedrucken wollen: der Ausdruck sieht schöner aus, wenn wir einen ordentlichen Rand frei lassen.

Zur Beschreibung des Druckbereiches führen wir daher drei Integer-Variablen "prBorder", "prWidth" und "prHeight" ein, die die Breite des Randes, die Breite des verwendeten Druckbereiches und dessen Höhe beschreiben, alles in Pixeln des Printer.Canvas. Mit
    prBorder := Printer.PageWidth Div 10;
    prWidth  := Printer.PageWidth * 8 Div 10;
erhalten wir einen Druckbereich, der 80% der Seitenbreite einnimmt, mit 10% Rand rechts und links. Die Höhe des Druckbereichs müssen wir nun mit Hilfe des Seiten-Verhältnisses unserer Zeichnung bestimmen. Für eine verzerrungsfreie Wiedergabe muss gelten:
     (prHeight : prWidth) = (ymax - ymin) : (xmax - xmin)
was sich leicht nach prHeight auflösen läßt.

Damit erhalten wir ein Ausgabe-Rechteck mit der linken oberen Ecke (prBorder | prBorder) und der rechten unteren Ecke (prBorder + prWidth | prBorder + prHeight). Zur Übergabe dieser Daten an die neue Version von InitScale müssen wir diese Werte in eine TRect-Struktur packen:
    Rect(prBorder, prBorder, prBorder + prWidth, prBorder + prHeight)

Es fehlt noch die Positionierung des Ursprungs im Ausgabefenster. Analog zu den Verhältnissen auf dem Bildschirm setzen wir den Ursprung in die Mitte des Druckbereiches, also an die Stelle (prBorder + prWidth Div 2 | prBorder + prHeight Div 2).

Nun sind alle Vorarbeiten abgeschlossen, und wir können die Einzelteile zusammenbauen. Insgesamt erhält man damit die folgende Druckroutine:
    procedure TForm1.Button2Click(Sender: TObject);
      var xSize, prppcm    : Double;
          prBorder,
          prWidth, prHeight: Integer;
      begin
      { Druckauftrag starten }
      Printer.BeginDoc;

      { Ausgabe-Canvas vorbereiten }
      prBorder := Printer.PageWidth Div 10;   { 5% Rand soll frei bleiben }
      prWidth  := Printer.PageWidth * 8 Div 10;
      prHeight := Round(prWidth * ((ymax - ymin) / (xmax - xmin)));
      { Wieviele Pixel pro Zentimeter hat der Drucker? }
      xSize    := GetDeviceCaps(Printer.Canvas.Handle, HorzSize) / 10;
      prppcm   := Printer.PageWidth / xSize;
      { Skalierung setzen }
      InitScale(prppcm, prBorder + prWidth Div 2, prBorder + prHeight Div 2,
                Rect(prBorder, prBorder, prBorder + prWidth, prBorder + prHeight));

      { Ausgabe }
      DrawCoordSys(Printer.Canvas);
      DrawFunction(Printer.Canvas);
      { Druckauftrag abschließen }
      Printer.EndDoc;

      { Interne Skalierung auf die für den Bildschirm gültigen Werte zurücksetzen }
      InitScale(Screen.PixelsPerInch / 2.54,
                PaintBox1.Width Div 2, PaintBox1.Height Div 2,
                PaintBox1.ClientRect);
      end;

Gerade mal 2 Zeilen beschäftigen sich mit der tatsächlichen "Druck-Ausgabe"! Alles andere ist "Skalierungs-Arbeit", ohne die es aber nun mal nicht geht. Die letzte Anweisung setzt die internen Skalierungsparameter auf die für den Bildschirm gültigen Werte zurück. Das ist notwendig, denn unser Programm wird nach Abschluß des Ausdruck ja bei nächster Gelegenheit wieder Ausgaben auf dem Bildschirm machen müssen!



10.5 Schlußwort des Angeklagten

Schrecklich viel Arbeit ist nötig, um unter Windows eine Grafik auszudrucken, und nicht ohne Grund gilt die Programmierung einer Druck-Routine als schwierigere Aufgabe. Was mussten Sie nicht alles tun, um eine den Erwartungen entsprechende Druckausgabe unseres Funktionenplotters zu erhalten! Da fragt man sich doch, ob das Prinzip des "virtuellen Geräts" zum gegenwärtigen Zeitpunkt denn eigentlich schon mehr ist als eine Vision. "Geräte-unabhängige Programmierung" wurde versprochen, und dann reicht gar der Funktionsumfang von Delphi nicht aus, und wir müssen das Windows-API benutzen, um die physikalische Breite des Druckbereiches zu ermitteln!


Wenn es Delphi aber gelingt, uns den Paintbox1.Canvas sozusagen betriebsfertig zur Verfügung zu stellen, warum kann uns dann nicht auch der Printer.Canvas genau so geliefert werden?


Aber 80% der Anweisungen in der Druck-Routine beschäftigen sich mit der Skalierung, also der "Vorbereitung" des Drucker-Canvas, und sind damit doch tatsächlich geräte-spezifisch!





Aufgaben:


  1. Implementierung einer Druckausgabe:

    Kopieren Sie die letzte Version des Funktionenplotter-Projekts aus dem vorigen Kapitel in ein neues Verzeichnis. Vollziehen Sie dann die obige Entwicklung einer Druckroutine Schritt für Schritt nach. Schauen Sie sich auch die "Zwischenstände" an; machen Sie sich klar, was bis zu der jeweiligen Entwicklungsstufe schon erreicht wurde, und was noch fehlt!



  2. Skalierung für den User:

    Was unserem Funktionen-Plotter noch fehlt, ist ein Zoom-Werkzeug. Dies läßt sich ohne großen Aufwand nachrüsten: Sie müssen nur InitScale mit einem modifizierten Wert für ppcm aufrufen! Implementieren Sie eine Zoom-Funktion mit Hilfe von 3 Knöpfen: "Einzoomen", "Auszoomen" und "1:1", also dem ursprünglichen Maßstab, bei dem der PixelPerInch-Wert des Screen-Objekts für die Bildschirmausgabe verwendet wird. Der Zoomfaktor kann zunächst konstant sein, z.B. 2,000.

    Sorgen Sie nun aber dafür, dass auch die Druckausgabe den gezoomten Zustand des Fensters ordentlich wiedergibt! Verändern Sie dazu den Wert für prppcm entsprechend der Veränderung von ppcm gegenüber dem Standard-Wert.



  3. Zoomen für Profis:

    (Die folgende Aufgabe ist für die "unterforderten Star-Programmierer" gedacht. Sie stellt nicht nur höhere Anforderungen an Ihre Programmierkünste, sondern benötigt darüberhinaus auch Informationen bzw. Programmiertechniken, die wir erst zu einem späteren Zeitpunkt behandeln werden. Um diese Aufgabe jetzt schon bewältigen zu können, müssen Sie fleißig in der Delphi-Hilfe lesen.)

    In manchen Grafik-Programmen kann man mit der Maus einen gestrichelten rechteckigen Rahmen aufziehen und dann auf Knopfdruck den Bereich innerhalb des Rahmens vergrößert ("eingezoomt") im aktuellen Fenster bzw. den aktuellen Fensterinhalt verkleinert ("ausgezoomt") innerhalb des Rahmens darstellen. Entwerfen Sie eine solche komfortable Zoom-Funktion.

    Zum Zeichnen des "Auswahlrahmens" können Sie die "FrameRect"-Methode von TCanvas benutzen. Allerdings ist in jedem Fall eine trickreiche Benutzung mehrerer Mausereignisse (OnMouseDown, OnMouseMove, OnMouseUp) notwendig. Machen Sie sich zunächst einen Plan, was wann zu geschehen hat, ehe Sie daran gehen, die Darstellung des Auswahlrahmens konkret zu programmieren!

    Wenn Sie damit fertig sind, können Sie die eigentliche Zoom-Funktion realisieren. Obwohl es sich auch hier wieder nur(?) um eine lineare Transformation handelt, ist dies mathematisch schon eine nicht gerade anspruchslose Aufgabe.



Zum vorigen Kapitel Zum Inhaltsverzeichnis