Schnell & scharf

Nein, kein Thai-Curry-Instantsuppen-Rezept hier (ich kann Ihnen aber gerne ein paar vegane Kochbücher nennen, die über meinen Schreibtisch gelaufen sind), sondern um Retina- bzw. HiDPI-kompatible Grafiken geht es mir an dieser Stelle, noch dazu um solche, die möglichst wenig CPU-Last verursachen.

Der Canvas ist unter Xojos Desktop-Steuerelementen das meistbenutzte Arbeitstier: Durch seinen Paint-Event lässt sich jeder beliebige Grafikinhalt darstellen, und er kann auf jede Menge von Benutzeraktivitäten reagieren.

Das verleitet dazu, den Paint-Event grundsätzlich zum Aufbau der Grafik zu verwenden. Kann man machen, sollte man aber besser nicht. Weil es viele Fälle geben mag, in denen eigentlich der unveränderte Inhalt neu angezeigt werden muss – wenn der Canvas von einem anderen Fenster (teil)verdeckt wurde etwa. Soll heißen: Wenn Sie so vorgehen, ist die Chance groß, dass Sie Ihrem Canvas eine sinnlose Arbeitsbeschaffungsmaßnahme verordnen. Als ethischer Programmierer macht man so etwas freilich nicht. Außerdem müssen Sie sich bei einem komplexeren Bildinhalt nicht wundern, wenn ihr Programm suboptimal performt. Das reduziert die Consumer Satisfaction Ratio.

Verzeihung, da bin ich wohl neoliberal gehackt worden. Also in richtiger Sprache: Der Canvas zeichnet seinen Inhalt unter Umständen viel zu häufig neu, was die CPU beschäftigt hält und damit die Leistung Ihres Programms beeinträchtigen kann.

Man sollte also besser den Inhalt in einem Picture speichern und dieses nur wenn’s nötig wird neu aufbauen.

Jetzt das große ABER: Was früher einfach funktionierte, ist durch die Retina-Unterstützung ein bisschen komplizierter. Insbesondere wenn der Anwender mehrere Bildschirme hat, einen mit klassischer niedriger Auflösung und einen HiDPI-/Retina-fähigen, muss das Bild auch in entsprechender Auflösung abgelegt werden. Sonst sieht alles etwas verwaschen aus und man wundert sich, wo denn die versprochene HiDPI-Unterstützung bleibt.

Die Window-Klasse besitzt seit einiger Zeit eine wenig beachtete, weil recht neue Methode, die einem umständliche Berechnungen der Bildgröße/Auflösung abnimmt: BitmapForCaching. Diese erwartet Breite und Höhe und erzeugt ein Bitmap-Picture mit genau den richtigen Parametern.

Ein Nutzbeispiel dafür anbei. Es baut auf diesem Blog-Beitrag auf und erweitert ihn noch ein bisschen:

Ich lege eine Canvas-Subklasse an, gebe ihr eine private Property Bild As Picture und verpasse ihrem Paint-Event den folgenden Code:

 ErzeugeCacheWennNötig
 g.DrawPicture Bild, 0,0

ErzeugeCacheWennötig wird eine neue private Methode. Das darin aufgebaute Bild ist nun nicht wirklich rechenintensiv, dafür aber kürzer im Code:

Private Sub ErzeugeCacheWennNötig()
 if Bild <> Nil then Return // Nur aufbauen, wenn es kein Bild gibt.
 Bild = Self.Window.BitmapForCaching(self.Width, self.Height)
 Dim g as Graphics = Bild.Graphics
 g.FillOval(0, 0, g.Width, g.Height)
 g.ForeColor = Color.White
 g.TextSize = 20
 Dim T as Text = "Gestochen scharfe Grafik mit wenig CPU-Last!"
 Dim Width as Double = g.StringWidth (T)
 g.DrawString T, g.Width/2-width/2, g.Height/2
End Sub

Wenn es dem Benutzer einfallen sollte, das Fenster mit dem Canvas auf den anderen Bildschirm zu ziehen, feuert der ScaleFactorChanged-Event des Canvas. Also nutzen wir diesen:

Sub ScaleFactorChanged() Handles ScaleFactorChanged
 Bild = Nil
End Sub

Soweit sehr überschaubar, oder? Das wär es auch schon fast, nur: Es gibt noch einen weiteren Fall, in dem das Bild neu aufgebaut werden muss. Beim Verändern der Canvas-Größe nämlich. (Also in meinem Fall, in dem das Bild immer canvasgroß sein soll. Das muss aber nicht so sein; es gibt auch genug Fälle, in denen das Bild (und eventuell auch der Canvas) eine feste Größe behält.)

Dummerweise besitzt der Canvas keinen Resizing-Event. Den kennt dafür die Window-Klasse. Wenn Sie nur einen Canvas dieser Art auf einem Fenster benutzen wollen, reicht es, dafür aus dem Window-Resizing-Event eine öffentliche Methode des Canvas zu benutzen, etwa LöscheBild, die auch nichts anderes macht als Bild = Nil zu setzen.

Wenn Sie mehrere dieser Canvas-Klassen gleichzeitig benutzen wollen, ist ein weitaus eleganterer Weg, ein Class Interface zu verwenden. Etwa so:

Ein in Xojo angelegtes Window ist eigentlich eine eigene Window-Subklasse (nur wird diese häufig durch den entsprechenden Schalter im Inspector implizit instanziiert, also schon bei Programmstart eine Instanz des gleichen Namens erzeugt, weshalb man das manchmal gar nicht merkt.) Ergo kann ein Window ein Class Interface erhalten – der Name sagt’s ja, es ist eine Schnittstelle für Klassen.

Also klicke ich im Navigator auf mein Resize-Window und ganz oben im Inspector hinter Interfaces auf Choose. Und dann choose ich, nämlich das actionSource-Interface.

resizewindow

Dadurch tauchen zwei Methoden im Window auf, addActionNotificationReceiver und das Gegenstück RemoveActionNotificationReceiver. Die werden wie folgt mit Code versorgt:

Public Sub addActionNotificationReceiver(receiver As actionNotificationReceiver)
 // Part of the actionSource interface. 
 Receivers.Append receiver
End Sub
Public Sub removeActionNotificationReceiver (receiver As actionNotificationReceiver)
 // Part of the actionSource interface.
 Dim Index As Integer = receivers.IndexOf (Receiver)
 If Index > -1 then receivers.Remove index
End Sub

Der Resizing-Handler des Windows bekommt nun die Aufgabe, alle Receiver zu notifyen – ähem, sie zu benachrichtigen vielmehr:

Sub Resizing() Handles Resizing
 for Each r as actionNotificationReceiver in receivers
 r.PerformAction
 next
End Sub

Noch flugs dem Window eine private Property Receivers() As ActionNotificationReceiver hinzugefügt und zum BufferCanvas gewechselt. Auch der bekommt ein Interface spendiert, nämlich actionNotificationReceiver.

Damit hat auch dieser eine neue Methode: PerformAction. Und diesen sehr überschaubaren Code

Private Sub PerformAction()
 // Part of the actionNotificationReceiver interface.
 Bild = Nil
End Sub

Nun muss ich nur noch dafür sorgen, dass sich der BufferCanvas auch bei seinem Fenster registriert (und wenn ich ordentlich sein will, beim Schließen auch wieder abmeldet). Fürs erstere nutze ich den Open-Event des Canvas:

Sub Open() Handles Open
 ResizeWindow(me.Window).addActionNotificationReceiver self
 RaiseEvent Open
End Sub

und für letzteres – da es guter Stil ist – den Close-Event:

Sub Close() Handles Close
 RaiseEvent Close
 ResizeWindow(me.Window).removeActionNotificationReceiver self
End Sub

Beide Events sollen trotzdem noch feuern – kann ja gut sein, dass individuelle Einrichtungen und Deinstallationen nötig sein. Also jeweils einen Rechtsklick auf die gerade angelegten Event-Handler und „Create Event Definition from Event“ gewählt.

create-event-definition

Tja, und das war’s dann auch schon. Wenn Sie mehr Kommunikation zwischen Window und Canvas wünschen, können Sie natürlich auch ein Interface verwenden, das mehr Features beinhaltet. Für sehr viele Fälle wird das Actionusw-Interface aber völlig genügen.

Hier ist das Projekt zum Experimentieren:

hidpi-buffercanvas


Und hier noch ein Nachsatz nach eigenen Experimenten, um alles auch mit Zahlen zu belegen.

Ich habe das Bild ein bisschen gehübscht, damit es auch wirklich etwas zum Rechnen gibt. In Worten:

Private Sub ErzeugeCacheWennNötig()
  if Bild <> Nil then Return // Nur aufbauen, wenn es kein Bild gibt.
  Bild = Self.Window.BitmapForCaching(self.Width, self.Height)
  Dim g as Graphics = Bild.Graphics
  dim Midx as double = g.Width/2
  dim Midy as double = g.Height / 2
  dim z as double = Floor(Min(Midx, Midy))
  Dim ColStep As Double = 1/z
  Dim colval as double
  For q as integer = 0 to z
    g.ForeColor = HSV(colval, 1, 1)
    colval = colval + colstep
    g.DrawOval midx-q, midy-q, q*2, q*2
  Next
  g.ForeColor = Color.black
  g.TextSize = 20
  Dim T as Text = "Gestochen scharfe Grafik mit wenig CPU-Last!"
  Dim Width as Double = g.StringWidth (T)
  Dim Height as Double = g.StringHeight(T, 3000)
  g.DrawString T, g.Width/2-width/2, g.Height/2+height/4 
End Sub

Und das Ganze auch einmal pur in den Paint-Event eines Canvas gelegt und direkt in dessen Graphics-Objekt gezeichnet, also die Property Bild ganz weggelassen.

Profiler angeworfen und mit beiden Projekten dasselbe gemacht: Einmal Vollbild und zurück, einmal minimiert und aus dem Dock geholt. Das hat bei beiden Projekten 5  Redraws ausgelöst, wobei der Buffer 2 mal einsprang. Hier die Profiler-Ergebnisse: direkter Canvas oben, BufferCanvas unten. Der direkte Canvas brauchte bei diesen wenigen Aktionen ca. 175% der Aufbauzeit des BufferCanvas.

Allerdings ist der Umweg über das Picture nicht langsamer, was ich eigentlich bei einmaligem Bildaufbau erwartet hätte. In den meisten Fällen hatte auch dann der BufferCanvas die Nase leicht vorn – aber nicht so dramatisch. Ein bisschen verblüfft bin ich aber schon darüber.

Damit wär – zumindest auf dem Mac – die Titelbehauptung noch wahrer als gedacht.

bildschirmfoto-2017-01-08-um-11-32-17

 

 

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s