Ein simples Rechtsklick-Menü sollte eine VB6-Anwendung nicht ausbremsen. Genau das kann bei älteren Owner-Draw-Controls im Dauerbetrieb jedoch passieren: Die Oberfläche wirkt zunehmend träge, während die Zahl belegter GDI-Handles wächst. Deshalb habe ich MenuEx gebaut — ein natives Kontextmenü-Control, das Windows zeichnen lässt und seine Ressourcen kontrolliert wieder freigibt.
In diesem Beitrag zeige ich, wie MenuEx aufgebaut ist, warum Icons nur so lange wie nötig im Speicher bleiben und wie identische Hotkeys auch bei mehreren geöffneten Formularinstanzen zuverlässig funktionieren.
Hinweis: Die im Beitrag verwendeten Bezeichner und Pfade sind exemplarisch.


Warum ein neues Menü-Control?
Viele gewachsene VB6-Anwendungen verwenden für Kontextmenüs und Menüleisten noch kommerzielle Drittanbieter-Controls — häufig betagte ActiveX-Komponenten, die seit Jahren keine Updates mehr erhalten. Zwei Probleme fallen dabei besonders ins Gewicht.
Erstens zeichnen solche Controls ihre Menüs meist selbst (Owner-Draw). Dadurch passen sie selten zum aktuellen Windows-Theme und wirken schnell wie ein Fremdkörper in einer ansonsten modernen Oberfläche.
Zweitens kostet jedes Öffnen eines solchen Menüs Windows-Ressourcen, die nicht immer sauber wieder freigegeben werden: GDI-Handles. Läuft die Anwendung im Dauerbetrieb, führt das irgendwann zu Rucklern oder im schlimmsten Fall zum Absturz.
Die Konsequenz war für mich: kein weiteres Drittanbieter-Control, sondern ein eigenes, schlankes Menü-Control — von Grund auf mit dem Ziel entwickelt, alle verwendeten Ressourcen kontrolliert wieder freizugeben.
Windows zeichnet, MenuEx verwaltet
Der zentrale Architekturentscheid: MenuEx zeichnet selbst nichts. Es arbeitet direkt über die Windows-API — unter anderem mit TrackPopupMenuEx und InsertMenuItemW — und überlässt das komplette Rendering dem Betriebssystem. Das Menü sieht damit wie ein natives Windows-Menü aus, inklusive aktuellem Theme und alpha-transparenten Icons.
Der eigentliche Clou liegt im Ressourcen-Management. Menü-Handles und Icon-Bitmaps werden konsequent freigegeben, sobald ein Menü schließt — nicht erst beim nächsten Öffnen und nicht erst beim Beenden der Anwendung. In einem Langzeittest mit mehreren tausend aufeinanderfolgenden Menüöffnungen blieb die Zahl der belegten GDI-Handles konstant. Genau das war von Anfang an das wichtigste Entwurfsziel.
Eine echte Hierarchie statt einer flachen Sammlung
Viele klassische Menü-Controls halten sämtliche Menüpunkte in einer einzigen, flachen Sammlung — unabhängig davon, wie tief sie im Menübaum verschachtelt sind. Das wirkt zunächst praktisch, wird aber schnell zum Einfallstor für schwer auffindbare Fehler: Eine Prüfung, die eigentlich nur die aktuelle Ebene meint, trifft plötzlich auch Einträge tief in einem Untermenü.
MenuEx dreht das Prinzip deshalb um: Jede Sammlung gehört ihrem Eintrag. Ein Untermenü ist einfach Item.Items. Für die Suche im gesamten Baum gibt es zwei bewusst unterschiedliche Werkzeuge — eines löst bei einem unbekannten Schlüssel einen Fehler aus, das andere gibt lediglich False zurück. Damit ist direkt an der Aufrufstelle klar, welches Verhalten beabsichtigt ist.
Ein Kontextmenü aufbauen
Der komplette Lebenszyklus eines einfachen Kontextmenüs bleibt bewusst überschaubar:
Option Explicit
Private WithEvents mMenu As MenuExManager
Private mPopupKontext As MenuExItem
Private Sub Form_Load()
Set mMenu = New MenuExManager
Set mPopupKontext = mMenu.Items.AddPopup("Kontext", "Kontext")
mPopupKontext.Items.Add "Ausschneiden", "Kontext_Cut", "C:IconsAusschneiden.ico"
mPopupKontext.Items.Add "Kopieren", "Kontext_Copy", "C:IconsKopieren.ico"
mPopupKontext.Items.Add "Einfügen", "Kontext_Paste", "C:IconsEinfuegen.ico"
End Sub
Private Sub picArbeitsbereich_MouseUp(Button As Integer, Shift As Integer, _
X As Single, Y As Single)
If Button = vbRightButton Then
mMenu.ShowPopup mPopupKontext, hWndOwner:=Me.hWnd
End If
End Sub
Private Sub mMenu_ItemClick(ByVal Item As MenuExItem)
Select Case Item.Key
Case "Kontext_Copy"
' ... eigentliche Aktion ...
End Select
End Sub
Private Sub Form_Unload(Cancel As Integer)
If Not mMenu Is Nothing Then mMenu.Dispose
Set mPopupKontext = Nothing
Set mMenu = Nothing
End Sub
Warum Icons nur kurz leben
Spannend ist vor allem das dritte Argument von .Add. Dort steht kein fertiges Bildobjekt, sondern ein Dateipfad. Genau darin steckt der Kern des GDI-Themas:
' Variante A: Ein fertiges StdPicture-Objekt wird übergeben
Dim picKopieren As StdPicture
Set picKopieren = LoadPicture("C:IconsKopieren.ico")
mPopupKontext.Items.Add "Kopieren", "Kontext_Copy", picKopieren
' Variante B: Nur der Dateipfad wird übergeben
mPopupKontext.Items.Add "Kopieren", "Kontext_Copy", "C:IconsKopieren.ico"
Beide Varianten zeigen dasselbe Icon. Sie unterscheiden sich jedoch grundlegend darin, wem das geladene Bild gehört und wie lange es als GDI-Objekt lebt.
- Variante A: Der Aufrufer lädt das
StdPictureselbst. Das zugrunde liegende GDI-Handle bleibt so lange bestehen, wie eine Referenz auf das Objekt gehalten wird — in der Praxis häufig über die gesamte Lebensdauer des Formulars. - Variante B: MenuEx speichert zunächst nur den Pfad. Das Bild wird erst geladen, wenn der Menüeintrag tatsächlich dargestellt werden muss, in ein GDI-Bitmap umgewandelt und direkt nach dem Schließen des Menüs wieder freigegeben.
Damit bleibt kein Handle unnötig offen. Bei Menüzweigen, die der Anwender nie aufklappt, findet außerdem überhaupt kein Ladevorgang statt. Selbst ein tiefer Menübaum mit vielen Einträgen benötigt im Ruhezustand deshalb praktisch keine GDI-Ressourcen für seine Icons.
Die einfache Faustregel lautet: Wenn möglich, immer den Bildpfad übergeben und nicht selbst LoadPicture aufrufen. Eine Ausnahme ist nur sinnvoll, wenn dasselbe Bild bewusst an vielen Stellen wiederverwendet und seine Lebensdauer selbst verwaltet werden soll.
Hotkeys für mehrere Formularinstanzen
Windows reserviert globale Tastenkombinationen normalerweise systemweit exklusiv. Das wird zum Problem, sobald zwei Instanzen desselben Formulars geöffnet sind — etwa dieselbe Übersichtsmaske mit unterschiedlichen Filtern. Die zweite Instanz kann denselben Hotkey dann nicht erneut registrieren.
MenuEx verwendet stattdessen einen eigenen, thread-lokalen Tastatur-Hook. Bei jedem Tastendruck wird geprüft, welches Fenster aktiv ist. Nur der passende Menüpunkt dieses Fensters wird ausgelöst. Dadurch kann jede Formularinstanz denselben Shortcut verwenden:
Dim mnuSpeichern As MenuExItem
Set mnuSpeichern = mMenu.Items.Add("Speichern", "Datei_Speichern")
mnuSpeichern.SetHotKey True, , , vbKeyS ' Strg+S
' Bei einer reinen Kontextmenü-Instanz einmalig aufrufen:
mMenu.AttachHotKeysToForm Me.hWnd
Es gibt keinen Registrierungsfehler und keine Sonderbehandlung im Formularcode — auch dann nicht, wenn dieselbe Maske mehrfach gleichzeitig geöffnet ist.
Tooltips mit Titel und Symbol
Hover-Tooltips an Menüeinträgen können optional eine fett hervorgehobene Titelzeile und eines von drei Standardsymbolen anzeigen: Information, Warnung oder Fehler.
mnuSpeichern.ToolTipText = "Speichert das Dokument unter dem aktuellen Dateinamen"
mnuSpeichern.ToolTipTitle = "Speichern"
mnuSpeichern.ToolTipIcon = smtiInfo
Technisch steckt dahinter keine eigene Zeichenroutine, sondern eine native Nachricht an das Tooltip-Fenster, das MenuEx ohnehin verwendet. Frei wählbare Symbole sind bewusst nicht vorgesehen: Dafür wäre eine komplett selbst gezeichnete Tooltip-Logik nötig — mit deutlich mehr Aufwand und zusätzlichen Ressourcen.
Eingebaute Fehlerdiagnose
Bei einer Komponente, die gleichzeitig in vielen Formularen läuft, zählt vor allem, wie schnell sich ein Randfall nachvollziehen lässt. Deshalb schreibt MenuEx bei jedem internen Fehler automatisch eine strukturierte Logzeile — ein kompaktes JSON-Objekt pro Eintrag in einer tagesaktuellen Datei.
Durchläuft ein Fehler mehrere interne Ebenen, protokolliert jede Ebene ihren Kontext. Von unten nach oben gelesen entsteht so ein vollständiger Stacktrace, ganz ohne Debugger. Zusätzliche Details lassen sich für die Diagnose gezielt ein- und wieder ausschalten, damit im normalen Betrieb nur das Nötigste geloggt wird.
Die Menühöhe im Griff
Ein langes, dynamisch befülltes Menü kann schnell über den sichtbaren Bildschirmbereich hinauswachsen. MenuEx begrenzt deshalb die Zahl sichtbarer Einträge pro Ebene standardmäßig. Bei Bedarf blendet Windows automatisch native Scrollpfeile ein. Für Spezialfälle lässt sich diese Begrenzung abschalten und die verfügbare Bildschirmhöhe vollständig nutzen.
Bewusst gesetzte Grenzen
MenuEx bildet nicht jede Funktion klassischer Menü-Controls nach. Einige Grenzen sind bewusst Teil der Architektur:
- keine automatische MDI-Fensterliste im Menü
- kein eigenes Farbschema und kein Gradient-Look — das Erscheinungsbild kommt vollständig vom nativen Windows-Theme
- keine Integration des Windows-Systemmenüs für Minimieren, Maximieren und Schließen
Diese Funktionen würden den nativen, ressourcenschonenden Ansatz aufweichen, um den es bei MenuEx geht. Deshalb sind sie nicht einfach unfertig, sondern bewusst ausgeschlossen.
Fazit
MenuEx fühlt sich wie ein natives Windows-Menü an, gibt seine Ressourcen konsequent wieder frei und funktioniert auch bei mehreren gleichzeitig geöffneten Formularinstanzen zuverlässig. Der wichtigste Hebel gegen GDI-Probleme ist dabei oft nicht das Menü selbst, sondern eine einfache Frage: Wem gehören die geladenen Bilder — und wie lange müssen sie wirklich leben?
Weitere VB6-Projekte im Blog: moderne Diagramme mit WebView2 und Chart.js, ein Time-Picker für VB6 und ein Profiler für VB6.