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