Window position and size

Windows knows everything that you need to know about every windowís positionóboth the rectangle of the window and the rectangle of its client area. A rectangle includes the screen coordinates of the left, right, top, and bottom, thus describing both the size and the position of the window. Various Windows API functions use the rectangle coordinates to control the size and the position of a window.

Of course, you usually wonít need these functions in Visual Basic. When youíre working on forms and controls, itís easier to read or modify the Left, Top, Width, and Height attributes or to use the Move method. If youíre interested only in the client area, you can use the ScaleLeft, ScaleTop, ScaleWidth, and ScaleHeight properties.

Occasionally, however, youíll need to perform operations that Visual Basic does not support. When you convert Visual Basic rectangles to Windows rectangles, the following utility functions can make your code shorter:

Function xRight(obj As Object) As Single
    xRight = obj.Left + obj.Width
End Function

Function yBottom(obj As Object) As Single
    yBottom = obj.Top + obj.Height
End Function

Just be sure that anything you pass to these functions really has Left, Top, Width, and Height properties.

WinWatch has to solve a window coordinate problem. It must find the window located at a given positionóspecifically, the position the user points to. The cmdPoint_Click event toggles point mode on and off, the Form_MouseMove event displays information for the window to which the user is currently pointing, and the Form_MouseDown event selects the window being pointed to as the current window, forcing an update in various list boxes. Letís look at the code that makes this happen, starting with cmdPoint_Click:

Private Sub cmdPoint_Click()
    If cmdPoint.Caption = "&Point" Then
        fCapture = True
        cmdPoint.Caption = "End &Point"
        Call SetCapture(Me.hWnd)
        lblMsg.Caption = "Move mouse for window information"
        fCapture = False
        cmdPoint.Caption = "&Point"
        lblMsg.Caption = sMsg
    End If
End Sub

This sub toggles the form-level flag fCapture, changes messages and captions, and, most important, sets the form to capture all mouse movements anywhere on the screen. Normally, each window captures only its own mouse events; but with SetCapture on, you get everything.

The Form_MouseMove sub illustrates some of the problems involved in converting Windows screen coordinates to Visual Basic form coordinates:

Private Sub Form_MouseMove(Button As Integer, Shift As Integer, _
                           x As Single, y As Single)
    If fCapture Then
        Dim pt As POINTL, hWnd As Long
        Static hWndLast As Long
        ' Set point and convert it to screen coordinates
        pt.x = x / Screen.TwipsPerPixelX
        pt.y = y / Screen.TwipsPerPixelY
        ClientToScreen Me.hWnd, pt
        ' Find window under it
        hWnd = WindowFromPoint(pt.x, pt.y)
        ' Update display only if window has changed
        If hWnd <> hWndLast Then
            lblWin.Caption = GetWndInfo(hWnd)
            hWndLast = hWnd
        End If
    End If
End Sub

This code does nothing unless capture mode is on (as indicated by fCapture). If it is on, the mouse position that is received by the form is specified in twips, so you must divide by the TwipsPerPixelX or TwipsPerPixelY property of the Screen object to get x or y in pixels. These adjusted values are put in a POINTL variable (Windows calls it POINT, but that name conflicts with the Visual Basic Point method) and passed to ClientToScreen to make them relative to the screen rather than to WinWatch. The adjusted values can then be passed to the WindowFromPoint function, which will return the window under the mouse pointer.

WindowFromPoint uses one of the ugliest hacks in this book. To understand how it works, look carefully at the C prototypes for both ClientToScreen and WindowFromPoint:

BOOL ClientToScreen(HWND hWnd, LPPOINT lpPoint);
HWND WindowFromPoint(POINT Point);

In ClientToScreen, the POINTL variable is passed as a pointer (by reference, in Visual Basic terms). Most functions in the Windows API pass structures this
way. WindowFromPoint, its cousin ChildWindowFromPoint, and a few others break the rules by passing POINT parameters by value. You might remember from Chapter 2 that structures (UDTs in Visual Basic) are always passed by reference in order to save stack space. But take a look at the size of a POINTL variable:

Public Type POINTL
    x As Long
    y As Long
End Type

You have to know a little Windows history to understand the problem. In 16-bit mode, a POINT was two Integers, which equaled one Long. A pointer is also a Long. You would gain nothing if you passed by reference. Apparently, a performance-conscious designer of the first version of Windows decided to save a few clock cycles by passing by value, never anticipating that Windows might someday be ported to 32 bits. Well, it turns out that in 32-bit C you can pass a 64-bit POINTL variable by value. It might not be efficient, but at least itís portable. In Visual Basic, however, you canít pass a UDT variable by value, periodónot 16-bit, not 32-bit, not 64-bit. Visual Basic could have supported this for small UDTs, but the Visual Basic designers must have assumed that no one would ever want to do it.

But there is an indirect way of passing a 64-bit structure by value. When you pass any argument, the processor pushes the arguments onto the stack. It turns out that 32-bit Intel processors always push in 32-bit chunks. When you pass a 64-bit chunk, the processor actually pushes two 32-bit values. To make this work in Visual Basic, you make explicit whatís actually happening under the surface. The following function is in the Windows API type library, but a Declare statement would look like this:

Declare Function WindowFromPoint Lib "USER32" _
    (ByVal xPoint As Long, ByVal yPoint As Long) As Long

In your code, you must pass the parts separately rather than in one chunk.

hWnd = WindowFromPoint(pt.x, pt.y)

If this seems complicated, just be glad that 16-bit Windows is dead. The portable version of this hack described in the last version of my book was twice as complicated.

You might want to examine the Form_MouseDown event procedure to get more details on how a window being clicked is identified and converted into the current window.