|
Coole Hover-Effekte auch bei einer ListBox oder einer FileListBox?
Das ist schließlich ein ganz netter Effekt, wenn die Markierung in
einer ListBox genau so wie in der Aufklappliste einer ComboBox dem
Mauszeiger folgt.
Auf den ersten Blick scheint es dafür eine einfach Lösung zu
geben. Sie bräuchten nur das Element der ListBox ermitteln, das
sich gerade unter dem Mauszeiger befindet, und den ListIndex darauf
setzen.
Private Type PointInt
X As Integer
Y As Integer
End Type
Private Type TypeLong
l As Long
End Type
Private Declare Function SendMessage Lib "user32" _
Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, _
ByVal wParam As Long, lParam As Any) As Long
Private Const LB_ITEMFROMPOINT = &H1A9
Private Const LB_SETCURSEL = &H186
Private Sub List1_MouseMove(Button As Integer, Shift As Integer, _
X As Single, Y As Single)
Dim nPointInt As PointInt
Dim nLong As TypeLong
Dim nIndex As Long
With List1
With nPointInt
.X = X \ Screen.TwipsPerPixelX
.Y = Y \ Screen.TwipsPerPixelY
End With
LSet nLong = nPointInt
nIndex = SendMessage(.hwnd, LB_ITEMFROMPOINT, 0, ByVal nLong.l)
Select Case nIndex
Case 0 To &H10000
.ListIndex = nIndex
End Select
End With
End Sub
Das funktioniert offensichtlich prächtig, aber... so dürfen Sie
den Mauszeiger nur auf einer horizontalen Linie nach rechts oder
links aus der ListBox herausbewegen, wenn die Auswahl nach einem
Click in die ListBox erhalten bleiben soll... (einmal ganz abgesehen
von dem auch nicht so schönen Effekt, dass bei jeder Änderung des
ListIndex das Click-Ereignis der ListBox ausgelöst wird).
Wir müssen also die Aufgabenstellung ein wenig modifizieren,
damit die mit dem Mauszeiger mitwandernde Auswahlmarkierung nicht
bloß zum völlig nutzlosen optischen Gag verkommt. Die
Aufgabenstellung lautet also treffender:
Die Markierung soll sich mit dem Mauszeiger bewegen. Jedoch wenn
der Mauszeiger die ListBox verlässt, soll die Markierung wieder zum
zuletzt per Klick oder Code gewählten Element zurückspringen.
Wurde ein Element angeklickt und damit der ListIndex geändert,
während sich der Mauszeiger nach wie vor innerhalb der ListBox
befindet, soll diese neue Auswahl als diejenige gelten, auf die die
Markierung gegebenenfalls zurückspringt, wenn der Mauszeiger
schließlich die ListBox verlässt.
Damit der Wandereffekt auch richtig Spaß macht, erweitern wir
die Aufgabenstellung noch ein wenig. Befindet sich der Mauszeiger
unmittelbar unter der Oberkante oder über der Unterkante der
ListBox, soll der Inhalt der ListBox schrittweise automatisch
gerollt werden, wenn in der jeweiligen Richtung noch Elemente
vorhanden sind.
Ich will Sie aber bezüglich der Lösung auch nicht noch länger
im Dunklen tappen lassen und verrate Ihnen daher, dass sie auf einem
sogenannten Hover-Effekt ("MouseOver/MouseLeave") beruht.
Effekte dieser Art kennen Sie ja längst von heutigen Toolbars, etwa
im Microsoft Internet Explorer, im Micosoft Office und in vielen
anderen Anwendungen. Wie Sie einen solchen Effekt sauber schreiben
können, habe ich bereits ausführlich in "Immer
schön cool bleiben" gezeigt. Sie brauchen zum einen
die dort vorgestellte Lösung nur noch um die Darstellung des
Hover-Effekts erweitern (also für das Mitwandern der Auswahl
sorgen) - in etwa anhand des oben bereits gezeigten
Lösungsansatzes, und dafür sorgen, dass der beim Eintreten des
Mauszeigers in die ListBox gültige ListIndex beim Verlassen wieder
hergestellt wird. Zum anderen wäre noch der Code notwendig, der die
Mauszeigerposition auf die Nähe zur Ober- oder Unterkante hin
prüft und die ListBox gegebenenfalls rollt.
Zunächst eine kurze Erläuterung, auch als Nachtrag zum
obenstehenden Code, wie die beiden Maus-Koordinaten X und Y in den
Long-Wert gepackt werden, den die SendMessage-Funktion
im letzten Parameter erwartet. Das Prinzip ist einfach:
Ein Long-Wert wird von vier Bytes im Speicher repräsentiert. Die
beiden Koordinaten werden (von Windows) als Integer-Werte in Pixels
betrachtet und ein Integer-Wert wird von Bytes im Speicher
repräsentiert. Zur Kombination des geforderten Long-Werts aus vier
Bytes entsteht werden die beiden Byte-Paare aneinandergereiht,
zuerst die beiden Bytes der X-Koordinate, dann die beiden Bytes der
Y-Koordinate. Damit Sie sich nicht in Speicher-Arithmetik verlieren
müssen, können Sie die Kombination über zwei benutzerdefinierte
Variablen vornehmen. Die eine (PointInt) enthält zwei
Integer-Elemente, die andere (TypeLong) lediglich ein Long-Element.
Füllen Sie nun die Elemente einer Variablen vom Typ von PointInt
mit den in Pixels umgerechneten Koordinaten und weisen Sie diese
Variable nun einer Variablen vom Type TypeLong zu, geschieht im
Hintergrund nichts weiter, als dass Byte für Byte von der
benutzerdefinierten Variablen des einen Typs in die andere kopiert
werden - die Kombination ist damit vollzogen.
Private Type PointInt
X As Integer
Y As Integer
End Type
Private Type TypeLong
l As Long
End Type
'...
Dim nPointInt As PointInt
Dim nLong As TypeLong
'...
With nPointInt
.X = X \ Screen.TwipsPerPixelX
.Y = Y \ Screen.TwipsPerPixelY
End With
LSet nLong = nPointInt
'...
Der SendMessage-Funktion übergeben Sie nun das Long-Element der
Variablen des Typs TypeLong:
nIndex = SendMessage(.hwnd, LB_ITEMFROMPOINT, 0, ByVal nLong.l)
Hier nun zunächst die im folgenden benötigten weiteren
Deklarationen:
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 GetCapture Lib "user32" () As Long
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 ReleaseCapture Lib "user32" () As Long
Private Declare Function SendMessage Lib "user32" _
Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, _
ByVal wParam As Long, lParam As Any) As Long
Private Declare Function SetCapture Lib "user32" _
(ByVal hwnd As Long) As Long
Private Declare Function WindowFromPoint Lib "user32" _
(ByVal xPoint As Long, ByVal yPoint As Long) As Long
Private Declare Function SetCursorPos Lib "user32" _
(ByVal X As Long, ByVal Y As Long) As Long
Private Const LB_ITEMFROMPOINT = &H1A9
Private Const LB_SETCURSEL = &H186
Private mLastY As Long
Private mListBoxWnd As Long
Private mStartIndex As Long
Diese Deklarationen liegen in einem Standard-Modul, wie auch die
beiden Prozeduren, die die gestellte Aufgabe erfüllen sollen. In
der privaten Variable mLastY wird jeweils die letzte Y-Position des
Mauszeigers zwischengespeichert. In der Variablen mListBoxWnd merken
wir uns weiterhin das Fenster-Handle der betroffenen ListBox. Wir
könnten zwar statt dessen auch eine Referenz auf die ListBox
speichern. Aber da eventuell vergessene Referenzen nur Probleme
bereiten und wir tatsächlich mit dem Fenster-Handle auskommen
werden, verzichten wir darauf. Schließlich wird noch in der
Variablen mStartIndex der ListIndex festgehalten, auf den die
Auswahl beim Verlassen der ListBox zurückgesetzt werden soll. Wann
und wie diese Variablen gesetzt werden, erfahren Sie gleich.
Betrachten wir die erste Prozedur einmal näher. Sie rufen sie im
MouseMove-Ereignis einer ListBox auf und übergeben ihr die Referenz
auf diese ListBox, den Button-Parameter und die X- und Y-Korrdinaten
des MouseMove-Ereignisses. Optional können Sie noch die Höhe (in
Pixels) der sensitiven Bereiche an der Ober- und Unterkante der
ListBox festlegen, in denen gegebenenfalls das Rollen der ListBox
ausgelöst werden soll.
Public Sub ListHighlightItem(ListBox As ListBox, _
ByVal Button As Integer, ByVal X As Single, ByVal Y As Single, _
Optional ByVal ScrollCheckHeight As Long = 2)
Dim nPointAPI As POINTAPI
Dim nHWnd As Long
Dim nPointInt As PointInt
Dim nLong As TypeLong
Dim nIndex As Long
Dim nRect As RECT
Dim nScroll As Boolean
Da wir das Mitwandern des Mauszeiger nur bei losgelassener
Maustaste bearbeiten wollen (bei gedrückter Maustaste erledigt das
Windows nämlich bereits von alleine), verlassen wir die Prozedur
gleich wieder, wenn in Button ein Wert übergeben wird. Die beiden
Variablen mListBoxWnd und mStartIndex werden sicherheitshalber auf
bedeutungslose Standardwerte zurückgesetzt.
If Button Then
mListBoxWnd = 0
mStartIndex = -1
mLastY = 0
Exit Sub
End If
With ListBox
If .hwnd <> mListBoxWnd Then
If mListBoxWnd Then
Wird in mListBoxWnd ein anderes Fenster-Handle angetroffen als
das der gerade übergebenen ListBox, setzen wir den ListIndex der
vorgefundenen ListBox auf den in mStartIndex noch abgelegten Wert.
Wir senden dazu mittels SendMessage die Nachricht LB_SETCURSEL
an diese ListBox. Zum einen haben wir ja keine Referenz speichern
wollen, sondern nur deren Fenster-Handle zur Verfügung, zum anderen
hat der Aufruf den Vorteil, dass dabei nicht unerwünschterweise das
Click-Ereignis dieser ListBox aufgerufen wird.
SendMessage mListBoxWnd, LB_SETCURSEL, mStartIndex, 0
End If
Dann legen wir das Fenster-Handle und den ListIndex der aktuell
übergebenen ListBox ab.
mListBoxWnd = .hwnd
mStartIndex = .ListIndex
End If
Nun folgt der Kern des zur Überwachung von Mauseintritt und
-austritt des Mauszeigers benötigte Code. Auf diesen gehe ich hier
nicht weiter ein. Er ist in dem oben bereits erwähnten Artikel "Immer
schön cool bleiben" zur Genüge beschrieben.
GetCursorPos nPointAPI
nHWnd = WindowFromPoint(nPointAPI.X, nPointAPI.Y)
If nHWnd = mListBoxWnd Then
If GetCapture() <> nHWnd Then
SetCapture nHWnd
End If
An dieser Stelle wissen wir, dass sich der Mauszeiger innerhalb
der ListBox befindet. Daher ermitteln wir nun den Index des sich
unter dem Mauszeiger befindlichen Elements. Hierzu verwenden wir
tatsächlich den bereits beschriebenen Ansatz.
With nPointInt
.X = X \ Screen.TwipsPerPixelX
.Y = Y \ Screen.TwipsPerPixelY
End With
LSet nLong = nPointInt
nIndex = SendMessage(mListBoxWnd, LB_ITEMFROMPOINT, 0, _
ByVal nLong.l)
Da wir den ListBox-Inhalt gegebenenfalls rollen lassen wollen,
prüfen wir nun, ob sich der Mauszeiger innerhalb der sensitiven
Bereiche an der Ober- bzw. Unterkante der ListBox befindet. Die
API-Funktion GetClientRect
liefert uns dazu in einer benutzerdefinierten Variablen des Typs
RECT die exakte Größe der reinen Innenfläche der ListBox
(innerhalb des 3-D-Rahmens und ohne eventuell
vorhandenem Rollbalken).
GetClientRect mListBoxWnd, nRect
Select Case nPointInt.Y
Wir prüfen zunächst den Bereich unterhalb der Oberkante.
Befindet sich der Mauszeiger dort, schließen wir aus, dass der
Mauszeiger wieder von der Oberkante in Abwärtsrichtung weg bewegt
worden ist. Den Vergleichswert halten wir in der modulglobalen
Variable mLastY weiter unten fest.
Case 0 To ScrollCheckHeight
If nPointAPI.Y <= mLastY Then
Ist der eben ermittelte Index größer als 0 (er ist
nicht das oberste Element), vermindern wir ihn um 1 und
merken uns, dass somit gerollt werden wird.
If nIndex > 0 Then
nIndex = nIndex - 1
nScroll = True
End If
End If
Nach dem gleichen Prinzip prüfen wir den Bereich oberhalb der
Unterkante. Hier kommt es nun darauf an, dass der Mauszeiger nicht
aufwärts von der Unterkante weg bewegt worden ist, und dass der
ermittelte Index kleiner als der höchstmögliche Index (ListCount
- 1) ist.
Case nRect.Bottom - ScrollCheckHeight To nRect.Bottom
If nPointAPI.Y >= mLastY Then
If nIndex < .ListCount - 1 Then
nIndex = nIndex + 1
nScroll = True
End If
End If
End Select
Nun merken wir uns die Y-Position in mLastY und setzen die
Auswahl auf den ermittelten Index des Elements unter dem Mauszeiger.
Auch hier verwenden wir wieder die Nachricht LB_SETCURSEL, um das
Auslösen des Click-Ereignisses zu vermeiden.
mLastY = nPointAPI.Y
SendMessage mListBoxWnd, LB_SETCURSEL, nIndex, 0
Nun folgt ein kleiner Trick, der das automatische Rollen besorgt,
solange der Mauszeiger in den sensitiven Bereichen steht oder sich
den Kanten noch weiter nähert. Der Trick besteht darin, künstlich
eine Mausbewegung hervorzurufen, damit das MouseMove-Ereignis der
ListBox erneut aufgerufen wird. Nicht ganz erklärbar ist der Grund
für die erneute Ermittlung der Mauszeiger-Position (bezogen auf den
Bildschirm). Nehmen wir es einfach als gegeben hin, dass mit dem
eben erfolgten Setzen des ListIndex auf ein zuvor noch nicht
sichtbares Element die ListBox intern bezüglich der ihr bis dahin
bekannten Mauszeiger-Position etwas in Verwirrung zu geraten
scheint.
If nScroll Then
GetCursorPos nPointAPI
Die künstliche Bewegung des Mauszeigers erreichen wir dadurch,
dass wir ihn um ein Pixel in der Waagerechten verschieben. Damit er
nicht versehentlich außerhalb der ListBox landet, prüfen wir seine
nähe zur rechten und linken Kante und verschieben ihn
gegebenenfalls zum ListBox-Inneren.
With nPointAPI
If .X < nRect.Right - 2 Then
SetCursorPos .X + 1, .Y
Else
SetCursorPos .X - 1, .Y
End If
Da ja aber keine tatsächliche Bewegung daraus resultieren soll,
setzen wir die Position des Mauszeigers sogleich wieder zurück.
SetCursorPos .X, .Y
End With
End If
Else
Hier folgt nun wieder Code aus dem bereits erwähnten Artikel.
Die API-Funktion ReleaseCapture
muss ausgeführt werden, da der Mauszeiger die ListBox verlassen
hat.
ReleaseCapture
Und an dieser Stelle verwirklichen wir nun unser eigentliches
Vorhaben: Wir setzen den ListIndex auf den Eingangs gemerkten bzw.
zwischenzeitlich aktualisierten Wert der Merkvariablen mStartIndex
zurück.
SendMessage mListBoxWnd, LB_SETCURSEL, mStartIndex, 0
Konsequenterweise setzen wir hier schließlich noch die
Merkvariablen wieder auf bedeutungslose Standard-Werte.
mListBoxWnd = 0
mStartIndex = -1
mLastY = 0
End If
End With
End Sub
Den Hauptteil der Arbeit hätten wir nun erledigt. Nun bleibt nur
noch die zweite angekündigte Prozedur übrig. Sie sorgt dafür,
dass in der Merkvariablen mStartIndex der aktuelle ListIndex
abgelegt wird, falls sich das eigentlich ausgewählte Element in der
ListBox durch einen Klick oder durch Code zwischenzeitlich geändert
hat, während sich der Mauszeiger innerhalb der ListBox befindet.
Sinnvollerweise erfolgt die Aktualisierung nur dann, wenn das
Fenster-Handle der hier übergebenen ListBox mit dem in mListBoxWnd
abgelegten übereinstimmt.
Public Sub ListBoxSetNewIndex(ListBox As ListBox)
With ListBox
If .hwnd = mListBoxWnd Then
mStartIndex = .ListIndex
End If
End With
End Sub
Das herunterladbare Modul modListHightlightItem.bas enthält
zusätzlich noch die Varianten der beiden Prozeduren zur Verwendung
mit einer FileListBox.
|