Die Community zu .NET und Classic VB.
Menü

Subclassing leicht gemacht

 von 

Übersicht 

Nachrichtenjagd?

Subclassing ist eine Technik um ohne zusätzliche Komponenten an standardmäßig nicht zur Verfügung gestellte Ereignisse, sprich Nachrichten, zu gelangen. Grundsätzlich ist sie schnell zu erlernen und meines Erachtens derzeit für VB noch sehr wichtig. Dieser Artikel soll den Einstieg erleichtern, und die elementaren Dinge erläutern, die es hierbei zu berücksichtigen gilt.

Mit freundlichen Grüßen
Götz Reinecke,

Der geheime Nachrichtendienst  

Hinter den Kulissen eines Fensters, vom VB-Anwender gut abgeschirmt, wird so einiges geflüstert und getuschelt. Gemeint ist der Nachrichtenstrom unter Windows. Womit sich der C++ Programmierer täglich auseinandersetzt, bleibt uns unbedarften VBler auf längere Zeit indirekt verborgen. Indirekt deshalb, da wir ihn nur in Form der einschlägig bekannten Ereignisse nutzen. Ein einfaches Beispiel wäre das Click-Ereignis in einer PictureBox. Erfahrungsgemäß ausgelöst durch einen Klick, setzt es sich in Wirklichkeit aus zwei Nachrichten zusammen, nämlich dem Herunterdrücken und als Folge natürlich auch dem Lösen der Maustaste, also in der Summe wieder ein Klick.

Wie läuft das intern nun genau ab? Dazu müssen wir wissen, daß jedes Fenster eine Nachrichtenprozedur sein Eigen nennt. Der Begriff Fenster ist vielleicht etwas irreführend, da auch CommandButtons und TextBoxen etc. im Windows-Sinne zu dieser Gruppe gehören und ein sogenanntes ChildWindow darstellen. Allesamt besitzen je ein Fenster-Handle, kurz hWnd. Ein solches Handle identifiziert ein Fenster im System eindeutig und wird niemals doppelt zugeteilt. Es sei denn, sein letzter Inhaber wurde geschlossen oder nach einem Reboot.

Die Handles werden dynamisch, nach Bedarf vergeben und es ist auszuschließen, daß Fenstern z.B. nach einem Neustart je ein und die selben Handles erhalten. Zum besseren Verständnis, das

hWnd
ist eine Zugriffsnummer, eine Art ID mit deren Hilfe so allerlei angestellt werden kann. Ergänzend sei erwähnt, daß hWnd eigentlich vom Typ
UnsignedInt
ist, aber auf Grund des nicht Vorhandenseins eines solchen in VB, als Long-Wert behandelt wird.

Haben wir diese Voraussetzung akzeptiert, stellt sich die Frage, wozu das ganze? Zwecks Klärung greifen wir den oben angeführten Klick wieder auf. Wir wollen auf Grund der Überschaubarkeit vorläufig annehmen, es gäbe unter Windows nur Klickereignisse und nichts anderes. Die Maus wird bewegt, mal hier hin und mal dort hin. Dabei ist dem Betriebsystem stets bekannt über welchem Fenster, und damit auch dessen hWnd, sich der Zeiger gerade befindet. Wird nun z.B. die linke Maustaste betätigt, sendet Windows eine Nachricht an die Nachrichtenprozedur des Fensters über dem das Ereignis gerade stattgefunden hat. Dort kann an Hand der Nachricht erkannt werden, daß es sich um eine MouseDown-Ereignis handelt, um anschließend die entsprechenden, gewollten Schritte zu unternehmen. Das gleiche geschieht, wenn die Taste wieder gelöst wird. Die Nachrichtenfunktion kann die verschiedenen Nachrichten deshalb unterscheiden, da jeder ein fester, genormter Long-Wert zugewiesen ist. Für das zu erwartende MouseDown-Ereignis der linken Maus-Taste ist dies der Wert = 513 oder in hexadezimaler Darstellung = &H201

Insgesamt verfügt Windows über ca. 1000 solcher vordefinierter Nachrichten, auch Messages genannt. So viele Zahlen sind schwerlich zu merken. Daher wurden Konstanten mit aussagekräftigen Namen eingeführt. Für unser kleines Beispiel wären das die Konstanten WM_LBUTTONDOWN [für WindowMessage_LeftButtonDown] und WM_LBUTTONUP [für WindowMessage_LeftButtonUp]. Weitere sind z.B. dem API-Viewer zu entnehmen. Öffnen Sie ihn und schauen Sie einfach mal nach Einträgen mit dem Präfix WM_

Darüber hinaus gibt es eine Anzahl weiterer controlspezifischer Nachrichten. Der VB-eigene API-Viewer führt lediglich die Geläufigsten. Werte für selten verwendete Messages sind zur Not den C-Headern des Visual-Studios zu entnehmen.


Abbildung 1: Der ApiViewer stellt eine große Liste der Konstanten zur Verfügung

Fassen wir kurz zusammen: Windows meint ein Ereignis stünde an, codiert dieses entsprechend einer genormten Konstanten, ruft die Nachrichten-Funktion des betroffenen Fensters auf und übergibt an diese den Wert. Dort wird dieser interpretiert und den Anforderungen gemäß abgearbeitet. In VB entspräche das dann dem Auslösen eines der bekannten Ereignisse, wie Picture1_Click() oder Text1_KeyDown etc. Der Vorgang ist in VB transparent, daß heißt unter normalen Gegebenheiten nicht erkennbar.

Unzulängliche Ereignisse?  

Irgendwann einmal kommt wahrscheinlich jeder an den Punkt, ein standardmäßig nicht implementiertes Ereignis zu benötigen. Wohlwissentlich, daß andere, bestehende Anwendungen mit den Gegebenheiten klarkommen, fragt man sich, wieso die Entwickler von VB gerade bei dem jetzt so dringend erforderlichen geschlampt haben und verdammt die vermeintlich beschränkte Programmiersprache in alle Himmelsrichtungen.

Weit gefehlt. Ein unter VB nicht gestelltes Ereignis, muß nicht zwangsläufig unabgreifbar sein. Die Betrachtung ist eher in umgekehrter Richtung vorzunehmen. VB bietet mit den bekannten Events lediglich den Luxus, gängige Ereignisse zu kapseln, so daß wir von den ganzen, damit verbundenen Unannehmlichkeiten verschont bleiben. Das spricht uns aber unlängst nicht von der Notwendigkeit frei, uns mit dem Rest der Welt auseinander setzen zu müssen. Hier liegt nur einer der entschiedenen Vorteile der Sprache VB, nämlich der Klassifizierung dieser Prozeduren. In C++ z.B. (ohne MCF) wäre für die Bereitstellung der VB-gegebenen Formereignisse eine Menge Code erforderlich. Wir hingegen beschränken uns gewohntermaßen darauf, mit ein paar lässigen Bewegungen ein Formular hinzuzufügen und zu bestücken.

Aber weiter. Um nicht gegebene Ereignisse müssen wir uns dann halt selber kümmern. Tja, aber wie? Kommen wir zum Joker: Die Fenster-Funktion ist letztendlich eine prozeduale Funktion die irgendwo an einer festen Adresse im Speicher steht. Windows gestattet es ihre Einsprungadresse (fast) beliebig zu ändern. Was spricht also dagegen, daß wir uns diesen Umstand zu nutze machen und den Zeiger der standardmäßigen VB-Fenster-Funktion auf eine andere, z.B. unsere eigene biegen? Seit VB5 rein gar nichts. Der AdressOf Operator gestattet es die Speicher-Adresse einer beliebigen eigenen Funktion an die API zu übergeben.

Zusammenfassung: Es gibt in VB eine bereits bestehende Fenster-Funktion die sich der Abarbeitung der von Windows gesendeten Nachrichten annimmt. An diese kommen wir aber nicht heran, also verbiegen wir die Einsprungadresse dieser Funktion auf unsere eigene. Weil wir uns aber nur für ein bis ein paar wenige Nachrichten interessieren und nicht wie in C++ das gesamte Eventhandling selber erledigen wollen, rufen wir nach getaner Arbeit die originale Funktion wieder auf. Praktisch gesehen haben wir uns mit diesem Kniff einfach vor die gängige Fenster-Prozedur gestellt und erhalten sämtliche Nachrichten ab sofort zeitlich vor ihr. Das versetzt uns wiederum in die Lage Nachrichten gezielt zu filtern, zu manipulieren oder zu unterdrücken. Diesen Vorgang nennt man im allgemeinen Subklassifizierung oder Subclassing. Mehr ist nicht dabei.

Das Grundgerüst  

Schauen wir uns das einfachstmögliche Gerüst für das oben Besprochene einmal an:

Option Explicit

Private Declare Function SetWindowLong Lib "user32" _
        Alias "SetWindowLongA" (ByVal hWnd As Long, _
        ByVal nIndex As Long, ByVal dwNewLong As Long) _
        As Long

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

Const GWL_WNDPROC = (-4&)

Dim PrevWndProc&

Public Sub Init(hWnd As Long)
    PrevWndProc = SetWindowLong(hWnd, GWL_WNDPROC, _
            AddressOf SubWndProc)
End Sub

Public Sub Terminate(hWnd As Long)
    Call SetWindowLong(hWnd, GWL_WNDPROC, PrevWndProc)
End Sub

Private Function SubWndProc(ByVal hWnd As Long, _
            ByVal Msg As Long, _
            ByVal wParam As Long, _
            ByVal lParam As Long) As Long

    SubWndProc = CallWindowProc(PrevWndProc, hWnd, Msg, _
            wParam, lParam)
End Function

In der Sub Init wird durch Aufruf der Funktion SetWindowLong die neue Adresse der Nachrichtenfunktion

SubWndProc
übergeben. Rückgabewert ist hier die Adresse der ursprünglichen VB-Fensterfunktion.
SetWindowLong
ist vielleicht mehr bekannt im Zusammenhang mit den Fensterstilen, Die Konstante
GWL_WNDPROC
bedingt hier aber das Ändern der Einsprungadresse.

Somit ist das Subclassing bereits initialisiert. Um es zu beenden müssen wir den Zeiger wieder auf die ursprüngliche Adresse zurücksetzen. Da wir ihren Wert ja in der Variablen

PrevWndProc
gespeichert haben, ist dies ein leichtes und durch einen erneuten Aufruf von SetWindowLong zu erreichen. So geschehen in der Sub
Terminate
. Hier interessiert uns der Rückgabewert, der ja dann die Adresse von
SubWndProc
enthalten würde, nicht mehr, daher reicht ein einfacher Call.

Achtung: das Subclassing muß unbedingt vor Beendigung des eigenen Programms aufgelöst werden, da die Nachrichten sonst an eine nicht mehr existierende Prozedur geschickt würden und das unweigerlich einen Absturz zur Folge hätte.

Hier noch eine kleine Ergänzung um die Verwirrung etwas zu steigern. Es spricht theoretisch nichts dagegen, ein bereits "subgeclasstes" Fenster wiederum zu subclassen. Man kann sich das ganze als eine Art Kette von Funktionen vorstellen, wobei die erste nach Beendigung die zweite aufruft, diese nach getaner Arbeit die dritte usw.

Im Prinzip gehen wir genauso vor, wenn wir ein VB-Fenster subclassen. Es ergibt sich eine Kette mit zwei Gliedern. Das erste ist unsere eben geschaffene eigene Fensterprozedur, das zweite, die von VB standardmäßig gestellte. Nun ist es leicht nachvollziehbar, daß je länger eine solche Kette ist, desto langsamer wird das gesamte System. Das gilt es zu beachten, und als Folge müßte klar sein, daß Subclassing nur angewandt werden sollte, wenn ein Problem nicht auf einem anderem Wege lösbar erscheint.

Kommen wir nun zum wesentlichen, zu der Funktion "SubWndProc". Hierbei handelt es sich um unsere vor die eigentliche Fensterfunktion geschobene Prozedur. Windows ruft nun bei jedem eintrudelnden Ereignis unsere Funktion auf und übergibt die anliegenden Nachrichten. Eintrudeln ist hierbei stark untertrieben, es können unter Umständen einige hundert pro Sekunde sein, dies sollte nicht unterschätzt werden. Schauen wir uns also die bei Aufruf übergebenen Parameter etwas genauer an:

  • hWnd ist relativ unkompliziert und beinhaltet einfach nur den hWnd auf den sich die übergebene Nachricht bezieht. Wenn wir also Form1 subclassen, wäre dieser Wert gleich
    Form1.hwnd
  • Msg ist die eigentliche Nachricht und entspricht in der Regel irgendeiner der oben besprochenen vordefinierten Nachrichten. Damit meine ich, es können auch andere sein, da es möglich ist eigene Nachrichten, sogenannte User-Messages, systemweit zu definieren.
  • wParam und lParam übergeben die zur Nachricht gehörende Parameter. Ich weiß das ist sehr allgemein, allerdings gibt es hier keine festen Regularien, vielmehr ist ihre Konstellation vom Nachrichtentyp abhängig und im Zweifelsfalle nachzuschlagen.
    Bei unserer Eingangs erwähnten Nachricht
    WM_LBUTTONDOWN
    stehen in lParam z.B. die aktuellen Mauskoordianten, wobei wParam den Status spezieller Tasten, wie z.B. Schift und Strg näher bezeichnet (Bemerken Sie bitte hier die große Ähnlichkeit zu dem Äquivalent-Ereignis in VB) . Im Zusammenhang mit wParam und lParam gibt es einige Besonderheiten zu beachten, auf die ich später noch zu sprechen kommen werden.

Das waren die Eingangsparameter, als nächstes würden dann unsere zukünftigen Lauschzeilen folgen, die wir aber fürs erste außen vor lassen. Abgeschlossen wird unsere Fensterprozedur, indem wir mit CallWindowProc die ursprüngliche Fenster-Funktion aufrufen und ihr alle von Windows exklusiv erhaltenen Werte weitergeben.
Da es sich um eine Funktion handelt wird im Zweifelsfalle auch ein Rückgabewert erwartet. Hier reichen wir den von der VB-eigenen zurückerstatteten Wert mit

SubWndProc = CallWinwowProc(...)
einfach durch.

Kommen wir damit auf eine weitere, wichtige Angelegenheit, die ich bisher verheimlicht habe, wie vielleicht bereits bemerkt wurde: Fenster lassen sich ja bekanntermaßen auch mit der API RegisterClass erstellen. Mit den entsprechenden Parameter versehen, kreiert Windows nach Ihren Wünschen das passende Fenster. Auffallend ist hierbei, daß keine Adresse einer eventuellen Fensterfunktion übergegeben werden kann. Trotzdem ist ein derart generiertes Fenster aber zugänglich für Ereignisse wie Minimieren, Maximieren, verschieben etc. Wie das?
Die Lösung ist recht banal. Windows stellt diese Standardmethoden in Form der Funktion DefWindowProc selbst zur Verfügung. Ein mit

RegisterClass
erstelltes Fenster handelt seine Events solange über diese interne Funktion, bis deren Einsprungadresse auf eine eigene Prozedur verbogen wird. Das bedeutet für uns, daß wir, falls wir ein Fenster mit RegisterClass erstellen, nach Abschluß unserer Fensterprozedur nicht die Folgeprozedur aufrufen (geht ja auch nicht, da nicht existent) , sondern die alles abschließende
DefWindowProc
, die übrigens auch von der VB-eigenen Prozedur nach getaner Arbeit bemüht wird.

Das verleitet uns zu einem weiteren Gedanken. Was wäre, wenn wir in unserer eigenen Prozedur abschließend nicht wie üblich die VB-Fenster-Funktion aufriefen, sondern ebenfalls direkt an

DefWindowProc
weiterleiten? Nunja, eigentlich nichts mehr, im wahrsten Sinne des Wortes. Zumindest nicht in VB, da ja jetzt sämtliche Events an der VB-Fensterprozedur vorbeilaufen. Probieren Sie dies ruhig einmal aus, ich selbst habe es noch nicht getestet.

Ergänzend, aber von großer Bedeutung, sei noch bemerkt, daß eigene Fensterprozeduren sich ausschließlich nur in Standardmodulen aufhalten dürfen, nicht in Formularen und auch nicht direkt in Klassen. Letzteres begründet sich darauf, daß VB mit dem

AdressOf
-Operator nur Module erfassen kann. Es handelt sich hier also um ein VB-typisches, hausgemachtes Manko. Daher setzen wir oben beschrieben Code in ein neues Standardmodul und rufen die
Init
- und
Terminate
Sub wie folgt von einem Formular aus auf:

Option Explicit

Private Sub Form_Load()
  Call Init(Command1.hWnd)
End Sub

Private Sub Form_Unload(Cancel As Integer)
  Call Terminate(Command1.hWnd)
End Sub

Das ist sehr wenig, muß aber so sein, da Standardmodule nicht wie Formulare und Klassen. über Initialisierungs- bzw. Terminate-Ereignisse verfügen.

Die unzulänglichen Gefahren  

Bevor wir nun zu unserer ersten verwertbaren Subklassifizierung kommen, noch ein paar wichtige Hinweise. Es gibt hier einige Besonderheiten im Zusammenhang mit der IDE und der Strukturierung zu berücksichtigen.


Abbildung 2: Mit diesen Fehlermeldungen wird man öfters beim Subclassen in der IDE konfrontiert.

So sollten Sie die folgenden Erläuterungen aufmerksam lesen und gegebenenfalls alles einmal praktisch durchspielen, um sich mit den Ecken und Kanten der Methoden vertraut zu machen:

Allgemeine Sicherheitshinweise

  • Wenn Sie an Ihren Subclassings entwickeln, ist es unumgänglich, daß Ihnen hin und wieder die IDE inklusive des zu bearbeitenden Programms wegen unzulässiger Vorgänge geschlossen wird. Das ist nicht weiter schlimm und gehört hier zum Handwerk, heißt aber in der Konsequenz, daß Sie Ihr Programm grundsätzlich vor dem Starten sichern sollten, sonst ist der Ärger später groß.

Fehler & Debuggen

  • Beides geht einher. Treten Fehler auf oder werden Haltepunkte gesetzt, so wird wie gewohnt im Entwurfsmodus Mr. Debugger angeworfen. Der verharrt in betroffener Zeile solange bis ihm aufgetragen wird fortzufahren. Das würde er auch tuen, wenn das aktuelle Programm für Events weiterhin empfänglich wäre. Dummerweise können wir uns aber gerade inmitten in der Routine befinden, durch die das benötigte Ereignis für die Fortsetzung ginge. Dieses kann aber erst dann abgearbeitet werden, wenn der aktuelle Durchlauf vollzogen ist und dies geht wiederum nur, wenn wir den Haltemodus des Debuggers ausschalten. Also eine Sackgasse. Bei Haltepunkten gibt es unter Umständen nur ein Entkommen, nämlich über die Tastenkombination Strg + Umschalt + F9
    Fehlermeldungen können mit der On Error Resume Next Anweisung unterdrückt werden. Andernfalls hilft manchmal nur der Affengriff [Strg + Alt + Entf] um die Anwendung zu schließen

Verschachtelungen als unerwünschte Rekursion

  • Ein Überlauf des Stapel kann eintreten, wenn aus einer Fensterprozedur ein Vorgang ausgelöst wird, welcher eine Nachricht an die Fensterprozedur veranlaßt, diese Nachricht wiederum löst dann erneut den ursprünglichen Vorgang aus der daraufhin ... usw. solange, bis der Stack überläuft. Daraus folgt letztendlich eine nicht abfangbare Fehlermeldung und damit ein Absturz. So etwas kann umgangen werden, indem durch das Setzen von statischen Flags ein weiteres Auslösen verhindert wird, so daß das ordnungsgemäße Abarbeiten der aktuellen Nachricht gewährleistet ist.

Das große Beenden

  • Ein immer wieder gemachter und Verwunderung auslösender Fehler, ist das Schließen des Programms bei laufendem Subclassing über den Stop-Button der IDE. Durch dessen abruptes Beenden wird im Formular nicht das übliche Form_Unload Ereignis ausgelöst, so daß das Subclassing unterminiert bleibt. Es kommt zum Absturz. Abhilfe schafft das Terminieren der Nachrichtenfunktion mitttels Abfangen der WM_DESTROY Nachricht oder das grundsätzliche Schließen des Programms über die X-Schaltfläche in der Titelleiste oder durch einen seperaten Button. Wohlhemerkt, dieser Effekt tritt nur in der IDE auf und kann daher in der kompilierten Exe unberücksichtigt bleiben.

Ein Beispiel  

Nun, jetzt wären wir soweit unser Wissen an anhand eines kleine Beispiels unter Beweis zu stellen. Es sollte etwas halbwegs nützliches, aber als Ereignis in VB noch nicht vorliegendes sein. Vorschlag zur Güte, ein MouseMove-Ereignis für den NonClient-Bereich. Das ist die Fläche des Formulars, die normalerweise in VB nicht direkt erreichbar ist. Genauer gesagt die Menüleiste, die Caption als auch je nach Einstellung die 4 Pixel breiten Ränder. Sie erkennen diese Fläche in dem weiter unten downloadbaren Beispiel sehr leicht, da sie einen farblichen Kontrast zur Client-Area bietet. Schauen wir einmal in den API-Viewer, ob wir einen passenden Nachrichtentyp zu unserer Aufgabe finden. Wir stoßen unter anderem auf folgende drei Konstanten:

Const WM_NCLBUTTONDBLCLK = &HA3
Const WM_NCLBUTTONDOWN = &HA1
Const WM_NCLBUTTONUP = &HA2

Das sieht schon sehr vielversprechend aus. Jetzt müssen wir nur in unserer Fensterprozedur auf diese Nachrichten lauern. Das machen wir vorerst mit nur einer Message wie folgt:

Option Explicit

Private Declare Function SetWindowLong Lib "user32" _
        Alias "SetWindowLongA" (ByVal hWnd As Long, _
        ByVal nIndex As Long, ByVal dwNewLong As Long) _
        As Long

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

Const GWL_WNDPROC = (-4&)

Dim PrevWndProc&

Public Sub Init(hWnd As Long)
  PrevWndProc = SetWindowLong(hWnd, GWL_WNDPROC, _
                              AddressOf SubWndProc)
End Sub

Public Sub Terminate(hWnd As Long)
  Call SetWindowLong(hWnd, GWL_WNDPROC, PrevWndProc)
End Sub

Private Function SubWndProc(ByVal hWnd As Long, _
                            ByVal Msg As Long, _
                            ByVal wParam As Long, _
                            ByVal lParam As Long) As Long

  If Msg = WM_NCLBUTTONDBLCLK Then MsgBox ("DoubleClick")

  SubWndProc = CallWindowProc(PrevWndProc, hWnd, Msg, _
                              wParam, lParam)
End Function

Zum Testen Doppel-Klicken Sie bitte auf eine beliebige Stelle des NonClient-Bereichs, wie z.B. die Menüleiste, die Caption oder die Ränder. Er ist leicht zu erkennen da der Client Bereich sich durch seine weiße Hintergrundfarbe abhebt. Bisher haben wir nur die Nachricht ausgewertet, was aber bieten dabei die übergebenen Paramter

wParam
und
lParam
? Ein Blick in die Win32.hlp verrät, daß wParam das Ergebnis des sogenannten HitTests wiedergibt, welcher Aufschluß darüber gibt, auf was letztendlich der Klick ausgeübt wurde, hierzu später mehr.
lParam
beinhaltet die absoluten x-, y-Koordinate, der Stelle auf die geklickt wurde. Wohlgemerkt die absoluten Bildschirmkoordinaten. Um die relativen zu erhalten, müssen die absoluten Eckkoordinaten des Fenster hiervon abgezogen werden.


Abbildung 3: Die win32.hlp Datei bietet umfassende Informationen über die Window Messages.

Fein, das wollen wir genauer wissen. Doch gibt es noch einen kleinen Stolperstein: Die übergebene

Long
-Variable bietet zwei Werte vom Typ
Short
in einem einzigen, wobei das obere Word die y-Koordinate und das unter den x-Wert darstellt. Da VB keine Variable vom Typ
Short
kennt. müssen wir hier ein wenig tricksen und uns eine kleine Funktion zwecks Umrechnung schreiben:

Private Sub GetShort(Value&, Lo&, Hi&)
  Lo = Value And &H7FFF
  Hi = Value \ &H10000
End Sub

Weiterhin interessiert uns die Sache mit dem HitTest in

wParam
. Ein erneuter Blick in den API-Viewer verrät uns weiterhin folgende Konstanten:

Const HTBORDER = 18
Const HTBOTTOM = 15
Const HTBOTTOMLEFT = 16
...
etc.

Da aber reine Zahlen als Rückgabewert nicht sehr aussagekräftig sind, stricken wir uns eine zweite, einfache Hilfs-Funktion, die aus dem von

wParam
zurückgegebenen Wert einen lesbaren Text erstellt:

Private Function HitTestToString(HitTest&) As String
  Dim aa$

    Select Case HitTest
      Case HTBORDER:      aa = "HTBORDER"
      Case HTBOTTOM:      aa = "HTBOTTOM"
      Case HTBOTTOMLEFT:  aa = "HTBOTTOMLEFT"
      Case HTBOTTOMRIGHT: aa = "HTBOTTOMRIGHT"
      Case HTCAPTION:     aa = "HTCAPTION"
      Case HTCLIENT:      aa = "HTCLIENT"
      Case HTERROR:       aa = "HTERROR"
      Case HTGROWBOX:     aa = "HTGROWBOX"
      Case HTHSCROLL:     aa = "HTHSCROLL"
      Case HTLEFT:        aa = "HTLEFT"
      Case HTMAXBUTTON:   aa = "HTMAXBUTTON"
      Case HTMENU:        aa = "HTMENU"
      Case HTMINBUTTON:   aa = "HTMINBUTTON"
      Case HTNOWHERE:     aa = "HTNOWHERE"
      Case HTRIGHT:       aa = "HTRIGHT"
      Case HTSYSMENU:     aa = "HTSYSMENU"
      Case HTTOP:         aa = "HTTOP"
      Case HTTOPLEFT:     aa = "HTTOPLEFT"
      Case HTTOPRIGHT:    aa = "HTTOPRIGHT"
      Case HTTRANSPARENT: aa = "HTTRANSPARENT"
      Case HTVSCROLL:     aa = "HTVSCROLL"
      Case HTCLOSE:       aa = "HTCLOSE"
      Case HTHELP:        aa = "HTHELP"
      Case Else:          aa = CStr(HitTest)
    End Select

    HitTestToString = aa
End Function

Zudem soll neben dem Click- auch ein MouseMove-Event ausgelöst werden. Dazu bedienen wir uns der Nachricht

WM_NCMOUSEMOVE
die sich ansonsten in nichts von den bereits verwendeten Maus-Tastennachrichten unterscheidet.

Um die Ereignisse letztendlich auch im Formular nutzen zu können, fügen wir in Form1 neben einigen Labeln zur Anzeige, folgenden Code ein:

Public Sub NonClient_LeftMouseButton(State As Integer, _
                                     x As Long, _
                                     y As Long)
  Label4.Caption = x
  Label5.Caption = y

  Select Case State
   Case 1: Label6.Caption = "MouseDown"
   Case 2: Label6.Caption = "MouseUp"
  End Select
End Sub

Public Sub NonClient_LeftDblClick(x As Long, y As Long)
  Static DblClkCount&

    Label4.Caption = x
    Label5.Caption = y

    DblClkCount = DblClkCount + 1
    Label7.Caption = DblClkCount
End Sub

Die Subs müssen vom Typ Public sein, damit sie vom Modul aus aufgerufen werden können. Das Modul als solches wird jetzt noch um die neue Nachricht und die Auswert-Funktionen erweitert, womit unser kleines Projekt auch schon abgeschlossen wäre:

Private Function SubWndProc(ByVal hWnd As Long, _
                            ByVal Msg As Long, _
                            ByVal wParam As Long, _
                            ByVal lParam As Long) As Long

  Dim x&, y&, Text$, State%

    Select Case Msg
      Case WM_NCLBUTTONDOWN, _
           WM_NCLBUTTONUP, _
           WM_NCLBUTTONDBLCLK:

        Call GetShort(lParam, x, y)
        x = x - Form1.Left \ Screen.TwipsPerPixelX
        y = y - Form1.Top \ Screen.TwipsPerPixelY

        If Msg = WM_NCLBUTTONDOWN Then
          State = 1
        ElseIf Msg = WM_NCLBUTTONUP Then
          State = 2
        End If

        If Msg = WM_NCLBUTTONDBLCLK Then
          Call Form1.NonClient_LeftDblClick(x, y)
        Else
          Call Form1.NonClient_LeftMouseButton(State, x, y)
        End If



      Case WM_NCMOUSEMOVE:

        Call GetShort(lParam, x, y)
        x = x - Form1.Left \ Screen.TwipsPerPixelX
        y = y - Form1.Top \ Screen.TwipsPerPixelY
        Text = HitTestToString(wParam)
        Call Form1.NonClient_MouseMove(Text, x, y)



    End Select

    SubWndProc = CallWindowProc(PrevWndProc, hWnd, _
                                Msg, wParam, lParam)
End Function

Projekt als Download [10807 Bytes]

Ausblick auf Besonderheiten  

Sicherlich ist der hiesige Artikel nur eine knappe Einführung zu diesem umfassenden Thema. Letztendlich kann man hier sehr schnell abschweifen und halbe Bücher füllen, da es im wesentlichen um die unmaskierte Funktionsweise von Windows geht. Wie bereits Eingangs erwähnt, gibt es eine Fülle von Nachrichten, mit noch mehr unterschiedlichen Zusammenhängen, in denen sie in Erscheinung treten können.
Um ein Beispiel zu geben: Mit

lParam
können auch Strukturen übergeben werden, allerdings als Zeiger, da VB keine Zeiger beherscht, muß sich hier mit CopyMemory beholfen werden. Denkbar wäre es nun, nachdem die Struktur vorliegt, an ihr Änderungen vorzunehmen und sie abschließend zurück zu kopieren damit die Manipulationen auch wirksam werden.

Abschließend möchte ich noch bemerken, daß Subclassing zwangsläufig einen Einblick in Windows und dessen vielseitiges Nachrichtenwesen gewährt und damit auch wesentlich zum besseren Verständnis der Abläufe unter VB beiträgt.

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.