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.

No comments:

Post a Comment