|
Vor allem Einsteiger in die Bedienung von Windows und Windows-Anwendungen haben oftmals noch Schwierigkeiten den einfachen Mausklick und den Doppelklick auseinander zu halten. Zudem gibt es Mäuse und Maustreiber, die es erlauben, den Doppelklick auf eine (dritte) Maustaste oder bei Rad-Mäusen auf das Rädchen zu legen. Wie schnell wird da mal versehentlich ein Doppelklick auf eine Schaltfläche abgesetzt...
Solche versehentlichen Doppelklicks können Sie jedoch verhindern. Ein wenig Aufwand ist zwar nötig - er hält sich jedoch in Grenzen, wenn Sie die hierfür notwendige Funktionalität nur genügend abstrakt und allgemein verwendbar implementieren.
Der erste, und schon mal richtige, Gedanke ist, die vom Anwender eingestellte (oder unverändert belassene) Doppelklickzeit zu berücksichtigen. Diese Zeitspanne, innerhalb derer Windows einen Mausklick als Doppelklick interpretiert, können Sie über die API-Funktion GetDoubleClickTime erhalten, in Millisekunden. Sie halten den Zeitpunkt eines Mausklicks fest, und wenn der nächste Klick innerhalb dieser Zeitspanne erfolgen sollte, wird er als zweiter Klick eines Doppelklickpaares verworfen. Den Zeitpunkt eines Klicks erhalten Sie über die API-Funktion GetTickCount, praktischerweise ebenfalls in Millisekunden. Zwischen den Klick-Ereignissen wird der Zeitpunkt in einer statischen Variablen festgehalten.
Private Declare Function GetDoubleClickTime Lib "user32" () As Long
Private Declare Function GetTickCount Lib "kernel32" () As Long
Private Sub Command1_Click()
Dim nAction As Boolean
Dim nThisTime As Long
Static sPrevTime As Variant
nThisTime = GetTickCount()
If sPrevTime + GetDoubleClickTime() < nThisTime Then
nAction = True
End If
sPrevTime = nThisTime
If Not nAction Then
Exit Sub
End If
' eigentliche Aktion:
Debug.Print "Click"
End Sub
Das sieht schon recht gut. Und wenn Sie es ausprobieren, dürfte das auch wie gewünscht funktionieren - aber auch nur scheinbar! Denn es wird zum einen nicht berücksichtigt, dass der von GetTickCount gelieferte Wert eigentlich ein vorzeichenloser Wert ist, der den normalen positiven Wertebereich eines Long-Wertes überschreiten kann. Der Aufruf der API-Funktion stört sich zwar nicht daran und gibt Ihnen brav einen Long-Wert zurück. Doch wenn der Zähler eigentlich auf einem Wert steht, der größer als &H7FFFFFFF (= 2.147.483.647) ist, erscheint der Wert in Visual Basic negativ - der Größenvergleich würde fehlschlagen. Zum anderen wird nicht berücksichtigt, dass etwa alle 49,7 Tage der Wert des internen Zeitzählers, der von GetTickCount zurückgegeben wird, überläuft und auf 0 zurück gesetzt wird. Der erste Klick danach würde nicht durchkommen, da der Zeitvergleich auch hier falsch ausgehen würde.
Das erstere Problem lösen wir, indem wir den Wert des Zeitzählers in einen vorzeichenlosen Wert umwandeln, der wegen seiner Größe in einer Variant-Variablen abgelegt und dazu in den Variant-Unterdatentyp Decimal konvertiert wird. Die Beschreibung und den Hintergrund der dazu verwendeten Funktion CLongToULong finden Sie in "Vorzeichenlose Werte".
Damit ist auch die Grundlage zur Lösung des zweiten Problems geschaffen, des Überlaufs. Zunächst müssen wir den Fall ausklammern, dass noch gar kein Zeitpunkt für einen vorhergehenden Klick zwischengespeichert wurde. Denn da wir nun für die Zeitwerte Variant-Variablen verwenden müssen, sind diese im uninitialisierten "Roh"-Zustand "Leer" (Empty), und nicht etwa 0, wie eine frisch dimensionierte Long-Variable.
Für den Fall des Überlaufs prüfen wir, ob der aktuelle Zeitwert kleiner ist, als der vorhergehende. Ist das der Fall, setzt sich die Zeitspanne, die wir mit dem Wert von GetDoubleClickTime vergleichen müssen, aus dem aktuellen Zeitwert und der Differenz zwischen letztem Zeitwert und dem höchstmöglichen Zeitwert (&HFFFFFFFF - vorzeichenlos!) zusammen. Anderenfalls ist die Vergleichsspanne die normale Differenz zwischen aktuellem Zeitwert und vorhergehendem Zeitwert.
Private Declare Function GetDoubleClickTime Lib "user32" () As Long
Private Declare Function GetTickCount Lib "kernel32" () As Long
Private Sub Command1_Click()
Dim nAction As Boolean
Dim nThisTime As Variant
Dim nDiffTime As Long
Static sPrevTime As Variant
nThisTime = CLongToULong(GetTickCount())
If sPrevTime = Empty Then
nAction = True
Else
If sPrevTime <= nThisTime Then
nDiffTime = nThisTime - sPrevTime
Else
nDiffTime = CLongToULong(&HFFFFFFFF) - sPrevTime + nThisTime
End If
End If
If nDiffTime > GetDoubleClickTime() Then
nAction = True
End If
sPrevTime = nThisTime
If Not nAction Then
Exit Sub
End If
' eigentliche Aktion:
Debug.Print "Click"
End Sub
Das funktioniert nun ganz prächtig und zuverlässig. Allerdings ist das eine mächtige Menge Code, die Sie in jede Click-Ereignisprozedur einer jeden Schaltfläche einzufügen hätten. Sie können den Code jedoch auch in eine Funktion in einem Standard-Modul auslagern. Nun brauchen Sie nur noch den vorhergehenden Zeitwert als statische Variable in der Click-Ereignisprozedur zu speichern und diese Funktion aufzurufen. Gibt die Funktion True zurück, soll das Click-Ereignis verwendet werden.
Private Declare Function GetDoubleClickTime Lib "user32" () As Long
Public Declare Function GetTickCount Lib "kernel32" () As Long
Public Function SingleClick(PrevTime As Variant) As Boolean
Dim nAction As Boolean
Dim nThisTime As Variant
Dim nDiffTime As Long
nThisTime = CLongToULong(GetTickCount())
If PrevTime = Empty Then
nAction = True
Else
If PrevTime <= nThisTime Then
nDiffTime = nThisTime - PrevTime
Else
nDiffTime = CLongToULong(&HFFFFFFFF) - PrevTime + nThisTime
End If
End If
If nDiffTime > GetDoubleClickTime() Then
nAction = True
End If
PrevTime = nThisTime
If Not nAction Then
Exit Function
End If
SingleClick = True
End Function
Und der Code in einer Click-Ereignisprozedur reduziert sich auf folgendes:
Private Sub Command1_Click()
Static sPrevTime As Variant
If SingleClick(sPrevTime) Then
' eigentliche Aktion:
Debug.Print "Click" End If
End Sub
Ist Ihnen das immer noch zu viel? Dann hilft nur noch eine Klasse, die Sie für jede Schaltfläche instanzieren müssen. Eine Klasse bietet dabei einen etwas eleganteren Weg. Sie können nämlich auf das Click-Ereignis der Schaltfläche verzichten und die Klasse ein entsprechendes Click-Ereignis auslösen lassen, das sie dann ohne weiteren Code darin wie ein gewöhnliches Click-Ereignis verwenden können. Und falls Sie schon immer das tatsächliche Doppelklick-Ereignis einer Schaltfläche vermisst haben sollten: Die Klasse bietet nun auch dieses.
Die dafür notwendige Erweiterung des obenstehenden Codes ist gar nicht aufwändig - sie ist sogar schon eingebaut. Denn statt den unerwünschten Doppelklick zu verwerfen, lösen Sie einfach ein DblClick-Ereignis aus.
Der vollständige Code der Klasse (hier sehen Sie auch noch einmal die Konvertierfunktion für vorzeichenlose Werte CLongToULong):
Private Declare Function GetDoubleClickTime Lib "user32" () As Long
Private Declare Function GetTickCount Lib "kernel32" () As Long
Public Event Click()
Public Event DblClick()
Private mPrevTime As Variant
Private mDblClick As Boolean
Private WithEvents eCommand As CommandButton
Public Sub Init(CommandButton As CommandButton)
Set eCommand = CommandButton
End Sub
Private Sub Class_Initialize()
mPrevTime = 0
End Sub
Private Sub eCommand_Click()
Dim nAction As Boolean
Dim nThisTime As Variant
Dim nDiffTime As Long
nThisTime = CLongToULong(GetTickCount())
If mPrevTime <= nThisTime Then
nDiffTime = nThisTime - mPrevTime
Else
nDiffTime = CLongToULong(&HFFFFFFFF) - mPrevTime + nThisTime
End If
If (nDiffTime > GetDoubleClickTime()) Or mDblClick Then
nAction = True
Else
mDblClick = True
End If
mPrevTime = nThisTime
Select Case True
Case nAction
mDblClick = False
RaiseEvent Click
Case mDblClick
RaiseEvent DblClick
End Select
End Sub
Private Function CLongToULong(LongValue As Long) As Variant
Select Case LongValue
Case Is < 0
CLongToULong = CDec(LongValue - CDec(&H80000000) * 2)
Case &H80000000
CLongToULong = CDec(LongValue) * -1
Case Else
CLongToULong = CDec(LongValue)
End Select
End Function
Sie legen nun für jede Schaltfläche, die Sie mit der Funktionalität dieser Klasse nachrüsten möchten, eine Ereignisempfänger-Variable in dem Modul an, wo die Ereignisse verarbeitet werden sollen:
Private WithEvents mCommand1SingleClick As clsSingleClick
Die Instanz erzeugen Sie beispielsweise im Form_Load-Ereignis und übergeben der Klasse die betreffende Schaltfläche über den Aufruf der Init-Methode:
Private Sub Form_Load()
Set mCommand1SingleClick = New clsSingleClick
mCommand1SingleClick.Init Command1
End Sub
Nun haben Sie beide Ereignisse, Click und DblClick, separat zur Verfügung:
Private Sub mCommand1SingleClick_Click()
Debug.Print "Click"
End Sub
Private Sub mCommand1SingleClick_DblClick()
Debug.Print "DblClick"
End Sub
|