ABOUT Visual Basic Programmieren Programmierung Download Downloads Tips & Tricks Tipps & Tricks Know-How Praxis VB VBA Visual Basic for Applications VBS VBScript Scripting Windows ActiveX COM OLE API ComputerPC Microsoft Office Microsoft Office 97 Office 2000 Access Word Winword Excel Outlook Addins ASP Active Server Pages COMAddIns ActiveX-Controls OCX UserControl UserDocument Komponenten DLL EXE
Diese Seite wurde zuletzt aktualisiert am 29.11.1999

Diese Seite wurde zuletzt aktualisiert am 29.11.1999
Aktuell im ABOUT Visual Basic-MagazinGrundlagenwissen und TechnologienKnow How, Tipps und Tricks rund um Visual BasicAddIns für die Visual Basic-IDE und die VBA-IDEVBA-Programmierung in MS-Office und anderen AnwendungenScripting-Praxis für den Windows Scripting Host und das Scripting-ControlTools, Komponenten und Dienstleistungen des MarktesRessourcen für Programmierer (Bücher, Job-Börse)Dies&Das...

Themen und Stichwörter im ABOUT Visual Basic-Magazin
Code, Beispiele, Komponenten, Tools im Überblick, Shareware, Freeware
Ihre Service-Seite, Termine, Job-Börse
Melden Sie sich an, um in den vollen Genuss des ABOUT Visual Basic-Magazins zu kommen!
Informationen zur AVB-Web-Site, Kontakt und Impressum

Zurück...

Zurück...

Harald M. Genauck mailto:hg_listtooltip@aboutvb.de

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.


Ein überbreiter Listeneintrag wird automatisch als Tooltip angezeigt

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 BrautschauSteuerelemente 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 MSDN-Library - SendMessageSendMessage mit der Nachricht MSDN-Library - LB_ITEMFROMPOINTLB_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 MSDN-Library - GetClientRectGetClientRect 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 ListBoxInnenmaße einer ListBox).

        GetClientRect .hWnd, nRect

Ist die Innenbreite kleiner als die Textbreite, ermitteln wir mit MSDN-Library - GetWindowRectGetWindowRect 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 MSDN-Library - SendMessageSendMessage mit der Nachricht MSDN-Library - LB_GETITEMHEIGHTLB_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 MSDN-Library - GetParentGetParent fest, ob das UserControl bereits dem Desktop als Kind-Fenster zugeordnet ist. Falls nicht, erfolgt dies über einen Aufruf von MSDN-Library - SetParentSetParent.

            If GetParent(.hWnd) <> mDesktopWindow Then
              SetParent .hWnd, mDesktopWindow
            End If

Mit einem Aufruf der API-Funktion MSDN-Library - SetWindowPosSetWindowPos 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 MSDN-Library - VB Extender-ObjektExtender-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 MSDN-Library - GetCursorPosGetCursorPos die aktuelle Position des Mauszeigers in absoluten Bildschirmkoordinaten ermittelt und anschließend mit MSDN-Library - WindowFromPointWindowFromPoint 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 MSDN-Library - GetParentGetParent das Fenster-Handle des eigentlichen Besitzers des UserControls und mit MSDN-Library - GetDesktopWindowGetDesktopWindow 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 MSDN-Library - CreateWindowEx/WS_EX_TOOLWINDOWWS_EX_TOOLWINDOW zuweisen - Fenster mit diesem Fensterstil tauchen nicht in der Taskbar auf. Wir lesen den aktuellen Fensterstil mit MSDN-Library - GetWindowLongGetWindowLong aus, verknüpfen den Wert mit WS_EX_TOOLWINDOW und schreiben ihn mit MSDN-Library - SetWindowLongSetWindowLong 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.

Korrekturen und Ergänzungen

09.11.1999

Erweiterung des Partner-Suchmechanismus

Erweiterung des Partner-Suchmechanismus


Code des Controls ListToolTip Code des Controls ListToolTip

Das Projekt avbListToolTipOCX (listtooltip.zip - ca. 6,5 KB)

ActiveX-Control als Setup (ohne VB 6-Runtime!) (listtooltips.zip - ca. 273 KB)



Komponenten-Übersicht

Zum Seitenanfang

Copyright © 1999 - 2023 Harald M. Genauck, ip-pro gmbh  /  Impressum

Zum Seitenanfang

Zurück...

Zurück...