Enumerating Windows Using the API
Using VB's AddressOf keyword to hook into API enumerations - the only reliable way to find all windows.
Prior to VB5, it was impossible to use the enumeration methods provided in the Windows API without relying
on a proprietary custom control. This was a problem if you wanted to find out all the Windows showing
on the system, because the only reliable methods you can use to do this are through enumerations.
The introduction of the AddressOf operator to VB allows the enumeration methods to be used,
although it is not as simple as it ought to be. This article provide a robust and reusable
code to access the Window list.
How Enumeration Methods Work
Windows API enumeration methods work by having an initiating function which is
given the address of a function in your program to call. The call to the
initiating function then starts a repeatedly calling the function you have provided with
another item until all items are exhausted or your function returns False, which indicates
you don't want any further items.
Providing a Function Address
You can provide an address to a function in a VB application by using the AddressOf operator.
This takes the function name as a parameter and returns a long pointer. Whilst I am most happy this
has been at last provided (I'm a bit sad that way), there is a unfortunately a strict limitation on
where you can put a function you want to use with AddressOf. The function has to reside
within a .Bas module - you cannot place it within an object (i.e. form, usercontrol or class).
So if you want your enumeration to return values to an instance of an object, you either
have to hack around with global variables and methods to get the information between the
module and your object (urgh!) or somehow tell the module about the object instance that
is calling it.
An Attempt to Code Around This Elegantly
My code to work around this uses the Implements feature to specify a standard interface
your object will use to interface with the module. Here is a brief description of how it works,
using the EnumWindows call as an example.
The EnumWindows API call causes Windows to call your specied function for each
window in the system, providing the hWnd of the window. The declare for this
function is as follows:
Public Declare Function EnumWindows Lib "user32" ( _
ByVal lpEnumFunc As Long, _
ByVal lparam As Long _
) As Long
The function passed via lpEnumFunc must have the same parameters as Windows expects,
otherwise when it calls it you will either get a crash or a stack fault. In this case,
the parameters are the hWnd of the window, and the lParam value you passed in
when EnumWindows was first called. In addition, the function should be declared as
a long so you can return the API version of True (1) or False (0) back to Windows
to tell it whether to continue enumeration or not:
Private Function EnumWindowsProc( _
ByVal hwnd As Long, _
ByVal lparam As Long _
) As Long
...
End Function
Having set these up in a module, you then want to provide a method to initiate the
Windows enumeration, and to allow the calling object to get the items returned by the
enumeration. I do this by first setting up an interface class which specifies
what the calling object must do in order to respond to the enumeration. In this case,
the calling object should be notified with the hWnd whenever a new window is provided by
EnumWindowsProc, and should have the ability to stop the enumeration. I also allow
the object to specify its own Identifier number to pass into the lParam value of
EnumWindows via an Identifier property get:
Public Sub EnumWindow(ByVal hwnd As Long, ByRef bStop As Boolean)
End Sub
Public Property Get Identifier() As Long
End Property
This class just specifies the interface to the EnumWindows method, so it
doesn't have any code in it. Any code you want to run in these methods or
Property Gets must be coded in the calling object itself. In my code, I call it
IEnumWindowsSink because the object which is going to implement it is the 'sink' for
EnumWindows callbacks.
The module to enumerate the windows can then be set up as follows:
Private m_cSink As IEnumWindowsSink
Private Function EnumWindowsProc( _
ByVal hwnd As Long, _
ByVal lparam As Long _
) As Long
Dim bStop As Boolean
bStop = False
m_cSink.EnumWindow hwnd, bStop
If (bStop) Then
EnumWindowsProc = 0
Else
EnumWindowsProc = 1
End If
End Function
Public Function EnumerateWindows( _
ByRef cSink As IEnumWindowsSink _
) As Boolean
If Not (m_cSink Is Nothing) Then
Exit Function
End If
Set m_cSink = cSink
EnumWindows AddressOf EnumWindowsProc, cSink.Identifier
Set m_cSink = Nothing
End Function
You are now in a position to use this from any form or class. By telling the
form/class to implement the IEnumWindowsSink methods, VB will automatically put the
EnumWindow sub and Identifier Property Get into the code, requiring you to code them.
Here is a sample showing how to find all the Windows on the system from a form.
The items are placed into a ListView control called lvwWindows with 4 columns:
Implements IEnumWindowsSink
Private Sub IEnumWindowsSink_EnumWindow( _
ByVal hwnd As Long, _
bStop As Boolean _
)
Dim itmX As ListItem
Set itmX = lvwWindows.ListItems.Add(, , WindowTitle(hwnd))
itmX.SubItems(1) = ClassName(hwnd)
itmX.SubItems(2) = hwnd
itmX.SubItems(3) = IsWindowVisible(hwnd)
End Sub
Private Property Get IEnumWindowsSink_Identifier() As Long
IEnumWindowsSink_Identifier = Me.hwnd
End Property
The definitions of the functions to get a Window's title, class and visibility from a hWnd are as follows:
Public Declare Function IsWindowVisible Lib "user32" ( _
ByVal hwnd As Long) As Long
Public Declare Function GetWindowText Lib "user32" _
Alias "GetWindowTextA" ( _
ByVal hwnd As Long, _
ByVal lpString As String, _
ByVal cch As Long _
) As Long
Public Declare Function GetWindowTextLength Lib "user32" _
Alias "GetWindowTextLengthA" ( _
ByVal hwnd As Long _
) As Long
Public Declare Function GetClassName Lib "user32" _
Alias "GetClassNameA" ( _
ByVal hwnd As Long, _
ByVal lpClassName As String, _
ByVal nMaxCount As Long _
) As Long
Public Function WindowTitle(ByVal lHwnd As Long) As String
Dim lLen As Long
Dim sBuf As String
' Get the Window Title:
lLen = GetWindowTextLength(lHwnd)
If (lLen > 0) Then
sBuf = String$(lLen + 1, 0)
lLen = GetWindowText(lHwnd, sBuf, lLen + 1)
WindowTitle = Left$(sBuf, lLen)
End If
End Function
Public Function ClassName(ByVal lHwnd As Long) As String
Dim lLen As Long
Dim sBuf As String
lLen = 260
sBuf = String$(lLen, 0)
lLen = GetClassName(lHwnd, sBuf, lLen)
If (lLen <> 0) Then
ClassName = Left$(sBuf, lLen)
End If
End Function
The windows enumeration sample shows the methods described above, and
additionally provides a class which you can use to find a window based on its
partial title or class name.
|