Hilfstexte für eine Menüpunktanwahl anzeigen

Veröffentlicht: 28. Jul 2003 | Aktualisiert: 28. Jun 2004

Von Mathias Schiffer

Die Microsoft Newsgroups sind eine Quelle schier unerschöpflichen Wissens, das nahezu auf Knopfdruck abrufbar ist: Hunderte deutschsprachige Entwickler vom Einsteiger bis zum Profi treffen sich hier täglich virtuell, um Fragen zu stellen oder zu beantworten. Auch themennahe Probleme, Ansichten und Konzepte werden miteinander diskutiert. Sie kennen die Microsoft Newsgroups noch nicht? Detaillierte Information für Ihre Teilnahme finden Sie auf der Homepage der Microsoft Support Newsgroups

Diese Kolumne greift regelmäßig ein besonders interessantes oder häufig nachgefragtes Thema aus einer der Entwickler-Newsgroups auf und arbeitet es aus.

Aus der Visual Basic Newsgroup microsoft.public.de.vb:

In vielen Anwendungen wird in der Statusleiste einer Anwendung die Funktion eines Menüeintrages erklärt, wenn dieser den Fokus besitzt. Kann man so etwas auch unter Visual Basic 6.0 realisieren?

Visual Basic 6.0 bietet diese Möglichkeit nicht von Haus aus an: Ein Ereignis, das gefeuert würde, wenn man einen Menüeintrag mit dem Mauszeiger oder der Tastatur anwählt (ohne es auszuwählen) gibt es leider nicht.

MenuSel1.gif

Abbildung 1: Statusleisten-Hilfstexte zu Menüpunkten

Dennoch haben Sie eine Möglichkeit, von einer solchen Aktion des Benutzers Kenntnis zu erlangen. Da Visual Basic selber nicht unterstützend zur Hand geht, basiert das notwendige Vorgehen komplett auf dem Windows API und involviert eine Technik, die sich "Subclassing" nennt und es ermöglicht, Steuernachrichten an Fenster mitzuhören, abzufangen oder auch modifiziert an den originalen Empfänger weiterzuleiten. Da das Thema "Subclassing" den Rahmen dieses Artikels bereits alleine sprengen würde, werden entsprechende Grundkenntnisse dieser Technik im weiteren Verlauf vorausgesetzt.

Die Menüanwahl-Nachricht: WM_MENUSELECT

Wählen Sie mit Tastatur oder Maus einen Menüeintrag an, so wird an das Fenster, das Ihr Menü enthält (unter Visual Basic üblicherweise eine Form), die Fensternachricht WM_MENUSELECT gesendet. Würden Menüs unter Visual Basic ein Ereignis "MenuSelect" anbieten, so würde dieses Ereignis immer dann gefeuert, wenn diese Nachricht eintreffen würde.

Diese Information alleine hilft natürlich noch nicht deutlich weiter: Von Interesse ist besonders, welcher Menüpunkt angewählt wurde. Diese Informationen werden mit der Fensternachricht in den Parametern wParam und lParam der Fensterfunktion (nennen wir sie WindowProc) geliefert. Dabei stellt lParam ein Handle auf das Menü dar, in dem ein Menüpunkt angewählt wurde (beachten Sie dabei: Aus Sicht des API ist auch ein Untermenü ein eigenes Menü!). In den unteren vier Bytes von wParam findet sich eine ID, die den angewählten der Menüpunkte in diesem Menü repräsentiert.

Mit den bisher gewonnenen Informationen nehmen wir einen ersten Blick auf die Subclassing-Empfängerfunktion WindowProc:

Private Function WindowProc(ByVal hwnd As Long, _ 
 ByVal Message As Long, _ 
 ByVal wParam As Long, _ 
 ByVal lParam As Long _ 
 ) As Long 
Dim hMenu As Long 
Dim MenuItemId As Long 
  ' Die Nachricht wird ohne Umwege direkt an ihren 
  ' Originalempfänger weitergegeben - wir wollen hier 
  ' nur "mithorchen", aber nichts ändern: 
  WindowProc = CallWindowProc(lpOriginalWindowProc, _ 
   hwnd, Message, wParam, lParam) 
  Select Case Message 
 Case WM_MENUSELECT ' Ein Menupunkt wird angewählt 
   ' Handle des Menüs ermitteln: 
   hMenu = lParam 
   ' Menü-ID des Menüpunkts ermitteln 
   MenuItemId = wParam And &HFFFF& 
  End Select 
End Function

Nun haben wir ein Handle auf das betroffene Menü und die ID für den angewählten Menüpunkt - eigentlich alles an Informationen, das wir benötigen, um den angewählten Menüpunkt eindeutig identifizieren zu können.

Es bleibt allein die Frage: Welchem der Menu-Controls auf der Visual Basic Form entspricht dieser eindeutig bestimmte Menüpunkt?

Vom Menüpunkt zurück zum Menu-Control

An dieser Stelle offenbart sich eine weitere Schwäche der VB-eigenen Menu-Controls: Sie zur Zusammenarbeit mit dem API zu bewegen ist schwer - bis hin zu unmöglich. Denn die Controls geben weder Aufschluss über ein Menü (geschweige denn ein API-Handle darauf), noch über die Positionen von Menüpunkten innerhalb eines Menüs.

Für unseren Fall bedeutet das praktisch, dass wir zwischen den Menu-Controls der Entwicklungsumgebung und den über die Nachricht WM_MENUSELECT gewonnenen Informationen keinerlei direkten Zusammenhang herstellen können. Irgendeinen Kompromiss werden wir hier also eingehen müssen.

Die Informationen, die WM_MENUSELECT uns an die Hand gegeben hat, können aber selbstverständlich für weitere Arbeiten mithilfe des Windows API verwendet werden. Um unserem Ziel näher zu kommen müssen wir also herausfinden, welche Übereinstimmungen die Menühandhabung durch das API und durch VB's Menu-Controls aufweisen.

Um es kurz zu machen: Die einzige auffindbare Übereinstimmung ist die Beschriftung der Menüpunkte. Unter Visual Basic können wir diese über die Caption-Eigenschaft eines Menu-Controls setzen und auslesen. Um die Beschriftung eines Menüpunkts per API auslesen zu können, benötigen wir ein Handle auf das Menü und die ID-Nummer des Menüeintrags - diese Informationen liegen uns hier dank WM_MENUSELECT vor.

Die einfachste Variante des Auslesens der Menüpunktbeschriftung erfolgt über die API-Funktion GetMenuString:

' Deklaration: 
Private Declare Function GetMenuString _ 
  Lib "user32" Alias "GetMenuStringA" ( _ 
  ByVal hMenu As Long, _ 
  ByVal ItemId As Long, _ 
  ByVal Buffer As String, _ 
  ByVal BufferMax As Long, _ 
  Optional ByVal Flag As Long _ 
  ) As Long 
' [...] 
' Anwendung: 
Dim sMenuString As String 
Dim lLen As Long 
  ' Die Beschriftung eines Menüpunkts ermitteln: 
  ' Speicherplatz reservieren 
  sMenuString = Space$(256&)  
  ' Die Beschriftung des Menüpunkts abholen 
  lLen = GetMenuString(hMenu, MenuItemId, _ 
  sMenuString, 256&, 0&) 
  ' Ergebnis auf die tatsächliche Länge kürzen 
  sMenuString = Left$(sMenuString, lLen)

Haben wir auf diese Weise die Beschriftung des angewählten Menüpunkts ermittelt, führt der Weg zum zugehörigen Menu-Control über die Controls-Collection der Visual Basic Form: In ihr werden Verweise auf alle Steuerelemente einer Form gespeichert, so auch Verweise auf sämtliche Menu-Controls auf der Form, die über den Menü-Editor von VB angelegt werden.

Wir können also über sämtliche Steuerelemente, die in der Controls-Collection gespeichert sind, iterieren und im Fall eines Menu-Objekts dessen Caption-Eigenschaft abfragen und mit unserem API-Ergebnis vergleichen:

Dim ctrl As Control 
  ' Über die Controls-Collection der Form iterieren 
  ' und bei Menu-Controls deren Caption-Eigenschaft 
  ' mit unserem API-Ergebnis vergleichen: 
  For Each ctrl In Form1.Controls 
 If TypeOf ctrl Is VB.Menu Then 
   If ctrl.Caption = sMenuString Then 
  ' Passendes Menu-Steuerelement gefunden! 
   End If 
 End If 
  Next ctrl

Weiter oben erwähnte ich von einem Kompromiss, den wir auf dem Weg von hMenu und MenuItemId zu einem Menu-Steuerelement würden eingehen müssen - hier nun können Sie diesen Kompromiss deutlich erkennen: Dieser Ansatz funktioniert nur dann wirklich zuverlässig, wenn jeder Menüpunkt eine eindeutige Beschriftung hat. Allerdings reicht es bereits, wenn zwei Menüpunkte mit gleicher Beschriftung durch Verwendung des "&"-Zeichens in deren Caption-Eigenschaft unterschiedliche Kurzauswahltasten zugewiesen wurden.

Tag-Eigenschaft zur Beschreibung verwenden

Sich Menu-Controls als Steuerelemente vorzustellen fällt mitunter ein wenig schwierig, da der VB-eigene Menü-Editor von dieser eigentlichen Natur eines Menüpunkts deutlich ablenkt. Verwenden Sie aber auf einer Form die Steuerelement-Auswahlliste über dem Eigenschaftenfenster der Entwicklungsumgebung, werden Sie dort auch die mit dem Menü-Editor angelegten Menüpunkte vorfinden. Auf diesem Weg können Sie auch zur Entwicklungszeit die Eigenschaften des jeweiligen Menu-Controls im Eigenschaftenfenster anzeigen.

MenuSel2.gif

Abbildung 2: Menu-Controls in der Steuerelement-Auswahlliste

Hinterlegen Sie also, wo gewünscht, erweiterte Informationen zu Menüpunkten idealer weise in der Tag-Eigenschaft des zugehörigen Menu-Steuerelements. Leider ist dieses Vorgehen ein wenig aufwendig, da der Menü-Editor selber keinen Zugang anbietet und jedes Menu-Control separat in das Eigenschaftenfenster gewählt werden muss. Natürlich können Sie die Tag-Eigenschaften der Controls aber auch zur Laufzeit belegen und damit an übersichtlicher Stelle in Ihrem Sourcecode halten (etwa im Form_Load-Ereignis).

Da wir das zutreffende Menu-Control für den angewählten Menüpunkt ermittelt haben, können Sie aus dessen Tag-Eigenschaft nun den erweiterten Informationstext einfach auslesen und anzeigen.

Beispielprojekt mit Feinschliff

Etwas ungünstig an der gewählten Methode ist, dass bei jedem Anwählen eines Menüpunkts die gesamte Controls-Collection der Form durchlaufen werden muss, um ein jeweils passendes Menu-Control auszuwählen. Gerade im Fall der Callback-Funktion WindowProc sind derartig arbeitsintensive, sich ständig wiederholende Vorgänge kaum sinnvoll untergebracht.

Hier macht es Sinn, vor dem Starten des Subclassings die Controls-Auflistung nur einmalig zu durchlaufen und im Fall aufgefundener Menu-Steuerelemente deren Caption- und Tag-Eigenschaften in einer eigenen Collection abzulegen. Verwenden Sie die Caption-Eigenschaft dabei als Schlüssel und den Inhalt der Tag-Eigenschaft als Element, so können Sie später anhand der per API ermittelten Beschriftung des Menüpunkts die Beschreibung des Menüpunkts direkt dieser Collection entnehmen - dieses Vorgehen ist sehr viel performanter.

Diesen Ansatz finden Sie im folgenden Beispielprojekt in der Routine Subclass realisiert: Vor Starten des Subclassings werden die Tag- und Caption-Eigenschaften vorhandener Menu-Controls ausgelesen und in der separaten Collection MenuItemDescriptions abgelegt. Auf diese Collection wird dann in der Funktion WindowProc direkt zugegriffen. Wird das Subclassing beendet, wird auch die Collection vernichtet. Möchten Sie also zwischenzeitlich Beschreibungen Ihrer Menüpunkte verändern, beenden Sie das Subclassing, nehmen Sie die Änderungen vor und starten Sie das Subclassing erneut - schon ist die eigene Collection wieder auf aktuellstem Stand.

Tipp: Kopieren Sie den gesamten Quelltext zunächst in WRITE.EXE und erst von dort aus in Ihre Entwicklungsumgebung, um den Verlust von Zeilenumbrüchen zu vermeiden.

' --------------------------------------------------- 
' BEISPIELPROJEKT MENUSELECT: Ermöglicht die Anzeige 
' von Hilfstexten zu Menüpunkten, die der Anwender 
' anwählt (nicht: auswählt). 
' 
' Fügen Sie diesen Code in ein STANDARDMODUL ein! 
' --------------------------------------------------- 
' Verwenden Sie dieses Beispiel in einem Projekt 
' mit einer Form namens Form1, auf der sich ein Label 
' namens Label1 befindet. Versehen Sie Form1 mit 
' einer Menüstruktur und weisen Sie den Tag- 
' Eigenschaften der einzelnen Menüpunkte kurze 
' Hilfstexte zu den Menüpunkten zu. 
' Um die Funktionalität dieses Moduls zu verwenden, 
' rufen Sie die Funktion Subclass auf und übergeben 
' Sie ihr die hWnd-Eigenschaft von Form1 sowie die 
' Konstante True. Um die Funktionalität aufzuheben, 
' übergeben Sie die gleiche hWnd-Eigenschaft und als 
' zweiten Parameter den Wert False. 
' Subclassing-Hinweis: Während aktiven Subclassings 
' dürfen Sie Ihr Projekt nicht über den "Stop"- 
' Button der Entwicklungsumgebung beenden! 
' --------------------------------------------------- 
Option Explicit 
Private Declare Function SetWindowLong _ 
  Lib "user32" Alias "SetWindowLongA" ( _ 
  ByVal hwnd As Long, _ 
  ByVal nIndex As Long, _ 
  ByVal NewLong As Long _ 
  ) As Long 
Private Declare Function CallWindowProc _ 
  Lib "user32" Alias "CallWindowProcA" ( _ 
  ByVal lpOrigWndProc As Long, _ 
  ByVal hwnd As Long, _ 
  ByVal Message As Long, _ 
  ByVal wParam As Long, _ 
  ByVal lParam As Long _ 
  ) As Long 
Private Declare Function GetMenuString _ 
  Lib "user32" Alias "GetMenuStringA" ( _ 
  ByVal hMenu As Long, _ 
  ByVal ItemId As Long, _ 
  ByVal Buffer As String, _ 
  ByVal BufferMax As Long, _ 
  ByVal Flag As Long _ 
  ) As Long 
Private Const GWL_WNDPROC   As Long = -4& 
Private Const WM_MENUSELECT As Long = &H11F& 
Private Const WM_DESTROY As Long = &H2 
Private lpOriginalWindowProc As Long 
Private MenuItemDescriptions As Collection 
Public Sub Subclass(ByVal hwnd As Long, _ 
  ByVal Active As Boolean) 
' Startet oder beendet das Subclassing des Fensters, 
' dessen Fensterhandle in hWnd übergeben wird. 
' Active = True startet das Subclassing 
' Active = False beendet das Subclassing 
Dim ctrl As Control 
  If Active Then 
 ' Alle Steuerelemente der Form durchgehen und im 
 ' Fall eines Menüs dessen Beschriftung und Tag- 
 ' Eigenschaft in eine Collection sichern: 
 Set MenuItemDescriptions = New Collection 
 For Each ctrl In Form1.Controls 
   If TypeOf ctrl Is VB.Menu Then 
  MenuItemDescriptions.Add CStr(ctrl.Tag), ctrl.Caption 
   End If 
 Next ctrl 
 ' Das angebenene Fenster subclassen 
 If lpOriginalWindowProc = 0 Then 
   lpOriginalWindowProc = SetWindowLong( _ 
  hwnd, GWL_WNDPROC, _ 
  AddressOf WindowProc) 
 End If 
  Else ' Active = False 
 ' Die Collection der Menüdaten vernichten 
 Set MenuItemDescriptions = Nothing 
 ' Das Subclassing aufheben 
 If lpOriginalWindowProc <> 0 Then 
   SetWindowLong hwnd, GWL_WNDPROC, lpOriginalWindowProc 
   lpOriginalWindowProc = 0 
 End If 
  End If 
End Sub 
Private Function WindowProc(ByVal hwnd As Long, _ 
 ByVal Message As Long, _ 
 ByVal wParam As Long, _ 
 ByVal lParam As Long _ 
 ) As Long 
' Wertet die Nachricht WM_MENUSELECT aus und übergibt 
' vorhandene Menü-Hilfstexte an ein Label1 auf einer Form1. 
Dim hMenu As Long 
Dim MenuItemId As Long 
Dim sMenuString As String 
Dim lLen As Long 
  ' Die Nachricht wird ohne Umwege direkt an ihren 
  ' Originalempfänger weitergegeben - wir wollen hier 
  ' nur "mithorchen", aber nichts ändern: 
  WindowProc = CallWindowProc(lpOriginalWindowProc, _ 
   hwnd, Message, wParam, lParam) 
  On Error Resume Next ' Fehler können wir hier nicht brauchen 
  Select Case Message 
 Case WM_MENUSELECT ' Ein Menupunkt wird angewählt 
   ' Handle des Menüs ermitteln: 
   hMenu = lParam 
   ' Menü-ID des Menüpunkts ermitteln 
   MenuItemId = wParam And &HFFFF& 
   ' Die Beschriftung des zugehörigen Menüpunkts 
   ' ermitteln 
   sMenuString = Space$(256&) 
   lLen = GetMenuString(hMenu, MenuItemId, _ 
   sMenuString, 256&, 0&) 
   sMenuString = Left$(sMenuString, lLen) 
   ' Bei Erfolg versuchen, in der Collection eine 
   ' Menü-Klartextbeschreibung zu finden 
   If LenB(sMenuString) Then 
  Form1.Label1.Caption = MenuItemDescriptions(sMenuString) 
   Else 
  Form1.Label1.Caption = "" 
   End If 
 Case WM_DESTROY ' Das Fenster wird zerstört 
   ' Der Programmierer hat den Aufruf 
   ' Subclass(..., False) offenbar vergessen 
   Subclass hwnd, False 
  End Select 
End Function