|
Ein nettes Feature hinsichtlich des Bedienungskomforts sind ComboBoxen, die beim Überstreichen mit dem Mauszeiger automatisch aus- und wieder einklappen. Mit Hilfe von ein paar wenigen API-Funktionen und eines Timers ist das gar nicht so schwierig zu realisieren. Allerdings sollten Sie dieses Feature sparsam einsetzen. Der Effekt wird nämlich leicht lästig, wenn solche Automatik-ComboBoxen - vor allem in größerer Anzahl - quer über Ihr Form verstreut sind: Jeder Versuch des Anwenders, ein Menü oder die Toolbar oben im Form zu erreichen, artet dann nur zu leicht in ein regelrechtes "Gewitter" von auf- und zuklappenden ComboBoxen aus.
Das Prinzip ist einfach: Im Timer-Ereignis eines schnellen Timers (mit einem kurzen Intervall von beispielsweise 50 ms) wird geprüft, ob sich der Mauszeiger über der ComboBox befindet. Ist dies der Fall, wird die Liste der ComboBox ausgeklappt (siehe dazu auch: "Klappe auf - Klappe zu").
Zur Prüfung wird zunächst mittels der API-Funktion GetCursorPos die aktuelle Mauszeiger-Position ermittelt. Im zweiten Schritt wird festgestellt, ob das zu diesem Punkt von der API-Funktion WindowFromPoint gelieferte Fenster-Handle dasjenige der ComboBox ist. Bei einer ComboBox mit dem Stil "0 - Dropdown-Kombinationsfeld" ist es allerdings nicht ganz so einfach, da sich der Mauszeiger ja über dem Eingabefeld (einer TextBox) der ComboBox befinden kann. Daher ist zusätzlich zu prüfen, ob das Elternfenster des zur aktuellen Mauszeiger-Position ermittelten Fenster-Handles die betreffende ComboBox ist.
Ist die Liste der ComboBox ausgeklappt, wird von nun an im Timer-Ereignis geprüft, ob sich der Mauszeiger immer noch über der ComboBox oder ob er sich über der ausgeklappten Liste befindet. Ist weder das eine noch das andere der Fall, wird die Liste wieder eingeklappt.
Für den ersten Fall ist die Prüfung die gleiche wie schon beschrieben. Der zweite Fall ist jedoch nicht so einfach zu erledigen. Denn hier muss zusätzlich das Handle der ausgeklappten Liste ermittelt werden. Da wir auch nicht wissen können, ob die Liste nach oben oder ob sie nach unten ausgeklappt worden ist, müssen wir beide Richtungen berücksichtigen.
Mit einem kleinen Trick können wir aber auch dies in Erfahrung bringen - und zugleich auch das Fenster-Handle der Liste. Da sich die ausgeklappte Liste in aller Regel unmittelbar an die ComboBox anschließt, ermitteln wir vorbeugend bei noch geschlossener Liste die Fenster-Handles zu einem Punkt oberhalb der ComboBox, etwa in der horizontalen Mitte mit 2 Pixels Abstand, und spiegelbildlich dazu zu einem Punkt unterhalb der ComboBox. Diese beiden Fenster-Handles werden in (modul-)globalen Hilfsvariablen vorgemerkt.
Wird nun die Liste ausgeklappt, wird sie einen dieser beiden Punkte überdecken. Nach der Ermittlung der aktuellen Fenster-Handles unter diesen beiden Punkten prüfen wir, welches der beiden aktuellen Handles nicht mehr mit den vorgemerkten Handles übereinstimmt: Dieses wird das Handle der Ausklappliste sein.
Nun brauchen wir nur noch zu prüfen, ob sich die aktuelle Mauszeiger-Position über dem Fenster der Ausklappliste befindet. Befindet sich der Mauszeiger weder über der ComboBox noch über der Ausklappliste, wird die Liste wieder geschlossen.
Der Einfachheit halber verpacken Sie das Ganze am besten in eine Klasse, die Sie für jede ComboBox einmal instanzieren. In einer Init-Methode übergeben Sie einer Instanz eine Referenz auf die betreffende ComboBox und eine Referenz auf den Timer (einer reicht für alle ComboBoxen). Zusätzlich können Sie angeben, ob der Automatismus aktiviert sein soll, und ob die ComboBox beim automatischen Ausklappen auch zugleich den Fokus erhalten soll.
Den Automatismus können Sie jederzeit über die Eigenschaft Auto an- oder abstellen. Das Setzen des Fokus können Sie über die Eigenschaft SetFocusOnAutoOpen festlegen.
So lange der Automatismus aktiviert ist, empfängt die Klasse die Timer-Ereignisse direkt. Jede Instanz der Klasse reagiert somit individuell für die ihr zugewiesene ComboBox.
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 GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Private Declare Function GetParent Lib "user32" (ByVal hwnd 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, _
lParam As Any) As Long
Private Declare Function WindowFromPoint Lib "user32" _
(ByVal xPoint As Long, ByVal yPoint As Long) As Long
Private WithEvents eTimer As Timer
Private mAutoOpened As Boolean
Private mCombo As ComboBox
Private mInitDone As Boolean
Private mTimer As Timer
Private mWndAbove As Long
Private mWndBelow As Long
Private pAuto As Boolean
Private pSetFocusOnAutoOpen As Boolean
Public Property Get Auto() As Boolean
Auto = pAuto
End Property
Public Property Let Auto(New_Auto As Boolean)
Select Case New_Auto
Case pAuto
Case Else
pAuto = New_Auto
If pAuto Then
Set eTimer = mTimer
Else
Set eTimer = Nothing
End If
End Select
End Property
Public Property Get SetFocusOnAutoOpen() As Boolean
SetFocusOnAutoOpen = pSetFocusOnAutoOpen
End Property
Public Property Let SetFocusOnAutoOpen(New_SetFocusOnAutoOpen As Boolean)
pSetFocusOnAutoOpen = New_SetFocusOnAutoOpen
End Property
Public Sub Init(Combo As ComboBox, Timer As Timer, _
Optional ByVal Auto As Boolean = True, _
Optional ByVal SetFocusOnAutoOpen = True)
Set mCombo = Combo
Set mTimer = Timer
pSetFocusOnAutoOpen = SetFocusOnAutoOpen
pAuto = Auto
If pAuto Then
Set eTimer = mTimer
End If
End Sub
Public Sub Refresh()
zInit
End Sub
Private Sub Class_Terminate()
Set eTimer = Nothing
Set mTimer = Nothing
Set mCombo = Nothing
End Sub
Private Sub eTimer_Timer()
Dim nRect As RECT
Dim nPoint As POINTAPI
Dim nWndAbove As Long
Dim nWndBelow As Long
Dim nWnd As Long
Dim nTestWnd As Long
Dim nComboWnd As Long
Const CB_GETDROPPEDSTATE = &H157
Const CB_SHOWDROPDOWN = &H14F
With mCombo
GetCursorPos nPoint
With nPoint
nTestWnd = WindowFromPoint(.x, .y)
End With
nComboWnd = .hwnd
If CBool(SendMessage(nComboWnd, CB_GETDROPPEDSTATE, 0, 0)) Then
If mAutoOpened Then
If .Style = 0 Then
If nTestWnd = nComboWnd Then
Exit Sub
ElseIf GetParent(nTestWnd) = nComboWnd Then
Exit Sub
End If
Else
If nTestWnd = nComboWnd Then
Exit Sub
End If
End If
GetWindowRect nComboWnd, nRect
With nRect
.Right = .Left + ((.Right - .Left) \ 2)
nWndAbove = WindowFromPoint(.Right, .Top - 2)
nWndBelow = WindowFromPoint(.Right, .Bottom + 2)
Select Case True
Case nWndAbove <> mWndAbove
nWnd = nWndAbove
Case nWndBelow <> mWndBelow
nWnd = nWndBelow
End Select
If nWnd Then
If nTestWnd <> nWnd Then
SendMessage nComboWnd, CB_SHOWDROPDOWN, False, 0
mAutoOpened = False
zInit
End If
End If
End With
End If
Else
zInit
If .Style = 0 Then
If nTestWnd <> nComboWnd Then
If GetParent(nTestWnd) <> nComboWnd Then
Exit Sub
End If
End If
Else
If nTestWnd <> .hwnd Then
Exit Sub
End If
End If
If pSetFocusOnAutoOpen Then
.SetFocus
End If
mAutoOpened = True
SendMessage nComboWnd, CB_SHOWDROPDOWN, True, 0
.MousePointer = vbDefault
End If
End With
End Sub
Private Sub zInit()
Dim nRect As RECT
GetWindowRect mCombo.hwnd, nRect
With nRect
.Right = .Left + ((.Right - .Left) \ 2)
mWndAbove = WindowFromPoint(.Right, .Top - 2)
mWndBelow = WindowFromPoint(.Right, .Bottom + 2)
End With
End Sub
|