Wie erstelle ich eine eigene ProgressBar sauber und flüssig?

    • VB.NET

    Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von Trade.

      Wie erstelle ich eine eigene ProgressBar sauber und flüssig?

      Moin Leutz,

      da mir langweilig war wollte ich mich mal einem kleinen Tutorial widmen. Dies erfordert natürlich grundlegende Kenntnisse in VB.NET und Klassenbibliotheken.
      Ich wollte mich speziell darauf spezialisieren wie man Controls flüssig laufend erstellen kann. Als Beispiel nehmen wir eine ProgressBar als Control. Zwar gab es dazu schon mal einen Thread, aber dieser hatte Codeteile, die nicht mehr aktuell sind bzw. Nachteile haben.

      Vorwort:

      1. Bevor wir loslegen gibt es zu sagen, dass wir nur GDI+ für unser Control verwenden werden. Also keine Bilder oder sonstiges, da
        a) das unsauber und schlechte Programierung ist
        b) man an das Bild gebunden ist. (Mit GDI+ kann man mehr machen)
        c) bei einer ProgressBar sowas garnicht möglich ist

        Wenn Ihr neu in GDI+ seid, dann schaut euch das Tutorial von @FreakJNS: an.
        [VB 2008] [Tutorial] GDI+

      2. Dies soll nicht als C&P-Quelle dienen, sondern als grundlegendes Tutorial zum flüssigen Erstellen von Controls.


      Los geht's

      So, wir erstellen nun einfach mal eine simple ProgressBar.
      Diese wird einen Hintergrund besitzen und einen einfachen Balken. Ihr könnt natürlich mehr Effekte adden, indem Ihr HatchStyles etc benutzt. Steht alles im Tutorial, was ich oben gepostet habe.
      Zudem müsst Ihr dann bei manchen Sachen Änderungen vornehmen. ;)

      Also wir erstellen uns erstmal ein neues Projekt in Visual Studio (Visual Basic).
      Dazu wählen wir Visual Basic als Programmiersprache aus (wenn Ihr VB als IDE habt und nicht VS, dann fällt dieser Schritt weg) und schließlich als Typ: Klassenbibliothek. Gebt einen Namen ein (wir nehmen hier mal TestBar) und klickt auf "Erstellen".

      So nun haben wir eine Klasse in unserem Projekt, die heißt standardmäßig Class1. Um diese nun umzubenennen in den Namen unseres Projekts geht Ihr in den Projektmappen-Explorer:


      Dieser befindet sich im Projekt rechts oben.
      Dann macht Ihr einen Doppelklick auf die Class1 und könnt nun einen neuen Namen hineinschreiben. Bestätigt Eure Eingabe mit Enter und klickt bei der kommenden Meldung auf Ja.

      So, nun haben wir das Ganze umbenannt. Alles was jetzt in unserer Klasse steht ist:

      VB.NET-Quellcode

      1. Public Class TestBar
      2. End Class


      So, um nun ein Control zu erstellen müssen wir vom Control erben. Damit dies funktioniert fehlt uns ein Imports-Anweisung.
      Für GDI+ benötigen wir übrigens noch mehr, darum fasse ich gleich alle zusammen. Diese fügt Ihr über Public Class TestBar ein.
      Ich füge dort auch noch die Anweisung für Option Strict ein. Diese sollte bei euch schon als Standard auf "On" stehen. Wenn Ihr nicht wisst was das ist: Klick mich

      So, das sieht nun so aus:

      VB.NET-Quellcode

      1. Option Strict On
      2. Imports System.Windows.Forms 'Dies brauchen wir für das "Control", da dies zu Windows.Forms gehört
      3. Imports System.Drawing 'Brauchen wir für GDI+
      4. Imports System.Drawing2D 'Brauchen wir für Brushes etc.
      5. Imports System.Drawing.Text 'Brauchen wir für TextRendering
      6. Public Class TestBar
      7. End Class


      Ok, nun können wir loslegen. Wir lassen unsere Klasse nun von Control erben.
      Warum nicht ProgressBar?

      Ganz einfach. Wenn wir von ProgressBar erben haben wir zwar schon unsere Eigenschaften, wie Value etc., die wir mit MyBase.Value aufrufen können, jedoch wenn wir eigene Sachen implementieren wollen und unsere ProgressBar Änderungen hat, die nicht der normalen entsprechen, dann haben wir das Problem, dass wir all die nicht passenden Properties etc als Shadows deklarieren müssen, um sie zu verdecken. Wenn dann aber unsere Klasse zurück gecastet wird sind diese wieder sichtbar. Es besteht auch die Möglichkeit das Ganze als Overrides zu deklarieren und sie zu überschreiben, aber die meisten unterstützen keine Polymorphie und sind nicht als Overridable deklariert, sodass das im Grunde ziemlich blöd ist.
      Also erben wir vom Control. Die Sachen wie Value etc. müssen eh berechnet werden...
      Dazu schreiben wir unter Public Class TestBar:

      VB.NET-Quellcode

      1. Inherits Control


      So, bei manchen kann jetzt ein Fehler kommen. Wenn dies der Fall ist, dann nutzt einfach die Möglichkeit von Visual Studio, klickt doppelt auf den Fehler und wählt den Korrekturvorschlag.

      So, nun müssen wir wie für jedes Control, das erbt, einen Konstruktor angeben. Dies geschieht mit New(). In diesen Konstruktor können auch die Eigenschaften wie Size, denn wenn Ihr die ProgressBar später auf die Form zieht, dann wird dieser zuerst aufgerufen. NICHT IntializeComponent().
      In diesem Konstruktor wird nun unser Control erstellt:

      VB.NET-Quellcode

      1. Public Sub New()
      2. MyBase.New() 'Dies geschieht über diesen Befehl
      3. End Sub


      Nun folgen die Einstellungen zur Qualität, damit das Ganze flüssig und ohne flackern läuft.

      VB.NET-Quellcode

      1. SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.OptimizedDoubleBuffer Or ControlStyles.UserPaint Or ControlStyles.ResizeRedraw, True)
      2. UpdateStyles()


      Dies sind die Einstellungen, die am Besten geeignet sind. Wichtig ist auf jeden Fall hauptsächlich: Me.DoubleBuffered = True.
      Wichtig: Dies kommt auf keinen Fall ins OnPaint, sondern immer in den Konstruktor. Danke @Artentus: für den Tipp.

      Ihr könnt noch optional Me.SuspendLayout() und schließlich Me.ResumeLayout(False) einfügen. Damit wird beim Zeichnen einfach die ProgressBar kurz "eingefroren", sodass Einstellungen wie Size etc. übernommen werden und danach wird das Ganze freigegeben.

      So.
      Das wäre alles wichtige. Nun kommt das Zeichnen der ProgressBar selbst und Sachen wie Properties.

      Ich rate euch für eure Properties, Functions etc eigene Regions zu erstellen. Dies geht mit:

      VB.NET-Quellcode

      1. #Region "properties"
      2. #End Region


      Der Übersicht zu Liebe. Okay, um nun Sachen wie Value usw bereitzustellen benötigen wir ein paar Properties:

      VB.NET-Quellcode

      1. Dim max As Integer = 100
      2. <Category("Werte")>
      3. Public Property Maximum() As Integer
      4. Get
      5. Return max
      6. End Get
      7. Set(ByVal value As Integer)
      8. max = value
      9. Me.Invalidate()
      10. End Set
      11. End Property
      12. Dim min As Integer = 0
      13. <Category("Werte")>
      14. Public Property Minimum() As Integer
      15. Get
      16. Return min
      17. End Get
      18. Set(ByVal value As Integer)
      19. min = value
      20. Me.Invalidate()
      21. End Set
      22. End Property
      23. Dim val As Integer = 70
      24. <Category("Werte")>
      25. Public Property Value() As Integer
      26. Get
      27. Return val
      28. End Get
      29. Set(ByVal value As Integer)
      30. val = value
      31. Me.Invalidate()
      32. End Set
      33. End Property


      Erklärung:
      Die Variablen, die wir hier erstellen dienen dazu einen Standardwert zu geben.
      Bei Maximum ist dieser beispielsweise 100. Ihr könnt aber auch 50 nehmen, oder was Euch halt gefällt.
      Nehmt für die Variablenamen immer geeignete Abkürzungen, damit Ihr sie wiederfindet oder kommentiert sie.

      Das <Category("Werte")> erstellt für die Property eine neue Kategorie im Eigenschaftsfenster. Ihr könnt diese auch in bereits vorhandenen einordnen.
      Beispielsweise bei Verhalten. Dann schreibt Ihr: <Category("Verhalten")>.

      Bei der Property Value wird der Wert gesetzt und anschließend invalidiert. Das heißt, es wird der Bereich neu gezeichnet, wenn sich die Value ändert.
      Der Vorteil von Me.Invalidate() ist, dass man in den Parametern genaue Bereiche angeben kann.

      Für andere Properties, die Ihr erstellen wollt, macht Ihr das genauso. Ich werde nachher noch eine Property machen, mit der Ihr die Farbe des Balkens ändern könnt.
      Der Rest sollte klar sein.

      So, nun gehen wir zum letzten wichtigen (sogar wichtigsten) Schritt beim Erstellen des Controls.
      Wir zeichnen die ProgressBar in der OnPaint-Methode.

      VB.NET-Quellcode

      1. Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
      2. End Sub


      Schließlich folgen noch ein paar Sachen. Das OnPaintBackground kann auch als selbiges behandelt werden. Ich hab das hier nicht gemacht.

      Da kommt noch das:

      VB.NET-Quellcode

      1. MyBase.OnPaint(e)


      Anschließend fügen wir noch ein paar GDI-Einstellungen hinzu, damit wir eine gute Qualität bekommen.
      Alles was nun folgt wird mit e.Graphics gemacht. Also verwenden wir die With-Anweisung.

      VB.NET-Quellcode

      1. With e.Graphics
      2. .SmoothingMode = SmoothingMode.HighQuality
      3. .TextRenderingHint = TextRenderingHint.AntiAlias
      4. .PixelOffsetMode = PixelOffsetMode.HighQuality
      5. .InterpolationMode = InterpolationMode.HighQualityBilinear
      6. .CompositingQuality = CompositingQuality.HighQuality


      So, damit haben wir die höchsten Einstellungen, die die beste Grafik zeichnen werden.
      Zu guter Letzt folgt jetzt das Zeichnen der ProgressBar:

      VB.NET-Quellcode

      1. Dim HintergrundHoehe As Integer = MyBase.ClientRectangle.Height
      2. 'Die Höhe des Hintergrunds
      3. Dim HintergrundBreite As Integer = MyBase.ClientRectangle.Width
      4. 'Die Breite des Hntergrunds
      5. Dim BalkenHoehe As Integer = CInt(MyBase.ClientRectangle.Height)
      6. 'Die Höhe des Balken (entspricht der Höhe des Hintergrunds)
      7. Dim BalkenBreite As Integer = Width * (Value - Minimum) \ (Maximum - Minimum)
      8. 'Die Value ändert sich dann immer und das Ganze wird neu berechnet, sodass der Balken länger wird
      9. Dim Hintergrundfarbe As Color = Color.White
      10. 'Wir setzen die Hintergrundfarbe auf weiß. (benutzerdefiniert)
      11. Dim Balkenfarbe As Color = Me.BalkenColor
      12. 'Wir setzen die Balkenfarbe auf grün (benutzerdefiniert)
      13. Dim hintergrundrect As New Rectangle(0, 0, HintergrundBreite, HintergrundHoehe)
      14. 'Wir erstellen ein neues Rectangle mit der Location (0,0) und den Werten der oben deklarierten Variablen
      15. .FillRectangle(New SolidBrush(Hintergrundfarbe), hintergrundrect)
      16. 'Wir füllen das Rectangle mit einer SolidBrush (festen Farbe)
      17. Dim balkenrect As New Rectangle(0, 0, BalkenBreite, BalkenHoehe)
      18. 'Wir erstellen ein neues Rectangle für den Balken mit der Location (0,0) und den Werten der oben deklarierten Variablen
      19. .FillRectangle(New SolidBrush(Balkenfarbe), balkenrect)
      20. 'Wir füllen das Rectangle wieder mit einer SolidBrush (festen Farbe)
      21. End With


      Ok... Nach diesem großen Code hat unsere ProgressBar alles nötige und funktioniert nun. Lest euch den Code in Ruhe durch und versucht ihn zu verstehen.
      Ich gebe zu, dass man die Variablen zuvor hätte deklarieren müssen und den Rest in ein eigenes Sub packen. (Keine Sorge, bei meinen Projekten mache ich das auch so). Es ist halt nur hier der Übersicht zu Gute, damit man alles an einem Fleck hat.

      Wie binde ich die ProgressBar ein bzw. starte sie?
      Dazu benötigen wir natürlich ein neues Projekt (WinForms-Anwendung).
      Wenn Ihr eins erstellt habt, dann klickt im Menü oben auf Extras und dann Werkzeugkastenelemente auswählen....
      Nun klickt Ihr auf Durchsuchen..., wählt die .dll aus und klickt auf OK.

      Nun habt Ihr sie in der Toolbox. Zieht sie auf die Form, fügt einen Timer hinzu (Interval egal) und geht in den Code-Editor.
      Im Load-Event schreibt Ihr:

      VB.NET-Quellcode

      1. Timer1.Start()


      Im Timer1_Tick-Event:

      VB.NET-Quellcode

      1. TestBar1.Value += 1
      2. If TestBar1.Value = 100 Then
      3. TestBar1.Value = 0
      4. Timer1.Start()
      5. End If


      Mir ist natürlich bewusst, dass dies Pseudo-Code ist. Es dient hier rein zur Veranschaulichung der ProgressBar.
      Klickt auf "Debuggen" und Ihr seht eure eigene ProgressBar flüssig und ohne Flackern druchlaufen.

      Wie kann ich nun die Farbe des Balkens ändern lassen?

      Dies ist garnicht schwer. Wir brauchen nur eine weitere Property.
      Ich nenne diese: BalkenColor und gebe Ihr in einer Variable den Standardwert: Green

      VB.NET-Quellcode

      1. Dim balkencol As Color = Color.Green
      2. Public Property BalkenColor() As Color
      3. Get
      4. Return balkencol
      5. End Get
      6. Set(ByVal value As Color)
      7. balkencol = value
      8. End Set
      9. End Property


      Um nun die Property aufzurufen und die Farbe zu ändern müssen wir Dim Balkenfarbe As Color = Color.Green zu Dim Balkenfarbe As Color = Me.BalkenColor ändern. Heißt, dass die Balkenfarbe von der Property abhängig ist. Sobald sich der Wert dieser Eigenschaft ändert, wechselt die Farbe.

      Das ist alles.

      Wie bekommt mein Control in der Toolbox die ProgressBar-Bitmap?
      Dazu schreiben wir über Public Class TestBar: <ToolboxBitmap(GetType(ProgressBar))> _

      Okay, ich habe nochmal eine ZIP-Datei mit dem Ganzen angehängt, allerdings übersichtlicher und in eigenen Subs.
      Die hier gezeigten Codeteile sind natürlich zu verbessern, allerdings wäre dann die Übersicht des Tutorials in die Hose gegangen.
      Wenn Ihr noch Fehler findet, dann berichtet diese bitte.

      lg Trade

      PS: Ich hänge in Teil2 nochmal den kompletten Code an
      Dateien
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Fragen per PN, denn dafür ist das Forum da :!:

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

      Teil 2:

      So, hier nochmal der komplette Code ein wenig übersichtlicher (in eigenen Subs):

      Code

      VB.NET-Quellcode

      1. Imports System.Windows.Forms
      2. Imports System.Drawing
      3. Imports System.ComponentModel
      4. Imports System.Drawing.Drawing2D
      5. Imports System.Drawing.Text
      6. <ToolboxBitmap(GetType(ProgressBar))> _
      7. Public Class TestBar
      8. Inherits Control
      9. Public Sub New()
      10. MyBase.New()
      11. SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.OptimizedDoubleBuffer Or ControlStyles.UserPaint Or ControlStyles.ResizeRedraw, True)
      12. UpdateStyles()
      13. Me.SuspendLayout()
      14. Me.ResumeLayout(False)
      15. End Sub
      16. #Region "properties"
      17. Dim max As Integer = 100
      18. <Category("Werte")>
      19. Public Property Maximum() As Integer
      20. Get
      21. Return max
      22. End Get
      23. Set(ByVal value As Integer)
      24. max = value
      25. End Set
      26. End Property
      27. Dim min As Integer = 0
      28. <Category("Werte")>
      29. Public Property Minimum() As Integer
      30. Get
      31. Return min
      32. End Get
      33. Set(ByVal value As Integer)
      34. min = value
      35. End Set
      36. End Property
      37. Dim val As Integer = 70
      38. <Category("Werte")>
      39. Public Property Value() As Integer
      40. Get
      41. Return val
      42. End Get
      43. Set(ByVal value As Integer)
      44. val = value
      45. Me.Invalidate()
      46. End Set
      47. End Property
      48. Dim balkencol As Color = Color.Green
      49. Public Property BalkenColor() As Color
      50. Get
      51. Return balkencol
      52. End Get
      53. Set(ByVal value As Color)
      54. balkencol = value
      55. End Set
      56. End Property
      57. #End Region
      58. Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
      59. MyBase.OnPaint(e)
      60. InstallQuality(e)
      61. Drawbar(e)
      62. End Sub
      63. Public Sub Drawbar(ByVal e As System.Windows.Forms.PaintEventArgs)
      64. With e.Graphics
      65. Dim HintergrundHoehe As Integer = MyBase.ClientRectangle.Height
      66. 'Die Höhe des Hintergrunds
      67. Dim HintergrundBreite As Integer = MyBase.ClientRectangle.Width
      68. 'Die Breite des Hntergrunds
      69. Dim BalkenHoehe As Integer = CInt(MyBase.ClientRectangle.Height)
      70. 'Die Höhe des Balken (entspricht der Höhe des Hintergrunds)
      71. Dim BalkenBreite As Integer = Width * (Value - Minimum) \ (Maximum - Minimum)
      72. 'Die Value ändert sich dann immer und das Ganze wird neu berechnet, sodass der Balken länger wird
      73. Dim Hintergrundfarbe As Color = Color.White
      74. 'Wir setzen die Hintergrundfarbe auf weiß. (benutzerdefiniert)
      75. Dim Balkenfarbe As Color = Me.BalkenColor
      76. 'Wir setzen die Balkenfarbe auf grün (benutzerdefiniert)
      77. Dim hintergrundrect As New Rectangle(0, 0, HintergrundBreite, HintergrundHoehe)
      78. 'Wir erstellen ein neues Rectangle mit der Location (0,0) und den Werten der oben deklarierten Variablen
      79. .FillRectangle(New SolidBrush(Hintergrundfarbe), hintergrundrect)
      80. 'Wir füllen das Rectangle mit einer SolidBrush (festen Farbe)
      81. Dim balkenrect As New Rectangle(0, 0, BalkenBreite, BalkenHoehe)
      82. 'Wir erstellen ein neues Rectangle für den Balken mit der Location (0,0) und den Werten der oben deklarierten Variablen
      83. .FillRectangle(New SolidBrush(Balkenfarbe), balkenrect)
      84. 'Wir füllen das Rectangle wieder mit einer SolidBrush (festen Farbe)
      85. End With
      86. End Sub
      87. Public Sub InstallQuality(ByVal e As System.Windows.Forms.PaintEventArgs)
      88. With e.Graphics
      89. .SmoothingMode = SmoothingMode.HighQuality
      90. .TextRenderingHint = TextRenderingHint.AntiAlias
      91. .PixelOffsetMode = PixelOffsetMode.HighQuality
      92. .InterpolationMode = InterpolationMode.HighQualityBilinear
      93. .CompositingQuality = CompositingQuality.HighQuality
      94. End With
      95. End Sub
      96. End Class



      Okay, ich habe im Hauptpost nochmal ein Testprojekt angehängt...
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Fragen per PN, denn dafür ist das Forum da :!:

      Dieser Beitrag wurde bereits 6 mal editiert, zuletzt von „Trade“ ()

      @Trade: Sieht angenehm flüssig aus. :thumbup:
      Vielleicht wäre es gut, die Properties und Methoden in Deiner DLL zu kommentieren (''' über der jeweiligen Deklaration und dann ausfüllen; im Projekt -> Compile "Generate XML documentation file" anklicken).
      Diese Kommentare werden dann im Objektbrowser bei den Methoden und Parametern sowie im Editor bei der Eingabe angezeigt.
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      VB-Fragen über PN / Konversation werden ignoriert!
      Da streiten wir uns aber immer, Rod und ich.
      Weil ich bin der Ansicht, überflüssige Doku-Kommentare sind schädlich, weil sie die Code-Datei vollmüllen und vom wesentlichen ablenken.

      etwa eine Sub New muß nicht dokumentiert sein, denn wer nicht weiß was ein Konstruktor ist, brauch auch nicht in' Objektbrowser gugge.
      Ebenso die Properties Minimum und Maximum - was will man da dokumentieren? Dass die Property Maximum das Maximum angibt? Da fühlt ein Leser mit Brain-On sich doch verarscht, oder?
      Klar, die gläubige Microsoft-Guideline-Jüngerschaft ist brain-off-happy, weil so stehts inne Guideline
      Das sind nur die armen Selber-Denker, die bei sowas das denken anfangen, und denken: "Stuss ist Stuss, und sollteman besser weglassen, denn weniger (Stuss) ist mehr (Qualität) - egal was inne Guideline steht."

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

      Mit ner gewissen Geschwindigkeit ist das so.
      Da kann ich leider auch nichts dran ändern. Die besten Einstellungen sind eben gesetzt und es flackert halt nicht. Das war so das, was ich mit flüssig auch ausdrücken wollte.
      Versuche es einfach mal mit ner anderen Geschwindigkeit, eventuell geht es dann besser.
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Fragen per PN, denn dafür ist das Forum da :!:
      @Trade:

      Quellcode

      1. Width = 500
      2. Min = 0
      3. Max = 100
      4. Value = 50
      5. // -> Progressbar bei 250px

      Quellcode

      1. Width = 500
      2. Min = 0
      3. Max = 200
      4. Value = 50
      5. // -> Progressbar bei 125px


      Also wurde die Max-Property geändert, die Progressbar müsste an einer anderen Stelle sein, es wird aber nicht invalidated ;)
      Ich habe das gerade getestet und gesehen, dass Du recht hast. Das Problem ist, ich weiß nicht wieso das so ist, da beim Invalidieren die Paint-Routine aufgerufen und somit eigentlich die Bar verschoben wird/werden sollte.

      Ich habe leider nicht wirklich die Zeit und Ideen, den Code zu durchsuchen und das Problem zu finden. Wenn das jemand findet, kann er das gerne mitteilen. Afaik sollte ein Invalidieren beim Setzen hier halt seinen Job tun.

      Edit: Beim Durchlaufen passiert das dann, nur im Stand ändert sich die Value nicht. Nun gut, dann ist da wohl irgendwo ein kleiner Fehler.
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Fragen per PN, denn dafür ist das Forum da :!: