www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home Tutorials Direct2D und DirectWrite Chapter 1 - Grundlagen




















































Chapter 1 - Grundlagen
Freitag, den 23. Juli 2010 um 17:26 Uhr

 

Chapter 1

 

Zeichnen mit System.Drawing (Gdi)

Grafiken zeichnen mithilfe von System.Drawing ist nicht schwer, einfach die entsprechenden Methoden innerhalb des Paint-Ereignisses eines Controls aufrufen, fertig. Folgender Sourcecode etwa zeichnet einen einfarbigen Hintergrund:

  1. protected override void OnPaint(PaintEventArgs e)
  2. {
  3. base.OnPaint(e);
  4.  
  5. e.Graphics.FillRectangle(Brushes.LightBlue, e.ClipRectangle);
  6. }

Nicht wirklich schwer, oder? Auch weitere Objekte können mithilfe der Methode der Klasse Graphics sehr einfach gezeichnet werden. Der Programmierer braucht sich nicht um die Initialisierung des Render-Bereichs zu kümmern, da das schon alles von Windows.Forms erledigt wird. Ein Nachteil der ganzen Sache ist aber zum Beispiel die eher schlechte Performance beim Zeichnen von Bitmaps.

 

Direct2D und DirectWrite

Seit Windows 7 gibt es zusammen mit DirectX 11 die Bibliotheken Direct2D und DirectWrite. Zweck davon ist es, das veraltete Gdi abzulösen und 2D-Grafiken vollständig von moderner Hardwarebeschleunigung profitieren zu lassen. Da es sich bei diesen APIs grundsätzlich nicht um .Net Bibliotheken handelt, benötigt man zunächst einen Wrapper, um die Funktionen von C# aus aufrufen zu können. Die OpenSource-Bibliothek SlimDX erledigt diese Aufgabe mehr als gut.

 

Windows.Forms Projekt vorbereiten

Grundlage dieser Tutorialserie ist ein Windows.Forms Projekt. Die Ausgabe der Grafiken kann wahlweise direkt im Hauptfenster oder in einem extra dafür angelegten UserControl geschehen. Unabhängig davon müssen im Konstruktor der Form / des Controls folgende Windows-Styles gesetzt werden:

  1. //Update control styles
  2. this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
  3. this.SetStyle(ControlStyles.Opaque, true);
  4. this.SetStyle(ControlStyles.ResizeRedraw, true);

Unterm Strich sagen wir dem Control / der Form damit, dass wir das Zeichnen komplett übernehmen.

 

Direct2D initialisieren

Sobald SlimDX auf dem eigenen PC installiert ist und die Assembly ins Projekt eingebunden ist, kann auch direkt mit dem ersten Beispiel begonnen werden. Für Direct2D werden folgende Namespaces benötigt:

  1. using SlimDX;
  2.  
  3. //Some namespace mappings
  4. using D2D = SlimDX.Direct2D;

Der Namespace SlimDX enthält einige allgemeine Klassen und Methoden, wie Matrizen und Vektoren. Interessant für dieses Tutorial ist der Namespace SlimDX.Direct2D, welchen wir für diese Datei unter dem Namen D2D verfügbar machen. D2D deswegen, weil viele Klassen innerhalb der SlimDX-Namespaces gleiche Namen haben. Um Verwechslungen auszuschließen, kann man deswegen Abkürzungen wie D2D, DWrite, D3D11, ... verwenden.

 

Speziell für Direct2D benötigen wir in der neuen Klasse folgende Membervariablen:

  1. //Resources for Direct2D rendering
  2. private D2D.Factory m_factory;
  3. private D2D.WindowRenderTarget m_renderTarget;
  4. private D2D.LinearGradientBrush m_backBrushEx;
  5. private D2D.GradientStopCollection m_backBrushGradient;

Die erste Variable, die Factory, ist für die Initialisierung von Direct2D zuständig. Die verwendete Factory bestimmt ganz nach dem Factory-Pattern die Version von Direct2D. Da es aktuell nur eine Version gibt, ist dieser Punkt nicht weiter wichtig. Das WindowRenderTarget Objekt ist quasi die Direct2D Version des Graphics Objekts von System.Drawing. Mit diesem Objekt können alle Grafiken auf den Bildschirm gezeichnet werden. Die anderen beiden Member speichern lediglich den Brush für den Hintergrund.

 

Wie also eben schon beschrieben, ist das wichtigste erst einmal das Factory-Objekt. Es wird auf folgende Weise erstellt:

  1. //Get requested debug level
  2. D2D.DebugLevel debugLevel = D2D.DebugLevel.None;
  3. if (m_debugMode) { debugLevel = D2D.DebugLevel.Error; }
  4.  
  5. //Create factory object
  6. m_factory = new D2D.Factory(D2D.FactoryType.SingleThreaded, debugLevel);

Bereits beim Anlegen der Factory müssen wir also schon angeben, ob Direct2D nur von einem Thread oder von mehreren Threads aus angesprochen wird. Zusätzlich dazu kann ein DebugLevel angegeben werden. Abhängig von diesem Wert zeigt sich die Bibliothek mehr oder weniger gesprächig, was Fehler angeht. Um übrigens die Debug-Ausgaben im Ausgabefenster zu sehen, muss im aktuellen Projekt die Eigenschaft "Nicht verwaltetes Codedebugging" aktiviert sein (Siehe Projekteigenschaften -> Debuggen).

 

Nachdem die Factory erzeugt ist, kann das RenderTarget erstellt werden:

  1. //Create the render target
  2. m_renderTarget = new D2D.WindowRenderTarget(m_factory, new D2D.WindowRenderTargetProperties()
  3. {
  4. Handle = this.Handle,
  5. PixelSize = this.Size,
  6. PresentOptions = D2D.PresentOptions.Immediately
  7. });

Als Handle wird dem neuen RenderTarget einfach das aktuelle Control-Handle übergeben. PixelSize enthält einfach die Auflösung, in der gezeichnet wird (die Größe des Ziel-Controls in Pixel). Im Anschluss kann auch direkt der Brush für den Hintergrund angelegt werden:

  1. //Create linear gradient brush
  2. D2D.GradientStop[] gradientStops = new D2D.GradientStop[]
  3. {
  4. new D2D.GradientStop(){ Position = 0f, Color = new Color4(Color.LightGray) },
  5. new D2D.GradientStop(){ Position = 1f, Color = new Color4(Color.LightSteelBlue) }
  6. };
  7. m_backBrushGradient = new D2D.GradientStopCollection(m_renderTarget, gradientStops);
  8. m_backBrushEx = new D2D.LinearGradientBrush(
  9. m_renderTarget,
  10. m_backBrushGradient,
  11. new D2D.LinearGradientBrushProperties()
  12. {
  13. StartPoint = new PointF(0, this.Height),
  14. EndPoint = new PointF(0, 0)
  15. });

Hierzu brauch ich eigentlich nicht viel schreiben, da es unter System.Drawing sehr ähnlich funktioniert. Es wird ein Farbverlauf angelegt, welcher um unteren Ende des Controls anfängt und oben aufhört. Wichtig zu wissen ist nur, dass es sich bei dem Brush um eine Ressource handelt, und Ressourcen werden abhängig vom RenderTarget erzeugt.

 

Somit sind alle Schritte zusammen, die für die Initialisierung in diesem Beispiel benötigt werden. Zusammengefasst ergibt das folgende Methode:

  1. ///
  2. /// Loads all graphics resources.
  3. ///
  4. private void InitializeGraphics()
  5. {
  6. //Get requested debug level
  7. D2D.DebugLevel debugLevel = D2D.DebugLevel.None;
  8. if (m_debugMode) { debugLevel = D2D.DebugLevel.Error; }
  9.  
  10. //Create factory object
  11. m_factory = new D2D.Factory(D2D.FactoryType.SingleThreaded, debugLevel);
  12.  
  13. //Create the render target
  14. m_renderTarget = new D2D.WindowRenderTarget(m_factory, new D2D.WindowRenderTargetProperties()
  15. {
  16. Handle = this.Handle,
  17. PixelSize = this.Size,
  18. PresentOptions = D2D.PresentOptions.Immediately
  19. });
  20.  
  21. //Create linear gradient brush
  22. D2D.GradientStop[] gradientStops = new D2D.GradientStop[]
  23. {
  24. new D2D.GradientStop(){ Position = 0f, Color = new Color4(Color.LightGray) },
  25. new D2D.GradientStop(){ Position = 1f, Color = new Color4(Color.LightSteelBlue) }
  26. };
  27. m_backBrushGradient = new D2D.GradientStopCollection(m_renderTarget, gradientStops);
  28. m_backBrushEx = new D2D.LinearGradientBrush(
  29. m_renderTarget,
  30. m_backBrushGradient,
  31. new D2D.LinearGradientBrushProperties()
  32. {
  33. StartPoint = new PointF(0, this.Height),
  34. EndPoint = new PointF(0, 0)
  35. });
  36.  
  37. //Update initialization flag
  38. m_initialized = true;
  39. }

 

Zeichnen in Direct2D

Zum Zeichnen von Grafiken kann mit Direct2D genauso wie bei System.Drawing das bekannte Paint-Event verwendet werden. Folgender Programmcode zeichnet einen Hintergrund mit dem vorher erstellten Brush:

  1. ///
  2. /// Called when control has to paint itself.
  3. ///
  4. protected override void OnPaint(PaintEventArgs e)
  5. {
  6. base.OnPaint(e);
  7.  
  8. if (m_initialized)
  9. {
  10. m_renderTarget.BeginDraw();
  11. try
  12. {
  13. m_renderTarget.Clear(new Color4(this.BackColor));
  14.  
  15. //Perform all Direct2D rendering here
  16. m_renderTarget.FillRectangle(
  17. m_backBrushEx,
  18. new Rectangle(new Point(), this.Size));
  19. }
  20. finally
  21. {
  22. m_renderTarget.EndDraw();
  23. }
  24. }
  25. else
  26. {
  27. e.Graphics.FillRectangle(m_backBrushGdi, e.ClipRectangle);
  28. }
  29. }

Das Zeichnen selbst findet in der FillRectangle Methode des RenderTargets statt - funktioniert also wie gewohnt. Ein Unterschied zum Bekannten sind hier allerdings die beiden Aufrufe BeginDraw und EndDraw. Mit diesen Methoden sagt man Direct2D, wann mit dem Rendern begonnen, und wann damit abgeschlossen wird.

 

Auf Größenänderung des Controls reagieren

Generell ist jetzt alles soweit implementiert, wie es für den Screenshot ganz am Anfang dieser Seite nötig ist. Es bleibt allerdings noch ein Problem: Beim erstellen des RenderTargets wurde die Größe des Controls mit übergeben, um die Größe des RenderTargets festzulegen. Was passiert jetzt eigentlich, wenn der Benutzer die Größe des Controls verändert, also zum Beispiel einfach das Fenster größer zieht? Ganz einfach, das Control wird zwar größer, das RenderTarget aber nicht. Der Inhalt des RenderTargets wird nach dem Aufruf von EndDraw einfach hochskaliert, damit es der Größe des Controls entspricht - also eher ungünstig für uns, weil dadurch ein Unschärfeeffekt entsteht. Um genau das zu verhindern, müssen wir die Größe des RenderTargets im Resize-Event ändern:

  1. ///
  2. /// Called when size of the control changed.
  3. ///
  4. protected override void OnResize(EventArgs e)
  5. {
  6. base.OnResize(e);
  7.  
  8. if ((this.Width > 0) && (this.Height > 0))
  9. {
  10. if (m_initialized)
  11. {
  12. //Resize render target
  13. m_renderTarget.Resize(this.Size);
  14. m_backBrushEx.StartPoint = new PointF(0, this.Height);
  15. }
  16. }
  17. }

 

Resourcen freigeben

Zum Schluss ist es noch wichtig zu wissen, wie man die erzeugten Direct2D Objekte wieder richtig freigibt. Generell implementiert jede Resource das IDisposable Interface, was für uns heißt, dass wir für jedes Objekt die Dispose Methode aufrufen sollten. Besonders im Bezug zu SlimDX sollte man die Objekte in einer bestimmten Reihenfolge freigeben, und zwar genau andersherum, wie sie erstellt wurden. Folgender Sourcecode zeigt das für dieses Beispielprogramm:

  1. ///
  2. /// Unloads all graphics resources.
  3. ///
  4. private void UnloadGraphics()
  5. {
  6. if (m_backBrushEx != null) { m_backBrushEx.Dispose(); }
  7. if (m_backBrushGradient != null) { m_backBrushGradient.Dispose(); }
  8. if (m_renderTarget != null) { m_renderTarget.Dispose(); }
  9. if (m_factory != null) { m_factory.Dispose(); }
  10.  
  11. m_backBrushEx = null;
  12. m_backBrushGradient = null;
  13. m_renderTarget = null;
  14. m_factory = null;
  15. }

 

Weiter geht's

In diesem ersten Kapitel wurde gezeigt, wie überhaupt mit Direct2D gezeichnet werden kann und wie man ein RenderTarget erzeugt und verwaltet. Somit haben wir jetzt die Grundlage, um in den nächsten Kapiteln etwas mehrere Objekte, Text und Bilder zu zeichnen.

 

Quellen

  • DirectX SDK
    Die Beispiele und die Dokumentation im DirectX SDK sind zwar auf C++ ausgelegt, da sich SlimDX stark an der originalen API orientiert, ist das SDK aber dennoch sehr hilfreich.
  • Msdn
    Auf Msdn findet man eine sehr umfangreiche Hilfe zu Direct2D und DirectWrite.
  • SlimDX
    Hauptseite des SlimDX-Projekts.

 

Siehe auch...

/pre

 

Kommentar hinzufügen

Ihr Name:
Kommentar:

Kommentare (5)

5. Gerhard K.
Mittwoch, den 17. April 2013 um 17:49 Uhr
Hi Roland.

Danke für den Hinweis. Werde mir die Beispiele mal anschauen. Was hat es denn mit dieser 'RenderWindow' und 'Message-Pump-Klasse' auf sich?

Könnte mir auch vorstellen eine WPF-Anwendung zu haben. Die ein extra losgelöstes Ausgabefenster hat und nicht ein Steuerelement das sich in die WPF-Anwendung einbettet.

Gruß Gerhard

4. RolandK
Mittwoch, den 17. April 2013 um 05:08 Uhr
Hallo Gerhard,

WPF verwendet zwar Direct2D für das Rendering, um aber selbst per Direct2D in einen WPF Host zu zeichnen, muss man den Umweg über das D3DImage gehen. Zumindest wäre jetzt mir spontan kein einfacherer Weg dazu bekannt..

Schau dir mal die Beispiele von SharpDX an (http://sharpdx.org/). Da findest du bei den DirectX 10 Beispielen einen WPFHost. Als nächstes müsste man per Direct2D in eine Textur von DirectX 10 zeichnen - dazu fällt mir aber direkt kein Link zu einem Beispiel ein, sorry.

Achja: Das ich hier von Direct3D 10 schreibe, ist absicht. In Direct3D 11 ist das Feature, per Direct2D auf eine Textur zu zeichnen, optional.

Viele Grüße
Roland

Viele Grüße
Roland

3. Gerhard K.
Dienstag, den 16. April 2013 um 19:25 Uhr
Hi. Super Beispiele. Da schon WPF angesprochen worden ist, wie sieht denn die Implementierung dieses Beispiels in WPF aus?
Hab schon ein wenig probiert, aber kriege es nicht hin.

Auch 'RenderWindow' wurde angesprochen. Wo bekommt man Info´s bzw. Beispiele dafür?

2. RK
Mittwoch, den 18. Juli 2012 um 04:58 Uhr
Je nach Anwendungsfall geb ich dir recht bzw. unrecht.

Für ein Windows-Spiel, welches komplett auf Gui wie Windows.Forms verzichten kann und das Fenster eigentlich nur als Fläche zum Rendern braucht, dann ja. Im Fall, in dem nur auf ein oder mehrere Controls gezeichnet werden soll und ansonsten viel mit Windows.Forms oder WPF gearbeitet wird, dann nein.

Aber Danke für den Hinweis. Zumindest einen Verweis darauf im Tutorial hätte ich schon geben können.

1. Matthias Lukaseder
Dienstag, den 17. Juli 2012 um 11:51 Uhr
Will man möglichst Performant, alles selbst und mit möglichst hoher Framerate in einem Fenster Zeichnen (z.B. ein Spiel...) empfehlen sich das RenderWindow und die MessagePump-Klasse von SlimDX. Die Verwendung dieser Klassen machen auch die SetStyle-Anweisungen unnötig.