Categories: Sonstiges

Welche Bedeutung der Kontext in Core Graphics hat.

Bevor Sie sich diesen Beitrag durchlesen, empfehlen wir ihnen, sich mit unserem ersten Teil über Core Graphics auseinanderzusetzen.

Der Kontext ist das Herzstück von Quartz: Sie müssen mit dem aktuellen Core Graphics-Kontext in irgendeiner Weise interagieren, um wirklich etwas zu zeichnen, also ist es gut, zu eruieren, was es eigentlich genau macht und warum es entwickelt wurde.

Eine grundlegende Operation in Core Graphics ist die Erstellung eines Pfades. Ein Pfad ist eine mathematische Beschreibung einer Form. Ein Pfad kann ein Rechteck, ein Kreis, ein Cowboyhut oder sogar das Taj Mahal sein. Dieser Pfad kann mit einer Farbe gefüllt werden, d.h. alle Punkte innerhalb des Pfades werden auf eine bestimmte Farbe gesetzt. Der Weg kann auch skizziert werden, auch bekannt als stroked. Das ist so, als würde man einen Kalligraphie-Stift nehmen und um den Weg herum zeichnen und eine Umrisslinie hinterlassen.

Der eigentliche Umriss kann sehr komplex werden. Es ist möglich, diesen in einer bestimmten Farbe zu zeichnen. Die Linie kann ein Strichmuster haben. Es kann mit einer breiten oder einer schmalen Linie gestrichen werden. Die Enden der Linien können quadratische oder runde Ecken haben und so weiter. Das sind eine Menge Attribute.

Wenn Sie die Core Graphics API durchgehen, sehen Sie keinen Aufruf, der alle Einstellungen übernimmt:

Copy to Clipboard

Stattdessen haben Sie nur diesen Call:

Copy to Clipboard

Woher kommen dann all diese zusätzlichen Werte? Sie kommen aus dem Kontext.

Bucket of Bits.

Der Kontext enthält einen Haufen globaler Zustände über das Zeichnen, die ein Haufen unabhängiger Werte sind:

  • Derzeitiger Pfad.
  • Aktuelle Füll- und Strichfarben.
  • Linienbreite und -muster.
  • Linienkappen und Gehrungsschnitte.
  • Alpha (Transparenz), Antiliasing und Mischmodus.
  • Beschatter.
  • Transformationsmatrix.
  • Textattribute (Schriftart, Größe, Matrix).
  • Esoterische Dinge wie Linienebenheit und Interpolationsqualität.
  • Und mehr.

Das ist eine Menge. Der gesamte Zustand, den Core Graphics beibehält, ist undokumentiert, so dass noch mehr Einstellungen unter der Haube lauern können. Verschiedene Arten von Kontexten können zusätzliche Daten enthalten.

Immer wenn Core Graphics aufgefordert wird, etwas zu zeichnen, z.B. „einen Pfad zu füllen“, sucht es im aktuellen Kontext nach den notwendigen Daten. Die gleiche Codefolge kann je nach Kontext radikal unterschiedliche Ergebnisse haben. Auf der einen Seite ist das sehr mächtig. Ein generisches Bit Zeichnungscode kann über den Kontext in verschiedene Ergebnisse manipuliert werden. Auf der anderen Seite ist der Kontext ein großer Haufen globaler States und der globale State ist leicht ungewollt durcheinander zu bringen.

Sagen wir, sie haben einen Code wie diesen:

Copy to Clipboard

Dies wird in einem orangenen Quadrat enden. Nehmen wir an, sie zeichnen auch eine Valentinskarte:

Copy to Clipboard

Als Ergebnis erhalten wir ein rotes Herz. Nun sagen wir zusätzlich, dass Sie den Valentinscode in ihrer ersten Funktion hinzufügen:

Copy to Clipboard
Copy to Clipboard

Ihr Rechteck erscheint rot statt orange. Warum? Der Valentinscode hat die aktuelle Zeichnungsfarbe überschrieben. Früher war die Farbe orange, als du das Rechteck gefüllt hast, aber jetzt ist sie rot. Wie kann man solche Fehler vermeiden?

Es gibt zwei Ansätze. Eine Möglichkeit besteht darin, den Zustand zu speichern, bevor Sie ihn ändern – wenn Sie die globale Farbe ändern, speichern Sie die aktuelle Farbe, ändern Sie sie, machen Sie ihre Zeichnung und stellen Sie sie dann wieder her. Das ist mit einem oder zwei Parametern in Ordnung, skaliert aber nicht, wenn man ein Dutzend davon ändert. Es gibt auch einige Einstellungen, die als Nebenwirkungen geändert werden, so dass Sie diese berücksichtigen müssen. Das ist in Core Graphics eigentlich unmöglich, weil es keinen Getter für den aktuellen Kontext gibt.

A stack of buckets.

Der andere Ansatz besteht darin, den gesamten Kontext zu speichern, bevor Sie etwas ändern. Speichern Sie den Kontext, passen Sie die Farbe oder Linienbreite an, zeichnen Sie und stellen Sie den gesamten Kontext wieder her. Die Core Graphics API bietet Aufrufe zum Speichern und Wiederherstellen der Einstellungen des aktuellen Kontexts. Diese Einstellungen werden als Grafikstatus oder GState bezeichnet. In Wirklichkeit hält es einen Stack von GStates hinter den Szenen.

Wenn Sie die Einstellungen des aktuellen Kontexts speichern, werden sie auf einen Stack verschoben. Dieser Stack ist pro Kontext. Wenn Sie den Grafikstatus wiederherstellen, wird der zuvor gespeicherte GState von einem Stack entfernt und wird zum aktuellen Satz von Werten des Kontexts. Das Ändern des Valentinscodes wie folgt behebt den Fehler „orangefarbenes Rechteck ist rot“:

Copy to Clipboard

Dann sieht die gesamte Sequenz der Zeichnungsaufrufe so aus:

Copy to Clipboard

Core Graphics API.

Ok, das ist gut in der Zusammenfassung, aber wie sieht das vorher beschriebene eigentlich aus, wenn man es benutzt? Mit Sicherheit nicht so hübsch, wie Sie es sich erhofft haben. Core Graphics ist eine API mit Core Foundation-Geschmack. Es gibt eine Reihe von Konventionen der Core Foundation, die Core Graphics beeinflussen. Die größte Seltsamkeit ist die sehr seltsame Angst, die die Core Foundation hat, Dinge als Zeiger zu deklarieren. In Objective-C werden Sie mit „Ref“-Typen wie CGContextRef oder CGColorRef konfrontiert. Diese sind eigentlich ein Zeiger, um den Stern zu verstecken.

Das ist korrekt:

Copy to Clipboard

Das ist korrekt:

Copy to Clipboard

Ihr Zeichnungscode erzeugt selten den Kontext, in den er sich hineinzieht, sondern verwendet stattdessen den aktuellen Kontext. Sie können den aktuellen Kontext in iOS mit:

Copy to Clipboard

und in Desktop Cocoa, vor OS X 10.10 mit:

Copy to Clipboard

und in 10.10 und darüber hinaus:

Copy to Clipboard

Da die Core Graphics-API eine objektorientierte C-API ist, werden Funktionen nach den „Objekten“ benannt, die sie manipulieren (z.B. CGColor oder CGContext) und das erste Argument der Funktion dient als Empfänger in Objective-C. Das wird das „Objekt“ sein, das durch den Aufruf manipuliert wird.

Hier ist zum Beispiel ein CGContext-Aufruf, der ein Rechteck umreißt:

Copy to Clipboard

CGContextStrokeRect nimmt ein CGContextRef als erstes Argument. Zweck der Funktion ist es, ein bestimmtes Rechteck zu umreißen. Ist der Kontext ein Bildkontext oder der Kontext, in dem Grafiken auf dem Bildschirm dargestellt werden, wird der Wert der Pixel eines Rechtecks in der aktuellen Farbe des Kontexts festgelegt. Wenn der aktuelle Kontext ein PDF-Kontext ist, werden ein paar Bytes von Anweisungen aufgezeichnet, um schließlich ein Rechteck zu zeichnen, wenn das PDF zu einem späteren Zeitpunkt gerendert wird.

Context Hygiene.

Zeit für einen quasi realen Code. GrafDemo ist eine Anwendung, die immer mehr Demos entwickelt, je mehr Aspekte von Core Graphics behandelt werden. Sie können die Quelle auf Github finden. Diese spezielle Version kann als Release cg-pt2 gefunden werden.

GrafDemo enthält ein NSView, das einen grünen Kreis, umgeben von einer dicken blauen Linie, auf einem weißen Hintergrund mit einem dünnen schwarzen Rand um die gesamte Ansicht zeichnet.

Es gibt zwei Versionen des Codes: eine mit guter GState Hygiene und eine ohne. Beachten Sie, dass in der ungenauen Version die dicke blaue Linie durchsickerte und den Rand verunreinigte. Wenn Sie das Programm tatsächlich ausführen, sehen Sie zwei Ansichten nebeneinander. Das eine ist Objective-C, das andere Swift.

Die View-Unterklasse bietet eine komfortable Möglichkeit, den aktuellen Kontext abzurufen. Dies sollte die Portierung auf iOS erleichtern.

Copy to Clipboard

Die -drawRect-Implementierung der Ansicht setzt naiv die Strich- und Füllfarbe und erwartet, dass sie von den Hintergrund- und Rahmenzeichnungsmethoden verwendet wird:

Copy to Clipboard

Der Hintergrund- und Randmethoden sind ziemlich einfach:

Copy to Clipboard

Beide gehen davon aus, dass der Kontext genauso konfiguriert ist, wie – drawRect es eingerichtet hat. Es gibt aber ein Problem:

Copy to Clipboard

Beachten Sie die Änderungen der Farbe und der Linienbreite. Der Kontext enthält einen globalen Stapel von Zuständen, so dass die vorhandene Füll- und Strichfarbe und die vorhandene Linienbreite vollständig überschrieben werden.

Push-me Pull you.

Der Weg, dieses Problem zu beheben, besteht darin, den Grafikkontext vor dem Zeichnen des Inhalts zu verschieben. CGContextSaveGState schiebt eine Kopie des vorhandenen Grafikkontextes/Grafikstatus auf den Stack. CGContextRestoreGState springt vom Anfang des Stacks und ersetzt den aktuellen Kontext.

Hier ist eine schönere Zeichnung der Content-Zeichnung, die den Grafikstatus speichert:

Copy to Clipboard

Ein einfaches Umhüllen der Zeichnung mit einem Save/Restore verhindert, dass diese Methode anderen Methoden schadet.

Ein wenig zu den geschweiften Klammern: Das ist eine Beeinträchtigung unsererseits, wenn es ausgewogene Aufrufe gibt, wie z.B. das Speichern oder Wiederherstellen eines GStates oder das Sperren und Entsperren eines Mutex. Es ist klar, was geschützt wird und es wird auch deutlich, dass ein einfacher linearer Scan durch einen Stapel von linksbündigem Code nicht schnell herauskommt. Sie sind eigentlich nicht Teil der Core Graphics API.

Swift.

Ok, was ist mit Swift? GrafDemo verfügt über parallele Objective-C und Swift-Implementierungen, so dass Sie sich den Beispielcode ihrer Wahl ansehen können.

Glücklicherweise sieht CG-Code in Swift genauso aus wie in Objective-C, nur ohne die nachfolgenden Semikolons. Hier ist die ungenaue Contentzeichnung in Swift. CurrentContext ist eine Eigenschaft, die den aktuellen Zeichenkontext zurückgibt:

Copy to Clipboard

Der Scope-Trick.

Leider funktioniert mein kleiner Trick in Swift nicht: ein ansonsten schlichter Satz geschweifter Klammern wird zum Abschluss und erzeugt einen Sytaxfehler. Eine Möglichkeit besteht darin, eine Funktion zu erstellen, die einen Abschluss annimmt, den Kontext speichert und wiederherstellt. Anbei die Funktion:

Copy to Clipboard

und die entsprechende Verwendung:

Copy to Clipboard

Der Hauptnachteil ist, dass man sich selbst benutzen muss, um auf Dinge im Block zu verweisen. Ich bin noch unentschlossen, ob ich mit dieser Technik dauerhaft leben werde.

Den Kontext ermitteln.

Einen CGContext zu bekommen ist unter iOS und OS X 10.10 ziemlich einfach. Glücklicherweise lässt Swift den „Ref“-Teil des Typs fallen, so dass man sich nur mit CGContext beschäftigt. So greifen Sie auf den aktuellen Kontext in iOS zu:

Copy to Clipboard

Context hat einen abgeleiteten Typ von CGContext. Es kann Null sein, weil es möglich ist, das es keinen aktuellen Kontext gibt. UIHraphicsGetCurrentContext() in Objective-C gibt ein CGContextRef zurück, so dass die Konvertierung trivial ist.

OS X 10.10 fügt eine CGContext-Eigenschaft zu NSGraphicsContext hinzu:

Copy to Clipboard

OS X 10.9 ist nicht so glücklich. Der Aufruf NSGraphicsContext-graphicsPort liefert eine Lücke. Swift macht es nicht einfach, Zeigertypen zu ändern. Hier ist eine Lösung:

Copy to Clipboard

Kurz gesagt, unsafeContextPainter ist ein unsafeMutablePointer? Wenn es nicht Null ist, wird es in einen COpaquePointer gewickelt. Auf dieser Basis können Sie etwas an Unmanaged weitergeben. Unmanaged kann den Zeigerwert verwenden und da CGContext einen Referenzwert wie NSObjects hat, überträgt der nicht erhaltene Wert den Lebenszyklusbesitz an ARC. TakeUnretained sagt: „ARC, nimm diesen Wert unter deine Fittiche. Sie ist derzeit nicht erhalten und kann beim Entleeren des Autorelease Pools wegfließen, Tu, was du tun musst, damit es so bleibt, wie es gebraucht wird.“

GState der Union.

Dieses Mal trafen Sie auf Core Graphics-Kontexte, die Bereiche mit verschiedenen Zeichnungsattributen sind. Ein Kontext ist eine undurchsichtige Struktur, so dass Sie keine wirkliche Ahnung haben, was wirklich in ihm lauert. Aufgrund dieses globalen Zustands und der Tatsache, dass einige Core Graphics-Aufrufe Nebeneffekte haben, ist es unmöglich, Zeichenattribute zu speichern, bevor sie geändert werden.

Core Graphics hat das Konzept eines Grafikstatus-Stacks, der auch in PostScript existiert. Sie können mit CGContextSaveGstate eine Kopie des aktuellen Grafikstatus auf einen Stack schieben und alle Änderungen am Kontext rückgängig machen, indem Sie den gespeicherten Status mit CGContextRestoreGstate aufrufen. Haben Sie Code, der dem Kontext schadet, so dass das spätere Zeichnen falsch ist? Dann wickeln Sie es in ein Save/Restore ein.

Core Graphics Code ist in Objective-C und Swift nahezu identisch. Es ist auch unter OS X und unter iOS nahezu identisch, so dass Core-Graphics-Code zwischen den verschiedenen teilen des Apple-Ökosystems ziemlich portable sein kann. Der einzige wirklich schwierige Teil in Swift ist, dass Mavericks eine Lücke von Cocao n einen CGContext konvertiert, da Swift versucht, Programmierer von solchen Low-Level-Details zu isolieren. Wenn Sie auf Yosemite oder darüber hinaus abzielen, ist das kein Problem.

Vielen Dank fürs Lesen.

3DMaster