List-Control à la Firefox' Download-Manager

  • VB.NET

Es gibt 91 Antworten in diesem Thema. Der letzte Beitrag () ist von Jonas Jelonek.

    Jonas Jelonek schrieb:

    und zum eigentlichen Thema zurückkommen, und zwar, wie ich jetzt so ein Control in WinForms am besten mache.

    Eben das hat mich ja verwirrt. Also war Semitransparenz und Blur eigentlich total offtopic ^^

    Zum Thema:
    Zuerst mal stellt sich ja die Frage, wie man diesen blauen Hintergrund hinbekommt. Das ist ja scheinbar dem Explorerfenster nachempfunden, dort haben selektierte Einträge auch diesen blauen Verlauf. Nun gibt es eine Möglichkeit, diesen Explorerstyle auch in eigenen Programmen zu nutzen. Man macht folgendes:

    VB.NET-Quellcode

    1. <DllImport("uxtheme", CharSet:=CharSet.Unicode)> _
    2. Public Shared Function SetWindowTheme(hWnd As IntPtr, textSubAppName As String, textSubIdList As String) As Int32
    3. End Function
    4. SetWindowTheme(Me.Handle, "explorer", Nothing)

    Damit wird Windows gesagt, dass für dieses Control nicht der Style des eigenen Programms, sondern der Style des Programms "explorer" benutzt werden soll. Nun wird man feststellen, dass dies keine Veränderung bringt, wenn man sein Control von Listbox erben lässt. Das liegt ganz einfach daran, dass auch der Explorer die Listbox nicht besonders style-t. Der benutzt ein ListView-Element. Wenn wir nun also von Listview erben und die View auf View.Details umstellen, dann haben wir ein Listbox-artiges Aussehen, mit schönem Explorer-Style:

    Aber natürlich gibt es einen Haken. Da wir statt einer Listbox eine Listview nehmen müssen, verlieren wir die ItemHeight-Eigenschaft. Damit sind also paktisch keine so hohen Items, wie im Firefox, möglich. Es gibt einen Trick, dass man zum Beispiel die SmallItemView benutzt und dann halt ein Bild benutzt, das so hoch ist, wie man das Item haben will. Aber das ist für uns ja auch nicht sehr geeignet, da das Item im Firefox circa doppelt so hoch wie das Icon der Datei ist.
    Also können wir diesen Ansatz eigentlich vergessen. Dann gibt es ja noch den schönen VisualStyleRenderer. Wenn du dann mal ein bisschen damit rumspielst, ungefähr so:

    VB.NET-Quellcode

    1. Public Class Form1
    2. Private renderer As New VisualStyleRenderer(VisualStyleElement.ListView.Item.Hot)
    3. Private Sub Form1_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
    4. renderer.DrawBackground(e.Graphics, New Rectangle(10, 10, 200, 15))
    5. End Sub
    6. End Class

    Dann wirst du feststellen, dass all die tollen ListView-Styles im aktuellen Windows-Style gar nicht definiert sind:
    Die gegebene Kombination aus "Class", "Part" und "State" wird vom aktuellen visuellen Stil nicht definiert.


    Aaaaber, es gibt da ja noch eine Möglichkeit, einen VisualStyleRenderer zu erstellen, nämlich mit New VisualStyleRenderer(VisualStyleElement.CreateElement(className As String, part As Integer, state As Integer)). Nun, was hat es mit className, part und state auf sich? Dazu kann man mal einen Blick in die Datei vsstyle.h werfen, die man üblicherweise in \Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include findet. Dort sind lauter Enums zu finden. Das sind praktisch die Enum-Werte, die hinter den Auflistungen stecken, die wir oben schon verwendet haben, wie zum Beispiel: VisualStyleElement.ListView.Item.Hot. Nun kann man sich die numerischen Enum-Werte raussuchen und die Style-Elemente praktisch von Hand erstellen. Auch da kommt bei den typischen Kombinationen die Meldung, dass das vom aktuellen Stil nicht definiert wird. Nun finden sich da aber auch noch eine ganze Reihe weiterer Werte in den Enums, die einem im Code gar nicht zugänglich gemacht werden. Und von denen sind einige sehr wohl im aktuellen Stil definiert. Eine dieser Kombinationen sieht zum Beispiel so aus:

    VB.NET-Quellcode

    1. Const LVP_GROUPHEADER As Integer = 6
    2. Const LVGH_OPENSELECTEDNOTFOCUSEDHOT As Integer=6
    3. Private renderer As New VisualStyleRenderer(VisualStyleElement.CreateElement("LISTVIEW", LVP_GROUPHEADER, LVGH_OPENSELECTEDNOTFOCUSEDHOT))
    4. Private Sub Form1_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
    5. renderer.DrawBackground(e.Graphics, New Rectangle(10, 10, 200, 50))
    6. End Sub

    Tja, und was bekommt man dann? Folgendes:

    Das sieht doch schon ganz gut aus, oder?

    Setzen wir das auf unser eigenes Control, unter Ausnutzung der ItemHeight einer Listbox, um, dann haben wir folgenden Code:

    VB.NET-Quellcode

    1. Imports System.Windows.Forms.VisualStyles
    2. Public Class FirefoxListbox
    3. Inherits ListBox
    4. Const LVP_GROUPHEADER As Integer = 6
    5. Const LVGH_OPENSELECTEDNOTFOCUSEDHOT As Integer = 6
    6. Private renderer As New VisualStyleRenderer(VisualStyleElement.CreateElement("LISTVIEW", LVP_GROUPHEADER, LVGH_OPENSELECTEDNOTFOCUSEDHOT))
    7. Private Sub FirefoxListbox_DrawItem(sender As Object, e As System.Windows.Forms.DrawItemEventArgs) Handles Me.DrawItem
    8. e.Graphics.Clip = New Region(e.Bounds)
    9. e.Graphics.Clear(Color.White)
    10. renderer.DrawBackground(e.Graphics, e.Bounds)
    11. End Sub
    12. Public Sub New()
    13. Me.DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
    14. Me.ItemHeight = 72
    15. Me.IntegralHeight = False 'finde ich einfach besser
    16. End Sub
    17. End Class


    Das Endergebnis sieht dann so aus:


    ich denke, von hier aus lasse ich dich mal alleine weitermachen. Aber als kleiner Hinweis: Das VisualStyle-Gemache ist ja betriebssystemabhängig. Also schau es dir auch mal auf Win 8, XP, Vista an, je nachdem, was du supporten willst. Ein ganz anderer Ansatz wäre auch das vollständige Selbstzeichnen mit Gradients usw. Dann siehts natürlich überall gleich aus, ist aber noch viel mehr Arbeit, bis es gut aussieht.

    PS: Für solch ausführliche Antworten sollte man geld verlangen können :rolleyes:

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    Vielen Dank, vb-checker. Ich bedanke mich für diese sehr große Hilfe. Ich werde es jetzt ausprobieren, was ich dann daraus basteln kann.

    PS: Das mit der Semitransparenz und Blur kam nur vor, weil ich gefragt hatte, ob man in WPF auch mit den DWM APIs für den Aero Effekt arbeitet und dann hat Artentus gesagt, dass das auch mit Semitransparenz und Blur möglich wäre.
    @Jonas Jelonek: Falls du noch Probleme bei der Liste hast, einfach fragen ;) Ich habe die mittlerweile aus Spaß vollständig implementiert. Allerdings habe ich dabei auch festgestellt, dass die Listbox durch ihre DrawItem-Umsetzung gar nicht vernünftig auf Invalidate(Rectangle) reagiert und immer viel zu viel neuzeichnet, daurch kommt es manchmal zu flackern :/

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    Ich habe leider noch Probleme damit. Wenn ich mit dem Graphics-Objekt im DrawItem-Event einen Text auf das Element zeichne und dann mehrere Elemente erstelle, wird der Text erst angezeigt, wenn ich umher scrolle, und wenn die Items aus dem Scroll-Gebiet verschwinden und ich dann wieder hinscrolle ist der Text nicht mehr da.

    ErfinderDesRades schrieb:

    man braucht glaub garkein Invalidate - das weiß die Listbox selbst, wann und welchen Bereich sie neuzeichnen muß.

    Diese Firefox-Listbox, die imitiert werden soll, ändert das kleine Ordnerbildchen des Items, auf dem sich die Maus befindet. Man muss also zwangsläufig ab und zu im MouseMove-Event invalidaten. Aber anstatt dann nur das alte und das neue 16x16 Icon neuzuzeichnen, wird DrawItem in meinem Test für 3 von 4 Items aufgerufen. Eines davon ist auch immer das aktuell selektierte, sodass der blaue Hintergrund da schön flackert.

    @Jonas Jelonek: dann machst du im DrawItem-Event irgendetwas falsch, zeig deinen Code doch mal.

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    VB.NET-Quellcode

    1. Protected Overrides Sub OnDrawItem(e As DrawItemEventArgs)
    2. MyBase.OnDrawItem(e)
    3. e.Graphics.Clip = New Region(e.Bounds)
    4. e.Graphics.Clear(Color.White)
    5. Renderer.DrawBackground(e.Graphics, e.Bounds)
    6. With e.Graphics
    7. .DrawString("Listbox Item", Me.Font, Brushes.Black, 20, 10)
    8. End With
    9. End Sub
    Ich weiß leider auch nicht warum das nicht richtig angezeigt wird.

    vb-checker hatte ja oben im ausführlichen Beitrag folgenden Code gepostet und ein Bild, wo das angewendet wird, darunter eingefügt.

    VB.NET-Quellcode

    1. <DllImport("uxtheme", CharSet:=CharSet.Unicode)> _
    2. Public Shared Function SetWindowTheme(hWnd As IntPtr, textSubAppName As String, textSubIdList As String) As Int32
    3. End Function
    4. SetWindowTheme(Me.Handle, "explorer", Nothing)
    Damit komme ich aber leider nicht so ganz klar. vb-checker hatte gesagt, man könne das nur mir einer ListView machen, aber ich habe die View auf View.Details gestellt, eine Spalte und ein paar Items hinzugefügt und dann im Form_Load Event die SetWindowTheme-Methode aufgerufen, jedoch erschien nicht der gewünschte Effekt wie bei vb-checker.

    ErfinderDesRades schrieb:

    clippen und Clearen kannstedir glaub sparen - sogar base.OnDrawItem
    beim base.OnDrawItem kann ich zustimmen. Aber das Clearen ist nötig, da sonst der neue Text über den alten drübergezeichnet wird. Das resultiert bei Antialiasing/ClearType in kantiger Schrift. Da scheinbar die Listbox (On-)DrawItem aufruft, ohne vorher sinnvoll das ClippingRectangle zu setzen, muss man das selbst machen, sonst löscht man mit dem Clear() direkt alle Items.

    Jonas Jelonek schrieb:

    Damit komme ich aber leider nicht so ganz klar. vb-checker hatte gesagt, man könne das nur mir einer ListView machen, aber ich habe die View auf View.Details gestellt, eine Spalte und ein paar Items hinzugefügt und dann im Form_Load Event die SetWindowTheme-Methode aufgerufen, jedoch erschien nicht der gewünschte Effekt wie bei vb-checker.
    Ich verstehe nicht wirklich, warum du das jetzt probierst. Ich schrieb doch:
    Aber natürlich gibt es einen Haken. Da wir statt einer Listbox eine Listview nehmen müssen, verlieren wir die ItemHeight-Eigenschaft. Damit sind also paktisch keine so hohen Items, wie im Firefox, möglich. [..] Also können wir diesen Ansatz eigentlich vergessen.
    Meine Endlösung funktioniert ohne diesen Aufruf, falls das nicht klar wurde.
    Dein OnDrawItem-Code sieht wie von EdR schon gesagt völlig ok aus, der dürfte keine Probleme machen.

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    Ich habe das probiert, weil sich gerade das für eine andere Situation perfekt eignet, undzwar nur ein paar Elemente wie in einer Listbox mit diesem Effekt darzustellen. Das funktioniert leider nicht.
    Dass deine Endlösung auch ohne diesen Aufruf funktioniert wurde mir auf jeden fall klar.

    Und zu dem Code weiß ich leider nicht warum das nicht funktioniert. Wenn ihr sagt, dass der Code gut so ist, dann sollten eigentlich keine Probleme auftauchen, jedoch tun sie das.

    Auf dem Bild im Anhang ist das Problem mit meinem Code mit dem VisualStyleRenderer gezeigt. Zu dem anderen Problem mit der API kann ich kein Screenshot zeigen, da ich es nicht hinbekomme.
    Bilder
    • Screenshot(1).PNG

      1,86 kB, 592×279, 92 mal angesehen
    'Tschuldigung, da ist doch ein Fehler in deinem Code.

    VB.NET-Quellcode

    1. .DrawString("Listbox Item", Me.Font, Brushes.Black, 20, 10)

    Da zeichnest du ja andauernd auf den gleichen Punkt.
    Das muss so:

    VB.NET-Quellcode

    1. .DrawString("Listbox Item", Me.Font, Brushes.Black, 20, e.bounds.Y + 10)


    edit: Um meine Invalidate-Problematik mal zu verdeutlichen, habe ich das Flackern mal aufgenommen. Leider sieht man es auf der AUfnahme nicht so gut (zu wenige FPS), aber man sieht, wie beim ersten Item die "..." andauernd verschwinden, obwohl dieses Item gar keinen Grund hat, sich neuzuzeichnen.:

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    Achso. Ja jetzt geht es auch. Ich habe erst gedacht, dass das Graphics-Objekt nur für das Item ist, welches gerade gezeichnet wird, dem ist aber so wie es aussieht nicht.

    Nur wie mache ich das jetzt wie du, dass bei dir die Items nicht alle so aussehen, dass so alle selektiert wären. Und wie erkenne ich, über welchem Item sich die Maus gerade befindet.

    Jonas Jelonek schrieb:

    Nur wie mache ich das jetzt wie du, dass bei dir die Items nicht alle so aussehen, dass so alle selektiert wären.

    VB.NET-Quellcode

    1. If e.State.HasValue(komischerEnumname.Selected) then ... Else ... End If

    Jonas Jelonek schrieb:

    Und wie erkenne ich, über welchem Item sich die Maus gerade befindet.
    Im MouseMove-Event:

    VB.NET-Quellcode

    1. Me.IndexFromPoint(e.Location)

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    Ja, sorry, das meinte ich. Hatte gerade keinen Zugriff auf Visual Studio. Die Funktion ist übrigens neu in .Net 4.0. Wenn du also für 3.5 oder kleiner kompilieren willst, musst du wohl mit den logischen Verknüpfungen für Enums arbeiten, die ich mir nie merken kann.

    Skybird schrieb:

    Das sind ja Ubisoftmethoden hier !

    vb-checker schrieb:

    logischen Verknüpfungen für Enums
    Diese wären:

    VB.NET-Quellcode

    1. If (Variable And Enum.Wert) = Enum.Wert Then 'Abfragen eines bestimmten Flags
    2. Variable = Variable Or Enum.Wert 'Setzen eines bestimmten Flags
    3. Variable = Variable And Not Enum.Wert 'Entfernen eines bestimmten Flags