Attribute VB_Name = "MSubclass" Option Explicit ' ====================================================================================== ' Name: vbAccelerator SSubTmr object ' MSubClass.bas ' Author: Steve McMahon (steve@vbaccelerator.com) ' Date: 25 June 1998 ' ' Requires: None ' ' Copyright © 1998-1999 Steve McMahon for vbAccelerator ' -------------------------------------------------------------------------------------- ' Visit vbAccelerator - advanced free source code for VB programmers ' http://vbaccelerator.com ' -------------------------------------------------------------------------------------- ' ' The implementation of the Subclassing part of the SSubTmr object. ' Use this module + ISubClass.Cls to replace dependency on the DLL. ' ' Fixes: ' 27 Dec 99 ' DetachMessage: Fixed typo in DetachMessage which removed more messages than it should ' (Thanks to Vlad Vissoultchev ) ' DetachMessage: Fixed resource leak (very slight) due to failure to remove property ' (Thanks to Andrew Smith ) ' AttachMessage: Added extra error handlers ' ' ====================================================================================== ' declares: Private Declare Function IsWindow Lib "user32" (ByVal hWnd As Long) As Long Private Declare Function GetProp Lib "user32" Alias "GetPropA" (ByVal hWnd As Long, ByVal lpString As String) As Long Private Declare Function SetProp Lib "user32" Alias "SetPropA" (ByVal hWnd As Long, ByVal lpString As String, ByVal hData As Long) As Long Private Declare Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hWnd As Long, ByVal lpString As String) As Long 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 GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex 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 Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long Private Declare Function GetCurrentProcessId Lib "kernel32" () As Long Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ lpvDest As Any, lpvSource As Any, ByVal cbCopy As Long) Private Const GWL_WNDPROC = (-4) Private Const WM_DESTROY = &H2 ' SubTimer is independent of VBCore, so it hard codes error handling Public Enum EErrorWindowProc eeBaseWindowProc = 13080 ' WindowProc eeCantSubclass ' Can't subclass window eeAlreadyAttached ' Message already handled by another class eeInvalidWindow ' Invalid window eeNoExternalWindow ' Can't modify external window End Enum Private m_iCurrentMessage As Long Private m_iProcOld As Long Private m_f As Long Public Property Get CurrentMessage() As Long CurrentMessage = m_iCurrentMessage End Property Private Sub ErrRaise(e As Long) Dim sText As String, sSource As String If e > 1000 Then sSource = App.EXEName & ".WindowProc" Select Case e Case eeCantSubclass sText = "Can't subclass window" Case eeAlreadyAttached sText = "Message already handled by another class" Case eeInvalidWindow sText = "Invalid window" Case eeNoExternalWindow sText = "Can't modify external window" End Select Err.Raise e Or vbObjectError, sSource, sText Else ' Raise standard Visual Basic error Err.Raise e, sSource End If End Sub Private Property Get MessageCount(ByVal hWnd As Long) As Long Dim sName As String sName = "C" & hWnd MessageCount = GetProp(hWnd, sName) End Property Private Property Let MessageCount(ByVal hWnd As Long, ByVal count As Long) Dim sName As String m_f = 1 sName = "C" & hWnd m_f = SetProp(hWnd, sName, count) If (count = 0) Then RemoveProp hWnd, sName End If logMessage "Changed message count for " & Hex(hWnd) & " to " & count End Property Private Property Get OldWindowProc(ByVal hWnd As Long) As Long Dim sName As String sName = hWnd OldWindowProc = GetProp(hWnd, sName) End Property Private Property Let OldWindowProc(ByVal hWnd As Long, ByVal lPtr As Long) Dim sName As String m_f = 1 sName = hWnd m_f = SetProp(hWnd, sName, lPtr) If (lPtr = 0) Then RemoveProp hWnd, sName End If logMessage "Changed Window Proc for " & Hex(hWnd) & " to " & Hex(lPtr) End Property Private Property Get MessageClassCount(ByVal hWnd As Long, ByVal iMsg As Long) As Long Dim sName As String sName = hWnd & "#" & iMsg & "C" MessageClassCount = GetProp(hWnd, sName) End Property Private Property Let MessageClassCount(ByVal hWnd As Long, ByVal iMsg As Long, ByVal count As Long) Dim sName As String sName = hWnd & "#" & iMsg & "C" m_f = SetProp(hWnd, sName, count) If (count = 0) Then RemoveProp hWnd, sName End If logMessage "Changed message count for " & Hex(hWnd) & " Message " & iMsg & " to " & count End Property Private Property Get MessageClass(ByVal hWnd As Long, ByVal iMsg As Long, ByVal index As Long) As Long Dim sName As String sName = hWnd & "#" & iMsg & "#" & index MessageClass = GetProp(hWnd, sName) End Property Private Property Let MessageClass(ByVal hWnd As Long, ByVal iMsg As Long, ByVal index As Long, ByVal classPtr As Long) Dim sName As String sName = hWnd & "#" & iMsg & "#" & index m_f = SetProp(hWnd, sName, classPtr) If (classPtr = 0) Then RemoveProp hWnd, sName End If logMessage "Changed message class for " & Hex(hWnd) & " Message " & iMsg & " Index " & index & " to " & Hex(classPtr) End Property Sub AttachMessage( _ iwp As ISubclass, _ ByVal hWnd As Long, _ ByVal iMsg As Long _ ) Dim procOld As Long Dim msgCount As Long Dim msgClassCount As Long Dim msgClass As Long ' -------------------------------------------------------------------- ' 1) Validate window ' -------------------------------------------------------------------- If IsWindow(hWnd) = False Then ErrRaise eeInvalidWindow Exit Sub End If If IsWindowLocal(hWnd) = False Then ErrRaise eeNoExternalWindow Exit Sub End If ' -------------------------------------------------------------------- ' 2) Check if this class is already attached for this message: ' -------------------------------------------------------------------- msgClassCount = MessageClassCount(hWnd, iMsg) If (msgClassCount > 0) Then For msgClass = 1 To msgClassCount If (MessageClass(hWnd, iMsg, msgClass) = ObjPtr(iwp)) Then ErrRaise eeAlreadyAttached Exit Sub End If Next msgClass End If ' -------------------------------------------------------------------- ' 3) Associate this class with this message for this window: ' -------------------------------------------------------------------- MessageClassCount(hWnd, iMsg) = MessageClassCount(hWnd, iMsg) + 1 If (m_f = 0) Then ' Failed, out of memory: ErrRaise 5 Exit Sub End If ' -------------------------------------------------------------------- ' 4) Associate the class pointer: ' -------------------------------------------------------------------- MessageClass(hWnd, iMsg, MessageClassCount(hWnd, iMsg)) = ObjPtr(iwp) If (m_f = 0) Then ' Failed, out of memory: MessageClassCount(hWnd, iMsg) = MessageClassCount(hWnd, iMsg) - 1 ErrRaise 5 Exit Sub End If ' -------------------------------------------------------------------- ' 5) Get the message count ' -------------------------------------------------------------------- msgCount = MessageCount(hWnd) If msgCount = 0 Then ' Subclass window by installing window procedure procOld = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WindowProc) If procOld = 0 Then ' remove class: MessageClass(hWnd, iMsg, MessageClassCount(hWnd, iMsg)) = 0 ' remove class count: MessageClassCount(hWnd, iMsg) = MessageClassCount(hWnd, iMsg) - 1 ErrRaise eeCantSubclass Exit Sub End If ' Associate old procedure with handle OldWindowProc(hWnd) = procOld If m_f = 0 Then ' SPM: Failed to VBSetProp, windows properties database problem. ' Has to be out of memory. ' Put the old window proc back again: SetWindowLong hWnd, GWL_WNDPROC, procOld ' remove class: MessageClass(hWnd, iMsg, MessageClassCount(hWnd, iMsg)) = 0 ' remove class count: MessageClassCount(hWnd, iMsg) = MessageClassCount(hWnd, iMsg) - 1 ' Raise an error: ErrRaise 5 Exit Sub End If End If ' Count this message MessageCount(hWnd) = MessageCount(hWnd) + 1 If m_f = 0 Then ' SPM: Failed to set prop, windows properties database problem. ' Has to be out of memory ' remove class: MessageClass(hWnd, iMsg, MessageClassCount(hWnd, iMsg)) = 0 ' remove class count contribution: MessageClassCount(hWnd, iMsg) = MessageClassCount(hWnd, iMsg) - 1 ' If we haven't any messages on this window then remove the subclass: If (MessageCount(hWnd) = 0) Then ' put old window proc back again: procOld = OldWindowProc(hWnd) If Not (procOld = 0) Then SetWindowLong hWnd, GWL_WNDPROC, procOld OldWindowProc(hWnd) = 0 End If End If ' Raise the error: ErrRaise 5 Exit Sub End If End Sub Sub DetachMessage( _ iwp As ISubclass, _ ByVal hWnd As Long, _ ByVal iMsg As Long _ ) Dim msgClassCount As Long Dim msgClass As Long Dim msgClassIndex As Long Dim msgCount As Long Dim procOld As Long ' -------------------------------------------------------------------- ' 1) Validate window ' -------------------------------------------------------------------- If IsWindow(hWnd) = False Then ' for compatibility with the old version, we don't ' raise a message: ' ErrRaise eeInvalidWindow Exit Sub End If If IsWindowLocal(hWnd) = False Then ' for compatibility with the old version, we don't ' raise a message: ' ErrRaise eeNoExternalWindow Exit Sub End If ' -------------------------------------------------------------------- ' 2) Check if this message is attached for this class: ' -------------------------------------------------------------------- msgClassCount = MessageClassCount(hWnd, iMsg) If (msgClassCount > 0) Then msgClassIndex = 0 For msgClass = 1 To msgClassCount If (MessageClass(hWnd, iMsg, msgClass) = ObjPtr(iwp)) Then msgClassIndex = msgClass Exit For End If Next msgClass If (msgClassIndex = 0) Then ' fail silently Exit Sub Else ' remove this message class: ' a) Anything above this index has to be shifted up: For msgClass = msgClassIndex To msgClassCount - 1 MessageClass(hWnd, iMsg, msgClass) = MessageClass(hWnd, iMsg, msgClass + 1) Next msgClass ' b) The message class at the end can be removed: MessageClass(hWnd, iMsg, msgClassCount) = 0 ' c) Reduce the message class count: MessageClassCount(hWnd, iMsg) = MessageClassCount(hWnd, iMsg) - 1 End If Else ' fail silently Exit Sub End If ' --------------------------------------------------------------------- ' 3) Reduce the message count: ' --------------------------------------------------------------------- msgCount = MessageCount(hWnd) If (msgCount = 1) Then ' remove the subclass: procOld = OldWindowProc(hWnd) If Not (procOld = 0) Then ' Unsubclass by reassigning old window procedure Call SetWindowLong(hWnd, GWL_WNDPROC, procOld) End If ' remove the old window proc: OldWindowProc(hWnd) = 0 End If MessageCount(hWnd) = MessageCount(hWnd) - 1 End Sub Private Function WindowProc( _ ByVal hWnd As Long, _ ByVal iMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long _ ) As Long Dim procOld As Long Dim msgClassCount As Long Dim bCalled As Boolean Dim pSubClass As Long Dim iwp As ISubclass Dim iwpT As ISubclass Dim iIndex As Long Dim bDestroy As Boolean ' Get the old procedure from the window procOld = OldWindowProc(hWnd) Debug.Assert procOld <> 0 If (procOld = 0) Then ' we can't work, we're not subclassed properly. Exit Function End If ' SPM - in this version I am allowing more than one class to ' make a subclass to the same hWnd and Msg. Why am I doing ' this? Well say the class in question is a control, and it ' wants to subclass its container. In this case, we want ' all instances of the control on the form to receive the ' form notification message. ' Get the number of instances for this msg/hwnd: bCalled = False If (MessageClassCount(hWnd, iMsg) > 0) Then iIndex = MessageClassCount(hWnd, iMsg) Do While (iIndex >= 1) pSubClass = MessageClass(hWnd, iMsg, iIndex) If (pSubClass = 0) Then ' Not handled by this instance Else ' Turn pointer into a reference: CopyMemory iwpT, pSubClass, 4 Set iwp = iwpT CopyMemory iwpT, 0&, 4 ' Store the current message, so the client can check it: m_iCurrentMessage = iMsg With iwp ' Preprocess (only checked first time around): If (iIndex = 1) Then If (.MsgResponse = emrPreprocess) Then If Not (bCalled) Then WindowProc = CallWindowProc(procOld, hWnd, iMsg, _ wParam, ByVal lParam) bCalled = True End If End If End If ' Consume (this message is always passed to all control ' instances regardless of whether any single one of them ' requests to consume it): WindowProc = .WindowProc(hWnd, iMsg, wParam, ByVal lParam) End With End If iIndex = iIndex - 1 Loop ' PostProcess (only check this the last time around): If Not (iwp Is Nothing) And Not (procOld = 0) Then If iwp.MsgResponse = emrPostProcess Then If Not (bCalled) Then WindowProc = CallWindowProc(procOld, hWnd, iMsg, _ wParam, ByVal lParam) bCalled = True End If End If End If Else ' Not handled: If (iMsg = WM_DESTROY) Then ' If WM_DESTROY isn't handled already, we should ' clear up any subclass pClearUp hWnd WindowProc = CallWindowProc(procOld, hWnd, iMsg, _ wParam, ByVal lParam) Else WindowProc = CallWindowProc(procOld, hWnd, iMsg, _ wParam, ByVal lParam) End If End If End Function Public Function CallOldWindowProc( _ ByVal hWnd As Long, _ ByVal iMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long _ ) As Long Dim iProcOld As Long iProcOld = OldWindowProc(hWnd) If Not (iProcOld = 0) Then CallOldWindowProc = CallWindowProc(iProcOld, hWnd, iMsg, wParam, lParam) End If End Function Function IsWindowLocal(ByVal hWnd As Long) As Boolean Dim idWnd As Long Call GetWindowThreadProcessId(hWnd, idWnd) IsWindowLocal = (idWnd = GetCurrentProcessId()) End Function Private Sub logMessage(ByVal sMsg As String) Debug.Print sMsg End Sub Private Sub pClearUp(ByVal hWnd As Long) Dim msgCount As Long Dim procOld As Long ' this is only called if you haven't explicitly cleared up ' your subclass from the caller. You will get a minor ' resource leak as it does not clear up any message ' specific properties. msgCount = MessageCount(hWnd) If (msgCount > 0) Then ' remove the subclass: procOld = OldWindowProc(hWnd) If Not (procOld = 0) Then ' Unsubclass by reassigning old window procedure Call SetWindowLong(hWnd, GWL_WNDPROC, procOld) End If ' remove the old window proc: OldWindowProc(hWnd) = 0 MessageCount(hWnd) = 0 End If End Sub