|
Bitte, was? "Präzisynchrone" Controls? Keine Bange -
dieser Begriff kommt nicht aus der Schlagwortschmiede in Redmond,
sondern stammt ausnahmsweise von mir. Dahinter steckt nichts
besonderes, nur eben die präzise synchrone Steuerung von
Steuerlementen. Eigentlich wollte ich sie ja "Affen-Steuerlemente
" nennen, weil unsere Primaten-Vettern ja bekanntlich alles
nachmachen. Aber irgendwie klingt "Präzi...dingsbums"
doch viel technischer und computerischer, nicht wahr?
Langer Rede kurzer Sinn: Es geht darum, wie Sie mit Hilfe eines
einfachen Tricks zwei Steuerelemente zur synchronen Reaktion auf
Maus- und Tastenereignisse bringen. Sie werden nämlich den Affen
spielen... Pardon!... Sie werden genau das gleiche machen, was
Windows macht, nämlich Ereignis-Nachrichten versenden. Wenn ein
Maus- oder Tastenereignis bei einem Steuerelement eingetroffen ist,
werden Sie dieses sofort über eine entsprechende Windows-Nachricht
an ein anderes, fernzusteuerndes Steuerelement weiterreichen. Und
darum geht es in diesem Beitrag: Sie nutzen die Maus- und
Tastenereignisse eines Steuerelements, um in einem anderen genau die
Resultate zu erzielen, als ob dort ebenfalls und gleichzeitig Maus-
und/oder Tastatur-Tasten gedrückt worden wären.
Zunächst mag sich das nach komplizierter API-Trickserei
anhören, nach Subclassing oder Hooks und dergleichen. Das ist gar
nicht nötig. Die von VB ausgelösten Ereignisse reichen vollauf.
Sie liefern bereits die benötigten Informationen, die Sie lediglich
etwas anders verpacken müssen und dann einfach mit den
Allerwelts-API-Funktionen SendMessage bzw. PostMessage abschicken.
Bei den Tastenereignissen können wir uns sogar auf das
KeyDown-Ereignis beschränken.
Den VB-Ereignissen genau entsprechend gibt es
Windows-Nachrichten: WM_XBUTTONDOWN bzw. WM_XBUTTONUP für das
MouseDown- und das MouseUp-Ereignis, WM_MOUSEMOVE für das
MouseMove-Ereignis, und schließlich noch WM_KEYDOWN für das
KeyDown-Ereignis. In Wirklichkeit hat nämlich Windows eben diese
Nachrichten an VB gesendet. VB übersetzt sie lediglich für uns,
und wir brauchen nichts weiter zu tun, als für den Weiterversand
die Rückübersetzung vorzunehmen. Das mag zwar widersinnig klingen.
Aber so ist es für uns VBler wesentlich einfacher handzuhaben, als
wenn wir uns mit jenen weitaus komplizierteren Subclassing- und
Sonstwas-Techniken herumplagen würden - die sind immer eine etwas
heikle und leicht wackelige Angelegenheit.
Ich habe allerdings ein klein wenig geschummelt. API-Kenner
werden es schon längst gesehen haben: Von XBUTTONS hat noch nie
jemand etwas gehört. Genau genommen verschickt Windows für jede
Maustaste eine eigene Nachricht. Ersetzen Sie das X jeweils durch
ein L, M oder R... - alles klar? VB sagt uns im Prinzip dasselbe.
Welche Maustaste gedrückt oder losgelassen worden ist, entnehmen
wir dem Button-Parameter der Mausereignisse. Windows ist jedoch
einen Tick genauer. Neben der für jede Maustaste eindeutigen
Nachricht bekommen wir noch im WParam-Parameter (warum das Ding
bloß so heißt??) einen Wert, der die gedrückten Tasten
repräsentiert. Das ist insofern genauer, als dass beim Loslassen
einer Maustaste eine andere ja weiterhin gedrückt sein kann. Genau
das erfahren wir hier, während VB uns das vorenthält - wir
erfahren nur, welche Taste losgelassen wurde. Aber da wir hier ja
nicht VB die Aufgabe der Interpretation einer Windows-Nachricht
abnehmen wollen, sondern umgekehrt die VB-Nachricht wieder in eine
Windows-Nachricht zurückverwandeln wollen, ist das kein großes
Unglück. Beim MouseMove-Ereignis gibt es hingegen keine gerade erst
niedergedrückte oder losgelassene Maustaste, sondern lediglich die
Maustasten, die während der Mausbewegung konstant niedergedrückt
sind. Daher gibt es von Windows auch nur die Nachricht WM_MOUSEMOVE
mit der entsprechenden Maustastenkombination in WParam verpackt.
Aus Platzgründen - die eierlegende Wollmilch-Funktion SendMessage
(bzw. ihre Schwester PostMessage)
verfügt nun mal nur über eine begrenzte Anzahl an Parametern -
steckt in WParam auch noch die Information, ob eine Strg- oder eine
Umschalttaste gedrückt ist (ob es sich um die rechte oder linke
davon handelt, spielt hier keine Rolle). Im letzten Parameter LParam
stecken die Koordinaten des Mausereignisses. LParam ist vom Datentyp
Long, der aus zwei Integer-Werten zusammengesetzt ist, den
Koordinaten X und Y. Auch wenn im 32-bittigen Windows mittlerweile
überall Long-Werte verwendet werden, und auch in anderen
API-Funktionen X- oder Y-Koordinaten als Long-Werte gefragt sind,
reichen dafür in Wirklichkeit Integer-Werte vollkommen aus - von
Bildschirmen mit einer Superauflösung von mehr als 32.000 x 32.000
Pixels dürften wir wohl bis in alle Ewigkeit träumen.
Im Gegensatz zu den Mausereignissen, bei denen alle drei relevant
sind, benötigen wir von den Tastatur-Ereignissen in den
allermeisten Fällen nur KeyDown. Wahrscheinlich werden auch Sie das
KeyUp-Ereignis in Ihren Programmen nur äußerst selten verwenden.
Das KeyPress-Ereignis ist eigentlich kein echtes Ereignis, wie auch
die entsprechende Windows-Nachricht WM_CHAR es nicht ist. Beide sind
nur Vorauswertungen und Übersetzungen der in KeyDown bereits
gemeldeten Tastendrücke in das lokale Zeichen- und Schreibsystem.
Wir können also darauf ebenfalls verzichten.
Beginnen wir die Konkretisierung unserer Absicht beim
KeyDown-Ereignis. Doch zunächst noch die notwendigen
API-Deklarationen:
Private Const MK_LBUTTON = &H1
Private Const MK_RBUTTON = &H2
Private Const MK_SHIFT = &H4
Private Const MK_CONTROL = &H8
Private Const MK_MBUTTON = &H10
Private Const WM_MOUSEMOVE = &H200
Private Const WM_LBUTTONDOWN = &H201
Private Const WM_LBUTTONUP = &H202
Private Const WM_RBUTTONDOWN = &H204
Private Const WM_RBUTTONUP = &H205
Private Const WM_MBUTTONDOWN = &H207
Private Const WM_MBUTTONUP = &H208
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
Public Const WM_KEYDOWN = &H100
Public Declare Function PostMessage Lib "user32" _
Alias "PostMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Bei diesem brauchen Sie nichts weiter zu tun, als eine der
Funktionen SendMessage oder PostMessage aufzurufen. Meistens tut es
die geläufigere Funktion SendMessage voll und ganz, manchmal, etwa
bei TextBoxen, müssen Sie dagegen PostMessage nehmen, damit der
Trick funktioniert. Fragen Sie mich jetzt bitte nicht nach dem
Unterschied - da müsste ich sehr weit ausholen. Als Anhaltspunkt
mag Ihnen genügen (wenn Sie es nicht sowieso schon wissen), dass
eine mit SendMessage gesendete Nachricht im Prinzip sofort
bearbeitet wird, während eine mit PostMessage gesendete Nachricht
erst später bearbeitet wird, sozusagen wenn Zeit dafür da ist. Aus
Ihrer Sicht als Aufrufer werden beide Funktionen jedoch sofort
bearbeitet - Ihr Programm läuft sofort nach dem Aufruf weiter.
Als ersten Parameter übergeben Sie das Windows-Handle des
Steuerelements, das Sie synchronisieren wollen - daraus ergibt sich
natürlich, dass nur Steuerelemente davon profitieren können, die
über ein ebensolches Handle verfügen. Aber das ist sowieso bei den
meisten maus- und tastatur-"empfindlichen" Steuerelementen
der Fall. Im zweiten Parameter folgt die Nachricht WM_KEYDOWN und im
dritten Parameter der KeyCode-Wert, den das VB-KeyDown-Ereignis
übergeben hat. Der letzte Parameter (LParam) wird hier nicht
benötigt und bleibt daher Null.
Bei den Mausereignissen wird es komplizierter. Auch hier wird
wieder zuerst das Fenster-Handle des betreffenden Steuerelements
übergeben. Dann müssen wir zunächst aus dem Button-Wert
schließen, welche Maus-Nachricht zu senden ist. Diese Operation
verpacken wir in eine Funktion, damit wir nicht den gleichen Code in
allen drei Mausereignissen wiederholen müssen.
SendMessage CLng(hWnd), zMakeMsg(Button, True), _
zMakeWParam(Button, Shift), zMakeLParam(X, Y)
Damit die Funktion weiß, ob es sich um das Drücken oder um das
Loslassen der betreffenden Maustaste handelt, übergeben wir neben
dem Button-Wert als Parameter den Wert True bei gedrückter Taste (MouseDown-Ereignis),
anderenfalls bleibt er False (MouseUp-Ereignis). Im
MouseMove-Ereignis erübrigt sich diese Prüfung - wir setzen dort
die WM_MOUSEMOVE-Nachricht direkt ein.
Private Function zMakeMsg(ByVal iButton As Integer, _
ByVal iDown As Boolean) As Long
Select Case iDown
Case True
Select Case iButton
Case vbLeftButton
zMakeMsg = WM_LBUTTONDOWN
Case vbRightButton
zMakeMsg = WM_RBUTTONDOWN
Case vbMiddleButton
zMakeMsg = WM_MBUTTONDOWN
End Select
Case False
Select Case iButton
Case vbLeftButton
zMakeMsg = WM_LBUTTONUP
Case vbRightButton
zMakeMsg = WM_RBUTTONUP
Case vbMiddleButton
zMakeMsg = WM_MBUTTONUP
End Select
End Select
End Function
Im folgenden Schritt, den wir wieder in eine Funktion verpacken,
werten wir zunächst erneut den Button-Wert aus. Hier werden andere
Konstanten-Werte als bei den Nachrichten verwendet (MK_xxx), um
Windows die gedrückte(n) Maustaste(n) anzugeben. Aus dem gleichen
Konstanten-Satz stammen diejenigen, die Windows das Gedrücktsein
einer Strg- und/oder Umschalttaste angeben - dazu werten wir den
Shift-Wert des VB-Mausereignisses aus (mit der Alt-Taste kann
Windows in diesem Zusammenhang nichts anfangen). Alle Konstant-Werte
zusammen werden über eine ODER-Verknüpfung kombiniert und
schließlich als Wert für WParam zurückgegeben.
Private Function zMakeWParam(ByVal iButton As Integer, _
ByVal iShift As Integer) As Long
Dim wParam As Long
If (iButton And vbLeftButton) = vbLeftButton Then
wParam = MK_LBUTTON
End If
If (iButton And vbRightButton) = vbRightButton Then
wParam = wParam Or MK_RBUTTON
End If
If (iButton And vbMiddleButton) = vbMiddleButton Then
wParam = wParam Or MK_MBUTTON
End If
If (iShift And vbCtrlMask) = vbCtrlMask Then
wParam = wParam Or MK_CONTROL
End If
If (iShift And vbShiftMask) = vbShiftMask Then
wParam = wParam Or MK_SHIFT
End If
zMakeWParam = wParam
End Function
Auch das Zusammensetzen der X- und Y-Koordinaten zu dem
geforderten Long-Wert nehmen wir wieder in einer Funktion vor. Die
Regel lautet, dass der Y-Wert im höherwertigen Word des Long-Wertes
landen soll. Was das heißt? Kurz umrissen bedeutet das folgendes.
Ein Long-Wert wird mit genau doppelt so vielen Bytes im
Arbeitsspeicher abgelegt, wie ein Integer-Wert. Anders könnte man
auch sagen, dass es für die Rechnerlogik eigentlich egal ist, ob im
Arbeitsspeicher ein Long-Wert liegt, oder ob zwei Integer-Werte
genau hintereinander liegen. Genauso sieht und behandelt Windows das
auch, wenn im LParam als Long-Wert eigentlich die zwei
Integer-Koordinaten aufeinanderfolgend ankommen. Ich will Sie hier
nicht lange mit hexadezimaler Speicher-Arithmetik aufhalten -
glauben Sie mir einfach, dass Sie den Wert von Y mit 65536 bzw.
hexadezimal &H10000 multiplizieren müssen und dann einfach den
X-Wert hinzuaddieren. Vergessen Sie aber nicht, vorher die beiden
Koordinaten in Pixels umzurechnen. In den allermeisten Fällen
kommen die Koordinaten in der Maßeinheit TWIPS an (Ausnahmen gibt
es wohl nur bei Steuerelementen wie der PictureBox, bei denen Sie
die Skalierung über die Eigenschaft ScaleMode einstellen können).
Private Function zMakeLParam(ByVal iX As Single, _
ByVal iY As Single) As Long
zMakeLParam = (CLng(iX) \ Screen.TwipsPerPixelX) + _
(CLng(iY) \ Screen.TwipsPerPixelY) * &H10000
End Function
Nun haben wir alle Werte umgesetzt und verpackt und können
jeweils im MouseDown, im MouseMove- und im MouseUp-Ereignis die
Nachricht mit SendMessage oder PostMessage verschicken.
Eine kleine Falle müssen wir jedoch noch umgehen. Ohne dass wir
das ohne größeren Aufwand (zumindest nicht so einfach von VB aus)
beeinflussen könnten, verschiebt Windows den Eingabe-Fokus immer
sofort zu einem Steuerelement, wenn über diesem eine Maustaste
niedergedrückt wird - also das MouseDown-Ereignis ausgelöst wird.
Für unsere synchronisierten Steuerelemente hat das zur Folge, dass
der Fokus beim Eintreffen des via SendMessage weitergereichten
MouseDown-Ereignisses sofort zum zweiten Steuerelement
weiterspringt. Solange Sie nichts Besonderes mit den LostFocus- und
GotFocus-Ereignissen der mitspielenden Steuerelemente vorhaben (auch
das Validate-Ereignis ist betroffen), macht dies nicht allzu viel
aus. Sie schubsen den Fokus einfach nach dem Eintreffen der
Nachricht im MouseDown-Ereignis des zweiten Steuerelements wieder
per SetFocus-Methode zum ersten zurück.
Damit Sie den ganzen, wenn auch geringen Aufwand nicht immer
wieder treiben und schreiben müssen, habe ich den notwendigen Code
samt Funktions- und Konstanten-Deklarationen in ein separates Modul
verschoben. Am Prinzip ändert sich dabei nichts, außer dass Sie
nun die Maus-Ereignisse erst einmal an gleichartig aufgebaute (und
nur um den vorangestellten hWnd-Parameter erweiterte) Prozeduren in
diesem Modul weiterreichen. Die Tastatur-Ereignisse weiterzuschieben
und besonders zu verpacken, lohnt hingegen nicht. Die Details dazu
können Sie problemlos dem Code entnehmen.
Eines wäre noch wichtig anzumerken. Wie das Beispiel-Projekt
zeigt, funktioniert der Trick fast hundertprozentig bei ListBoxen
und TextBoxen. Er wird nicht immer funktionieren, zum Beispiel bei
CommandButtons nicht. Aber bei diesen sehe ich auch wenig Sinn
darin, da dessen Click-Ereignis ja die wesentliche Funktion
darstellt und auch direkt angesprochen werden kann. Andere, vor
allem komplexer aufgebaute Steuerelemente, können sich auch
widerborstig verhalten - das müssen Sie im Einzelfall einfach
ausprobieren. Sie werden dann auch schnell merken, dass es zum
Beispiel kein Mausereignis gibt, wenn etwa der Rollbalken einer
ListBox angeklickt wird - Sie können also auch keines weitersenden.
Um eine andere ListBox dennoch zu synchronisieren, greifen Sie zu
"gewöhnlichen" Tricks, wie etwa dem der Gleichsetzung der
TopIndex-Eigenschaft im Scroll-Ereignis...
|