ListPainter

Eigene Listen mit einem selbst gezeichneten Control anzeigen

Öfters mal brauche ich eine Listbox für eigene Zwecke. Allerdings sollte sie Text und Bilder anzeigen können. Da gibt's so schöne Beispiele im Internet, wie man einen ToolStrip für solche Zwecke einsetzen kann. Mit Hilfe eines ToolStripProfessionalRenderers und einer ProfessionalColorTable hat man dann seine Listenelemente auch gleich schick formatiert. Aber seit Windows Vista ist die von Windows gelieferte ProfessionalColorTable ziemlich arm an Farben, auch stößt man mit dem ToolStrip an Grenzen, wenn die anzuzeigende Liste etwas länger wird. Es ist einfach umständlich, immer wieder den Overflow-Button des ToolStrip zu bedienen.

Was wäre also zu tun?

Man bräuchte ein Control, das Listen wie ein senkrechter ToolStrip mit Hilfe einer Farbtabelle in fröhlichen Farben darstellen kann. So ein Control wäre hier zu sehen. Das besondere ist, dass es sich für Listen mit unterschiedlichen Arten von Items eignet. Die ListItems müssen nur eine Methode enthalten, die bedarfsweise ein Objekt liefert, das dieses Item grafisch darstellen kann. Das Control zeigt diese PaintItems mit farblichen Hoover-Effekten bei Maus-Aktionen. Nun hab ich ein bisschen mit der Professional Color Table experimentiert und ein paar Methoden ausgedacht, um die Farben in der Tabelle zu verändern und so alle abhängigen Controls in eigenen harmonischen Farben darstellen zu können. Um das zu komplettieren, enthält das Listen-Control noch eine selbst gezeichnete ScrollBar.

Erst mal ausprobieren und dann hier noch ein paar Erläuterungen:

Warum gehen die Farbübergänge so flüssig?

  • Auf der Windows Form im Beispiel befinden sich einige Standard-Controls, die nur ihre Farbpalette von der ColorTable beziehen. Bei denen geht's sowieso schnell. Bei den selbst gezeichneten Controls habe ich mir Mühe gegeben, sie geschwindigkeitsoptimiert zu gestalten.
    Um hier früher gesagtes zu korrigieren: Controls laufen nicht in einem eigenen Thread.
  • Diese selbst gezeichneten Controls sind in einzelne Zeichenflächen unterteilt. Bei der GlassScrollBar sind dies sechs Flächen, beim ListPainter so viele, wie Items gerade sichtbar sind. Und bei jedem Farbübergang von einer zur anderen Farbe (z.B. beim Hoovern) werden 20 verschiedenen Farbtöne angezeigt. 20 mal OnPaint in einer Sekunde ist nicht viel. Bei jedem Farbwechsel einer einzelnen Fläche wird nur diese Fläche nach Invalidate neu gezeichnet und nicht das ganze Control. Auch beim Hoovern mit der Maus werden im ListPainter nicht alle Items gleichzeitig zum Neu-Zeichnen aufgefordert, nur eben die, die von der Maus berührt werden. Und auch, wenn es alle gleichzeitig wären - nehmen wir mal an, es sind 30, dann wären es maximal 600 einzelne Zeichenereignisse (wenn ein Farbübergang eine Sekunde dauert). Auch bei jedem dieser Zeichenereignisse muss immer nur ein Teil des Controls neu gezeichnet werden.
  • Dass auch das Zeichnen so einer einzelnen Item-Fläche schnell geht, dafür habe ich anderweitig vorgesorgt. Das Zeichnen des Hintergrunds wird in der ListPaintItem-Basisklasse erledigt. Dafür sind nur ein Pen und ein LinearGradientBrush erforderlich, das Rechteck ist bekannt, also kann gezeichnet werden ohne große Berechnungen. Das Zeichnen des Vordergrunds eines Items erledigt in dem Beispiel das ImageTextItem (das von ListPaintItem erbt). Und hier werden nur ein paar Strings gezeichnet und eine Bitmap.
  • Achja, die Bitmap. Die beansprucht natürlich Speicher. Und wenn man die beim Zeichnen erst neu berechnen wollte, das wäre ein echter Bremsklotz. Und genau für diesen Zweck hab ich das umständliche Konstrukt mit ListItems, die ein ListPaintItem erzeugen können, gemacht.
  • Die ListItems sind die Listenelemente der für das Control zugrunde liegenden Liste, die z.B. eine Referenz auf ein größeres Image-Objekt oder nur den Pfad dazu enthalten. Und das ListPaintItem ist ein Objekt, das für das Zeichnen des Inhalts eines einzelnen ListItems sorgt. Also beim Erzeugen eines ListPaintItems durch ein ListItem wird die kleine 48-Pixel-Bitmap neu erstellt und nur dann.
  • Das ListPaintControl sorgt dafür, dass nur so viele ListPaintItems (mit Bitmap) im Speicher (in einem Dictionary) gehalten werden, wie gerade sichtbar sind. Beim Scrollen, Hinzufügen oder Entfernen von ListItems bleiben die noch sichtbaren ListPaintItems unverändert. Überflüssige werden aus dem Dictionary entfernt und neue hinzugefügt. Dann wird für alle diese sichtbaren Objekte das Zeichenrechteck neu definiert. Damit ist dafür gesorgt, dass nicht dauernd unnötigerweise Bitmaps zerstört und wieder erzeugt werden müssen. Nur dann, wenn ein Item im sichtbaren Bereich erscheint, wird eine neue Instanz eines PaintItems erzeugt, wenn es aus dem sichtbaren Bereich des Controls verschwindet, werden die Ressourcen des PaintItem wieder frei gegeben.
  • Die ScrollBar läuft ja in einem eigenen Thread und liefert OnScroll-Events. Und nach jedem dieser Events läuft die im vorherigen Absatz beschriebene Prozedur mit Neuberechnen der PaintItems(und Bitmaps) neu ab. Zum Glück arbeitet eine ScrollBar (und die hier verwendete Ownerdraw ScrollBar auch) Timer - gesteuert. Bei mir sind es glaube ich 200 Millisekunden, die zwischen zwei Scroll-Events vergehen, und die sollten ausreichen, um gegebenenfalls alle Bitmaps neu zu berechnen.
  • Hier ergibt sich eine Einschränkung: Die Quell-Bitmaps, aus denen die 48-Pixel-Thumbnails erzeugt werden, dürfen doch nicht so groß sein. Das würde den Ablauf merklich bremsen.
  • Und der "globale" Farbwechsel? Ist nur mit einem einzigen Mal OnPaint für jedes Control verbunden und auch Bitmaps brauchen dabei nicht neu berechnet zu werden.
  • Und Ihr solltet wissen, dass man den hier verwendeten ToolStripRenderer (ColorWheelToolStripRenderer) auch zum Rendern von MenuStrips und ContextMenuStrips verwenden kann. Schon mal ausprobiert?

Wer Lust hat, kann sich den Code mal gründlicher ansehen. Er ist hoffentlich verständlich genug kommentiert. Mich würde interessieren, wie Euch das Control gefällt und ob's auch auf langsameren Computern flüssig funktioniert. Also schreibt mir ruhig mal 'ne Mail.

Viel Spass beim Ausprobieren

Thomas Bergner