Subclassing mit Access

15.12.2006 von André Minhorst
Access und die Elemente der Benutzeroberfläche – allen voran Formulare – bieten eine ganze Menge Möglichkeiten, Abläufe durch das Programmieren der vorhandenen Ereignisse zu steuern. Doch nicht immer reichen die vorhandenen Ereignisse aus – so gibt es etwa kein Ereignis, welches das Schließen der Datenbank abfangen würde. Wie Sie dies einrichten, erfahren Sie in diesem Artikel.

Sicher haben Sie auch von den verschiedenen Techniken gehört, mit denen moderne Langfinger versuchen, an Ihr Geld zu kommen: Die setzen kleine Kästchen mit Nummerntastatur auf den Kartenschlitz von Geldautomaten, um die Daten Ihrer EC-Karte und Ihre Geheimnummer zu stehlen. Technisch noch versiertere Kollegen dieser Herren fahren auf der Online-Schiene und bauen sich ihre eigenen Webseiten, die so wie die der jeweiligen Kreditinstitute aussehen, und greifen so die gewünschten Daten ab. Was das mit Access zu tun hat? Ganz einfach: Auch Access tauscht Informationen mit seinen Objekten wie Fenstern und Steuerelementen aus, an die in diesem Fall zwar kein Langfinger, aber vielleicht Sie selbst herankommen wollen, nämlich um sich beispielsweise dazwischenzuklemmen, wenn der Benutzer auf die x-Schaltfläche von Access klickt, um die Anwendung zu beenden, und zwischen diesem Mausklick und dem tatsächlichen Beenden von Access noch eigene Aktionen einzuflechten und zu entscheiden, ob und wie die ursprüngliche Nachricht an Access weitergeleitet wird.

Subclassing-Basics

Wie der Titel andeutet, heißt die hier vorgestellte Technik „Subclassing“. Im Detail benötigen Sie dafür nicht mehr als ein paar API-Funktionen und ein paar Grundkenntnisse über die Besonderheiten beim Subclassing. Die erste API-Funktion heißt SetWindowLong und wird wie folgt deklariert:

Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal wNewLong As Long) As Long

Die Parameter haben folgende Bedeutung:

Der Rückgabewert von SetWindowLong ist die bisherige Adresse. Zwischenspeichern Sie sie in einer Variablen, damit Sie Nachrichten nach der Auswertung gegebenenfalls an den Empfänger weiterleiten und Zieladresse für das betroffene Objekt im Anschluss wieder herstellen können.

Nachricht an Empfänger weiterleiten

Nachdem Sie die Nachricht ausgewertet haben, reichen Sie sie meist an den eigentlichen Empfänger weiter – in diesem Fall an das Access-Fenster. Auch hierzu gibt es eine API-Funktion. Sie heißt CallWindowProc und wird wie folgt deklariert:

Private Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

Die Parameter haben folgende Bedeutung:

Zwischenhändler

Natürlich brauchen Sie auch noch die Funktion, deren Adresse Sie mit dem Parameter wNewLong der SetWindowLong festlegen. Sie hat die gleiche Syntax wie CallWindowProc mit Ausnahme von lpPrevWndFunc. Wenn Sie die Funktion fctHookAccessWindow nennen möchten, sieht ihr Prozedurrumpf so aus:

Public Function fctHookAccessWindow(ByVal hwnd As
Long, ByVal msg As Long, ByVal wParam As Long, ByVal
lParam As Long) As Long
End Function

In diese Funktion müssen Sie nun nur noch den Code einfügen, der die Nachricht auswertet, die gewünschten Schritte durchführt und die Nachricht – im Original oder geändert – an den eigentlichen Empfänger weitergibt. Bevor Sie dies tun, werfen Sie einen Blick auf das Zwischenergebnis (Listing 1).

Option Compare Database
Option Explicit
Private Declare Function CallWindowProcA Lib "user32" (ByVal lpPrevWndFunc As Long,
ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long)
As Long
Private Declare Function SetWindowLongA Lib "user32" (ByVal hwnd As Long, ByVal
nIndex As Long, ByVal wNewWord As Long) As Long
Private Const GWL_WNDPROC As Long = (-4)
Private lngProc As Long
Private lngHwnd As Long
Private boolCancel As Boolean
Public Function fctHookAccessWindow(ByVal hwnd As Long, ByVal msg As Long, ByVal
wParam As Long, ByVal lParam As Long) As Long
fctHookAccessWindow = CallWindowProcA(lngProc, hwnd, msg, wParam, lParam)
End Function
Public Function Subclass() As Long
lngHwnd = Application.hWndAccessApp
lngProc = SetWindowLong(lngHwnd, GWL_WNDPROC, AddressOf fctHookAccessWindow)
End Function
Public Function UnSubclass()
SetWindowLong lngHwnd, GWL_WNDPROC, lngProc
lngProc = 0
End Function

Der Grund, warum Sie nun noch nicht einfach loslegen sollen, ist folgender: Subclassing verträgt sich nicht mit dem geöffneten VBA-Editor; zumindest dann nicht, wenn das Subclassing in Access- Modulen realisiert ist. Wenn Sie dafür eine VB-DLL erstellen und von Access aus referenzieren, können Sie wie gewohnt mit dem VBA-Editor arbeiten, aber ansonsten gilt: Speichern Sie jedes Mal, bevor Sie die Subclassing-Routinen verwenden, alle Objekte ab, denn sobald Sie das Subclassing gestartet haben und zum VBA-Editor wechseln, stürzt Access ab – hier hilft dann nur noch der Task-Manager. Das gilt übrigens auch für den Fall, dass Sie den VBA-Editor einmal geöffnet und wieder geschlossen haben: Der VBA-Editor bleibt im Hintergrund geöffnet, und ein eventuell folgendes Subclassing führt zum Absturz.

Das bedeutet, dass die Debugging-Möglichkeiten stark eingeschränkt sind – immerhin können Sie nicht mal eben einen Haltepunkt setzen oder Werte in das Direktfenster schreiben lassen. Zumindest für Letzteres gibt es Auswege: Sie können Variablenwerte oder Meldungen über ein Meldungsfenster ausgeben oder in eine Textdatei oder Tabelle schreiben lassen. Das mit dem Meldungsfenster kann allerdings schnell nervig werden, denn etwa das Verschieben des Access- Hauptfensters erzeugt so viele Nachrichten und damit Aufrufe der Subclassing-Routinen, dass das andauernde Bestätigen der Meldungsfenster schnell nervt.

Wenn Sie also zuerst schauen wollen, was etwa beim Verschieben des Access-Hauptfensters an Nachrichten gesendet wird, legen Sie eine kleine Tabelle wie in Bild 1 an und ergänzen die Routine fctHookAccessWindow wie in Listing 2.

Bild 1: Tabelle zum Speichern von Nachrichten.

Public Function fctHookAccessWindow(ByVal hwnd As Long, ByVal msg As Long, ByVal
wParam As Long, ByVal lParam As Long) As Long
CurrentDb.Execute "INSERT INTO tblMessages(Message, wParam, lParam) VALUES(" &
msg & ", " & wParam & ", " & lParam & ")"
fctHookAccessWindow = CallWindowProcA(lngProc, hwnd, msg, wParam, lParam)
End Function

Nachrichten aufzeichnen

Sichern Sie nun alle Module, und erzeugen Sie ein Formular, mit dem Sie Subclassing an- und ausschalten können. Dazu reichen zwei Schaltflächen, die bei Mausklick jeweils eine der Funktionen Subclass und UnSubclass aufrufen.

Anschließend machen Sie etwas mit dem Fenster – klicken Sie es an, wechseln Sie zu einem anderen Fenster, verschieben Sie es, maximieren, minimieren oder schließen Sie es.

Die Tabelle tblMessages wird im Anschluss einige Nachrichten enthalten. Die Werte für wParam und lParam hängen dabei von der jeweiligen Message ab.

Noch interessanter ist es natürlich, live mitzuerleben, welche Aktion welche Message auslöst. Dazu erweitern Sie das Formular mit den beiden Schaltflächen zum Aktivieren und Deaktivieren des Subclassing einfach wie in Bild 2: Dazu stellen Sie dessen Datenherkunft auf die Tabelle tblMessages ein, fügen die passenden Steuerelemente hinzu und ergänzen die Funktion fctHook AccessWindow um die folgende Anweisung (vor dem Weiterleiten der Message):

Forms!frmSubclassing.Requery

Bild 2: Entwurfsansicht einer Formulars zur Live-Ausgabe von Windows-Messages.

Leider können Sie damit nicht alle Aktionen erfassen, da der Fokus beim Aktualisieren des Formulars immer wieder auf dieses verschoben wird und dadurch etwa das Anzeigen eines Kontextmenüs beim Rechtsklick unterbunden wird.

Bild 3 zeigt, wie das Formular in Aktion aussieht – hier wurde soeben ein Textfeld mit dem Mauszeiger angeklickt.

Bild 3: Das Formular zur Anzeige der aktuellen Windows-Messages im Livebetrieb.

Informationen zu den Messages und den passenden Werten der Parameter wParam und lParamfinden Sie an den verschiedensten Stellen – googeln Sie einfach nach den gesuchten Message- Codes (notfalls die hexadezimale Variante angeben, etwa &h10 für den Wert 16). Eine Suche mit „windows message &h20“ in http://groups.google.de/ liefert etwa knapp 1.000 Einträge.

Praxisbeispiel

Natürlich wollen wir unser Praxisbeispiel nicht ganz aus den Augen verlieren. Wir wollten per Subclassing verhindern, dass Access geschlossen wird, ohne dass eventuelle Aufräumarbeiten oder eine Rückfrage an den Benutzer erfolgt sind. Dazu erweitern Sie die Routine fctHookAccess- Window, fügen noch einige Konstanten hinzu und ergänzen die Funktion SchliessenAbfrage wie in Listing 3.

Option Compare Database
Option Explicit
Private Declare Function CallWindowProcA Lib "user32" (ByVal lpPrevWndFunc As Long,
ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long)
As Long
Private Declare Function SetWindowLongA Lib "user32" (ByVal hwnd As Long, ByVal
nIndex As Long, ByVal wNewWord As Long) As Long
Private Const GWL_WNDPROC As Long = (-4)
Private Const WM_SYSCOMMAND As Long = &H112
Private Const SC_CLOSE As Long = &HF060&
Private Const WM_CLOSE As Long = &H10
Private Const WM_QUERYENDSESSION As Long = &H11
Private lngProc As Long
Private lngHwnd As Long
Private boolCancel As Boolean
Public Function fctHookAccessWindow(ByVal hwnd As Long, ByVal msg As Long, ByVal
wParam As Long, ByVal lParam As Long) As Long
Select Case msg
Case WM_SYSCOMMAND
'Schließen übers Kreuz, Alt + F4 oder die Taskleiste
If wParam = SC_CLOSE Then
boolCancel = SchliessenAbfrage("Schließen via x, Alt + F4 oder
Tastleiste")
End If
Case WM_CLOSE
'Schließen über das Menü, das Application- bzw. Docmd-Objekt
'oder über externe SendMessages etc.
boolCancel = SchliessenAbfrage("Schließen via Menü, Application-Objekt/
DoCmd-Objekt")
Case WM_QUERYENDSESSION
'Windows wird heruntergefahren
boolCancel = SchliessenAbfrage("Schließen, weil Windows heruntergefahren
wird")
Case Else
End Select
If boolCancel = False Then
fctHookAccessWindow = CallWindowProcA(lngProc, hwnd, msg, wParam, lParam)
'UnSubclass
Else
boolCancel = False
fctHookAccessWindow = 0&
End If
End Function
Private Function SchliessenAbfrage(strGrund As String) As Boolean
Dim str As String
str = "Möchten Sie die Datenbank wirklich schließen (" & strGrund & ")?"
If MsgBox(str, vbQuestion Or vbYesNo) = vbNo Then
SchliessenAbfrage = True
End If
End Function
Public Function Subclass() As Long
lngHwnd = Application.hWndAccessApp
lngProc = SetWindowLongA(lngHwnd, GWL_WNDPROC, AddressOf fctHookAccessWindow)
End Function
Public Function UnSubclass()
SetWindowLongA lngHwnd, GWL_WNDPROC, lngProc
lngProc = 0
End Function

Die Funktion fctHookAccessWindowfragt nunmehr gezielt bestimmte Werte für die Message- Nummer ab, die alle zum Beenden von Access führen können. Die Routine SchliessenAbfrage fragt den Benutzer, ob er Access wirklich schließen soll und gibt das Ergebnis an fctHookAccess- Window zurück, die den Schließen-Vorgang entweder durchzieht oder abbricht.

Mehr zum Thema

In diesem Artikel haben Sie einige Grundlagen zum Subclassing kennen gelernt. Mit ein wenig Online-Recherche lassen sich damit interessante Dinge erledigen. In einer der folgenden Ausgaben stellt inside Access Verbesserungen der aktuellen Version dieses Beispiels sowie weitere Einsatzbereiche von Subclassing vor.