Linien, Figuren, Formen

    • WPF

      Linien, Figuren, Formen

      (Achtung - es fängt einfach an, und wird dann immer komplizierter ;) )

      Linien, Figuren, Formen werden in Wpf als Shape dargestellt, und deren gibt es mehrere, die von der Shape-Basisklasse erben:
      Ellipse, Line, Polygon, PolyLine, Rectangle, Path

      Die Path-Klasse ist am interessantesten, denn ihre Data-Property enthält eine beliebige Geometry.
      Während nun ein Shape ein im Xaml direkt sichtbares FrameworkElement ist, ist eine Geometry nur die abstrakte Beschreibung einer (Multi-)Linie / Form.
      Auch von Geometry wird geerbt, nämlich die folgenden nützlichen Klassen:
      EllipseGeometry, LineGeometry, RectangleGeometry, CombinedGeometry, GeometryGroup, StreamGeometry und PathGeometry.

      Um es erwähnt und geklärt zu haben:
      In eine GeometryGroup schmeisst man mehrere andere Geometries, die dabei nicht miteinander interagieren.
      Eine CombinedGeometry kombiniert zwei Geometries mit den Optionen Exclude, Intersect, Union oder Xor.
      Eine StreamGeometry ist eine optimierte PathGeometry, die ggfs. durch besonderes Xaml-Markup generiert wird - letzteres ist das Haupt-Thema dieses Tuts

      Das Xaml-Path-Markup
      Damit kann man wie gesagt eine StreamGeometry definieren, aber auch eine PathFigureCollection.
      Also bei den folgenden eiglich gleichen Path-Shapes enthält das erste eine StreamGeometry, und der zweite eine PathGeometry mit einer PathFigureCollection (der Unterschied wird aber nur in fortgeschrittenen Szenarien relevant):

      XML-Quellcode

      1. <Path Stroke="Blue" Data="M50,50 c-50,0 20,50 -20,40"/>
      2. <Path Stroke="Blue">
      3. <Path.Data>
      4. <PathGeometry Figures="M50,50 c-50,0 20,50 -20,40"/>
      5. </Path.Data>
      6. </Path>


      PathGeometry, PathFigure, PathSegment
      Ein Path-Markup definiert eine PathGeometry, mit mehrere PathFigures darinnen. Eine PathFigure wiederum besteht aus mehreren PathSegments. Unter "Segment" ist etwas zu verstehen, was den vorherigen Punkt mit einem weiteren Punkt verbindet.
      Ob nun gradlienig oder gekrümt: Ein Segment verbindet immer den vorherigen Punkt mit seinem eigenen - und deshalb kann man Segmente verketten - quasi: "malen, ohne den Stift abzusetzen".
      Oder anders: Eine PathGeometry enthält viele Linien, und jede Linie (PathFigure) verbindet zwei Punkte über viele PathSegment-Zwischenstationen.
      Also eine PathFigure hat einen Startpunkt und daran hängen viele Segmente.
      Beachte aber auch: ein Kreis, oder ein Rechteck, kann niemals ein Segment sein - denn als geschlossene Figur gibts da ja keinen Start- und End-Punkt, durch eine Linie zu verbinden.

      Der Startpunkt einer PathFigure wird durch M gemarkupt - und dann folgen die Segmente.
      Insgesamt ermöglicht das Markup folgende Anweisungen:
      Key
      BedeutungArgumenteBemerkung
      MStartpunkt{ptStart X,Y}kein Segment, sondern ptStart ist Eigenschaft
      der die Segmente beinhaltenden PathFigure
      LLine{ptTarget X,Y}LineSegment
      HHorizontal Line{target-X}LineSegment
      VVertical Line{target-Y}LineSegment
      ZCloseFigureLineSegment zurück zum Startpunkt
      Neue Figur beginnen, mit demselben Startpunkt
      AArc-Segment{radius X,Y} {rotation-Deg} {large 0/1} {clockwise 0/1} {ptTarget}Ellipse-Abschnitt (Erläuterungen folgen)
      QQuadratic Bezier{ptCtrl} {ptTarget}der Control-Punkt erzeugt eine Ausbauchung
      CCubic Bezier{ptCtrl1} {ptCtrl2} {ptTarget}ptCtrl1 + ptCtrl2 erzeugen 2 Ausbauchungen
      SSmooth Cubic Bezier{ptCtrl2} {ptTarget}Cubic Bezier (s.o.) mit knickfreiem Anschluss,
      da ptCtrl1 autom. berechnet wird
      FFill-Rule{fillRule 0/1}
      füllt die durch die figur aufgespannte Fläche
      0: even-odd-rule, 1: non-zero-rule
      0: innere Figuren gelten als ausserhalb (unausgefüllt)
      1: innere Figuren sind immer ausgefüllt

      Die Segmente unter diesen Anweisungen, also L H V A Q C S kann man auch als PolySegmente notieren - dabei muss man den Key nicht wiederholen. ZB sowas wäre eine PathFigure mit zweigliedriger PolyLine: M10,10 L60,10 35,35.
      Das gilt für alle anderen Segmente entsprechend - allerdings werden PolySegmente schnell unübersichtlich - denn es sind reine Zahlen-Wüsten.

      Segmente verketten
      PolySegmente sind nur eine Sonderform der Verkettung (nämlich desselben Segment-Typs). Bei Verkettung unterschiedlicher Segmente gibt man natürlich für jedes Glied den Key mit an:
      M10,10 L60,10 35,35 Q65,95,100,35 - das wäre etwa obige PolyLine mit angehängtem quadratischen Bezier-Bogen:
      (Beachte, dass Bezier-Control-Punkte nicht auf der Linie liegen.)

      Relative Koordinaten
      Alle Anweisungen können auch mit relativen Koordinaten vereinbart werden - dazu den Key in Kleinschrift notieren. Insbesondere Segment-Verkettungen sind in relativer Notation leichter lesbar. Ich setze am liebsten den Figur-Startpunkt absolut, und hänge die Segmente dann relativ notiert dran, zB die PolyLine-PathFigure von vorhin: M10,10 l50,0 -25,25 Man sieht sofort: zunächst eine Horizontale der Länge 50 nach rechts, dann eine 45° Diagonale nach links unten, X-Komponente des Zielpunktes liegt genau auf der Hälfte der ersten Linie, also insgesamt ist ein gleichseitiges rechtwinkliges Dreieck aufgespannt - naja - sieht man ja auch im Bildle oben, wenn man sich den Bezier wegdenkt :D .

      Das ArcSegment
      ist wohl am schwierigsten zu verstehen. Seine Argumente sind: {radius X,Y} {rotation-Deg} {large 0/1} {clockwise 0/1} {ptTarget}
      Betrachten wir folgendes Ellipsen-Segment:
      M100,100 a80,40,45,1,1, 20,20
      Die Radien sind 80,40, um 45°rotiert, gezeichnet wird 1 der große Bogen, und zwar 1 im Uhrzeigersinn, und verbunden wird die relative Strecke 20,20. Das ist recht kurz, und ebenfalls 45° geneigt, denn die X/Y - Komponenten der verbundenen Strecke sind ja gleich.
      Den großen Bogen 1 zeichnen stellt die Ellipse fast komplett dar, mit einer Lücke:
      Der kleine Bogen 0 ist entsprechend winzig - M100,100 a80,40,45,0,1, 20,20:

      Und gegen den Uhrzeigersinn gedreht sieht der große Bogen so aus - M100,100 a80,40,45,1,0, 20,20:

      Ich kann auch beides verketten - M100,100 a80,40,45,1,1, 20,20 a80,40,45,1,0, 20,20 oder als PolyArc:M100,100 a80,40,45,1,1, 20,20 80,40,45,1,0, 20,20:

      genau hingucken: Die erste Ellipse dreht rechtsrum und verbindet nach 20,20. Die zweite Ellipse setzt dort fort, dreht aber linksrum. Insgesamt ist also die Strecke 40,40 verbunden durch eine komische Schleife.
      Also es ist eine extrem gewöhnungsbedürftige Art, Ellipsen-Segmente zu definieren, aber sehr kompakt, und Schräglagen sind unterstützt, wofür man zB beim Ellipse-Shape zusätzlich in aller Umständlichkeit eine Matrix-Transformation ansetzen müsste.

      Eine Unmöglichkeit/Besonderheit entsteht übrigens, wenn die Ellipsen-Radien zu klein definiert sind, als dass ein Bogen überhaupt zum Zielpunkt gespannt werden könnte: WPFs Lösung besteht darin, dass die Ellipse dann proportional vergrößert wird, bis passt:

      Quellcode

      1. M80,100 a80,40,45,1,1, 150,0
      2. M100,100 a80,40,45,1,1, 20,20
      Nicht wahr: Dieselbe Ellipse (80,40,45,1,1), jedoch die Strecke 150,0 kann nur verbunden werden, wenn Ellipse#1 sich vergrößert.
      Bei vergrößerten Ellipsen ist die Angabe von largeArc auch irrelevant, denn übergroße ArcSegmente sind immer genau Halb-Ellipsen und beide Bögen somit gleichlang.

      Die Sample-App
      Ist eine Art Multi-Spielwiese. Auf einem Tab kann man Figuren eintippen, und gucken, wie's rauskommt. Und man kann auch verschiedene Experimente abspeichern.
      Auf dem anderen Tab hab ich was aufwändiges programmiert, dass man die Punkte der Figuren mit der Maus herumdraggen kann:


      Der dritte Tab ist eine Art runde Progressbar, die zeigt ein KreisSegment zwischen 0° und 359.999° (denn ein Vollkreis ist mit ArcSegment nicht möglich)


      Skalierbaren Zeichenbereich abstecken
      Beim Round-Progress-Tab verwende ich einen Trick. Nämlich beim Path-Shape kann man Stretch="Fill" einstellen, dann stretcht er die Figur auf maximale Größe im verfügbaren Layout-Bereich. Nur ein sich verkürzendes/verlängerndes ArcSegment ändert ja seine Gesamt-Größe. Und würde daher immer verschieden skaliert, sodass die Figur nie ordentlich auf einer Kreisbahn bleibt.
      Daher stecke ich zuvor mit zwei M-Anweisungen den Gesamt-Rahmen des Pathes ab, den mein Arc-Segment nicht überschreitet. Sodass die Gesamtfigur immer eine konstante Größe hat (die des Vollkreises)- egal wie groß oder klein das ArcSegment darinnen ist.
      Also das Markup obigen Bildles lautet:
      M-50,-50 M50,50, M0,-50 A50,50,0,1,1 -48.9, -10.4
      Die ersten beiden M-Anweisungen sind leere PathFigures, die den Bereich abstecken, übrigens so, dass die Bild-Mitte genau auf 0,0 liegt. Startpunkt des Arcs ist logischerweise M0,-50, also Oben-Mitte, und Endpunkt ist etwas rechts oberhalb von -50,0 (Mitte-Links).

      Vollkreis bei ArcSegment
      Ist nicht möglich. Denn dabei wäre die Länge der verbundenen Strecke 0, und eine Ellipse durch nur einen Punkt zu legen ist nicht eindeutig definierbar. Stattdessen assoziiere ich beim RoundProgress 100% mit 359,999°, sodass immer eine mikroskopisch kleine Strecke übrig bleibt: -0.000087,0.0000000077. Diese Lücke im Vollkreis ist unsichtbar winzig, und daher ist das Ergebnis auffm Bildschirm perfekt :D

      Further Readings
      Bei meinen Recherchen stieß ich auf eine wahre Tutorial-Monster-Site, deren etwa 30 kurze und ausgezeichnet strukturierte Kapitel zum Thema "Shapes und Pathes" nur einen Winz-Bruchteil des Gesamt-Fundus' ausmachen:
      Black Wasp
      Schließlich fund ich auch bei Microsoft eine Dokumentation, eigenartigerweise eiglich zu Silverlight: Path Markup Syntax
      Dort wird die "Mini-Language" so erklärt: Move (M) setzt den Stift. Das Draw-Command kann aus mehreren Formen (Shapes) bestehen, verschiedener Art: Line, Q-Bezier, Bezier, Arc
      Dateien

      Dieser Beitrag wurde bereits 12 mal editiert, zuletzt von „ErfinderDesRades“ ()