Subclassning
Förord
Introduktion till subclassning Att subclassa något är ungefär att byta ut window proceduren för ett fönster, men innan vi börjar så måste vi veta lite om hur messagesystemet i Windows fungerar. Vi tar och öppnar VB's utvecklingsmiljö och skapar ett vanilgt Stanard EXE program. Lägg ut en commando knapp och kompilera programmet till en EXE fil. Vi låter det vara så enkelt det går. Starta sedan programmet vi nyss gjorde. Stäng inte av det under tiden vi går igenom dettaInnehåll
»»
»
»
»
»
»
»
Vad betyder det då att subclassa något?
Spy++
Med i Visual Studio så kom det med ett program som heter Spy++. Titta under "Microsoft Visual Studio Tools" Skulle du inte ha detta program så finns det att ladda ner på nåtet. Microsoft hade en länk till det förut. Finns även andra program av samma typ. När Spy++ startat så visas ett lista med alla aktuella fönster i systemet, om inte tryck "Ctrl+W". I listan som visas letar vi upp fönstret till vårat program, något som ser ut så här: (0003084A "Form1" ThunderRT6FormDC). Tänk på att du inte alls kanske har sammma siffra i början som jag. Vad betyder då detta? 0002084A är hexvärdet på det handle som alla fönster och kontroller i Windows har. Du kan säljv se detta värde i VB med att använda
hWnd egenskapen som nästan alla kontroller har. Observera att den egenskapen är READ ONLY, dvs du kan själv inte ange vilken adress den ska ha.
"Form1" är caption på kontrollen eller fönstret.
ThunderRT6FormDC är namnet på den klass som VB's formulär bygger på.
ThunderRT6CommandButton heter klassen som VB's knappar bygger på osv...
Markera raden (0003084A "Form1" ThunderRT6FormDC) och högerklicka välj sedan "Messages" i popup menyn. Nu visas ett tomt fönster som har titeln "Messages (Window 0003084A)". Det är i detta fönster vi kommer att se allting. Aktivera det lilla program som vi gjorde i början. Nu när vårat program är aktiverat så börjar det rulla en massa text i "Messages (Window 0003084A)". Som du kanske märker så rullar det en väldig massa info då du klickar i fönstret eller flyttar det. Varje rad i detta fönster representerar ett meddelande. Vi ska inte gå igenom alla meddelande, det finns mängder av dem, men strukturen på dem är det samma för alla.
Windows Meddelande
Ett meddelande i Windows har sex olika delar:- hWnd till vilket fönster eller kontroll ska meddelandet
- Msg vad för typ av meddelande det är
- wParam värdet på denna beror på vad för meddelande det är
- lParam värdet på denna beror på vad för meddelande det är
- Time tiden då meddelande genererades
- Point postionen på markören då meddelande skickades
Du ser inte alla dessa delar i listan, vissa är inte representerade. Alla meddelanden finns dokumenterade i MSDN eller på msdn.microsoft.com. Meddelanden i Windows kommer från andra applikationer, Windows själv och användaren i form av tangentbordet eller musen. Meddelanden från användaren läggs först i något som kallas Raw Input Queue eller Raw Input Thread, därifrån konverteras de sedan till applikationsen privata messagekö. De meddelande som ligger i den kön var dem vi såg i Spy++ alledes nyligen och det är den kö vi jobbar med.
Subclassning
Något som Microsoft dolde för VB programmerare var själva window proceduren för VB's program. Självklart finns proceduren för alla VB applikationer, det skulle inte fungera annars. Den fungerar så att det ligger en loop och kör kontinuerligt så länge programmet är i gång. Den loopen ligger och kollar messagekön om det finns några meddelanden till något fönster inom den egna processtråden. Med subclassning innebär då att vi går in och lägger oss efter den loopen med en funktion så vi själva kan styra lite vad vill ska hända i vårat program. För att vi ska kunna komma till att göra detta behöver vi ta hjälp av Windows API. Det finns en funktion i API som man kan ändra attribut med för ett enskilt fönster. Vi kan stänga av vårat program om du fortfarande har i gång det och gå tillbaka till Visual Studio.OBS!, en sak att tänka på vid subclassning är att VB är VÄLDIGT känsligt för fel.
Minsta stavfel på en variabel eller funktion kan krascha hela utvecklingsmiljön, så var försiktig. Spara ofta. Eventuell felhantering i MyProc vore att använda On Error Resume Next.
Första steget
Det vi börjar med är att skapa den funktion som vi kan se alla meddelande i. Lägg till en Module till projektet. All subclassning sker i modulen, detta för att ett formulär eller en klass kan förstöras med Set obj = Nothing. En modul är alltid laddad. Självklart kan du komma åt metoder och funktioner i formulär eller klasser från modulen. Vår funktion som vi ska använda ska ha samma funktions signatur som orginal proceduren. Vad den heter spelar absolut ingen roll, kan kalla den för vad du vill. Min går under namnet MyProc. Den måste dock ha rätt antal och typ av parametrar.
Public Function MyProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
End Function
Så ska funktionen se ut, den behöver inte ha samma namn men annars måste den vara lika. Med en egen window procedur så skulle vi själva kunna hantera alla meddelande som skickades till vårat program. Detta vore dock INGEN bra lösning, så det kommer jag inte ta upp här. Det bästa är att låta Windows ta hand om det vi inte vill ta hand om. Därför anropar vi orginal proceduren i slutet på våran funktion och skickar bara vidare alla parametrar. Orginal proceduren anropar vi med en API funktion som heter:
Public 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
Klistra in raden oven in modulen, finns också i API-viewer att kopiera ifrån. CallWindowProc har dessa parametrar:
- lpPrevWndFunc minnesadressen på orginal proceduren
- hWnd till vilket fönster meddelandet ska till
- Msg vad för typ av meddelande det är
- wParam värdet på denna beror på vad för meddelande det är
- lParam värdet på denna beror på vad för meddelande det är
Public Function MyProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
' annan kod
MyProc = CallWindowProc(lorg_hWnd, hWnd, uMsg, wParam, lParam)
End Function
lorg_hWnd kommer jag strax till, skriv bara med den för tillfället. Vi anropar då orginal proceduren det sista vi gör i våran funktion. Tar du inte med det anropet så kraschar programmet.
Andra steget
Den funktion som nämndes ovan om att ändra attribut i ett enskilt fönster heter SetWindowLong. Den ser ut så här att klistra in i modulen:
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
De tre parametrar som SetWindowLong då har är:
- hwnd till vilket fönster ska vi skicka ändringen till
- nIndex vad är det vi ska ändra
- dwNewLong vad ska vi ändra till
Vi börjar med vad vi ska ändra till. Jo, vi ska ändra den befintliga window proceduren till våran egna. Det betyder att vi ska byta ut minnesadressen på orignal proceduren i formuläret till funktionen MyProc. Till detta använder vi konstanten GWL_WNDPROC, mer info om andra alternativ finns i MSDN.
Så vi lägger till den konstanten överts i modulen.
Public Const GWL_WNDPROC = (-4)
Public lorg_hWnd As Long
Jag lade även till en variabel lorg_hWnd av typen Long. När vi använder oss av SetWindowLong så returnerar den värdet på attributet innan vi ändrade på det. Det vi får då i lorg_hWnd är minnesadressen på orginal proceduren. Den måste vi spara för att återställa proceduren, gör vi inte detta så kommer programmet att krascha när vi avslutar. Den första parametern är det fönster vi ska subclassa, i vårat fall kommer det att vara värdet i Me.hWnd som ska in där. Så det betyder att vi kan subclassa precis allt som har ett window handle (hWnd). Den sista parametern i SetWindowLong är det nya värdet vi ska skicka in och det värdet ska är då minnesadressen till funktionen MyProc.
I klickhändelsen på knappen i vårat program så ska vi starta subclassningen. Vi skulle kunna starta den i Form_Load om vi vill. För att få ut minnesadressen på en funktion så använder vi oss av AddressOf operatorn.
Private Sub Command1_Click()
If lorg_hWnd = 0 Then
lorg_hWnd = SetWindowLong(Me.hwnd, GWL_WNDPROC, AddressOf MyProc)
End If
End Sub
Vi kontrollerar också om vi redan subclassat formuläret, skulle vi subclassa formulärdet två gånger så skriver vi över minnesadressen till orginalproceduren och då kommer det krascha när vi stänger av programmet. Nu när vi ändå håller på i formuläret måste vi ta bort subclassningen också. I formulärets Unload händelse lägger vi in den koden. Detta kräver då att vi avslutar med att klicka på krysset på formuläret. Vi kan inte använda Stop knappen i menyraden i VB's utvecklingsmiljö när vi har subclassat formuläret då den är samma som End i VB
och då anropas inte Unload händelsen.
Private Sub Form_Unload(Cancel As Integer)
If lorg_hWnd > 0 Then
SetWindowLong Me.hwnd, GWL_WNDPROC, lorg_hWnd
lorg_hWnd = 0
End If
End Sub
Vi använder samma funktion som när vi starta subclassningen. Denna gång så skickar vi i variabeln lorg_hWnd som sista parameter. Som du minns så innehöll den minnesadressen till orginal proceduren. Detta måste göras annars blir det fel.
Testning
Innan vi testar så titta över koden en gång till efter stavfel och diverse. I modulen ska det se ut så här:
Public Const GWL_WNDPROC = (-4)
Public lorg_hWnd As Long
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public 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
Public Function MyProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
MyProc = CallWindowProc(lorg_hWnd, hWnd, uMsg, wParam, lParam)
End Function
Om vi nu startar programmet och trycker på knappen så ska ingenting hända. Kraschar inte VB så har vi gjort rätt. Vi har nu ett formulär i VB som vi subclassat. Stäng av programmet med krysset i övre högra hörnet.
MouseMove
Det finns en otrolig massa saker vi skulle kunna gör nu med vårat formulär subclassat. Det vi ska tänka på är att inte göra för mycket i MyProc funktionen. Detta slöar bara ner programmet, så regeln är håll det kort och simpelt. Det första vi kan göra är att ta ut positionen på musen när vi flyttar omkring den på formuläret. Vi behöver ta reda på vad för meddelande som skickas när musen flyttar på sig. Efter lite läsande i MSDN så ser vi att WM_MOUSEMOVE som vi ska filtrera ut och titta på.De två parametrar (wParam och lParam), som följer med i WM_MOUSEMOVE meddelandet innehåller den information vi vill ha ut.
- wParam visar om någon kontroll tangent hålls nedrtyckt
- MK_CONTROL
- MK_LBUTTON
- MK_MBUTTON
- MK_RBUTTON
- MK_SHIFT
- MK_XBUTTON1 - endast 2000/XP
- MK_XBUTTON2 - endast 2000/XP
- lParam X och Y positionen på markören över formuläret. X = LOWORD Y = HIWORD
I MyProc gör vi en Select sats på vad för typ av meddelande vi ska ha ut. Vi skriver ut koordinaterna i debug. De parametrar som MyProc har motsvarar då dem i Wm_MOUSEMOVE meddelandet.
Public Const WM_MOUSEMOVE = &H200
Public Function MyProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
'Vilket meddelande vill vi ha ut
Select Case uMsg
Case WM_MOUSEMOVE
'Är det ett Wm_MOUSEMOVE så innehåller lParam positionerna på markören.
Debug.Print "X: " & LoWord(lParam) & " " & "Y: " & HIWord(lParam)
Case Else
'Alla andra meddelanden struntar vi
End Select
'Skicka vidare alla meddelande till orginal proceduren
MyProc = CallWindowProc(lorg_hWnd, hWnd, uMsg, wParam, lParam)
End Function
Private Function LoWord(DWord As Long) As Integer
If DWord And &H8000& Then
LoWord = DWord Or &HFFFF0000
Else
LoWord = DWord And &HFFFF&
End If
End Function
Private Function HiWord(DWord As Long) As Integer
HiWord = (DWord And &HFFFF0000) \ &H10000
End Function
De två sista funktionerna konverterar lParam av typen Long till två Integers. Titta över det en gång till efter stavfel och testa sedan. Allt detta skulle vi ju kunna gjort i formulärets MouseMove händelse för att slippa subclassning så vi kan prova en sak till som vi inte kunde gjort utan subclassning.
System Meny
Som du märkt så har alla program i Windows en systemmeny, den lilla meny som poppar upp när du klickar på ikonen i övre vänstra hörnet av programmet.Med lite API och subclassning kan vi lägga till ett eget menyval i den menyn. Till detta behöver vi två API funktioner, klistra in dessa i modulen.
Public Declare Function GetSystemMenu Lib "user32" (ByVal hwnd As Long, ByVal bRevert As Long) As Long
Public Declare Function AppendMenu Lib "user32" Alias "AppendMenuA" (ByVal hMenu As Long, ByVal wFlags As Long, ByVal wIDNewItem As Long, ByVal lpNewItem As String) As Long
GetSystemMenu är ingen större konstighet. Den returnerar en referens till systemmenyn.
- hWnd window handel till det fönster vi vill ändra system menyn på
- bRevert TRUE eller FALSE. TRUE = återställer menyn till orginal. FALSE = returnerar bara en referens till menyn
AppendMenu lägger till ett menyval i den meny som anges som parameter.
- hMenu referens till den meny vi ska lägga till ett menyval i. Kommer att få detta värde från GetSystemMenu
- wFlags finns lite olika värden beroende vad för typ av menyval vi ska lägga till
- wIDNewItem det ID som menyn kommer att få, det är detta värde vi använder i MyProc senare
- lpNewItem texten på det menyval vi lägger till
Dem menyval vi ska lägga till, det blir två. Den första är en separator och den andra en textsträng.
Public Const MF_STRING = &H0
Public Const MF_SEPARATOR = &H800
Public Const WM_SYSCOMMAND = &H112
När ett val sker i systemmenyn så skickas ett WM_SYSCOMMAND meddelande. Detta för att veta om systemmenyn har aktiveras. Den info som skickas med är:
- wParam vilken typ av WM_SYSCOMMAND det gäller
- lParam X/Y koordinater
I och med att det är ett eget menyval vi lägger till så får vi sätta ett eget unikt ID på det. Detta ID ska bör vara mellan 0000 och F000 för att inte krocka med de som är reserverade av Windows. För enkelhetens skull så kan vi sätta värdet 10 på menyvalets ID.
Public Const IDM_MYMENU = 10
Alla konstanter här skrivs in i modulen. För att nu lägga till menyvalet så öppnar vi formuläret och går till Form_Load händelsen och skriver in följande:
Private Sub Form_Load()
Dim lngDummy As Long
Dim hMenu As Long
hMenu = GetSystemMenu(Me.hwnd, False)
lngDummy = AppendMenu(hMenu, MF_SEPARATOR, 0, 0&)
lngDummy = AppendMenu(hMenu, MF_STRING, IDM_MYMENU, "&Min Meny...")
End Sub
Testa och kör programmet. Om allt stämmer så har vi ett extra menyval längst ner i systemmenyn. Nu händer det inte så mycket om vi klickar på det menyvalet. I MyProc så lägger vi till ett val i select satsen. Vi ska också känna av när vårat ID för menyvalet skickas. För det använder vi en If sats och kontrollerar mot wParam som kommer innehålla ID på menyvalet när vi klickar på det.
Select Case uMsg
Case WM_MOUSEMOVE
Debug.Print "X: " & LoWord(lParam) & " " & "Y: " & HiWord(lParam)
Case WM_SYSCOMMAND
If wParam = IDM_MYMENU Then
MsgBox "Hello World"
End If
Case Else
'Do nothing
End Select
Starta programmet och kör igång subclassningen. Klickar vi nu på vårat menyval så ska det visas en MsgBox. I detta exemple har vi subclassat ett formulär men vi skulle kunna subclassa en knapp för att göra den rund eller annan textfärg. Som nämnts tidigare bör du inte lägga för mycket jobb i MyProc det kan slöa ner programmet en aning. Men du kan nu göra så mycket mer än vad som tidigare var tillgängligt i VB. För få reda vad för meddelande som skickas till programmet så är Spy++ perfekt för detta. All info om meddelande och olika värden samt vad dem betyder och gör finns i MSDN.
0 Kommentarer