Ganz entsprechend Murphys berüchtigtem Gesetz ist eine ListBox fast immer zu schmal, um sämtliche Listeneinträge in voller Breite anzeigen zu können. Eine Abhilfe wäre, eine ListBox mit einem horizontalen Rollballen zu versehen - eine nicht gerade triviale Aufgabe, wenn sie ordentlich gelöst werden soll, und keine sonderlich benutzerfreundliche Lösung. Eine andere Möglichkeit wäre, überlange Listeneinträge im Tooltip der ListBox anzuzeigen. Doch der standardmäßige Tooltip hat den Nachteil, dass er immer ein Stück weit unter dem Mauszeiger und nach rechts versetzt erscheint. Besser und schöner wäre es, den Tooltip genau an der Stelle des Listeneintrags anzuzeigen - so wie es etwa das TreeView-Steuerelement richtigerweise vormacht.
Der praktischste Weg führt über ein UserControl, das als Zusatz-Steuerelement mit der betreffenden ListBox verbunden wird. Dieses Zusatz-Steuerelement, ListToolTip genannt, überwacht selbsttätig und unabhängig die Position des Mauszeigers über der ListBox und zeigt den Text eines Listeneintrags an, wenn dessen Breite die Breite der ListBox überschreitet. Sie brauchen an keiner anderen Stelle weiteren Code zu schreiben, etwa im Form-Modul auf dem die ListBox und das Zusatz-Steuerelement platziert sind.
Die Alternative, eine ListBox direkt auf einem UserControl zu platzieren und ein neues ListBox-Steuerelement zu schaffen, das um das gewünschte Feature erweitert wäre, hätte nur wenig Sinn wegen des Aufwandes, alle Kombinationsmöglichkeiten der nur zur Entwicklungszeit setzbaren Eigenschaften einer ListBox zu ermöglichen.
Zur Verknüpfung von ListBox und Zusatz-Steuerelement bedienen wir uns der in Steuerelemente auf Brautschau gezeigten komfortablen Technik und erübrigen uns daher, hier näher darauf einzugehen. Die Darstellung des Steuerelements zur Entwicklungszeit erfolgt ebenso wie dort dargestellt.
Neben der zur Verbindung notwendigen ListBox-Eigenschaft verfügt ListToolTip speziell lediglich noch über die Eigenschaft NoBotton. Über diese legen Sie fest, ob ein überbreiter Listeneintrag nur dann als Tooltip angezeigt werden soll, wenn keine Maustaste niedergedrückt ist, oder ob er in jedem Fall angezeigt werden soll.
Private pNoButton As Boolean
Public Property Get NoButton() As Boolean
NoButton = pNoButton
End Property
Public Property Let NoButton(ByVal New_NoButton As Boolean)
pNoButton = New_NoButton
PropertyChanged "NoButton"
End Property
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
'...
pNoButton = PropBag.ReadProperty("NoButton", False)
'...
End Sub
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
'...
PropBag.WriteProperty "NoButton", pNoButton, False
End Sub
Die eigentliche "harte" Arbeit wird bei der Bearbeitung des abgefangenen MouseMove-Ereignisses der ListBox getan. Das Grundprinzip der Tooltip-Anzeige lautet grob umrissen: Zuerst wird festgestellt, ob und über welchem Listeneintrag sich der Mauszeiger befindet. Falls die Anzeige wegen der Überbreite des gefundenen Listeneintrags notwendig sein sollte, wird das Fenster des UserControls als Kind dem Desktop zugeordnet, damit der Text des Tooltips gegebenenfalls auch die Fläche des Forms, auf dem sich die ListBox befindet, überragen kann. Steht der Mauszeiger nicht mehr über dem betreffenden Tooltip, wird dieser wieder verborgen.
Die Umsetzung dieser Funktion erfordert ein paar Griffe in die API-Trickkiste - die Deklarationen hierfür lauten:
Private Type POINTAPI
X As Long
Y As Long
End Type
Private Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Private Declare Function GetClientRect Lib "User32" _
(ByVal hWnd As Long, lpRect As RECT) As Long
Private Declare Function GetCursorPos Lib "User32" _
(lpPoint As POINTAPI) As Long
Private Declare Function GetDesktopWindow Lib "User32" () As Long
Private Declare Function GetParent Lib "User32" _
(ByVal hWnd As Long) As Long
Private Declare Function GetWindowLong Lib "User32" _
Alias "GetWindowLongA" (ByVal hWnd As Long, _
ByVal nIndex As Long) As Long
Private Declare Function GetWindowRect Lib "User32" _
(ByVal hWnd As Long, lpRect As RECT) As Long
Private Declare Function SendMessage Lib "User32" _
Alias "SendMessageA" (ByVal hWnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) _
As Long
Private Declare Function SetParent Lib "User32" _
(ByVal hWndChild As Long, ByVal hWndNewParent As Long) As Long
Private Declare Function SetWindowLong Lib "User32" _
Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, _
ByVal dwNewLong As Any) As Long
Private Declare Function SetWindowPos Lib "User32" _
(ByVal hWnd As Long, ByVal Order As Long, ByVal X As Long, _
ByVal Y As Long, ByVal cX As Long, ByVal cY As Long, _
ByVal Flags As Long) As Long
Private Declare Function WindowFromPoint Lib "User32" _
(ByVal xPoint As Long, ByVal yPoint As Long) As Long
Private Const GWL_EXSTYLE = (-20)
Private Const WS_EX_TOOLWINDOW = &H80
Private Const LB_GETITEMHEIGHT = &H1A1
Private Const LB_ITEMFROMPOINT = &H1A9
Private Const SWP_NOACTIVATE = &H10
Private Const HWND_TOPMOST = -1
Kommen wir nun zu der Ereignisprozedur eListBox_MouseMove.
Private Sub eListBox_MouseMove(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
Dim nXPoint As Long
Dim nYPoint As Long
Dim nIndex As Long
Dim nItemHeight As Long
Dim nLeft As Long
Dim nTop As Long
Dim nWidth As Long
Dim nHeight As Long
Dim nTopOffset As Integer
Dim nLeftOffset As Integer
Dim nRect As RECT
Dim nToolTip As String
Ist einer der Mausknöpfe niedergedrückt, entscheidet die Einstellung der Eigenschaft NoButton darüber, ob fortgefahren werden soll, oder ob ein eventuell sichtbarer Tooltip verborgen und die Prozedur ohne weitere Bearbeitung auf jeden Fall verlassen werden soll.
If Button Then
If pNoButton Then
zHideToolTip
Exit Sub
End If
End If
Anschließend werden die Koordinaten des Mauszeigers innerhalb der ListBox-Fläche für die weitere Verwendung mit den API-Funktionen in Pixels umgerechnet.
nXPoint = X / Screen.TwipsPerPixelX
nYPoint = Y / Screen.TwipsPerPixelY
Über einen Aufruf der API-Funktion SendMessage mit der Nachricht LB_ITEMFROMPOINT erfragen wir, ob sich ein Listeneintrag unter dem Mauszeiger befindet und, wenn ja, dessen Index. Die Koordinaten werden zu dem im Parameter lParam erwarteten Long-Wert zusammengesetzt.
With eListBox
nIndex = SendMessage(.hWnd, LB_ITEMFROMPOINT, 0, _
nYPoint * 65536 + nXPoint)
War die Abfrage erfolgreich, lesen wir den zu dem Index gehörenden Text des Listeneintrags aus und ermitteln dessen Breite. Da wir einen Wert in Pixels benötigen, haben wir der Einfachheit halber den ScaleMode des UserControls auf vbPixels voreingestellt, und können nach der Zuweisung des aktuellen Fonts der ListBox an das UserControl dessen Methode TextWidth zur Ermittelung der Textbreite verwenden.
Select Case nIndex
Case 0 To .ListCount - 1
nToolTip = .List(nIndex)
Set UserControl.Font = .Font
nWidth = UserControl.TextWidth(nToolTip)
Dann ermitteln wir mittels der API-Funktion GetClientRect das Innenrechteck und somit die exakte Innenbreite der ListBox. Sie unterscheidet sich in jedem Fall von der Außenbreite der ListBox und ist dazu noch je nach Einstellung der Eigenschaft Appearance der ListBox (0 - 2D oder 1 - 3D) unterschiedlich (siehe auch Innenmaße einer ListBox).
GetClientRect .hWnd, nRect
Ist die Innenbreite kleiner als die Textbreite, ermitteln wir mit GetWindowRect die Koordinaten des Rechtecks der ListBox in absoluten Bildschirmkoordinaten - indem wir uns im weiteren auf der Basis der Bildschirmkoordinaten bewegen, ersparen wir uns jegliche Hin- und Her-Rechnerei zwischen den verschiedenen Fensterbezugssystemen.
If nRect.Right - nRect.Left < nWidth Then
GetWindowRect .hWnd, nRect
Für den Fall, dass bei der ListBox ein 3D-Rahmen eingestellt sein sollte, legen wir einen Offset von jeweils einem Pixel in vertikaler und in horizontaler Richtung fest.
If .Appearance = 1 Then ' 3D
nLeftOffset = 1
nTopOffset = 1
End If
Die tatsächliche Höhe eines Listeneintrags in Pixels ermitteln wir mit einem Aufruf von SendMessage mit der Nachricht LB_GETITEMHEIGHT. Anhand des bereits ermittelten Index und des obersten in der ListBox angezeigten Index (TopIndex) sowie der Oberkante des Rechtecks der ListBox, der Listeneintragshöhe (ItemHeight) und dem vertikalen Offset legen wir die Top-Position des anzuzeigenden Tooltips fest. Die Left-Position ergibt sich aus der linken Kante des ListBox-Rechtecks und des horizontalen Offsets.
nItemHeight = SendMessage(.hWnd, LB_GETITEMHEIGHT, _
0, 0)
nTop = nRect.Top + (nIndex - .TopIndex) * nItemHeight _
+ nTopOffset
nLeft = nRect.Left + nLeftOffset
Die Breite des als Tooltip anzuzeigenden Textes des Listeneintrags vergrößern wir um die Breite eines Leerzeichens und die Höhe des Tooltips entspricht der Texthöhe zuzüglich 3 Pixels als Rahmendistanz.
With UserControl
nWidth = nWidth +.TextWidth(" ")
nHeight = .TextHeight("A") + 3
Nun prüfen wir noch, ob der Tooltip über den rechten oder unteren Bildschirmrand hinausragen würde und verschieben die Koordinaten gegebenenfalls.
With Screen
If nLeft + nWidth > .Width \ .TwipsPerPixelX Then
nLeft = (.Width \ .TwipsPerPixelX) - nWidth
End If
If nTop + nHeight > .Height \ .TwipsPerPixelY Then
nTop = (.Height \ TwipsPerPixelY) - nHeight
End If
End With
Bereits während der Bearbeitung des Ereignisses UserControl_ReadProperties haben wir das Fenster-Handle des Desktops ermittelt (siehe unten) und stellen mit der API-Funktion GetParent fest, ob das UserControl bereits dem Desktop als Kind-Fenster zugeordnet ist. Falls nicht, erfolgt dies über einen Aufruf von SetParent.
If GetParent(.hWnd) <> mDesktopWindow Then
SetParent .hWnd, mDesktopWindow
End If
Mit einem Aufruf der API-Funktion SetWindowPos erledigen wir gleich zweierlei. Erstens sorgen wir sicherheitshalber dafür, dass das Fenster des UserControls (der Tooltip) vor allen anderen Fenstern erscheinen wird. Und zweitens bringen wir es (ähnlich einer Move-Anweisung) an die richtige Position und auf die gewünschte Größe.
SetWindowPos .hWnd, HWND_TOPMOST, nLeft, nTop, _
nWidth, nHeight, SWP_NOACTIVATE
Wir verwenden hier bewusst nicht das mögliche Flag SWP_SHOWWINDOW, um das UserControl als Tooltip sichtbar zu machen, da dies die interne Fensterverwaltung von VB durcheinanderbringen und zu unschönen Flackereffekten beim Verbergen des Tooltips führen würde. Außerdem müssen wir auch erst noch den Inhalt zeichnen. Damit wir ihn noch im Verborgenen zeichnen können, setzen wir die Eigenschaft AutoRedraw auf True. Und für den Fall, dass das UserControl bereits sichtbar sein sollte, jetzt nur aber einen anderen Text anzeigen soll, löschen wir den Inhalt sicherheitshalber. Da wir hier nur einen relativ kurzen Text ausgeben, geht das Löschen und erneute Zeichnen flackerfrei vonstatten und ist im Endeffekt zeitsparender, als eine zuverlässige Prüfung, ob der Tooltip bereits sichtbar sein könnte.
.AutoRedraw = True
.Cls
UserControl.Line (0, 0)-Step(.ScaleWidth - 1, _
.ScaleHeight - 1), vbInfoText, B
.CurrentX = 3
.CurrentY = 1
UserControl.Print nToolTip
Nun machen wir (endlich) das UserControl über dessen Extender-Objekt sichtbar:
Extender.Visible = True
End With
Wir aktivieren jetzt noch einen Timer, der dafür sorgt, dass der Tooltip wieder automatisch verborgen wird, wenn der Mauszeiger den Bereich der ListBox verlässt.
tmrHide.Enabled = True
Wurde Eingangs kein Listeneintrag unter dem Mauszeiger gefunden, oder war ein gefundener Listeneintrag schmal genug, um noch vollständig innerhalb der ListBox dargestellt zu werden, wird die private Prozedur zHideToolTip aufgerufen, die auf jeden Fall einen eventuell sichtbaren Tooltip verschwinden lässt. Somit ist die Bearbeitung des MouseMove-Ereignisses der verbundenen ListBox abgeschlossen und der Tooltip wenn nötig angezeigt.
Else
zHideToolTip
End If
Case Else
zHideToolTip
End Select
End With
End Sub
In der privaten Prozedur zHideTooltip wird zunächst der Timer abgeschaltet, dann das Extender-Objekt des UserControls verborgen und das Fenster wieder dem ursprünglichen Besitzer zugeordnet. Ebenso löschen wir den Inhalt vorsorglich und setzen die AutoRedraw-Eigenschaft wieder auf False.
Private Sub zHideToolTip()
tmrHide.Enabled = False
Extender.Visible = False
With UserControl
SetParent .hWnd, mParentWnd
.Cls
.AutoRedraw = False
End With
End Sub
Der Timer tmrHide sorgt dafür, dass der Tooltip verborgen wird, wenn der Mauszeiger sich nicht mehr direkt über der ListBox befinden sollte. Hierzu wird mit GetCursorPos die aktuelle Position des Mauszeigers in absoluten Bildschirmkoordinaten ermittelt und anschließend mit WindowFromPoint geprüft, ob sich dieser Punkt entweder über dem gerade angezeigten Tooltip oder über der ListBox befindet. Befindet sich dieser anderswo, wird der Tooltip über zHideToolTip wieder verborgen.
Private Sub tmrHide_Timer()
Dim nPoint As POINTAPI
Dim nWnd As Long
GetCursorPos nPoint
nWnd = WindowFromPoint(nPoint.X, nPoint.Y)
Select Case nWnd
Case UserControl.hWnd, eListBox.hWnd
Case Else
zHideToolTip
End Select
End Sub
Um bei der häufig aufeinanderfolgenden Bearbeitung des MouseMove-Ereignisses der ListBox wenigstens ein klein wenig Zeit zu sparen, ermitteln wir mit GetParent das Fenster-Handle des eigentlichen Besitzers des UserControls und mit GetDesktopWindow das Fenster-Handle des Desktops bereits im Ereignis UserControl_ReadProperties. Ebenfalls machen wir das UserControl auf jeden Fall unsichtbar und deaktivieren es, damit Mausereignisse weiterhin ungehindert die darunter liegende ListBox erreichen, während das UserControl als Tooltip angezeigt wird.
Private mParentWnd As Long
Private mDesktopWindow As Long
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
Dim nWindowLong As Long
'...
If Ambient.UserMode Then
mDesktopWindow = GetDesktopWindow()
Extender.Visible = False
With UserControl
mParentWnd = GetParent(.hWnd)
.Enabled = False
Ein dem Desktop als Kind zugeordnetes UserControl wird von Windows anscheinend als vollwertiges Popup-Fenster betrachtet und wird in der Taskleiste angezeigt. Dies verhindern wir, indem wir dem Fenster den erweiterten Fensterstil WS_EX_TOOLWINDOW zuweisen - Fenster mit diesem Fensterstil tauchen nicht in der Taskbar auf. Wir lesen den aktuellen Fensterstil mit GetWindowLong aus, verknüpfen den Wert mit WS_EX_TOOLWINDOW und schreiben ihn mit SetWindowLong wieder zurück.
nWindowLong = GetWindowLong(.hWnd, GWL_EXSTYLE)
nWindowLong = nWindowLong Or WS_EX_TOOLWINDOW
SetWindowLong .hWnd, GWL_EXSTYLE, nWindowLong
End With
'...
End If
End Sub
Sie können das Zusatz-Steuerelement ListToolTip sowohl als kompiliertes OCX einsetzen, es aber auch einfach als UserControl-Modul in ein Projekt aufnehmen.
|