19 March 2026

Fitting a Window to its monitor

How to detect when a window moves to a smaller screen and automatically adjust its size and position to fit.

Modern Windows setups routinely mix monitors of different sizes and resolutions. Drag a window from a large 27-inch primary display onto a smaller 24-inch secondary screen, and it may overflow the work area — clipped behind the taskbar, or hanging off an edge entirely. In a DPI-aware app Windows can help, but in a classic - non-dpi aware - GB32 application we're on our own. Fortunately, the Win32 API gives us everything we need to solve this cleanly in a few dozen lines of GFA-BASIC.

The two API calls that do all the work
The whole technique rests on just two USER32 functions:

MonitorFromWindow - Returns a handle identifying the monitor a given hWnd is currently (mostly) on. Pass MONITOR_DEFAULTTONEAREST so a window that's off-screen still maps to the nearest monitor rather than returning zero.

GetMonitorInfo - Fills a MONITORINFO structure for a monitor handle. The key field is rcWork — the usable rectangle after subtracting the taskbar and any docked toolbars.

Both are part of the multi-monitor API introduced in Windows 98 and fully available in every Win32 environment. GB32's $Library "winuser.inc" directive imports them along with the required types and constants.

Detecting the monitor change
Because GB32 is not DPI-aware, Windows will never send WM_DPICHANGED. Instead, we watch WM_MOVE and compare monitor handles. A monitor handle is stable for the lifetime of a display configuration, so a change in handle value is a reliable signal that the window has crossed onto a different screen.

We capture the initial monitor handle at startup, then re-check it on every move:

' Adjust window size for monitor

' Include API defintions from USER32.DLL
$Library "winuser.inc"

' Create a window & store it's monitor handle
FullW 1
Global Handle hMonWin1 = _
  MonitorFromWindow(Win_1.hWnd, MONITOR_DEFAULTTONEAREST)
Do
  Sleep
Until Me Is Nothing

' Handle WM_MOVE
Sub Win_1_MessageProc(hWnd%, Mess%, wParam%, lParam%, retval%, ValidRet?)
  Local Handle hMon
  Switch Mess%
  Case WM_MOVE
    hMon = MonitorFromWindow(hWnd%, MONITOR_DEFAULTTONEAREST)
    If hMon != hMonWin1
      FitWindowToWorkArea(hWnd%)
      hMonWin1 = hMon
    EndIf
  EndSwitch
EndSub

' ============================================================
' Fit a window into a work area after a monitor change
' Shrinks if necessary, moves if partially off-screen.
' All values in screen pixels.
' ============================================================
Sub FitWindowToWorkArea(ByVal hWnd As Handle)
  Dim mi     As MONITORINFO
  Dim hMon   As Handle
  Dim rc     As RECT         ' current window rect (pixels)
  Dim wa     As RECT         ' work area
  Local Long wLeft, wTop, wWide, wTall, waWide, waTall

  ' Get current window rect in screen pixels
  ~GetWindowRect(hWnd, rc)

  ' Get work area of the monitor the window is (mostly) on
  hMon = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST)
  mi.cbSize = Len(mi)
  If GetMonitorInfo(hMon, V:mi) = 0 Then Exit Sub
  wa = mi.rcWork

  ' Current size
  wLeft = rc.Left
  wTop  = rc.Top
  wWide = rc.Right  - rc.Left
  wTall = rc.Bottom - rc.Top

  ' Work area size
  waWide = wa.Right  - wa.Left
  waTall = wa.Bottom - wa.Top

  ' 1. Shrink if wider or taller than work area
  If wWide > waWide Then wWide = waWide
  If wTall > waTall Then wTall = waTall

  ' 2. Nudge so the window doesn't hang off any edge
  If wLeft < wa.Left Then wLeft = wa.Left
  If wTop  < wa.Top  Then wTop  = wa.Top
  If wLeft + wWide > wa.Right  Then wLeft = wa.Right  - wWide
  If wTop  + wTall > wa.Bottom Then wTop  = wa.Bottom - wTall

  ' 3. Apply only if something actually changed
  If wLeft <> rc.Left  Or wTop  <> rc.Top  Or _
    wWide <> (rc.Right - rc.Left) Or _
    wTall <> (rc.Bottom - rc.Top) Then

    ~SetWindowPos(hWnd, 0, wLeft, wTop, wWide, wTall, _
      SWP_NOZORDER Or SWP_NOACTIVATE)
  End If
End Sub

The three-phase fit algorithm
Once we know the window has changed monitors, we retrieve the new work area and apply a three-phase adjustment. Each phase is independent and only nudges values when needed:

Phase 1 - Shrink
Cap width and height to the work area dimensions. Handles windows that are simply too large for the destination monitor.

Phase 2 - Nudge
Push the top-left corner back inside the work area edges. Left/top checks run before right/bottom so a freshly shrunk window can't be pushed back off the opposite edge.

Phase 3 - Guard
Skip the SetWindowPos call entirely if nothing changed, avoiding unnecessary repaints and recursive WM_MOVE noise.

A few things to keep in mind
rcWork vs rcMonitor. MONITORINFO contains two rectangles. rcMonitor is the full physical screen including the taskbar area. rcWork is the usable rectangle — always use rcWork when positioning application windows.

GetMonitorInfo needs cbSize. The mi.cbSize = Len(mi) line is mandatory. The API validates the struct size before filling it, and will return zero (failure) if you omit it.

The V: prefix for structs. GB32 passes UDTs by reference automatically, but GetMonitorInfo is declared with a pointer parameter. Writing V:mi explicitly passes the address of mi, which is the correct calling convention for this API.

Minimum window sizes. If your window has a minimum size enforced via WM_GETMINMAXINFO, SetWindowPos will silently respect it. On a very small monitor the window may still partially overflow — this is expected behaviour and the right trade-off over making the window unusable.

SWP_NOACTIVATE. Including this flag prevents SetWindowPos from stealing focus while the user is mid-drag, which would cause a jarring activation flash on the destination window.

07 January 2026

Set and reference counting

You're probably familiar with the Set command for working with objects. In GB32, Set plays a crucial role when working with COM (Component Object Model) objects, and understanding what happens behind the scenes will help you write more reliable code.

What is a COM Object?
Think of a COM object as a box sitting somewhere in your computer's memory. Inside that box are two things:

  • A reference counter - a simple number that tracks how many variables are currently "pointing to" this box
  • The actual data and methods - the useful stuff your object does

When you create a COM object in GB32, what you actually get is a pointer - essentially an address that tells your program where to find that box in memory.

COM Variables Are Pointers
When you declare a COM variable in GB32:

Dim MyForm As Form

You're not creating the 'box' or Form itself. You're creating a variable that will point to the box. The variable contains just an address, not the entire object. At this point, MyForm doesn't point to anything yet - it contains Nothing.

To actually create the Form object, you use the Form command:

Form MyForm = "My Window", 100, 100, 640, 480

This single command does two things: it declares the variable MyForm and creates the actual Form object in memory, with the variable pointing to it. The Form now exists with the title "My Window" at position (100, 100) with dimensions 640×480.

The Reference Counter
The reference counter inside the COM object is like a visitor log. Every time a new variable points to the object, the counter should go up by one. Every time a variable stops pointing to it (goes out of scope or gets reassigned), the counter should go down by one. When the counter reaches zero, the system knows nobody is using the object anymore and it's safe to destroy it and free up that memory.

Interestingly, when you create a Form object, its reference counter starts at 2, not 1. This is because two variables point to it: the variable you declared (MyForm in our example) and the special Me keyword, which always points to the Form object from within its own event handlers and methods.

How Set Works
The Set command does two important things:

Dim MyForm As Form
Set MyForm = Win_1
  1. If MyForm was already pointing to something, it tells that old object "I'm done with you" (decrements its reference counter, unless it is already 0)
  2. It makes MyForm point to the new object (Win_1) and tells that object "I'm using you now" (increments its reference counter)

This keeps the reference counting accurate so the system knows which objects are still in use.

You can also use Set with the special keyword Nothing:

Set MyForm = Nothing

Nothing means "point to no object at all" - it's like erasing the address from your variable. When you set a COM variable to Nothing:

  • If MyForm was pointing to an object, it tells that object "I'm done with you" (decrements its reference counter)
  • MyForm now contains no valid address - it's empty and safe to check with If MyObject Is Nothing

Setting variables to Nothing when you're done with them is good practice. It explicitly releases your reference to the object, potentially allowing the system to free up memory sooner rather than waiting for the variable to go out of scope. It also makes your code clearer by showing when you're intentionally finished with an object.

Set MyObject = CreateObject("Some.Component")
' Use MyObject...
Set MyObject = Nothing  ' Done with it now

Passing COM Variables to subroutines
Here's where GB32 does something interesting. When you pass a COM variable to a procedure. Here is test program that allows you to obtain the current reference count of an object. The RefCount() function takes the address of COM variable and returns the current number of variables that reference the COM object:

Form frm1 = "RefCount example", 30, 30, 400, 400

Print "After creation RefCount = "; RefCount(V:frm1)
testbyval frm1
testbyref frm1

Do
  Sleep
Until Me Is Nothing

Proc testbyval(frm As Form)
  Print "In testbyval RefCount = "; RefCount(V:frm)
EndProc
Sub testbyref(frm As Form)
  Print "In testbyref RefCount = "; RefCount(V:frm)
EndSub

Function RefCount(ByVal ComVarPtr As Intptr) As Long

  Local Intptr ComPtr = {ComVarPtr}     // address of COM object
  Local Intptr ComPtr_Vtbl = {ComPtr}   // address of the vtbl
  Local Intptr ComPtr_AddRef = {ComPtr_Vtbl + 4}  // address of AddRef
  Local Intptr ComPtr_Release = {ComPtr_Vtbl + 8} // address of Release

  // Increment reference count and obtain the new number of references
  ~StdCall(ComPtr_AddRef)(ComPtr)       // pass the this pointer
  RefCount = {ComPtr + 4} - 1           // the new count - 1
  // Immediately decrease the reference count
  ~StdCall(ComPtr_Release)(ComPtr)      // pass the this pointer

EndFunc

The testbyval procedure shows that GB32 makes a copy of the pointer (so the function has its own variable pointing to the same box), but it does not increment the reference counter. This means the counter doesn't reflect that the function is also using the object.

In most cases, this works fine - your original variable keeps its reference alive, so the object stays valid while the function runs. However, if something unusual happens during the function (like the original variable getting reassigned or the function triggering code that releases the last reference), the object could be destroyed while the function is still trying to use it.

This is something to keep in mind when writing complex code, particularly code that might call back into other parts of your program or handle Windows messages while a function is executing.

Summary

  • COM objects live in memory and contain a reference counter
  • COM variables are pointers to these objects
  • Set properly manages the reference counter when assigning objects
  • Nothing is used to clear a COM variable and release its reference
  • Passing COM variables to functions creates a copy of the pointer without updating the counter
  • The reference counter helps the system know when it's safe to clean up unused objects

Understanding these mechanics will help you write more robust GB32 applications, especially as your programs grow in complexity.