Subclassing

Hilfreiche Erklärungen und Tipps zum Lernen von Autohotkey

Moderator: jNizM

just me
Posts: 9406
Joined: 02 Oct 2013, 08:51
Location: Germany

Subclassing

19 May 2014, 03:39

Was ist das?

Na ja, eine treffende Übersetzung fällt mir schwer. Eine Möglichkeit wäre "Subklassieren" im Sinne von "zu einer Subklasse (Unterklasse) machen". Alle Fenster unter Windows müssen mit einem Klassennamen registriert werden. Auch die mit Windows gelieferten vorgefertigten Common Controls, die AHK für seine Gui Controls verwendet, haben registrierte Klassennamen. Das deutet darauf hin, das Microsoft die Grunddefinitionen der Controls als Klassen versteht, deren Instanzen dann die wirklichen Controls bilden.

Die Eigenschaften der Controls werden über Stile (styles / ex(tended) styles) und teilweise über Nachrichten (messages) gesteuert. Die Interaktion mit dem Benutzer / System erfolgt durch Nachrichten. Für die Verarbeitung der Nachrichten ist für jedes Fenster oder Control eine spezielle Routine verantwortlich, die Nachrichtenschleife (message loop). Und genau hier ist der Ansatz für das Subclassing. Wenn man per Subclassing eine eigene Funktion bereitstellt, wird diese für die Nachrichtenverarbeitung anstelle der originären Routine des Controls aufgerufen. Damit sind direkte Eingriffe in die Nachrichtenverarbeitung möglich.

Wozu ist das gut?

Als Beispiel soll ein kürzlich im Forum diskutiertes Problem dienen: Wie verhindere ich, dass Text aus einem Edit-Control herauskopiert wird?

Die Kopieraktionen werden über die Nachrichten WM_COPY (kopieren) bzw. WM_CUT (ausschneiden) gesteuert. So sieht das dann auf den ersten Blick einfach aus: Einfach die Nachrichten WM_COPY und WM_CUT mit zwei OnMessage() Anweisungen auf eine eigene Funktion umleiten und dort die Verarbeitung verhindern. Gesagt, getan, und der Erfolg? Keiner! Offensichtlich kann man diese Nachrichten zumindest in diesem Fall nicht per OnMessage() abfangen.

Jetzt schlägt die Stunde für das Subclassing. Indem man die komplette Nachrichtenverarbeitung des Edit-Controls auf eine eigene Routine umleitet, kann man auch die dem Edit-Control gesendeten Nachrichten WM_COPY und WM_CUT abgreifen und verhindern, dass sie an die originäre Nachrichtenschleife des Controls durchgeleitet werden. Und siehe, Kopieren und Ausschneiden funktionieren nicht mehr.

Wie wird das gemacht?

Mit der Version 6 der ComCtl32.dll (also mit Win XP) hat Microsoft ein vereinfachtes Verfahren für das Subclassing eingeführt. Es besteht im Wesentlichen aus nur drei API-Funktionen: Dazu gibt es noch unter SUBCLASSPROC eine Beschreibung der Parameter, die die aufzurufende Funktion entgegennehmen muss.

Wie die wortgetreuen Übersetzungen der Namen nahelegen, wird mit SetWindowSubclass() eine Subclassfunktion gesetzt und mit RemoveWindowSubclass() wieder entfernt. Die aufzurufende Funktion wird als stinknormale AHK Funktion mit genau sechs Parametern definiert:

Code: Select all

MeineSubclassFunktion(HWND, Msg, wParam, lParam, IdSubclass, RefData) {
   ; Parameter:
   ;     HWND        -  Das Handle (HWND) / die ID des Controls.
   ;     Msg         -  Die Nummer der Nachricht als vorzeichenloser Integerwert.
   ;     lParam      -  Ein nachrichtenabhängiger Wert in Pointergröße.
   ;     wParam      -  Ein nachrichtenabhängiger Wert in Pointergröße.
   ;     IdSubclass  -  Die eindeutige ID in Pointergröße, die in SetWindowSubclass() festgelegt wurde.
   ;     RefData     -  Ein zusätzlicher Wert in Pointergröße, der in SetWindowSubclass() festgelegt wurde.
   ...
}
Ihre Adresse, die sowohl an SetWindowSubclass() als auch an RemoveWindowSubclass() im Parameter pfnSubclass übergeben werden muss, holt man sich mit der Funktion RegisterCallback(): SubclassProcAddr := RegisterCallback("MeineSubClassFunktion").

Und wie sieht das dann in AHK aus?

Na ja, wie bereits im vorigen Abschnitt gesagt, es braucht folgende Schritte:
  1. Zunächst braucht es die Subclassfunktion mit ihren 6 Parametern. Für alle Nachrichten, die innerhalb der Funktion bearbeitet werden sollen, muss man sich im MSDN und/oder anderen Quellen die Nachrichtennummer und die Beschreibung des Inhalts der Parameter wParam und lParam besorgen. Dann kann dieser Teil mit Leben gefüllt werden. Alle Nachrichten, die in der Subclassfunktion nicht vollständig verarbeitet oder unterdrückt werden sollen, müssen der ursprünglichen Nachrichtenschleife des Controls übergeben werden, damit das Control sie verarbeiten kann.
    Das erledigt ein Aufruf von DefSubclassProc().
    Für den Returnwert der Subclassfunktion gilt:
    • wenn eine Nachricht in der Funktion komplett verarbeitet wird, sucht man sich den passenden Wert im MSDN (in vielen Fällen reicht eine 0).
    • wenn die Nachricht an DefSubclassProc() übergeben wird, wird der Returnwert dieser Funktion zurückgegeben.
    Das Ganze sieht dann in etwa so aus:

    Code: Select all

    MeineSubclassFunktion(HWND, Msg, wParam, lParam, IdSubclass, RefData) {
       ; Definitionen
       ; ...
       ; Eigene Verarbeitungsroutinen
       If (Msg = ????) {
          ; mach das!
          Return 0 ; oder auch nicht!!!
       }
       ; ...
       Return DllCall("Comctl32.dll\DefSubclassProc", "Ptr", HWND, "UInt", Msg, "Ptr", wParam, "Ptr", lParam, "Ptr")
  2. Dann brauchen wir die Adresse der Funktion. Das macht man mit:

    Code: Select all

    SubclassProcAddr := RegisterCallback("MeineSubclassFunktion")
  3. Jetzt kann die Subclassfunktion mit SetWindowSubclass() aktiviert werden. Die beiden ersten Funktionsparameter sind fast selbsterklärend. hWnd ist (natürlich) das Handle (HWND) des Controls. pfnSubclass ist die Adresse der aufzurufenden Funktion, hier also SubclassProcAddr.
    Im dritten Parameter uIdSubclass wird ein numerischer Wert übergeben, der als fünfter Parameter an die Subclassfunktion durchgereicht wird. Microsoft sagt dazu, dass die Kombination aus zweitem und drittem Parameter die Subklasse eindeutig idenfiziert. Hintergrund ist, dass man einem Control durchaus mehrere Subclassfunktionen bzw. ein und dieselbe Funktion auch mehrfach mit unterschiedlichen IDs zuweisen kann (der Sinn hat sich mir noch nicht erschlossen), die per DefSubclassProc() schön nacheinander aufgerufen werden. Im letzeren Fall kann man dann (wozu auch immer) anhand der ID zwischen den Aufrufen unterscheiden. Wenn man das nicht braucht, darf es auch eine 0 sein.
    Im letzten Parameter dwRefData kann ein weiterer Integerwert übergeben werden, der dann als sechster Parameter an die Subclassfunktion durchgereicht wird. Das kann auch die Adresse einer Variablen oder eines Objekts sein. Auf das Objekt kann dann in der Subclassfunktion per MeinObjekt := Object(RefData) zugegriffen werden. Wenn man das nicht braucht, darf es auch eine 0 sein.
    Als AHK-Code sieht das dann z.B. so aus:

    Code: Select all

    DllCall("Comctl32.dll\SetWindowSubclass", "Ptr", HCTRL, "Ptr", SubclassProcAddr, "Ptr", 0, "Ptr", 0)
  4. Sollte man das Subclassing wieder beenden wollen, ist das jederzeit per RemoveWindowSubclass() möglich. Im MSDN wird empfohlen, das Subclassing so zu beenden, bevor das Control per Gui, Destroy zerstört wird. Die drei Parameter kennen wir bereits von SetWindowSubclass(). Sie haben hier identische Bedeutungen, d.h. sie müssen dieselben Inhalte haben wie im zugehörigen SetWindowSubclass() Aufruf. Als AHK-Code sieht das dann z.B. so aus:

    Code: Select all

    DllCall("Comctl32.dll\RemoveWindowSubclass", "Ptr", HCTRL, "Ptr", SubclassProcAddr, "Ptr", 0)

Beispiel:

Als praktisches Beispiel soll eine Umsetzung des unter "Wozu ist das gut?" beschriebenen Problems dienen. Die DllCalls für das Subclassing habe ich darin in der Funktion SubclassControl() verpackt, weil ich das so übersichtlicher finde. Auf den Aufruf von RemoveWindowSubclass() habe ich hier verzichtet, weil keine betroffenen Controls zerstört werden, bevor das Skript endet.

Code: Select all

#NoEnv
Gui, Add, Edit, W400 h300 hwndHED, Kopier mich mal!
Gui, Show, , Edit
SubclassControl(HED, "NotAllowed")
Return
GuiClose:
ExitApp
; ======================================================================================================================
NotAllowed(HWND, Msg, wParam, lParam, SubclassID, RefData) {
   ; WM_CUT = 0x0300, WM_COPY = 0x0301
   If (Msg = 0x0300) || (Msg = 0x0301) {
      EM_SHOWBALLOONTIP(HWND, "Nein!", "Kopieren ist hier verboten!", 3)
      Return 0
   }
   Return DllCall("Comctl32.dll\DefSubclassProc", "Ptr", HWND, "UInt", Msg, "Ptr", wParam, "Ptr", lParam)
}
; ======================================================================================================================
; SubclassControl    Installs, updates, or removes the subclass callback for the specified control.
;                    Installiert, ändert oder entfernt den Subclass-Funktionsaufruf für das angegebene Control.
; Author:            just me (www.ahkscript.org)
; Tested with:       AHK 1.1.15.00 U64
; Tested on:         Win 8.1 x64
; Parameters:        HCTRL    -  Handle to the control.
;                                Handle (HWND) des Controls.
;                    FuncName -  Name of the callback function as string.
;                                Name der aufzurufenden Subclass-Funktion als Zeichenkette.
;                                If you pass an empty string (""), the subclass callback will be removed.
;                                Wenn eine leere Zeichenkette ("") übergeben wird, wird die Subclass-Funktion entfernt.
;                    RefData  -  Optional integer value passed to the callback function.
;                                Ein optionaler Integer-Wert, der an die Subclass-Funktion übergeben wird.
; Return value:      Non-zero if the subclass callback was successfully installed, updated, or removed;
;                    otherwise, False.
; Remarks:           The callback function must have exactly six parameters, see
;                    Die Sublass-Funktion muss genau sechs Parameter entgegennehmen, siehe
;                    SUBCLASSPROC -> msdn.microsoft.com/en-us/library/bb776774(v=vs.85).aspx
; ======================================================================================================================
SubclassControl(HCTRL, FuncName, RefData := 0) {
   Static ControlCB := []
   If ControlCB.HasKey(HCTRL) {
      DllCall("Comctl32.dll\RemoveWindowSubclass", "Ptr", HCTRL, "Ptr", ControlCB[HCTRL], "Ptr", HCTRL)
      DllCall("Kernel32.dll\GlobalFree", "Ptr", ControlCB[HCTRL], "Ptr")
      ControlCB.Remove(HCTRL, "")
      If (FuncName = "")
         Return True
   }
   If !DllCall("User32.dll\IsWindow", "Ptr", HCTRL, "UInt")
   Or !IsFunc(FuncName) || (Func(FuncName).MaxParams <> 6)
   Or !(CB := RegisterCallback(FuncName, , 6))
      Return False
   If !DllCall("Comctl32.dll\SetWindowSubclass", "Ptr", HCTRL, "Ptr", CB, "Ptr", HCTRL, "Ptr", RefData)
      Return (DllCall("Kernel32.dll\GlobalFree", "Ptr", CB, "Ptr") & 0)
   Return (ControlCB[HCTRL] := CB)
}
; ======================================================================================================================
; Displays a balloon tip associated with an edit control.
; Zeigt einen Ballontipp an der Position der Eingabemarkierung.
; Parameter:
;     Title -  Titel des Ballontipps.
;     Text  -  Text des Ballontipps.
;     Icon  -  Einer der Schlüssel des Objekts Icons.
; Return Werte:
;     Bei Erfolg größer Null, im Fehlerfall False (0).
; ======================================================================================================================
EM_SHOWBALLOONTIP(HWND, Title, Text, Icon := 0) {
   ; EM_SHOWBALLOONTIP = 0x1503 -> http://msdn.microsoft.com/en-us/library/bb761668(v=vs.85).aspx
   Static Icons := {0: 0, 1: 1, 2: 2, 3: 3, NONE: 0, INFO: 1, WARNING: 2, ERROR: 3}
   NumPut(VarSetCapacity(EBT, 4 * A_PtrSize, 0), EBT, 0, "UInt")
   If !(A_IsUnicode) {
      VarSetCapacity(WTitle, StrLen(Title) * 4, 0)
      VarSetCapacity(WText, StrLen(Text) * 4, 0)
      StrPut(Title, &WTitle, "UTF-16")
      StrPut(Text, &WText, "UTF-16")
   }
   If !Icons.HasKey(Icon)
      Icon := 0
   NumPut(A_IsUnicode ? &Title : &WTitle, EBT, A_PtrSize, "Ptr")
   NumPut(A_IsUnicode ? &Text : &WText, EBT, A_PtrSize * 2, "Ptr")
   NumPut(Icons[Icon], EBT, A_PtrSize * 3, "UInt")
   Return DllCall("User32.dll\SendMessage", "Ptr", HWND, "UInt", 0x1503, "Ptr", 0, "Ptr", &EBT, "Ptr")
}
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Subclassing

08 Feb 2015, 06:59

Ich war grade in der Lage das Wissen, dass ich durch dieses Tutorial erarbeitet habe in die Praxis umzusetzen.
Ich kann den Tipp geben, dass es nicht ratsam ist ein ActiveX control zu subclassen.
Recommends AHK Studio
just me
Posts: 9406
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: Subclassing

08 Feb 2015, 07:04

Diese Art des Subclassing wird im MSDN ja auch für die Common Controls beschrieben. Ich habe keine Ahnung, ob für die ActiveX Controls so etwas wie DefSubclassProc() überhaupt existiert.
tmplinshi
Posts: 1604
Joined: 01 Oct 2013, 14:57

Re: Subclassing

05 Mar 2015, 09:11

I don't understand German language. But found the example code useful :D, especially EM_SHOWBALLOONTIP(). Thank you just me!
User avatar
TLM
Posts: 1608
Joined: 01 Oct 2013, 07:52
Contact:

Re: Subclassing

10 Dec 2019, 18:39

Ich versuche das Gleiche zu tun, etwas umgekehrt.
Ich möchte erkennen, wann die EM_SHOWBALLOONTIP-Nachricht für ein Steuerelement ausgelöst wird, und dann einen Handler aufrufen.
Ich bin mir nicht sicher, ob dies mit Unterklassen möglich ist

I'm trying to do the same thing, somewhat in reverse.
What I'd like to do is detect when the EM_SHOWBALLOONTIP message fires for a control and then call a handler.
Not sure if this is possible using subclassing
:thinking:
just me
Posts: 9406
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: Subclassing

11 Dec 2019, 05:32

Hi @TLM,

Code: Select all

#NoEnv
Gui, Margin, 20, 50
Gui, Add, Text, , Password:
Gui Add, Edit, w200 y+5 +HwndHED +Number +Password
Gui, Show, , Window
SubclassControl(HED, "EditSubclass")
Return
GuiClose:
ExitApp
; ======================================================================================================================
EditSubclass(HWND, Msg, wParam, lParam, SubclassID, RefData) {
   Static OffTitle := A_PtrSize
   Static OffText  := A_PtrSize * 2
   ; EM_SHOWBALLOONTIP = 0x1503 -> http://msdn.microsoft.com/en-us/library/bb761668(v=vs.85).aspx
   If (Msg = 0x1503) {
      Txt := StrGet(NumGet(lParam + OffText, "UPtr"), "UTF-16")
      ToolTip, EM_SHOWBALLOONTIP: %Txt%
      Return 0
   }
   Return DllCall("Comctl32.dll\DefSubclassProc", "Ptr", HWND, "UInt", Msg, "Ptr", wParam, "Ptr", lParam)
}
; ======================================================================================================================
; SubclassControl    Installs, updates, or removes the subclass callback for the specified control.
;                    Installiert, ändert oder entfernt den Subclass-Funktionsaufruf für das angegebene Control.
; Author:            just me (www.ahkscript.org)
; Tested with:       AHK 1.1.15.00 U64
; Tested on:         Win 8.1 x64
; Parameters:        HCTRL    -  Handle to the control.
;                                Handle (HWND) des Controls.
;                    FuncName -  Name of the callback function as string.
;                                Name der aufzurufenden Subclass-Funktion als Zeichenkette.
;                                If you pass an empty string (""), the subclass callback will be removed.
;                                Wenn eine leere Zeichenkette ("") übergeben wird, wird die Subclass-Funktion entfernt.
;                    RefData  -  Optional integer value passed to the callback function.
;                                Ein optionaler Integer-Wert, der an die Subclass-Funktion übergeben wird.
; Return value:      Non-zero if the subclass callback was successfully installed, updated, or removed;
;                    otherwise, False.
; Remarks:           The callback function must have exactly six parameters, see
;                    Die Sublass-Funktion muss genau sechs Parameter entgegennehmen, siehe
;                    SUBCLASSPROC -> msdn.microsoft.com/en-us/library/bb776774(v=vs.85).aspx
; ======================================================================================================================
SubclassControl(HCTRL, FuncName, RefData := 0) {
   Static ControlCB := []
   If ControlCB.HasKey(HCTRL) {
      DllCall("Comctl32.dll\RemoveWindowSubclass", "Ptr", HCTRL, "Ptr", ControlCB[HCTRL], "Ptr", HCTRL)
      DllCall("Kernel32.dll\GlobalFree", "Ptr", ControlCB[HCTRL], "Ptr")
      ControlCB.Remove(HCTRL, "")
      If (FuncName = "")
         Return True
   }
   If !DllCall("User32.dll\IsWindow", "Ptr", HCTRL, "UInt")
   Or !IsFunc(FuncName) || (Func(FuncName).MaxParams <> 6)
   Or !(CB := RegisterCallback(FuncName, , 6))
      Return False
   If !DllCall("Comctl32.dll\SetWindowSubclass", "Ptr", HCTRL, "Ptr", CB, "Ptr", HCTRL, "Ptr", RefData)
      Return (DllCall("Kernel32.dll\GlobalFree", "Ptr", CB, "Ptr") & 0)
   Return (ControlCB[HCTRL] := CB)
}
User avatar
TLM
Posts: 1608
Joined: 01 Oct 2013, 07:52
Contact:

Re: Subclassing

11 Dec 2019, 06:02

Hello @just me. That works perfectly, thanks as usual :)
I originally tried to detect this using

Code: Select all

NotAllowed(HWND, Msg, wParam, lParam, SubclassID, RefData) {

If Msg = 0x1503 || Msg = 0x1504
   msgbox % msg

;.........
but it didn't catch either message :?

I'll study this further ty

Return to “Tutorials”

Who is online

Users browsing this forum: No registered users and 18 guests