OwnerDrawing

    • VB.NET

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      OwnerDrawing

      Im Anhang eine Sammlung von Samples, die OwnerDrawing verwenden.
      OwnerDrawing ist ein Prinzip, bei dem im PaintEvent mit dem Graphics des EventArgs die Zeichnung vollführt wird. Um OwnerDrawing zu verstehen muss man sich von der Vorstellung eines fertigzustellenden Bildes verabschieden - bei OD wird kein Bild fertiggestellt.
      Sondern die Anwendung muss jederzeit bereitstehen, und die Zeichnung neu vollführen können.
      Denn das Paint-Event kann sich jederzeit erneut ereignen, wann immer ein Bildschirm-Ausschnitt ungültig wird: Etwa wenn ein Fenster vergrößert wird, muss ja die hinzukommende Fläche gezeichnet werden. Aber ebenso auch, wenn Daten sich geändert haben.

      Das ist vor allem eine Umstellung der Denkweise, ein wirklicher Mehr-Aufwand ist es nicht unbedingt. Denn ob man ein Bild nur einmal zeichnet, und dann ist es fertig, oder ob man es immer zeichnet, wenn das Event feuert - der Zeichnungs-Code muss so ode so geschrieben werden.
      Dafür setzt uns das häufige Zeichnen in den Stand, die Darstellung zeitnah zu ändern, also in Interaktion mit dem User zu treten - Beispiel:

      VB.NET-Quellcode: Mouse-Line-Draw

      1. ''' <summary>
      2. ''' zeichnet eine Linie vom Mouse-Down-Punkt zur aktuellen MousePosition bei gehaltenem LinksButton
      3. ''' </summary>
      4. Public Class frmDrawSimple
      5. Private _Pt0 As Point
      6. Private _DrawObject As New DrawObject
      7. Private Sub frmDrawSimple_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) _
      8. Handles Me.MouseDown
      9. _Pt0 = e.Location
      10. End Sub
      11. Private Sub frmDrawSimple_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) _
      12. Handles Me.MouseMove
      13. If e.Button = Windows.Forms.MouseButtons.Left Then
      14. Me.Invalidate(_DrawObject.GetRange) 'Neuzeichnen des alten Ranges anstoßen
      15. _DrawObject.SetLine(_Pt0, e.Location) ' ZeichnungsDaten ändern
      16. Me.Invalidate(_DrawObject.GetRange) 'Neuzeichnen des neuen Ranges anstoßen
      17. End If
      18. End Sub
      19. Private Sub frmDrawSimple_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
      20. _DrawObject.Draw(e.Graphics)
      21. 'die Debug-Meldung meldet jeden Zeichnungsvorgang, ob nun innerhalb der Anwendung ausgelöst oder durch externe Vorgänge (Verdeckung durch andere Anwendungen)
      22. 'Später entfernen, denn Debug ist sehr langsam
      23. Static Counter As Integer = 0
      24. Debug.WriteLine(String.Concat("Paint", Counter))
      25. Counter += 1
      26. End Sub
      27. End Class

      Gezieltes OwnerDrawing
      WinForms' Grafik ist vergleichsweise langsam, gleicht aber viel aus durch die Möglichkeit, den Bildschirm nur ausschnittsweise zu aktualisieren.
      Dazu ein paar Grundsätze:
      • Nur im Paint-Event zeichnen, und nur mit dem dort übergebenen Graphics.
        Bei Multi-Items: Immer alle ZeichenObjekte zeichnen - auch die, die garnet sichtbar sind, oder unverändert geblieben.
        Die Auswahl, was tatsächlich gezeichnet wird, erfolgt über das Invalidieren. Und Code, der überflüssigerweise an Positionen zeichnet, die nicht zuvor invalidiert wurden, wird intern abgebrochen und kostet (fast) keine Performance.
      • Zeichnungs-Informationen merken.
      • Veränderungen im Dreischritt umsetzen:
      1. alten Zeichnungsbereich invalidieren
      2. Zeichnungs-Daten verändern
      3. neuen Zeichnungsbereich invalidieren
      Vergleiche obiges _MouseMove(), Zeilen #17 - 19: da wird nichts gezeichnet, sondern es werden nur Zeichnungs-Daten geändert und Zeichnungsvorgänge angestoßen, für genau die Bereiche, die von der Änderung betroffen sind - nicht mehr. Betroffen sind genau zwei Bereiche, mämlich die Fläche, wo die Zeichnung vorher war, und die Fläche, wo sie nach der Änderung ist.
      Auf diese Weise löscht der darauf folgende Zeichnungsvorgang (im Paint-Event) die vorherige Darstellung, und malt dann die Neue hin.
      Das erfolgt übrigens im selben Paint-Event, also mehrfaches Invalidieren führt nicht zu vielfach-redundanten ZeichenVorgängen.

      Hier ein fast noch simpleres Sample - noch simpler, weil nichtmal ein DrawObjekt existiert, sondern alles findet im Form selbst statt:
      labeled Triangle-Draw

      VB.NET-Quellcode

      1. ' zeichnet ein mit Ecken- und Kanten-Beschriftungen versehenes Dreieck
      2. Public Class Form1
      3. Private ptA, ptB, ptC As PointF
      4. Private fFont As New Font("Arial", 10.0)
      5. Private bBrush As New SolidBrush(Color.Green)
      6. Public Sub New()
      7. InitializeComponent()
      8. DoubleBuffered = True
      9. Form1_MouseMove(Me, New MouseEventArgs(Windows.Forms.MouseButtons.Left, 0, 20, 20, 0))
      10. End Sub
      11. Private Sub Form1_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove
      12. If e.Button <> Windows.Forms.MouseButtons.Left Then Return
      13. With ClientSize
      14. ptA = New PointF(.Width / 2.0F, .Height / 2.0F)
      15. End With
      16. ptB = New PointF(e.X, e.Y)
      17. ptC = New PointF(ptB.X, ptA.Y)
      18. Invalidate()
      19. End Sub
      20. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
      21. Dim points() As PointF = {ptA, ptB, ptC}
      22. e.Graphics.DrawPolygon(Pens.Red, points)
      23. e.Graphics.DrawString("A", fFont, bBrush, ptA)
      24. e.Graphics.DrawString("B", fFont, bBrush, ptB)
      25. e.Graphics.DrawString("C", fFont, bBrush, ptC)
      26. e.Graphics.DrawString("a", fFont, bBrush, middleSite(ptB, ptC))
      27. e.Graphics.DrawString("b", fFont, bBrush, middleSite(ptA, ptC))
      28. e.Graphics.DrawString("c", fFont, bBrush, middleSite(ptA, ptB))
      29. End Sub
      30. Private Shared Function middleSite(ByVal a As PointF, ByVal b As PointF) As PointF
      31. Return New PointF(0.5F * (a.X + b.X), 0.5F * (a.Y + b.Y))
      32. End Function
      33. End Class
      (Ups! Dieses Beispiel ist auch deswegen so einfach, weil kein gezieltes OwnerDrawing stattfindet, sondern es wird das ganze Form invalidiert (zeile#21).)
      Bildle von Listing#1 und Listing#2:
      ...

      Canvas-Control
      Wie man sieht, kann Zeichnungs-Code komplex werden, und noch viel komplexer kann die Interaktions-Logik werden, die auf Maus und evtl. auch Tasten reagiert.
      Dann ist geraten, ein eigenes Control zu entwickeln - ich nenne die Dinger immer "Canvas" - Zeichenfläche.
      Dieses Control kann man nach Kompilieren aus der Toolbox aufs Form ziehen, und hat so Zuständigkeiten leidlich gut eingeteilt (ein Form hat ja meist noch wesentlich mehr zu tun, als sich um Zeichnungs-Angelegenheiten zu kümmern).
      Hier ein Cuboid-(Canvas-)Control, was einen Quader mit einstellbaren Kantenlängen zeichnet - ist schon recht umfangreich, deswegen im Spoiler
      Cuboid-Canvas-Class

      VB.NET-Quellcode

      1. Imports System.Drawing.Drawing2D
      2. Imports System.ComponentModel
      3. ''' <summary> Displays the Cuboid of Dimensions: W, H, D as 3d-Drawing </summary>
      4. <DesignerCategory("code"), DefaultEvent("Paint")> _
      5. Public Class CuboidControl : Inherits Control
      6. Private _Gp As New GraphicsPath
      7. Private _Pen As New Pen(Brushes.AliceBlue, 1)
      8. Protected Overrides Sub OnHandleCreated(e As EventArgs)
      9. MyBase.OnHandleCreated(e)
      10. _Pen.Color = ForeColor
      11. MyBase.SetStyle(ControlStyles.ResizeRedraw, True)
      12. UpdateCuboid()
      13. End Sub
      14. Protected Overrides Sub OnForeColorChanged(e As EventArgs)
      15. _Pen.Color = ForeColor
      16. MyBase.OnForeColorChanged(e)
      17. End Sub
      18. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      19. MyBase.OnPaint(e)
      20. e.Graphics.DrawPath(_Pen, _Gp)
      21. End Sub
      22. Protected Overrides Sub OnSizeChanged(e As EventArgs)
      23. MyBase.OnSizeChanged(e)
      24. UpdateCuboid()
      25. End Sub
      26. Private Sub UpdateCuboid()
      27. #If False Then
      28. __
      29. /__/|
      30. |__|/
      31. die Zeichnung startet mitte links, läuft die 4 Front-Punkte ab, nochmal durch den Startpunkt und dann die 3 Back-Punkte, und Anschluss an pts(2) (front unten-rechts). Dann noch die fehlende Kante oben rechts.
      32. Da 2 Punkte doppelt durchlaufen werden pts(0), pts(2), sind dem GP insgesamt also 9 pts anzugeben.
      33. Die 3 BackPoints sind Kopien der ersten 3 frontPoints, aber für Fluchtpunkt-Perspektive nach Strahlensatz etwas verkleinert und um D/2 45° verschoben.
      34. Die Definition der frontPoints ist relativ zum Front-Mittelpunkt
      35. #End If
      36. _Gp.Reset()
      37. Dim ptsFront = {New PointF(-W, -H), New PointF(W, -H), New PointF(W, H), New PointF(-W, H), New PointF(-W, -H)}
      38. Dim ptsBack = ptsFront.Take(3).ToArray
      39. 'Diese Berechnungen stellen v.a. immer eine Perspektive her, bei der 3 Seiten des Quaders sichtbar sind. Die Tiefen-Darstellung ist geometrisch inkorrekt.
      40. Dim d = _D * 0.5F 'perspektivisch verkürzen
      41. Dim lFluchtpunktDistanz = Math.Max(W, H) * 2
      42. lFluchtpunktDistanz = {W, H, d}.Max * 2
      43. Dim scale = Math.Max(0.0F, 1.0F - d / lFluchtpunktDistanz) ' Strahlensatz-Skalierung, darf aber nicht negativ werden
      44. Using mtrBackPoints = New Matrix
      45. mtrBackPoints.Scale(scale, scale, MatrixOrder.Append) 'verkleinern
      46. mtrBackPoints.Translate(d, -d, MatrixOrder.Append) 'verschieben in Fluchtpunkt-Richtung: -45°
      47. mtrBackPoints.TransformPoints(ptsBack)
      48. End Using
      49. Dim pts(8) As PointF
      50. Array.Copy(ptsFront, pts, 5)
      51. Array.Copy(ptsBack, 0, pts, 5, 3)
      52. pts(8) = pts(2)
      53. _Gp.AddLines(pts)
      54. _Gp.AddLine(pts(1), pts(6)) 'die fehlende Kante, die nicht in durchgezogener Linie gezeichnet werden konnte.
      55. ScaleFigure()
      56. End Sub
      57. Private Sub ScaleFigure()
      58. #If False Then
      59. Verwendet wird der Matrix-Konstruktor, der ein source-Rectangle auf einen durch 3 Punkte aufgespannten Trapezoid projeziert. Die TrapezPunkte sind so gewählt, dasses genau die Control-Fläche ist.
      60. Herausforderung ist nun, aus den Bounds des Graphicspaths ein Drumrum-Rechteck zu abzuleiten, dessen Projektion dann die ImageLayout-Bedeutungen (s. Objectbrowser) richtig umsetzt.
      61. zb .None und .Center erzeugen ein rctFigureSurround, genauso groß wie das Control - sodass keinerlei Vergrößerung/Verkleinerung eintritt.
      62. Bei .Center ist rctFigureSurround aber eine Vergrößerung in alle Richtungen (Inflate), sodass die Figur mittenzentriert bleibt.
      63. Auch .Stretch ist ein Inflate, aber auf ein umschließendes Quadrat. Das ist fragwürdig, denn man hätte auch die Original-Bounds belassen können. Nur bei sonem "Real-Stretch" wären überhaupt keine Höhe/Breite-Proportion der Zeichnung mehr erkennbar - daher wird hier der Quadrat-Rahmen als Bezugssystem eingeführt.
      64. .Zoom inflated die Bounds genau so, dass das Seitenverhältnis (w/h) identisch ist mit dem des Controls. Das ergibt dann eine verzerrungsfreie Vergrößerung.
      65. Zu beachten auch das 10% Padding, damit die Zeichnung nicht an die Wände stößt.
      66. #End If
      67. Dim rctFigureSurround = _Gp.GetBounds
      68. With rctFigureSurround
      69. Select Case _ScaleMode
      70. Case ImageLayout.None 'bnds vergrößern auf Control-Masse
      71. .Size = New SizeF(Width, Height)
      72. Case ImageLayout.Center 'bnds inflaten auf Control-Masse
      73. .Inflate((Width - .Width) * 0.5F, (Height - .Height) * 0.5F)
      74. Case ImageLayout.Zoom 'bnds inflaten auf ein Rechteck mit gleichem Seitenverhältnis wie das Control
      75. Dim edgeRatio = Width / CSng(Height)
      76. If Width / .Width < Height / .Height Then
      77. .Inflate(0, (.Width / edgeRatio - .Height) * 0.5F)
      78. Else
      79. .Inflate((.Height * edgeRatio - .Width) * 0.5F, 0)
      80. End If
      81. Case ImageLayout.Tile
      82. _Gp.Reset()
      83. _Gp.AddString("ImageLayout.Tile wird nicht unterstützt", Font.FontFamily, Font.Style, Font.Size, rctFigureSurround, StringFormat.GenericTypographic)
      84. Case ImageLayout.Stretch
      85. 'bnds inflaten aufs Quadrat-Form. Dieser Stretch nimmt an, dass die Figur als in ein Quadratisches Bezugssystem eingezeichnet gemeint ist
      86. If .Width > .Height Then
      87. .Inflate(0, .Width - .Height)
      88. Else
      89. .Inflate(.Height - .Width, 0)
      90. End If
      91. End Select
      92. Dim Padding = Math.Max(.Width, .Height) * 0.1F
      93. .Inflate(Padding, Padding)
      94. End With
      95. Using mtr = New Matrix(rctFigureSurround, {Point.Empty, New PointF(Width, 0), New PointF(0, Height)})
      96. 'die Matrix transformiert rctMatrixSource dann auf Control-Masse
      97. _Gp.Transform(mtr)
      98. End Using
      99. End Sub
      100. #Region "Public Props W, H, D, Scalemode"
      101. Private _D As Single = 1
      102. <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
      103. Public Property D() As Single
      104. Get
      105. Return _D
      106. End Get
      107. Set(ByVal value As Single)
      108. If _D = value Then Return
      109. _D = value
      110. UpdateCuboid()
      111. Invalidate()
      112. End Set
      113. End Property
      114. Private _H As Single = 1
      115. <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
      116. Public Property H() As Single
      117. Get
      118. Return _H
      119. End Get
      120. Set(ByVal value As Single)
      121. If _H = value Then Return
      122. _H = value
      123. UpdateCuboid()
      124. Invalidate()
      125. End Set
      126. End Property
      127. Private _W As Single = 1
      128. <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
      129. Public Property W() As Single
      130. Get
      131. Return _W
      132. End Get
      133. Set(ByVal value As Single)
      134. If _W = value Then Return
      135. _W = value
      136. UpdateCuboid()
      137. Invalidate()
      138. End Set
      139. End Property
      140. Private _ScaleMode As ImageLayout = ImageLayout.Stretch
      141. <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
      142. Public Property ScaleMode() As ImageLayout
      143. Get
      144. Return _ScaleMode
      145. End Get
      146. Set(ByVal value As ImageLayout)
      147. If _ScaleMode = value Then Return
      148. _ScaleMode = value
      149. UpdateCuboid()
      150. Invalidate()
      151. End Set
      152. End Property
      153. #End Region 'Public Props W, H, D, Scalemode
      154. End Class
      Hinweisen wollte ich damit auf die Klassen Matrix und GraphicsPath.
      Eine Matrix kann geometrische Transformationen speichern, also Drehen, zoomen, verschieben, scheren.
      Ein GraphicsPath kann eine Zeichnung speichern, sodass sie entweder als Outlined Figur wiedergegeben werden kann, oder ausgefüllt.
      Das Cuboid-Control konstruiert bei Änderungen also einen GraphicsPath, in den die komplette Zeichnung hineingeht.
      Die Konstruktion gliedert sich in 2 Teile: Zum einen die richtige Anordnung der 7 Punkte, und die .DrawLine - Befehle, denen die Punkte in richtiger Reihenfolge zu übergeben sind, dass ein Quader draus wird. Zum anderen die Skalierung der Zeichnung, entsprechend dem eingestellten SizeMode.
      Sind diese Vorbereitungen getan, so ist das Zeichnen selbst denkbar lapidar:

      VB.NET-Quellcode

      1. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      2. MyBase.OnPaint(e)
      3. e.Graphics.DrawPath(_Pen, _Gp)
      4. End Sub
      Zu beachten, dass es sich hier nicht um das Paint-Event handelt, sondern um die OnPaint-Überschreibung. Ist im Endeffekt dasselbe, insbesondere das Eventargs ist dasselbe.


      MultiItem-Controls
      Oft will man eine Liste von Daten grafisch darstellen. In derlei Fällen empfiehlt es sich, zusätzlich zum Canvas auch noch eine Figur-Klasse anzulegen. Von diesen Figuren kann das Canvas dann eine Liste beinhalten und darstellen.
      Dabei ist meist günstig, wenn die Figur-Klasse sich einfach selbst zeichnet. Das Canvas verarbeitet das Paint-Event/Override, und ruft darin alle Figuren auf, sich zu zeichnen.
      Card-Figure-Class

      VB.NET-Quellcode

      1. Public Class Card
      2. Private Shared HalfSize As New Size(45, 30)
      3. Private Shared HoverSize As Integer = 15
      4. Public Shared ReadOnly CardSize As New Size(HalfSize.Width * 2, HalfSize.Height * 2)
      5. Public Shared ReadOnly CardRect As New Rectangle(New Point(-HalfSize.Width, -HalfSize.Height), CardSize)
      6. Public Shared HalfHoverSize As New Size(HalfSize.Width + HoverSize, HalfSize.Height + HoverSize)
      7. Public Shared ReadOnly CardHoverSize As New Size(HalfHoverSize.Width * 2, HalfHoverSize.Height * 2)
      8. Public Shared ReadOnly CardHoverRect As New Rectangle(New Point(-HalfHoverSize.Width, -HalfHoverSize.Height), CardHoverSize)
      9. Private Shared _SelectionPen As New Pen(Color.FromArgb(&HFFFF9999), 4)
      10. Public ReadOnly Image As Image
      11. Public ReadOnly Name As String
      12. Public InvalidateCallback As Action(Of Rectangle)
      13. Private _isselected As Boolean
      14. Private _ishovered As Boolean
      15. Private _Location As Point
      16. Public Property Location As Point
      17. Get
      18. Return _Location
      19. End Get
      20. Set(value As Point)
      21. If _Location = value Then Return
      22. _Location = value
      23. ApplyChanges()
      24. End Set
      25. End Property
      26. Private _Bounds As New Rectangle(-2, -2, 2, 2)
      27. Public ReadOnly Property Bounds() As Rectangle
      28. Get
      29. Return _Bounds
      30. End Get
      31. End Property
      32. Private Sub ApplyChanges()
      33. Dim newBounds = If(_ishovered, CardHoverRect, CardRect)
      34. newBounds.Offset(Location)
      35. If newBounds <> _Bounds Then InvalidateCallback(_Bounds)
      36. _Bounds = newBounds
      37. InvalidateCallback(_Bounds)
      38. End Sub
      39. Public Sub Draw(g As Graphics)
      40. g.DrawImage(Image, _Bounds)
      41. If _isselected Then
      42. Dim rct = _Bounds
      43. rct.Inflate(-2, -2)
      44. g.DrawRectangle(_SelectionPen, rct)
      45. End If
      46. End Sub
      47. Public Property Ishovered() As Boolean
      48. Get
      49. Return _ishovered
      50. End Get
      51. Set(ByVal value As Boolean)
      52. If _ishovered = value Then Return
      53. _ishovered = value
      54. ApplyChanges()
      55. End Set
      56. End Property
      57. Public Property IsSelected() As Boolean
      58. Get
      59. Return _isselected
      60. End Get
      61. Set(ByVal value As Boolean)
      62. If _isselected = value Then Return
      63. _isselected = value
      64. ApplyChanges()
      65. End Set
      66. End Property
      67. Public Sub New(name As String, image As Image)
      68. Me.Name = name : Me.Image = image
      69. End Sub
      70. End Class
      Wir haben wieder den Drei-schritt in ApplyChanges, wo der vorherige und der neue Bereich invalidiert werden:

      VB.NET-Quellcode

      1. Private Sub ApplyChanges()
      2. Dim newBounds = If(_ishovered, CardHoverRect, CardRect)
      3. newBounds.Offset(Location)
      4. If newBounds <> _Bounds Then InvalidateCallback(_Bounds)
      5. _Bounds = newBounds
      6. InvalidateCallback(_Bounds)
      7. End Sub
      Und wieder ist das Zeichnen recht einfach - diesmal wird hauptsächlich eine Bitmap gedrawt:

      VB.NET-Quellcode

      1. Public Sub Draw(g As Graphics)
      2. g.DrawImage(Image, _Bounds)
      3. If _isselected Then
      4. Dim rct = _Bounds
      5. rct.Inflate(-2, -2)
      6. g.DrawRectangle(_SelectionPen, rct)
      7. End If
      8. End Sub
      Allerdings wird hier ausserdem noch ein Rechteck gezeichnet, um zu markieren, wenn diese Karte selektiert ist.
      Ausserausserdem hat ApplyChanges ja evtl. die Bounds vergrößert, sodass die Zeichnung im Draw insgesamt vergrößert ausfällt - als Hover-Indikator.

      Das mit den Karten hab ich sogar zweimal programmiert - obiges zeigt die Karten einfach der Reihen nach, und wenn eine Zeile voll ist, die nächste Reihe. Ausserdem kann man auch Karten rauslöschen, dann rücken die anderen Karten entsprechend zusammen.
      Sowas ist aber Angelegenheit des Canvas-Controls, und ist nicht ganz trivial, die Verwaltung aller Karten, der selektierten und der gehoverten funktional korrekt zu managen:
      Card-Canvas

      VB.NET-Quellcode

      1. Imports System.Collections.ObjectModel
      2. Imports System.Collections.Specialized
      3. Imports System.ComponentModel
      4. <DesignerCategory("code"), DefaultEvent("Paint")> _
      5. Public Class Canvas : Inherits Control
      6. Private WithEvents _Cards As New ObservableCollection(Of Card)
      7. <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
      8. Public ReadOnly Property Cards() As ObservableCollection(Of Card)
      9. Get
      10. Return _Cards
      11. End Get
      12. End Property
      13. <Category("Appearance")> _
      14. Public Shadows Property DoubleBuffered As Boolean
      15. Get
      16. Return MyBase.DoubleBuffered
      17. End Get
      18. Set(value As Boolean)
      19. MyBase.DoubleBuffered = value
      20. End Set
      21. End Property
      22. Private Sub Cards_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs) Handles _Cards.CollectionChanged
      23. Select Case e.Action
      24. Case NotifyCollectionChangedAction.Reset
      25. Case NotifyCollectionChangedAction.Add
      26. Dim crd = Cards(e.NewStartingIndex)
      27. crd.InvalidateCallback = AddressOf Invalidate
      28. Case NotifyCollectionChangedAction.Remove
      29. Dim crd = DirectCast(e.OldItems(0), Card)
      30. If crd Is _SelectedCard Then SelectedCard = Nothing
      31. If crd Is _HoveredCard Then HoveredCard = Nothing
      32. NewLayout()
      33. Case NotifyCollectionChangedAction.Move
      34. Case NotifyCollectionChangedAction.Replace
      35. End Select
      36. End Sub
      37. Public Sub NewLayout()
      38. Dim pt0 = New Point(Card.HalfHoverSize)
      39. Dim maxWidth = ClientSize.Width - Card.CardSize.Width * 2
      40. For i = 0 To _Cards.Count - 1
      41. _Cards(i).Location = pt0
      42. If pt0.X < maxWidth Then
      43. pt0.X += Card.CardSize.Width + 2
      44. Else
      45. pt0.X = Card.HalfHoverSize.Width
      46. pt0.Y += Card.CardSize.Height + 2
      47. End If
      48. Next
      49. End Sub
      50. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      51. MyBase.OnPaint(e)
      52. For Each crd In _Cards
      53. If Not crd.Ishovered Then crd.Draw(e.Graphics)
      54. Next
      55. If _HoveredCard IsNot Nothing Then _HoveredCard.Draw(e.Graphics)
      56. End Sub
      57. Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
      58. MyBase.OnMouseMove(e)
      59. HoveredCard = _Cards.FirstOrDefault(Function(crd) crd.Bounds.Contains(e.Location))
      60. End Sub
      61. Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
      62. MyBase.OnMouseDown(e)
      63. If _HoveredCard IsNot Nothing Then SelectedCard = _HoveredCard
      64. End Sub
      65. Protected Overrides Sub OnMouseLeave(e As EventArgs)
      66. MyBase.OnMouseLeave(e)
      67. HoveredCard = Nothing
      68. End Sub
      69. Private _SelectedCard As Card
      70. Public Property SelectedCard() As Card
      71. Get
      72. Return _SelectedCard
      73. End Get
      74. Set(ByVal value As Card)
      75. If _SelectedCard Is value Then Return
      76. If _SelectedCard IsNot Nothing Then _SelectedCard.IsSelected = False
      77. _SelectedCard = value
      78. If _SelectedCard IsNot Nothing Then _SelectedCard.IsSelected = True
      79. End Set
      80. End Property
      81. Private _HoveredCard As Card
      82. Public Property HoveredCard() As Card
      83. Get
      84. Return _HoveredCard
      85. End Get
      86. Set(ByVal value As Card)
      87. If _HoveredCard Is value Then Return
      88. If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = False
      89. _HoveredCard = value
      90. If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = True
      91. End Set
      92. End Property
      93. End Class
      Wie gut, dass wir ein Canvas haben, was diese Funktionalität zusammenhält - sowas möchte man ja nicht unbedingt in seinem Form rumfahren haben.
      Noch komplizierter wird, wenn die Karten nicht (nur) automatisch angeordnet werden, sondern auch mit der Maus zu draggen sind:
      Card-Canvas mit Drag und AutoScroll

      VB.NET-Quellcode

      1. Imports System.Collections.ObjectModel
      2. Imports System.Collections.Specialized
      3. Imports System.ComponentModel
      4. Imports System.Diagnostics
      5. <DesignerCategory("code"), DefaultEvent("Paint")> _
      6. Public Class Canvas : Inherits ScrollableControl
      7. Private _MouseOffset As Size
      8. Private WithEvents _Cards As New ObservableCollection(Of Card)
      9. <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
      10. Public ReadOnly Property Cards() As ObservableCollection(Of Card)
      11. Get
      12. Return _Cards
      13. End Get
      14. End Property
      15. Protected Overrides Sub OnHandleCreated(e As EventArgs)
      16. MyBase.OnHandleCreated(e)
      17. Me.AutoScroll = True
      18. MyBase.DoubleBuffered = True
      19. End Sub
      20. Private Sub Cards_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs) Handles _Cards.CollectionChanged
      21. Select Case e.Action
      22. Case NotifyCollectionChangedAction.Reset
      23. Case NotifyCollectionChangedAction.Add
      24. Dim crd = Cards(e.NewStartingIndex)
      25. crd.Canvas = Me
      26. UpdateAutoScrollMinSize()
      27. Case NotifyCollectionChangedAction.Remove
      28. Dim crd = DirectCast(e.OldItems(0), Card)
      29. If crd Is _SelectedCard Then SelectedCard = Nothing
      30. If crd Is _HoveredCard Then HoveredCard = Nothing
      31. crd.Image.Dispose()
      32. UpdateAutoScrollMinSize()
      33. Case NotifyCollectionChangedAction.Move
      34. Case NotifyCollectionChangedAction.Replace
      35. End Select
      36. End Sub
      37. Public Sub NewLayout()
      38. Dim pt0 = New Point(Card.CardHoverRect.Right, Card.CardHoverRect.Bottom)
      39. For i = 0 To _Cards.Count - 1
      40. _Cards(i).Location = pt0
      41. pt0.X += Card.CardSize.Width + 2
      42. Next
      43. UpdateAutoScrollMinSize()
      44. End Sub
      45. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      46. MyBase.OnPaint(e)
      47. For Each crd In _Cards
      48. If Not crd.Ishovered Then crd.Draw(e.Graphics)
      49. Next
      50. If _HoveredCard IsNot Nothing Then _HoveredCard.Draw(e.Graphics) ' _HoveredCard als alleroberste zeichnen
      51. End Sub
      52. Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
      53. MyBase.OnMouseMove(e)
      54. Dim pt = e.Location - New Size(AutoScrollPosition)
      55. Dim newHovered = _Cards.LastOrDefault(Function(crd) crd.Bounds.Contains(pt))
      56. If _HoveredCard Is Nothing OrElse Not _HoveredCard.Bounds.Contains(pt) Then HoveredCard = newHovered
      57. If e.Button <> MouseButtons.Left OrElse _SelectedCard Is Nothing Then Return
      58. _SelectedCard.Location = e.Location - _MouseOffset
      59. End Sub
      60. Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
      61. MyBase.OnMouseDown(e)
      62. If e.Button <> MouseButtons.Left OrElse _HoveredCard Is Nothing Then Return
      63. SelectedCard = _HoveredCard
      64. With _SelectedCard.Location
      65. _MouseOffset = New Size(e.X - .X, e.Y - .Y)
      66. End With
      67. End Sub
      68. Protected Overrides Sub OnMouseLeave(e As EventArgs)
      69. MyBase.OnMouseLeave(e)
      70. HoveredCard = Nothing
      71. End Sub
      72. Protected Overrides Sub OnMouseUp(e As MouseEventArgs)
      73. MyBase.OnMouseUp(e)
      74. UpdateAutoScrollMinSize()
      75. End Sub
      76. Private _SelectedCard As Card
      77. Public Property SelectedCard() As Card
      78. Get
      79. Return _SelectedCard
      80. End Get
      81. Set(ByVal value As Card)
      82. If _SelectedCard Is value Then Return
      83. If _SelectedCard IsNot Nothing Then _SelectedCard.IsSelected = False
      84. _SelectedCard = value
      85. If _SelectedCard IsNot Nothing Then ' _SelectedCard ans Ende von _Cards zu räumen bewirkt, dass sie als oberste gezeichnet wird
      86. Dim i = _Cards.IndexOf(_SelectedCard)
      87. _Cards.Move(i, _Cards.Count - 1)
      88. _SelectedCard.IsSelected = True
      89. End If
      90. End Set
      91. End Property
      92. Private _HoveredCard As Card
      93. Public Property HoveredCard() As Card
      94. Get
      95. Return _HoveredCard
      96. End Get
      97. Set(ByVal value As Card)
      98. If _HoveredCard Is value Then Return
      99. If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = False
      100. _HoveredCard = value
      101. If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = True
      102. End Set
      103. End Property
      104. Private Sub UpdateAutoScrollMinSize()
      105. Dim w = _Cards.Max(Function(c) c.Bounds.Right)
      106. Dim h = _Cards.Max(Function(c) c.Bounds.Bottom)
      107. Me.AutoScrollMinSize = New Size(w, h)
      108. End Sub
      109. Private Sub CardInvalidate(rct As Rectangle)
      110. rct.Offset(AutoScrollPosition)
      111. Invalidate(rct)
      112. End Sub
      113. End Class
      In diesen Canvas sind auch Scrollbars vorgesehen, daher wird im Code mit AutoScrollMinSize und AutoScrollPosition umgegangen, und die Zeichnungen müssen auch einen Offset berücksichtigen:

      VB.NET-Quellcode

      1. Public Sub Draw(g As Graphics)
      2. Dim offset = Canvas.AutoScrollPosition
      3. Dim rct = _Bounds
      4. rct.Offset(offset)
      5. If _ishovered Then
      6. rct.Inflate(-2, -2)
      7. g.DrawRectangle(_HoverPen, rct)
      8. rct.Inflate(-2, -2)
      9. End If
      10. Dim rct2 As RectangleF = rct
      11. g.DrawImage(Image, rct)
      12. If _isselected Then
      13. rct.Inflate(-2, -2)
      14. g.DrawRectangle(_SelectionPen, rct)
      15. End If
      16. Dim sz = g.MeasureString(Name, Canvas.Font, rct.Size, _SF) + New SizeF(2, 2)
      17. With rct2
      18. .Offset((.Width - sz.Width) / 2.0F, .Height - sz.Height)
      19. .Size = sz
      20. End With
      21. g.FillRectangle(Brushes.White, rct2)
      22. g.DrawString(Name, Canvas.Font, Brushes.Black, rct2, _SF)
      23. End Sub
      Ausserdem wird auch ein Namenszug gezeichnet, auf weissem Untergrund (zeile#21).
      Diese Card muss jetzt recht viel vom Canvas wissen, auf dem sie sich zeichnet: Den Font, die AutoScrollPosition, die Invalidate-Methode sowieso. Daher übergebe ich einfach der Figur einen Verweis auf "ihr" Canvas (findet im Canvas statt, im Cards_CollectionChanged-Event).

      Die Bildle:
      ...
      Im linken layoutet das Canvas die Cards zeilenweise, wie Platz ist. Die erste Card ist als selekted berahmt (naja, rosa), und die letzte Card ist hovered/vergrößert.
      Im rechten Canvas hat der User die Cards durch Dragging angeordnet. "deutschland" ist selected (rosa Rahmen) und "bulgarien" gehovert (blauer Rahmen - nicht vergrößert).

      OwnerDrawnGrid
      Ist wieder eine andere Anforderung, und wieder muss man OwnerDrawing etwas umstrukturieren.
      Bei Grid zeichnet sich die Figur nicht selbst, sondern wird gezeichnet, als Zell-Inhalt, dahin und in der Größe, wie es die Gegebenheiten (Abmasse des Canvas, Anzahl Reihen und Spalten) erfordern.
      Ansonsten gibts auch hier wieder die Anforderungen von Selected und Hovered, nur hab ich diesmal MultiSelection (gelber Zell-Background) reingebastelt:

      Die Anlage des OwnerDrawnGrid ist recht flexibel, was es angeht, was in die Zellen reinkommt.
      OwnerDrawnGrid

      VB.NET-Quellcode

      1. #Region "FileHeader"
      2. #If False Then
      3. Zur Addressierung der Zellen wird hier nicht X, Y genutzt, sondern Point, was ja X und Y enthält. Darf man halt nicht verwechseln mit tatsächlichen BildschirmPunkten. Also ein pos(0, 3) addressiert die Zelle inne 1.Reihe, 4.Zeile.
      4. Das GridCanvas hat einstellbare Row-/Column-Count, skaliert die Zeichnung, dass das Gitter das Control auch ausfüllt, hovert die Zelle unter der Maus, bietet MultiSelection.
      5. Mit wenigen Änderungen - an Sub Draw und anne CellData-Klasse - liesse sich auch ganz was anderes darstellen (etwa Bilder, oder Texte, oder Figuren aller Art) als nur die Durch-Numerierung der Zellen.
      6. Zu beachten ist, dass ein CellData **nicht** seine Bounds kennt, und sich auch nicht selber zeichnet. Grund ist, dass die Zell-Abmasse überhaupt nicht von der Zelle selbst abhängen, sondern von der Größe des Controls, der Anzahl Reihen/Spalten, und von der Zell-Addresse (dem pos).
      7. Das sind alles Parameter, die im Control verwaltet werden, nicht in der Zelle, und deswegen bleibt die Zelle "dumm".
      8. #End If '-- Options, Imports
      9. #End Region 'FileHeader
      10. <System.ComponentModel.DesignerCategory("Code")> _
      11. Public Class GridCanvas : Inherits Control
      12. Private Shared _SF As New StringFormat With {.Alignment = StringAlignment.Center, .LineAlignment = StringAlignment.Center}
      13. Private _DrawSize, _CellSize As Size
      14. Private _Cells()() As CellData
      15. Private Shared _HoverPen As New Pen(Color.FromArgb(&HFFFF9999), 4)
      16. Private Shared _NoPoint As New Point(-1, -1)
      17. Private _HoveredPos As Point = _NoPoint
      18. Public Property HoveredPos() As Point
      19. Get
      20. Return _HoveredPos
      21. End Get
      22. Set(ByVal value As Point)
      23. If _HoveredPos = value Then Return
      24. If _HoveredPos <> _NoPoint Then Invalidate(GetBounds(_HoveredPos))
      25. _HoveredPos = value
      26. If _HoveredPos <> _NoPoint Then Invalidate(GetBounds(_HoveredPos))
      27. End Set
      28. End Property
      29. Public ReadOnly Property Cell(pos As Point) As CellData
      30. Get
      31. Return _Cells(pos.Y)(pos.X)
      32. End Get
      33. End Property
      34. Private _Columns As Integer = 7
      35. Public Property Columns() As Integer
      36. Get
      37. Return _Columns
      38. End Get
      39. Set(ByVal value As Integer)
      40. If _Columns = value Then Return
      41. _Columns = value
      42. ChangeCellCount()
      43. End Set
      44. End Property
      45. Private _Rows As Integer = 7
      46. Public Property Rows() As Integer
      47. Get
      48. Return _Rows
      49. End Get
      50. Set(ByVal value As Integer)
      51. If _Rows = value Then Return
      52. _Rows = value
      53. ChangeCellCount()
      54. End Set
      55. End Property
      56. Private Sub ChangeCellCount()
      57. ReDim _Cells(_Rows - 1)
      58. For y = 0 To _Rows - 1
      59. ReDim _Cells(y)(_Columns - 1)
      60. For x = 0 To _Columns - 1
      61. _Cells(y)(x) = New CellData With {.Value = y * _Columns + x + 1}
      62. Next
      63. Next
      64. InitSizes()
      65. Invalidate()
      66. End Sub
      67. Public Function GetSelectedPositions() As IEnumerable(Of Point)
      68. Return GetPositions.Where(Function(pos) Cell(pos).Checked)
      69. End Function
      70. Public Function GetPositions() As IEnumerable(Of Point)
      71. Return From y In Enumerable.Range(0, _Rows), x In Enumerable.Range(0, _Columns) Select New Point(x, y)
      72. End Function
      73. Public Sub New()
      74. SetStyle(ControlStyles.OptimizedDoubleBuffer Or ControlStyles.ResizeRedraw, True)
      75. End Sub
      76. Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
      77. MyBase.OnMouseMove(e)
      78. HoveredPos = GetPositionFromPoint(e.Location)
      79. End Sub
      80. Protected Overrides Sub OnMouseLeave(e As EventArgs)
      81. MyBase.OnMouseLeave(e)
      82. HoveredPos = _NoPoint
      83. End Sub
      84. Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
      85. MyBase.OnMouseDown(e)
      86. Dim pos = GetPositionFromPoint(e.Location)
      87. Cell(pos).Checked = Not Cell(pos).Checked
      88. Invalidate(GetBounds(pos))
      89. End Sub
      90. 'Protected InvalidateCell(
      91. Protected Overrides Sub OnHandleCreated(e As EventArgs)
      92. MyBase.OnHandleCreated(e)
      93. ChangeCellCount()
      94. End Sub
      95. Private Function GetPositionFromPoint(pos As Point) As Point
      96. Dim i = 0
      97. For i = 1 To _Columns - 1
      98. If pos.X <= i * _CellSize.Width Then Exit For
      99. Next
      100. pos.X = i - 1
      101. For i = 1 To _Rows - 1
      102. If pos.Y <= i * _CellSize.Height Then Exit For
      103. Next
      104. pos.Y = i - 1
      105. Return pos
      106. End Function
      107. Private Function GetBounds(pos As Point) As Rectangle
      108. Return New Rectangle(New Point(pos.X * _CellSize.Width, pos.Y * _CellSize.Height), _CellSize)
      109. End Function
      110. Protected Overrides Sub OnSizeChanged(e As EventArgs)
      111. MyBase.OnSizeChanged(e)
      112. InitSizes()
      113. End Sub
      114. Private Sub InitSizes()
      115. _DrawSize = Me.ClientSize - New Size(1, 1)
      116. _CellSize = New Size(_DrawSize.Width \ _Columns, _DrawSize.Height \ _Rows)
      117. _DrawSize = New Size(_CellSize.Width * _Columns, _CellSize.Height * _Rows)
      118. End Sub
      119. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      120. MyBase.OnPaint(e)
      121. For x = 0 To _DrawSize.Width Step _CellSize.Width
      122. e.Graphics.DrawLine(Pens.Black, x, 0, x, _DrawSize.Height)
      123. Next
      124. For y = 0 To _DrawSize.Height Step _CellSize.Height
      125. e.Graphics.DrawLine(Pens.Black, 0, y, _DrawSize.Width, y)
      126. Next
      127. For Each pos In GetPositions()
      128. Dim rct = GetBounds(pos)
      129. rct.Inflate(-1, -1)
      130. Dim cll = Cell(pos)
      131. If cll.Checked Then e.Graphics.FillRectangle(Brushes.Yellow, rct)
      132. e.Graphics.DrawString(cll.Value.ToString, Font, Brushes.Black, rct, _SF)
      133. If pos = _HoveredPos Then
      134. rct.Inflate(-2, -2)
      135. e.Graphics.DrawRectangle(_HoverPen, rct)
      136. End If
      137. Next
      138. End Sub
      139. End Class
      140. Public Class CellData
      141. Public Value As Integer, Checked As Boolean = False
      142. End Class
      zu beachten, dass ich die Zellen nicht nach X und Y addressiere, sondern einfach nach Point - ein Point enthält ja X und Y.

      Clock-Control
      Zum Schluss noch ein Clock-Control, das arbeitet viel mit Matrix und GraphicsPath.
      ZB ein Uhrzeiger ist eine recht komplizierte Figur:

      VB.NET-Quellcode

      1. 'Uhrzeiger-Figur: langgezogenes Rechteck mit Dreieck als Spitze, Rückseite abgerundet
      2. Dim HeadWidth As Single = LineWidth * 4
      3. Length -= HeadWidth
      4. 'die Y-Werte sind negativ, weil der Zeiger nach oben zeigen soll
      5. _NormPath.AddLines(New PointF() { _
      6. New PointF(LineWidth / 2, 0), _
      7. New PointF(LineWidth / 2, -Length), _
      8. New PointF(HeadWidth / 2, -Length), _
      9. New PointF(0, -(Length + HeadWidth)), _
      10. New PointF(-HeadWidth / 2, -Length), _
      11. New PointF(-LineWidth / 2, -Length), _
      12. New PointF(-LineWidth / 2, 0)})
      13. Dim rctCenter As New RectangleF(-LineWidth / 2, -LineWidth / 2, LineWidth, LineWidth)
      14. _NormPath.AddArc(rctCenter, 0, 180) 'Rückseite: Halbkreis
      (naja, evtl. wäre einen custom-Pen einzurichten einfacher).
      Jedenfalls diesen GraphicsPath kann ich klonen, strecken, und so leiten sich aus dem Stunden-Zeiger auch Minuten- und Sekunden-Zeiger ab.
      Aber das Uhr-Sample enthält auch ein "Color-Generator"-Gimmick, womit - wenn aktiviert - der Invalidierte Bereich farblich markiert wird.
      Auf diese Weise wird (wörtlich!) vor Augen geführt, dass der invalidierte Bereich auch bei so einer großen Figur wie einem SekundenZeiger doch wesentlich kleiner ist, als die Gesamtfläche. Und dementsprechend weniger Resourcen werden gebraucht.
      Bildle ohne und mit ColorGenerator:
      .....

      Zusammenfassung
      OwnerDrawing deckt einen enorm weiten Bereich unterschiedlichster Anforderungen ab.
      Knackpunkt ist meist das Umdenken, von der Vorstellung eines Bildes, was man fertigstellt hin zu einer Datenhaltung, die das Bild jederzeit reproduzieren kann.
      Je nach Anforderung sind unterschiedliche Architekturen gefragt, einfache Sachen kann man auch in einem Paint-Event abhandeln, ohne ein besonderes Control dafür zu erfinden.
      Will man aber User-Interaktion bereitstellen, hovern, selektieren, löschen, draggen, dann wächst die notwendige Logik schnell an, und ein eigenes Control anzulegen wird sinnvoll.
      Will man auch eine Mehrzahl von Figuren präsentieren empfiehlt sich meist auch eine besondere Figur-Klasse, die sich ihre Abmasse selbst merkt, und die sich auch selbst zeichnen kann.
      Aber eben auch nicht immer, wie das OwnerDrawnGrid-Beispiel zeigt.
      Es sind auch noch komplexere Szenarien denkbar, etwa dass verschiedene Figur-Klassen (für Text, Zeichnungen, Bilder) gemeinsam auf einem Canvas präsentiert werden: gezieltes OwnderDrawing
      Oder mit Datenverarbeitung verknüpftes OwnerDrawing, sodass man die Gebilde auch abspeichern kann: AdvancedPainting.
      Evtl. wird man geneigt sein, sich für OwnerDrawing Pattern anzueignen, oder diese sogar codifizieren durch entsprechende Klassen, Basisklassen, Interfaces. Derlei Ansätze sind für ein Szenario evtl. nützlich, erweisen sich aber oft beim nächsten schon wieder als unzulänglich - die berühmte "eierlegende Wollmilchsau" kann nicht gefunden werden.
      Man muss eben vor allem das Prinzip mit dem gezielten Invalidieren kennen, man sollte Matrix und GraphicsPath bedienen können, und dann ist je nach Anforderung zu entwickeln, vorzugsweise natürlich angelehnt an ähnliche Lösungen - wenn vorhanden.

      Generell ist WinForms für Dauer-Animationen mit fließenden Bewegungen (Filme, Laufschriften, Chart-Ticker) schlecht geeignet, und je größer die animierte Fläche und je höher Bildwechsel-Frquenz der Animation wird im Grunde unverhältnismäßig viel Cpu-Ressource verbraten.
      Dateien
      • OwnerDrawing.zip

        (229,06 kB, 235 mal heruntergeladen, zuletzt: )

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

      FreehandDraw + Save

      Der Projekte-Sammlung in Post#1 hab ich nun ein "FreehandDraw"-Projekt hinzugefügt:

      Man kann Farben wählen, Strichbreite, und bei Save savets.
      Die Architektur ist wie üblich: Ein Canvas hält eine Liste von FreehandLine-Zeichenobjekte, kümmert sich um Maus-Interaktion, und ruft beim Painten die Draw()-Methode der Zeichenobjekte auf. Am Canvas kann man Strich-Breite und -Farbe einstellen:
      Canvas komplett

      VB.NET-Quellcode

      1. Imports System.ComponentModel
      2. <DesignerCategory("code"), DefaultEvent("Paint")> _
      3. Public Class Canvas : Inherits Control
      4. Private _Lines As New List(Of FreehandLine)
      5. Private _Pen As New Pen(Drawing.Color.Black, 2)
      6. Public Property LineWidth() As Single
      7. Get
      8. Return _Pen.Width
      9. End Get
      10. Set(ByVal value As Single)
      11. If _Pen.Width = value Then Return
      12. ChangePen(_Pen.Color.ToArgb, value)
      13. End Set
      14. End Property
      15. Public Property Color() As Color
      16. Get
      17. Return _Pen.Color
      18. End Get
      19. Set(ByVal value As Color)
      20. If _Pen.Color.ToArgb = value.ToArgb Then Return
      21. ChangePen(value.ToArgb, _Pen.Width)
      22. End Set
      23. End Property
      24. Private Sub ChangePen(col As Integer, width As Single)
      25. If _Lines.Count > 0 AndAlso _Lines(_Lines.Count - 1).Pen IsNot _Pen Then _Pen.Dispose()
      26. _Pen = New Pen(Drawing.Color.FromArgb(col), width)
      27. End Sub
      28. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      29. MyBase.OnPaint(e)
      30. DrawAllLines(e.Graphics)
      31. End Sub
      32. Public Sub DrawAllLines(g As Graphics)
      33. For Each line In _Lines
      34. line.Draw(g)
      35. Next
      36. End Sub
      37. Protected Overrides Sub Dispose(disposing As Boolean)
      38. MyBase.Dispose(disposing)
      39. If Not disposing Then Return
      40. For Each line In _Lines
      41. line.Pen.Dispose()
      42. Next
      43. End Sub
      44. Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
      45. MyBase.OnMouseMove(e)
      46. If e.Button <> Windows.Forms.MouseButtons.Left Then Return
      47. 'Linie verlängern, und das Verlängerungs-Stück invalidieren
      48. Dim rct = _Lines(_Lines.Count - 1).AddPoint(e.Location)
      49. If rct <> Rectangle.Empty Then Invalidate(rct)
      50. End Sub
      51. Protected Overrides Sub OnMouseDown(e As MouseEventArgs) ' neue Linie anfangen
      52. MyBase.OnMouseDown(e)
      53. _Lines.Add(New FreehandLine(e.Location, _Pen))
      54. End Sub
      55. End Class


      Invalidierung
      Eine starke Vereinfachung etwa im Vergleich zum Card-Sample ergibt sich daraus, dass die Zeichnung unbeweglich ist.
      Es kommen zwar immer wieder Linien hinzu, und zur aktuellen Linie kommen immer mehr Punkte hinzu, aber einmal gesetzt, verbleiben sie, unveränderlich.
      Und Invalidiert werden muss nur ein ganz kleiner Bereich, nämlich der vom bisher letzten Punkt zum nun neuen letzten Punkt.
      Dieses Invalidate-Rectangle kann sogar direkt durch die FreehandLine.AddPoint()-Funktion zurückgegeben werden, und feddich - keine Bounds der Zeichen-Objekte müssen gemerkt werden oder dergleichen.
      (Siehe 'Canvas komplett', Zeilen #57, #58)

      ImageSaving
      Eine weitere Besonderheit ist Public Canvas.DrawAll(), welches zunächstmal vom OnPaint() aufgerufen wird, und die Canvas-Graphics übergeben bekommt:

      VB.NET-Quellcode

      1. Public Class Canvas : Inherits Control
      2. '...
      3. Protected Overrides Sub OnPaint(e As PaintEventArgs)
      4. MyBase.OnPaint(e)
      5. DrawAll(e.Graphics)
      6. End Sub
      7. Public Sub DrawAll(g As Graphics)
      8. For Each line In _Lines
      9. line.Draw(g)
      10. Next
      11. End Sub
      Warum dieser Umstand - warum führt OnPaint() die Schleife nicht selbst aus (wie in den anderen Samples), und warum ist DrawAll() Public?
      Weil auf diese Weise kann das Canvas seine Zeichen-Informationen auch in andere Graphics drawen, nicht nur in seine eigenen.

      Genutzt wird das im MainForm: Dort wird eine Bitmap erstellt, davon ein Graphics, und da hinein zeichnet das Canvas ebensogut wie in seine eigenen Graphics. Und die Bitmap ich kann dann saven:

      VB.NET-Quellcode

      1. Public Class MainForm
      2. '...
      3. Private Sub SaveCanvas(path As String)
      4. Using bmp = New Bitmap(Canvas1.Width, Canvas1.Height), g = Graphics.FromImage(bmp)
      5. Canvas1.DrawAll(g)
      6. bmp.Save(path, Imaging.ImageFormat.Png)
      7. End Using
      8. End Sub


      ZeichenFigur FreehandLine - Klasse
      Last not least der Zeichenfigur-Part dieser Standard-Architektur.
      Ist sehr einfach, FreehandLine hat einen Pen, eine List(Of Point), man kann Points adden, und sie kann sich zeichnen.
      Das komplizierteste ist, beim Point-Adden das Rechteck richtig zu ermitteln, was vom aufrufenden Canvas zu invalidieren ist, damit genau in dem Bereich neu gezeichnet wird.

      VB.NET-Quellcode

      1. Public Class FreehandLine
      2. Private _Points As New List(Of Point)
      3. Public ReadOnly Pen As Pen
      4. Public Sub New(ptStart As Point, pen As Pen)
      5. _Points.Add(ptStart)
      6. Me.Pen = pen
      7. End Sub
      8. ''' <summary> returnt ein umschließendes Rechteck der hinzugekommenen Teil-Linie </summary>
      9. Public Function AddPoint(pt As Point) As Rectangle
      10. Dim ptPrev = _Points(_Points.Count - 1)
      11. If pt = ptPrev Then Return Rectangle.Empty
      12. _Points.Add(pt)
      13. Dim szMin = New Size(Math.Min(pt.X, ptPrev.X), Math.Min(pt.Y, ptPrev.Y))
      14. Dim szMax = New Size(Math.Max(pt.X, ptPrev.X) + 1, Math.Max(pt.Y, ptPrev.Y) + 1)
      15. Dim rct = New Rectangle(New Point(szMin), szMax - szMin)
      16. Dim infl = CInt(Math.Ceiling(Pen.Width))
      17. rct.Inflate(infl, infl)
      18. Return rct
      19. End Function
      20. Public Sub Draw(ByVal g As Graphics)
      21. If _Points.Count > 1 Then g.DrawLines(Pen, _Points.ToArray)
      22. End Sub
      23. End Class

      Wie mans nicht macht

      Habe das DrawTriangle-Sample um ein zusätzliches Form erweitert, welches die naive Art zu zeichnen demonstriert, nach dem Motto "Bei Maus-Aktion zeichnen".
      Code: Bei Maus-Aktion zeichnen

      VB.NET-Quellcode

      1. Public Class frmDrawNaive
      2. Private fFont As New Font("Arial", 10.0)
      3. Private bBrush As New SolidBrush(Color.Green)
      4. Private Sub Form1_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove, Me.MouseDown
      5. If e.Button <> Windows.Forms.MouseButtons.Left Then Return
      6. Dim ptA = middleSite(Point.Empty, Point.Empty + Me.ClientSize)
      7. Dim ptB = New PointF(e.X, e.Y)
      8. Dim ptC = New PointF(ptB.X, ptA.Y)
      9. Dim points() As PointF = {ptA, ptB, ptC}
      10. Using g = Me.CreateGraphics
      11. g.DrawPolygon(Pens.Red, points)
      12. g.DrawString("A", fFont, bBrush, ptA)
      13. g.DrawString("B", fFont, bBrush, ptB)
      14. g.DrawString("C", fFont, bBrush, ptC)
      15. g.DrawString("a", fFont, bBrush, middleSite(ptB, ptC))
      16. g.DrawString("b", fFont, bBrush, middleSite(ptA, ptC))
      17. g.DrawString("c", fFont, bBrush, middleSite(ptA, ptB))
      18. End Using
      19. End Sub
      20. Private Shared Function middleSite(ByVal a As PointF, ByVal b As PointF) As PointF
      21. Return New PointF(0.5F * (a.X + b.X), 0.5F * (a.Y + b.Y))
      22. End Function
      23. End Class

      Solch sieht für einen Moment gut aus, nämlich genau so lange, bis man die Maus ein zweites mal betätigt.
      Oder bis zuvor überdeckte Bild-Teile erscheinen - die sind dann nämlich leer 8| :

      Man sieht: "Bei Maus-Aktion zeichnen" ist das falsche Motto.
      Nochmal das richtige:
      Code: Bei Maus-Aktion Zeichen-Infos ändern und invalidieren - Zeichnen nur im Paint-Event

      VB.NET-Quellcode

      1. Public Class Form1
      2. Private ptA, ptB, ptC As PointF
      3. Private fFont As New Font("Arial", 10.0)
      4. Private bBrush As New SolidBrush(Color.Green)
      5. Private Sub Form1_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove, Me.MouseDown
      6. If e.Button <> Windows.Forms.MouseButtons.Left Then Return
      7. ptA = middleSite(Point.Empty, Point.Empty + Me.ClientSize)
      8. ptB = New PointF(e.X, e.Y)
      9. ptC = New PointF(ptB.X, ptA.Y)
      10. Invalidate()
      11. End Sub
      12. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
      13. Dim points() As PointF = {ptA, ptB, ptC}
      14. e.Graphics.DrawPolygon(Pens.Red, points)
      15. e.Graphics.DrawString("A", fFont, bBrush, ptA)
      16. e.Graphics.DrawString("B", fFont, bBrush, ptB)
      17. e.Graphics.DrawString("C", fFont, bBrush, ptC)
      18. e.Graphics.DrawString("a", fFont, bBrush, middleSite(ptB, ptC))
      19. e.Graphics.DrawString("b", fFont, bBrush, middleSite(ptA, ptC))
      20. e.Graphics.DrawString("c", fFont, bBrush, middleSite(ptA, ptB))
      21. End Sub
      22. Private Shared Function middleSite(ByVal a As PointF, ByVal b As PointF) As PointF
      23. Return New PointF(0.5F * (a.X + b.X), 0.5F * (a.Y + b.Y))
      24. End Function
      25. End Class


      (Update in post#1)