Die Community zu .NET und Classic VB.
Menü

Skins & Icons mit vbRichClient

 von 

Des Königs neue Kleider 

Fluch oder Segen? Diese Frage drängt sich auf, wenn Softwarehersteller bei der grafischen Gestaltung der GUIs tief in die Trickkiste greifen. Unter dem Begriff Skin oder Theme sind grafische Zusammenstellungen von GUI-Einstellungen gemeint, die das Aussehen des Betriebssystems oder (zum Leidwesen mancher Anwender) einzelner Anwendungen bestimmen.

Im letzten Tutorial haben wir eine Eingabemaske erstellt, die sich schon unübersehbar von einer klassischen VB-Maske unterscheidet. Aber Designs unterliegen trendigem Wandel und Individual-Geschmäcker sowieso. Deshalb wünscht sich manch Programmierer oder Anwender eine individuelle Oberfläche bei seinen Programmen. Unter VB ist das gar nicht so einfach. Mehr als die Umschaltung der Appearance-Eigenschaft, die zur Laufzeit auch noch schreibgeschützt ist, ist kaum möglich. Es sei denn, man erweitert den VB-Lieferumfang mit Zusatzkomponenten, wie z.B. das vor einiger Zeit hier vorgestellte Xtreme SkinFramework von Codejock-Software.

Die RC5-Widgets sind von Grunde auf so programmiert, dass das Aussehen der Steuerelemente zentralisiert konfigurierbar ist. Das will ich Ihnen am Beispiel der Eingabemaske aus dem letzten Tutorial vorstellen. Zur Erinnerung, hier noch mal ein Windows7-Screenshot dieser Maske:


Abbildung 1: Fenster aus Kolumne #3 zum RC5

Um ein Skin (Theme) umzuschalten, erweitern wir erst mal das Ansicht-Menü mit diesen Untermenüs (s. Screenshot):

Private Function MainMenuAsJSONString() As String
  With Cairo.CreateMenuItemRoot("menuBarMain", "")
    ...
    With .AddSubItem("mnuAnsicht", "&Ansicht")
      ...
      With .AddSubItem("Skin", "&Skin")
          .AddSubItem "mnuSkin1", "Standard", , , True
          .AddSubItem("mnuSkin2", "Geändert").IsCheckable = True
      End With
    End With
    ...
    MainMenuAsJSONString = .ToJSONString
  End With
End Function

Um auf die Skin-Umschaltung zu reagieren, ergänzen wir das Click-Event wie folgt:

Private Sub MenuBar_Click(ByVal CurMenuItemPath As String)
  ...
  Select Case True
    ...
    Case Left$(CurMenuItemPath, 15) = "mnuAnsicht>Skin"
      mForm.WidgetRoot.LockRefresh = True
      If CurMenuItemPath = "mnuAnsicht>Skin>mnuSkin1" Then
        Set Cairo.Theme = New_w("cThemeWin7")
      ElseIf CurMenuItemPath = "mnuAnsicht>Skin>mnuSkin2" Then
        Set Cairo.Theme = New cMyTheme
      End If
      mForm.WidgetRoot.LockRefresh = False
    ...
    End Select
End Sub

Wie Sie sehen, erfolgt die Skin-Umschaltung in drei Schritten: Das Neuzeichnen der Oberfläche wird kurzzeitig unterbunden, ein neues Skin in Form einer neuen Objektinstanz wird der Eigenschaft Cairo.Theme zugewiesen, anschließend wird die Refresh-Sperre wieder aufgehoben. Letzte Anweisung bewirkt auch ein Neuzeichnen des Fensters. Dabei kommt bereits das neue Themen-Objekt zur Geltung.

Eine Theme-Klasse implementiert die Schnittstelle cTheme aus dem vbRichClient. Folglich müssen Sie die Methoden DrawTo, FillThemeColorArray, GetIconFontName, GetScrollerSize und GetThemeColor implementieren. Über diese Methoden besorgt sich der vbRichClient z.B. Farbeinstellungen aus Ihrer Klasse. Am interessantesten ist jedoch die Methode DrawTo. Diese Methode wird beim Zeichnen von Widgets aufgerufen. Die Methode hat mehrere Parameter, die Wichtigsten schauen wir uns kurz an:

  • CC ist der CairoContext auf dem gezeichnet wird. Daruf werden die grafischen Methoden angewendet.
  • W ist eine Referenz auf das Steuerelement, bzw. auf das WidgetBase Objekt. Damit können Sie z.B. die Steuerelement-Klasse bestimmen.
  • What vom Typ enmThemeDrawingType zeigt was gezeichnet werden soll. Das könnte z.B. ein Rahmen für eine Textbox oder eine Checkbox oder ein Kreis für eine Optionsschaltfläche sein.
  • x, y, dx und dy dienen der Positionierung
  • Radius ist optional und bestimmt die Abrundung eines Rechtecks, z.B. die einer Schaltfläche.

Was und wie Sie nun was anpassen, bleibt Ihnen überlassen. Zu schnellen Ergebnissen kommen Sie, wenn Sie sich den Code der cThemeWin7-Klasse (im Lieferumfang der vbWidgets.dll) in eine neue Klasse kopieren und diese abändern. Sie können z.B. mit den Farben beginnen. In der Initialize-Prozedur der Klasse wird das Array mThemeColorArray mit Farben für Hintergründe, Linien usw. vorbelegt:

Private Sub Class_Initialize()
  mThemeColorArray(thmBackColor) = RGB(128, 128, 128)
  mThemeColorArray(thmForeColor) = RGB(241, 241, 241)
  mThemeColorArray(thmBorderColor) = RGB(33, 33, 33)
  mThemeColorArray(thmHoverColor) = RGB(255, 228, 190)
  mThemeColorArray(thmFocusColor) = RGB(0, 195, 64)
  mThemeColorArray(thmSelectionColor) = RGB(178, 214, 255)
  mThemeColorArray(thmDisabledColor) = RGB(166, 166, 166)
  ...
End Sub

Ich habe diese Klasse zur Demo mit folgenden, sich voneinander abhebenden Werten initialisiert und dabei ein paar kleine Änderungen in der DrawTo-Methode vorgenommen.

mThemeColorArray(thmBackColor) = RGB(128, 201, 255)
mThemeColorArray(thmForeColor) = RGB(0, 0, 255)
mThemeColorArray(thmBorderColor) = RGB(0, 0, 255)
mThemeColorArray(thmHoverColor) = RGB(0, 255, 255)
mThemeColorArray(thmFocusColor) = RGB(0, 255, 255)
mThemeColorArray(thmSelectionColor) = RGB(255, 0, 255)
mThemeColorArray(thmDisabledColor) = RGB(255, 0, 0)

Hier das (grässliche) Ergebnis:


Abbildung 2: Fenster mit alternativem Skin

Ein Bild sagt mehr als tausend Worte  

Software ohne Icons ist heute kaum noch vorstellbar. Ein Vorteil von Icons liegt in ihrem hohen Wiedererkennungswert. Deshalb sind sie wichtige Bestandteile grafischer Benutzeroberflächen. Wir treffen sie auf dem Desktop und in Dateimanagern, auf Toolbars und Schaltflächen, in Menüs und Listen an. Microsoft hat dem VB-Entwickler diese Gestaltungsmerkmale nur sehr eingeschränkt zur Verfügung gestellt. Schaltflächen können Icons nur im vbButtonGraphical-Style anzeigen alle anderen Standard-Steuerelemente, Menüs mit eingeschlossen, noch nicht mal so viel. Lediglich bei den Common-Controls gibt es einige grafische Optionen, die aber auch sehr eingeschränkt sind.

Was macht diesbezüglich der VB-Entwickler? Entweder er bemüht wieder mal das API und investiert viel Arbeit in Nebenschauplätzen oder er gibt Geld für Fremdkomponenten aus. Oder er verzichtet auf beides und akzeptiert dass seine Anwendungen, sagen wir, etwas altbacken aussehen. Diese Zeiten sind mit dem vbRichClient vorbei. Hier kommt die Cairo-Grafikbibliothek zum Einsatz und bietet neben einer vektorbasierten API auch umfangreiche Unterstützung für Pixelformate mit Alpha-Kanal. Der VB-Entwickler erfährt keine grafischen Einschränkungen mehr in der Gestaltung seiner GUI. Widgets leiten sich von der cWidgetBase-Klasse ab und hier finden Sie die Eigenschaft ImageKey, mit der Sie einen Schlüssel für ein Icon bestimmen können. Widgets, welche Listen von Elementen darstellen, verfügen indes über ein OwnerDraw-Ereignis, mit dem Sie das Aussehen des Listenelementes selbst steuern können.

Wenn Sie in VB zur Entwurfszeit einer Picture- oder Icon-Eigenschaft ein Bild zugewiesen haben oder die Common-Controls-Imagelist mit Icons befüllt haben, dann speichert VB diese binären Daten in frx-Dateien. Diese Ressourcen werden beim Kompilieren in die Exe eingebaut. Mit dem vbRichClient gibt es diese Art der Bild-Bereitstellung nicht mehr. RC5-Programme bedienen sich i. d. R. aus einem globalen Bildspeicher, der über die Klasse cImageList abgebildet wird. Um die Instanziierung dieses Objektes müssen sie sich nicht kümmern, es ist Teil des globalen Cairo-Objektes. Wir haben die ImageList bereits im ersten Tutorial vbRichClient Fenster kennengelernt. Dort haben wir die Liste unter anderem mit Bildern für den Fensterhintergrund befüllt.

Die cImageList Klasse  

Die cImageList-Klasse hat das Attribut PublicNotCreatable. Das bedeutet, Sie können von der Klasse keine eigenen Objekte erzeugen. Das brauchen Sie auch nicht, weil der RC5 dieses Objekt zusammen mit dem Cairo-Objekt erzeugt. Sie müssen sich lediglich um die Befüllung der Liste kümmern. Intern basiert die Klasse auf einer cCollection, deren Schlüssel eindeutig sein müssen. Die StringCompare-Methode für die Schlüssel steht auf TextCompare, das bedeutet Groß- und Kleinschreibung spielen keine Rolle.

Ansonsten bietet die Klasse die typischen Methoden zum Befüllen und Entleeren von Collections, also Add-Methoden für das Hinzufügen von Bildern und Remove-Methoden zum Entfernen selbiger. Auf einzelne Elemente greifen Sie per Key oder per nullbasierten Index zu. Um Laufzeitfehler beim Hinzufügen oder Abrufen von Items zu vermeiden, liefert die Methode Exists einen boolschen Wert über das Vorhandsein eines Schlüssels. Es gibt unterschiedliche Wege wie Sie Bildressourcen der Imagelist hinzufügen können. Folgendes Schaubild gibt Ihnen eine Übersicht.


Abbildung 3: ImageList-Quellen

Externe Ressourcen befinden sich außerhalb ihrer Exe-Datei. Das können einzelne Bilddateien oder Ressourcen-DLLs sein. Da diese Daten nicht einkompiliert werden, müssen Sie diese mit ihrer Exe mitliefern. Der große Vorteil dieser Dateien ist die einfache Verwaltung. Um Computer-Ressourcen (Arbeitsspeicher) zu sparen können sie diese Daten erst bei Bedarf nachladen und ebenso einfach wieder entladen. Die Daten sind relativ leicht austauschbar ohne dass Sie die Exe neu kompilieren müssen. So können Sie länderspezifische Bilder oder kontrastreiche Bilder für schwierige Lichtverhältnisse oder Sehbehinderte anbieten. Sie könnten sogar ihren Anwendern die Möglichkeit einräumen, diese Bilder selbst zu gestalten.

Interne Ressourcen stellen eine praktische Methode zum Speichern von Bild-Daten für eine Anwendung dar, wobei dem Benutzer die Quelldateien verborgen bleiben. Die Daten werden in die Exe einkompiliert. Das macht die Installation oder Weitergabe der Anwendung einfacher.

Externe Ressourcen  

Bilddateien

Erstellen Sie einen Ressourcen-Ordner, packen Sie hier alle Bilddateien hinein und laden Sie die Bilder bei Bedarf in die Imagelist wie folgt:

With Cairo.ImageList
  .AddImage "icnPopupCopy", AppPath & "Res\Copy.png"
  .AddImage "icnPupupPaste", AppPath & "Res\Paste.png" 
  ...
End With

Die Methode AddImage verfügt über optionale Parameter, mit denen Sie gewünschte Abmessungen und die Original-Abmessungen der Bilder angeben können. Intern erzeugt die Klasse für jedes Bild ein Surface-Objekt. Stellen Sie sich ein Surface als InMemory-Bitmap vor (hDIB). Entsprechend dem Bedarf werden diese Bitmaps bei der Angabe von DesiredWidth und DesiredHeight skaliert. Hier greift auch der Parameter KeepAspectRatio, der dafür sorgt, dass das Seitenverhältnis beibehalten bleibt oder alternativ das Bild verzerrt werden kann.

Statt eines Dateinamens kann die AddImage-Methode auch ein Byte-Array mit Bilddaten entgegen nehmen, dazu kommen wir später.

Die AddImage-Methode fügt der Internen Collection das erzeugte Sourface-Objekt zu und gibt eine Referenz darauf zurück. Folglich könnten Sie gleich im Anschluss Cairo-Grafikmethoden auf dessen Kontext anwenden, um so das Bild zu bearbeiten oder zu ergänzen.

Bildressource in DLL-Dateien

Machen Sie es wie Microsoft: packen Sie alle wichtigen Icons in eine DLL, siehe die shell32.dll. Aus dieser Bibliothek können Sie zur Laufzeit ihres Programms einzelne oder alle Bilder laden:

With Cairo.ImageList
  .AddIconFromResourceFile Key:="icnPopupCopy", _
        AppPath & "popup.dll", RessourceID:=1, _
        DesiredWidth:=24, DesiredHeight:=24
  .AddIconFromResourceFile "icnPupupCut", _
        AppPath & "popup.dll", 2, 24, 24
End With

Die Methode AddIconFromResourceFile erwartet wieder einen eindeutigen Bezeichner, die Quelldatei, die eine DLL oder eine EXE sein kann und schließlich eine RessourceID aus der Quelle. Die Parameter DesiredWidth und DesiredHeight sind auch hier optional. Als Rückgabewert erhalten Sie auch diesmal eine Referenz auf das interne Surface.

Auch wenn das nicht mehr ein RC5-Thema ist, möchte ich doch noch einen einfachen Weg aufzeigen, wie Sie Ressourcen-DLLs selbst erzeugen können. VB unterstützt so etwas nicht und auch im vbRichClient finden sich dafür keine Funktionen. Am einfachsten dürfte wohl der Einsatz eines VB-AddIns sein. Auf der Seite http://vb.mvps.org/tools/vbAdvance/ können Sie sich das kostenlose vbAdvance-AddIn herunterladen und installieren. Erzeugen Sie nun ein neues VB-Projekt und binden hier eine Ressource mit den Icons ein. Dafür können Sie den mit VB mitgelieferten Ressourcen-Editor (resEdit6) verwenden. Dieser ist aber inzwischen auch etwas veraltet und unterstützt nicht mehr alle ICO-Formate. Es gibt im Internet diverse Icon-Editoren, die ebenfalls Ressource-Dateien (.res) erzeugen können, z.B. der Greenfish Icon Editor (Anmerkung: In der von mir verwendeten Version 3.31 kann dieser auch direkt DLLs erzeugen). In den "erweiterten Optionen" von vbAdvance finden Sie den Tab "pure Resourcen.DLL". Damit können Sie die Ressource zu einer DLL kompilieren. Genaugenommen dient vbAdvance an dieser Stelle nur als Frontend für das cvtres-Tool von Microsoft.

WebArchiv

Die cWebArchiv-Klasse ist Teil vom vbRichClient und dient eigentlich als Quelle für Web-Ressourcen, also HTM-Dateien, JavaScripts und Web-Bilder. Jedoch kann man sie auch zweckentfremden und so zusätzliche Vorteile nutzen. Mit einer kleinen Hilfsroutine lesen Sie alle Dateien einer beliebigen Ordnerstruktur ein (Ordner und Unterordner). Diese Daten können danach in eine einzelne Datei exportiert werden:

Public Sub CreateWebArchive()
Dim webArchiv As cWebArchive
    Set webArchiv = New_c.WebArchive
    webArchiv.ReadContentsFromDirectory "C:\Cairo\04\PNG\"
    webArchiv.SaveContentsToArchiveFile "C:\Cairo\04\archiv.war"
End Sub

Diese Prozedur ist nicht Teil Ihrer Anwendung, sie dient lediglich der Erzeugung des WebArchivs. Sie können sich gerne ein kleines Programm schreiben, dass per Kommandozeilenparameter den Quellpfad und die Zieldatei entgegennimmt und so das WebArchiv erzeugt.

In Ihrer Anwendung können Sie anschließend dieses WebArchiv aus der Datei auslesen und die Bilder per GetContent (liefert ein Byte-Array) in die ImageList übertragen. Als Schlüssel verwenden Sie den ursprünglichen Dateinamen des Bildes. Sollte der Quellordner aus Verzeichnissen und Unterverzeichnissen bestanden haben, so geben Sie die relativen Pfade im Key-Parameter an.

Dim webArchiv As cWebArchive
  Set webArchiv = New_c.WebArchive
  webArchiv.ReadContentsFromArchiveFile AppPath & "archiv.war"
  With Cairo.ImageList
    .AddImage "Bayern", webArchiv.GetContent("by.png"), 15, 18
    .AddImage "Berlin", webArchiv.GetContent("be.png"), 15, 18
    ...
  End With

Der Vorteil der Webarchive ist, dass Sie hier beliebige Dateien zusammenpacken können und die Daten hierarchisch in Ordnerstrukturen verwalten. Optional können Sie beim Speichern und Lesen den Parameter GZcompressed angeben. Das sorgt für platzsparende komprimierte Dateien. Sie benötigen für WebArchive keine externen Zusatztools. Der Vollständigkeit halber sei erwähnt, dass die cWebArchive noch weitere Methoden besitzt, die aber im aktuellen Kontext keine Rolle spielen.

Interne Ressourcen  

Ressourcedatei (.res)

Eine Ressourcedatei ermöglicht es Ihnen, alle Texte und Bitmaps einer Anwendung an einer Stelle zu sammeln. Ein vbRichClient-Projekt kann wie jedes andere VB-Projekt eine einzelne Ressource-Datei enthalten. Sie können diese mit dem Ressourcen-Editor Add-In erstellen. Mit der VB-Funktion LoadResData lesen Sie ein Byte-Feld aus der Ressource und fügen das ihrer ImageList hinzu:

Cairo.ImageList.AddImage "appIcn", LoadResData(1, 3)

Auch wenn Sie Ihre Bildressourcen anders verwalten wollen, empfiehlt sich eine Ressource mit zumindest einem Applikations-Icon. Dieses wird von Windows im Datei-Explorer oder in der Desktop-Verknüpfung ihrer Anwendung angezeigt.

Base64-kodierte Strings

Eine weitere Methode um Bildressourcen einzubetten besteht darin, dass Sie ein Bild binär einlesen und per Base64 zu einem String kodieren. Die so entstehenden Strings können Sie direkt in den Quellcode ihrer Prozeduren einfügen. Auch dafür erzeugen wir uns eine kleine Behelfsprozedur, die später nicht Teil ihrer Anwendung sein wird.

Public Sub CreateBase64Icon()
Dim code As String, txt As String
Dim i As Long
Dim icn() As Byte

    icn = New_c.FSO.ReadByteContent(AppPath & "save.png")
    txt = New_c.Crypt.Base64Enc(icn)

    For i = 1 To Len(txt) Step 800
        code = code & """" & Mid$(txt, i, 800) & """ & _" & vbCrLf
    Next i
    
    Clipboard.Clear
    Clipboard.SetText Left$(code, Len(code) - 5)
End Sub

Wir verwenden hier die Klasse cFSO aus dem RC5 um eine Bilddatei in ein Byte-Feld einzulesen. cCrypt ist ebenfalls eine Klasse aus dem RC5, die verschiedene Ver- und Entschlüsselungsfunktionen bietet. Wir benutzen die Base64Enc-Methode und erhalten so einen String. Um diesen besser in VB verwenden zu können, wird er in der For-Schleife zu 800-Zeichen langen Teilstrings zerlegt. Das Ergebnis wird in die Zwischenablage kopiert. Den so erhaltenen Code können Sie direkt in Ihrer Anwendung verwenden:

Dim icnCancel As String
Dim cry As cCrypt

  Set cry = New_c.Crypt

  icnCancel = "iVBORw0KGgoAAAANSUhEUgAAAB"  
  
  With Cairo.ImageList
    .AddImage "icnCancel", cry.Base64Dec(icnCancel, True)
  End With

Freilich taugt dieses Verfahren nur für kleine Bild-Dateien, hat aber den großen Vorteil, dass Sie die Bilddaten so direkt in Ihre Klassen einbetten können.

Selbstgezeichnete Bilder

Zu guter Letzt können Sie Ihre Bilder auch direkt in Ihrer Anwendung erzeugen. Sie erstellen sich ein CairoSurface per Cairo.CreateSurface und zeichnen auf dessen CairoContext mit den umfangreichen Cairo-Grafik-Methoden ihr Bild. Dieses können Sie dann in die ImageList einfügen. So gesehen kann die ImageList auch als Cache für selbstgezeichnete Bilder oder Bildvorlagen dienen.

Jedem Widget sein Bild  

Im letzten Tutorial haben wir uns eine Eingabemaske aus unterschiedlichen Widgets zusammengebaut. Da wir nun unsere ImageList mit allen möglichen Bildern und Icons befüllt haben, wollen wir diese zum Aufhübschen unserer Widgets verwenden. Fangen wir mit dem Kontextmenü an. Hier ist es ganz einfach. Wir müssen lediglich den Key eines Bildes aus der ImageList bei der SubItem-Erzeugung mit angeben:

With CreatePopUpForTextBox
  .AddSubItem "puCut", "&Ausschneiden", "icnPupupCut", HasSelection
  .AddSubItem "puCopy", "&Kopieren", "icnPopupCopy", HasSelection
  ...
End With

Das Ergebnis kann sich sehen lassen:


Abbildung 4: Kontext-Menü für Textfelder

Kommen wir zu den Schaltflächen. Hier ist der Aufwand auch nicht größer. Die WidgetBase verfügt über die Eigenschaft ImageKey, der wir den gewünschten Bild-Schlüssel zuweisen.

Set btn = mForm.Widgets.Add(New_w("cwButton"), "btnSpeichern", _
    .X(31), .Y(13), 90, 23)
btn.Caption = "Speichern"
btn.Widget.ImageKey = "icnSave"

Hier das Ergebnis:


Abbildung 5: Dialog-Buttons

Etwas aufwendiger ist die DropDown-Liste. Vorab hier ein Screenshot der geöffneten und mit Icons bestückten Liste:


Abbildung 6: DropDown-Liste

Intern setzt sich das DropDownList-Widget aus mehreren Widgets zusammen: cwDropdown ist der immer sichtbare Teil des Widgets, der wiederum eine Eigenschaft PopupWidget hat. Dahinter verbirgt sich eine cwVList, welche die aufklappbare Liste darstellt. Diese kann eine einspaltige Liste sein, ebenso eine mehrspaltige Tabelle. Wir benötigen einen Verweis auf diese interne VList. Dafür deklarieren wir eine Objektvariable vom Typ cwVList und verwenden dafür das Schlüsselwort WithEvents. Damit benachrichtigt uns die VList, wenn ihre Items neu gezeichnet werden.

Option Explicit

Private WithEvents ddlBundeslaender As cwDropDownList
Private WithEvents ddvlBundeslaender As cwVList

Private Sub mForm_Load()
...
Set ddlBundeslaender = mForm.Widgets.Add(New_w("cwDropDownList"), _
  "ddlBundesland", .X(20), .Y(8), 200, 23)            
  Set ddvlBundeslaender = ddlBundeslaender.VList
  ddvlBundeslaender.RowHeight = 22
  ddlBundeslaender.SetDataSource bundeslaender, "Bundesländer"
  ddlBundeslaender.DataSource.Sort = "Key Asc"
...
End Sub

...

Private Sub ddvlBundeslaender_OwnerDrawItem(ByVal Index As Long, _
    CC As vbRichClient5.cCairoContext, _
    ByVal dx As Single, _
    ByVal dy As Single, _
    ByVal Alpha As Single)
Dim blName As String
Dim widgetWidth As Single
Dim scrolbarWidth As Single
    widgetWidth = ddlBundeslaender.Widget.Width
    scrolbarWidth = ddvlBundeslaender.VScrollBar.Widget.Width
    blName = ddlBundeslaender.DataSource.ValueMatrix(Index, 0)
    CC.RenderSurfaceContent blName, _
        widgetWidth - scrolbarWidth - 22, 2, 15, 18
End Sub

Das Ereignis OwnerDrawItem bietet uns im CC-Parameter einen cCairoContext, also die Zeichenfläche auf der das aktuelle Item dargestellt wird. An dieser Stelle greifen wir kurz ein und holen uns den Namen des Bundeslandes aus der DataSource. Um die Icons rechts auszurichten brauchen wir noch die Breite der VList und die der Scrollbar. Nun können wir mit der Methode RenderSurfaceContent den Bild-Key eines Icons aus der globalen Cairo-ImageList übergeben, ebenso die berechnete Position für das Icon. Den Rest der Darstellung übernimmt der vbRichClient.

Vererbung  

Das bisher Erreichte nicht nur schick aus, es ist auch noch praktisch und entlastet den VB-Entwickler von einigen Routinearbeiten. Das alleine möchte ich aber noch nicht als Fazit stehen lassen. Es geht nämlich noch besser: Benötigen Sie die Bundesländer-DropDown-Liste in unterschiedlichen Masken oder Anwendungen? Wie wäre es mit einem eigenen Widget, in dem die Bundesländer und deren Icons schon enthalten sind? Die DropDown-Liste und deren Funktionalität sind schon vorhanden. Man müsste ja nur die Namen der Bundesländer und deren Icons integrieren. Ein klassischer Fall für Vererbung also.

In VB unterstützen COM-Objekte allerdings nur die Schnittstellenvererbung. VB bietet uns darüber hinaus zwei weitere Möglichkeiten, wie ein neues Objekt auf die Methoden und Eigenschaften eines übergeordneten Objektes zugreifen kann: Einschluss und Aggregation. Dabei fungiert das äußere Objekt als Client des Inneren Objektes. Wenn eine Anwendung die Schnittstelle des äußeren Objektes aufruft, wird von diesem die Methode einer Schnittstelle des inneren Objektes aufgerufen.

Es ist nicht schwierig ein Widget in eine eigene Klasse einzubinden und diese Klasse als neues Widget erscheinen zu lassen. Wir erstellen eine neue Klasse cwBundeslaender. Im Initialize-Ereignis dieser Klasse laden wir die Bundesländer-Namen und -Icons als Base64-Strings in eine Collection.

Option Explicit

Public Event Click()

Private WithEvents ddlBundeslaender As cwDropDownList
Private WithEvents ddvlBundeslaender As cwVList

Private Sub Class_Initialize()
Dim bundeslaender As cCollection
Dim blIcon(15) As cCairoSurface
Dim cry As cCrypt
  Set cry = New_c.Crypt

  Set blIcon(0) = Cairo.CreateSurface(15, 18, ImageSurface, _
      cry.Base64Dec(iVBORw0...
  ...
  Set bundeslaender = New_c.Collection()
  With bundeslaender
        .Add blIcon(0), "Baden-Württemberg"
        ...
  End With
  Set ddlBundeslaender = New_w("cwDropDownList")
  Set ddvlBundeslaender = ddlBundeslaender.VList
  
  ddvlBundeslaender.RowHeight = 22
  ddlBundeslaender.SetDataSource bundeslaender, "Bundesländer"
  ddlBundeslaender.DataSource.Sort = "Key Asc"
End Sub

Wir erzeugen eine cwDropDownList-Instanz und weisen der DataSource dieser Instanz die Bundesländer-Collection zu. Im OwnerDrawItem-Event der DropDownList zeichnen wir die Icons genauso wie wir das bisher in der Form gemacht haben. Das Click-Event der Liste leiten wir als Event der cwBundeslaender-Klasse einfach weiter. Ebenso verfahren wir mit ein paar weiteren internen Eigenschaften wie ListIndex, Text, ListCount usw. Wir werden jedoch nicht alle Eigenschaften der internen DropDownList weiterreichen, so zum Beispiel die DataSource. Es ist schließlich nicht erforderlich die Bundesländerliste von außen zu bearbeiten.

Private Sub ddvlBundeslaender_Click()
    RaiseEvent Click
    ddlBundeslaender.Widget.RaiseBubblingEvent Me, "Click"
End Sub

Public Property Get CurrentListItem() As String
  CurrentListItem = ddlBundeslaender.DataSource.FieldValue(0)
End Property

Unserem Widget spendieren wir eine neue schreibgeschützte Eigenschaft CurrentListItem, um die DataSource auch für den Lesezugriff unzugänglich zu machen. Der Code in der Fensterklasse cfMain schaut nun sehr viel aufgeräumter aus weil die Interna der Bundesländerliste in einer eigenen Widget-Klasse gekapselt sind. Sie sollen dieses Prinzip nach Möglichkeit immer anwenden. Sie bekommen zum einem wiederverwendbare Komponenten, zum anderen konzentriert sich der Code in den Fensterklassen auf das Wesentliche, ist viel kompakter und dementsprechend leichter zu lesen.

Private Sub mForm_Load()
  ...
  Set ddlBundeslaender = mForm.Widgets.Add(New cwBundeslaender, _
            "ddlBundesland", .x(20), .y(8), 200, 23)
  ...
End Sub

Private Sub ddlBundeslaender_Click()
    Debug.Print ddlBundeslaender.CurrentListItem
End Sub

Sie finden den Beispielcode für dieses spezialisierte Widget im Ordner 04.1 des Beispielprojekts.

Eine weitere Option wäre die Entwicklung eines neuen Widgets. Wie sie gesehen haben, ist die cwDropDownList auch nur ein zusammengesetztes Widget. Dessen Quellcode können wir uns von GitHub herunterladen und in ein eigenes Klassenmodul einfügen. Nun erweitern wir diesen Code mit den Base64-kodierten Icon-Strings. Im OwnerDrawItem-Ereignis der internen VList wird der Name des Bundeslandes mit der Methode DrawText des CairoContextes ausgegeben. Hier klinken wir uns ein und ergänzen die Ausgabe mit den Icons. Dafür müssen wir nicht die globale ImageList bemühen, weil wir die internen Surfaces verwenden, die wir mit den Daten der Icon-Strings befüllt haben. Gleiches gilt für die Namen der Bundesländer. Zu guter Letzt können wir in diesem Widget die SetDataSource-Funktion von Public auf Private ändern. Von außen sollen schließlich keine neuen Bundesländer hinzugefügt werden. Der Code dieses Widgets ist etwas umfangreicher. Theoretisch wäre jedoch dieses Widget etwas performanter, weil hier auf eine Vererbungsschicht verzichtet wird. Ob dieser Performance-Gewinn messbar ist, wage ich zu bezweifeln.

Den Beispielcode für dieses spezialisierte Widget finden Sie im Ordner 04.2 des Beispielprojekts.

Fazit  

Die altbackenen VB-Masken haben mit dem RC5 endgültig ausgedient. Sollte die VB-Entwicklergemeinde den vbRichClient annehmen, ist es nur eine Frage der Zeit bis sich neue Skins und Widgets von diversen Anbietern im Internet finden werden. Am Beispiel der Widget-Vererbung sehen Sie, dass auch die Programmarchitektur sich maßgeblich verbessern kann. Die VB-Usercontrols waren lange nicht so flexibel, dagegen aber viel schwergewichtiger als die neuen Widgets. Diese können Sie sehr leicht in eigene DLLs auslagern, um so die Wiederverwendung zu erleichtern. Wohlgemerkt, diese DLLs müssen beim Kunden nicht registriert werden.

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.