Grundsätzliche Überlegungen und optimiertes Drucken für Grafik-zentrierte Applikationen

Autor: Steffen Ploetz (Stand der EN Seite vom 03.04.2024)

Der Zweck dieses Code-Schnipsels ist
  • die Auswahl einer geeigneten Technologie für Grafik-zentrierte Applikationen (Vektor-Grafiken)

  • mit besonderem Augenmerk darauf, die Grafiken auch zu drucken.

Grundsätzliche Überlegungen zur Auswahl einer geeigneten Technologie für Grafik-zentrierte Applikationen

Bei der Planung einer Grafik-zentrierten Applikation (für Vektor-Grafiken) sollte man von Anfang an zwei Aspekte mit bedenken:
  • Wie ist die Benutzererfahrung, wenn die Grafik komplex ist - also viele Zeichenoperationen für die Anzeige der Grafik erforderlich sind?

  • Wie kann das Drucken der Grafik realisiert werden?

Benutzererfahrung bei der Anzeige komplexer Grafiken

Um Reste (übrig gebliebene Pixel) vorangegangener Zeichenoperationen sicher ausschließen zu können, muss jedes Neu-Zeichnen der Grafik ein vollständiges Löschen des Hintergrundes und ein komplettes Durchlaufen aller Zeichenoperationen umfassen. Das gilt - im übertragenen Sinne - auch für Ausschnitte der Grafik, z. B. wenn durch eine entsprechende Vergrößerung nur Teile der Grafik dargestellt werden.

Dieses komplettes Durchlaufen aller Zeichenoperationen kann aber länger dauern, als die Aktualisierungs-Rate des Monitors dafür Zeit lässt (bei LCD-Monitoren üblicherweise 60 Hz = 1/60 s). Wenn das Zeichnen unsynchronisiert zur Aktualisierung des Monitors stattfindet, steht möglicherweise nur einen Bruchteil der Zeit von 1/60 s zur Verfügung.

Um nicht auf einem Monitor-Bild eine unvollständig gezeichnete Grafik und 1/60 s später auf dem folgenden Monitor-Bild die vollständig gezeichnete Grafik anzuzeigen (was das Auge des Benutzers irritieren würde), wird mit Doppel-Pufferung gearbeitet. Das heißt, dass die Grafikkarte die "alte" Grafik so lange anzeigt, bis die "neue" Grafik vollständig gezeichnet ist. Dazu wird die "neue" Grafik außerhalb des Anzeige-Speichers der Grafikkarte gezeichnet und anschließend per "Refresh" in den Anzeige-Speicher der Grafikkarte eingeblendet. Insbesondere wenn sich der Speicher für die Doppel-Pufferung ebenfalls auf der Grafikkarte befindet, ist dieser Vorgang sehr schnell und kann von der Grafikkarte synchron zur Aktualisierungs-Rate des Monitors durchgeführet werden, dass dem Auge des Benutzers niemals eine unvollständig gezeichnete Grafik angeboten wird. Dafür wird aber ein zweiter Speicherbereich außerhalb des Anzeige-Speichers der Grafikkarte benötigt - der Grafik-Puffer.

Doppel-Pufferung der Grafik

Um die Vorteile der Doppel-Pufferung nutzen zu können, muss zur Anzeige der Grafik entweder ein Control gewählt werden, das Doppel-Pufferung automatisch unterstützt, oder die Doppel-Pufferung muss manuell bereitgestellt werden. Außerdem muss das gewählte Control alle Ereignisse unterstützen, die für die Bearbeitung der Grafik erforderlich sind (z. B. Mouse-Events für das Selektieren von Grafik-Elementen).

Wählt man ein Control, das Doppel-Pufferung automatisch unterstützt, ist man auf die vom Control unterstützten Zeichenoperationen zur Darstellung der Grafik angewiesen. Ob DrawingArea diesbezüglich eine gute Wahl darstellt, habe ich nicht untersucht. Ein solches Control wird schnelle Anfangserfolge ermöglichen - die Gefahr, dass später Einschränkungen auftreten und aufwändige Umgehungen programmiert werden müssen, ist aber real bis groß.

Wählt man ein Control, das keine Doppel-Pufferung unterstützt, hat man bei der Wahl der Zeichenoperationen zur Darstellung der Grafik weitgehende Freiheit und die Gefahr, dass später Einschränkungen auftreten, ist deutlich geringer. Das wird aber mit zusätzlichem Aufwand am Anfang erkauft. Gute Erfahrung habe ich bisher mit der PictureBox gemacht.

Gambas stellt mit gb.cairo, gb.opengl und gb.sdl2 gleich drei Grafik-Bibliotheken mit professionellen Zeichenoperationen bereit. Alle drei Bibliotheken sind Hardware-beschleunigt. Muss die Grafik keine 3D-Funktionen (Volumen-Modell, Beleuchtung, Kamerapositionierung, ...) unterstützen, ist gb.cairo eine gute Wahl. gb.cairo ist aufgrund der Beschränkung auf 2D einfacher und bietet ein schönes Extra: Die PDF-Backend CairoPdfSurface und SVG-Backend CairoSvgSurface. Damit können Vektor-Grafiken (statt Bitmaps) nach PDF geschrieben und eine bessere Qualität für PDF und SVG erreicht werden.

Fällt die Wahl auf ein Control, das keine Doppel-Pufferung unterstützt, muss die Grafik außerhalb des Anzeige-Speichers der Grafikkarte gezeichnet und anschließend per "Refresh" in den Anzeige-Speicher der Grafikkarte eingeblendet werden. Für die PictureBox kann zunächst in ein Image Objekt oder Picture Objekt gezeichnet werden und dieses Objekt kann dann per Image Property oder Picture Property zugewiesen und mit der Refresh Methode eingeblendet werden.

Display-Listen für die Grafik-Befehle

Ein weiterer Ansatz zum beschleunigten Zeichnen der Grafik ist die Verwendung von Display-Listen. In Display-Listen werden fertig berechnete (Koordinaten, Farben, ...) Zeichenoperationen abgelegt, die dann nur noch abgearbeitet werden müssen - was mit den Bibliotheken auch Hardware-beschleunigt geschehen kann. Gerade dann, wenn sich nur Teile der Grafik ändern, reduzieren Display-Listen die Systembelastung erheblich. gb.opengl bietet mit NewList und EndList bereits Display-Listen an. Fällt die Wahl auf gb.cairo, sollte ernsthaft die Implementierung von Display-Listen in Erwägung gezogen werden. Neben dem beschleunigten Zeichnen und der geringen Systembelastung ergibt sich dabei noch ein weiterer Vorteil: Mit Display-Listen können bei minimalem Aufwand alle spezifischen Backends (CairoPdfSurface und CairoSvgSurface) parallel in derselben Anwendung genutzt werden - das verbessert das Drucken in eine SVG-Datei erheblich.

Optimiertes Drucken

Fällt die Wahl der Grafik-Bibliothek auf gb.cairo, können die CairoPdfSurface und CairoSvgSurface Backends genutzt werden, um eine optimale Qualität der Ausdrucke zu erzielen. Mein Manjaro Linux bietet einen "Print to file" Drucker an, mit dem PFD, PS und SVG gedruckt werden können.

Der folgende Code-Schnipsel zeigt die Gambas-Klasse, die das optimierte Drucken übernimmt.

' Gambas class file

''' This class provides all the printing functionality for a **Cairo** based drawing application.

'' Buffer the printer, that provides the printing functionality.
Private $oPrinter As Printer

'' Buffer the image to print to paper, to PDF and PS.
Private $oImage As Image

'' Buffer the model to print to SVG.
Private $oModel As FlowDiagram

'' Buffer the file name for an print to SVG buffer-file.
Private $oBufferFile As String

'' The only method, that must be called from outside.
''
'' The **oImage** defines the image to print to Paper, PDF and PS.
'' The **oModel** defines the data-nodel (diagram) to print to SVG.
Public Sub Print(oImage As Image, oModel As FlowDiagram)
  $oImage = oImage
  $oModel = oModel
  $oBufferFile = ""
  $oPrinter = New Printer As "ctrlPrinter"

  $oPrinter.Paper = Printer.A4
  $oPrinter.Orientation = Printer.Landscape

  If $oPrinter.Configure() Then
    $oModel = Null
    $oImage = Null
    $oPrinter = Null
    Return
  Endif

  $oPrinter.Print()
  $oModel = Null
  $oImage = Null
  $oPrinter = Null

  ' We have to overwrite the standard print function result with the separate file for optimized printing.
  If $oBufferFile <> "" Then
    Try Move $oBufferFile Kill Replace$($oBufferFile, "_buffer.", ".")
  Endif
End

'' Event handler to set the number of pages.
'' Public to provide access for the event loop. Don't call this handler manually.
Public Sub ctrlPrinter_Begin()
  $oPrinter.Count = 1
End

'' Event handler to execute printing.
'' Public to provide access for the event loop. Don't call this handler manually.
Public Sub ctrlPrinter_Draw()
  If $oPrinter.Name = "Print to File" Then
    ' We have to use a separate file for optimized printing to a file. Otherwise, the standard print function would overwrite the result.
    If Right($oPrinter.OutputFile, 4) = ".svg" Then
      $oBufferFile = Replace$($oPrinter.OutputFile, ".svg", "_buffer.svg")
      Dim oSvgSurface As CairoSvgSurface = New CairoSvgSurface($oBufferFile, $oPrinter.PaperWidth, $oPrinter.PaperHeight)

      Cairo.Begin(oSvgSurface)
      CairoHelper.RunDisplayLists($oModel)
      Cairo.Save()
      Cairo.End()

      oSvgSurface = Null
      Return
    Else If Right($oPrinter.OutputFile, 4) = ".pdf" Then
      $oBufferFile = Replace$($oPrinter.OutputFile, ".pdf", "_buffer.pdf")
      Dim oPdfSurface As CairoPdfSurface = New CairoPdfSurface($oBufferFile, $oPrinter.PaperWidth, $oPrinter.PaperHeight)

      Cairo.Begin(oPdfSurface)
      CairoHelper.RunDisplayLists($oModel)
      Cairo.Save()
      Cairo.End()

      oPdfSurface = Null
      Return
    Endif
  Endif

  Paint.DrawImage($oImage, 0, 0)
End

Die Druck-Funktion im Haupt-Formular ist dann nur noch ein 3-Zeiler:

Public Sub btnPrint_Click()
  Dim printHelper As PrintHelper = New PrintHelper
  printHelper.Print(ctrlPictureBox.Image, $oDiagram)
  printHelper = Null
End


Änderungen der Seite


Grundsätzliche Überlegungen und optimiertes Drucken für Grafik-zentrierte Applikationen von: Steffen Ploetz - 3. April 2024