Symptom
A form that persists its position via TFormSize opens but is not visible anywhere on
the desktop. It has focus and is fully functional; only its position is impossible.
Pressing Alt+Space, M and then the arrow keys moves the invisible window back into
view, which confirms it exists.
Cause
1. Two of the four edge tests compare a coordinate against a size
formsize.pas 1.5.0.0, lines 481-507:
{ Make sure the window is entirely visible on the screen }
with ARect do
begin
if ((Right > Screen.DesktopWidth) and not (Maximize)) then // line 484
begin
Dec(Left, (Right - Screen.DesktopWidth));
Right := Screen.DesktopWidth;
end;
if (Left < Screen.DesktopLeft) then // line 490
begin
Inc(Right, (Screen.DesktopLeft - Left));
Left := Screen.DesktopLeft;
end;
if ((Bottom > Screen.DesktopHeight) and not(Maximize)) then // line 496
begin
Dec(Top, (Bottom - Screen.DesktopHeight));
Bottom := Screen.DesktopHeight;
end;
if (Top < Screen.DesktopTop) then // line 502
begin
Inc(Bottom, (Screen.DesktopTop - Top));
Top := Screen.DesktopTop;
end;
end;
ARect.Right and ARect.Bottom are absolute coordinates. Screen.DesktopWidth and
Screen.DesktopHeight are extents (SM_CXVIRTUALSCREEN / SM_CYVIRTUALSCREEN),
not edges. They are only equal to the desktop's right and bottom edge when the
desktop starts at 0,0.
The left and top tests are correct, because Screen.DesktopLeft and
Screen.DesktopTop are coordinates (SM_XVIRTUALSCREEN / SM_YVIRTUALSCREEN).
So as soon as a monitor is positioned left of or above the primary monitor,
DesktopLeft / DesktopTop become negative, and the right and bottom limits are too
generous by exactly that amount. A stored position beyond the real right or bottom
edge of the desktop is then not corrected at all.
Worked example. Two 1920-wide monitors, the secondary placed to the left of the
primary:
|
value |
Screen.DesktopLeft |
-1920 |
Screen.DesktopWidth |
3840 |
| real right edge of the desktop |
1920 |
| right limit the code enforces |
3840 |
Any stored Left between 1920 and 3840 is off-screen and passes the test untouched.
2. The check validates against the bounding box, not against each monitor
Even with DesktopLeft = DesktopTop = 0, the desktop is the union of the monitors,
while DesktopWidth/DesktopHeight describe the bounding rectangle of that union.
Two monitors of different heights side by side leave regions inside the bounding
rectangle that belong to no monitor. A window placed there passes the check and is
still invisible.
Suggestion: test against Screen.Monitors[i].WorkareaRect for each monitor rather
than against the bounding box.
3. The check runs before the DPI scale is applied
On the save side the position is stored in physical pixels but the size is divided by
the monitor scale (lines 619 and 624):
WriteInteger(Key, 'Left', (Owner as TForm).Left); // 611
WriteInteger(Key, 'Right', (Owner as TForm).Left
+ Round((Owner as TForm).Width/dpi)); // 624
On the load side the clamp at 481-507 works on that rectangle, and only afterwards is
the size multiplied back up by the target monitor's scale (lines 548-551):
dpi := mon.PixelsPerInch/96; // 548
MoveWindow((Owner as TForm).Handle, ARect.Left, ARect.Top,
Round(dpi * (ARect.Right - ARect.Left)),
Round(dpi * (ARect.Bottom - ARect.Top)), true); // 551
So the rectangle that was validated is not the rectangle the window ends up with. On
a 150 % monitor the final window is 1.5x the size that was checked, and can extend
past the edge the clamp believed it had corrected.
Suggestion: perform the visibility check on the final, scaled rectangle, immediately
before MoveWindow.
4. The size is never constrained
The block only ever translates the rectangle; it never reduces it. A window stored
larger than the monitor it is restored on keeps its size, and the two corrections
then cancel each other out. With a 3000 px wide window on a 1920 px monitor:
| step |
result |
| stored |
Left=0, Right=3000 |
Right > DesktopWidth moves it left by 1080 |
Left=-1080, Right=1920 |
Left < DesktopLeft moves it right by 1080 |
Left=0, Right=3000 |
The window is returned exactly as stored, 1080 px wider than the monitor, with its
right-hand controls unreachable. This happens whenever a screen resolution is reduced,
or the scale factor is raised, between saving and restoring.
Suggestion: clamp the size to the target monitor's work area before adjusting the
position.
How to reproduce
Two monitors are required, both connected. Call the primary monitor P and the
other one S; Wp is the width of P in pixels. P must remain the primary monitor
throughout - if S is made primary, Windows places it at 0,0, no negative
coordinate is produced, and the defect cannot occur.
Resolution and scale do not matter for this test; only the arrangement does.
Phase 1 - store a position on S
- Windows Settings, System, Display. Drag S so that it sits to the right of P.
Leave P as the main display. Apply.
- Start the application with its main window on P.
- Open a form that persists its placement with TFormSize.
- Drag that form onto S, roughly the middle of S, and close it.
- Close the application, so the placement is written.
Check the stored section: Left must be a positive value greater than Wp. This
confirms the form really was stored on S. If Left is negative, S was to the left of
P and the test cannot work - only the right and bottom tests are defective, so a
position off the left edge is always corrected properly.
Phase 2 - change the arrangement
- Display settings again. Drag S so that it now sits to the left of P. Keep P as
the main display. Apply.
Confirm the offset is now negative:
Left must be negative (it will be minus the width of S).
Phase 3 - observe
- Start the application and open the same form.
Result: the form does not appear. It exists and has focus; Alt+Space, M and the
arrow keys will move it into view. If the form is modal, the application looks frozen.
Expected: the form is placed somewhere reachable on one of the connected monitors.
The vertical case behaves identically: put S below P when storing and above P
when restoring, and the form lands below the bottom of both monitors.
Why this occurs so often in practice
The defect needs a monitor positioned left of or above the primary one, and the
arrangement to have changed since the position was stored. On a desktop PC the
monitors are arranged once and never moved, so this rarely happens.
A docked laptop is different. Windows does not always place an external monitor back
on the same side it was on before, so undocking and re-docking can change the
arrangement without the user doing anything. A position stored with the external
monitor on the right is then restored while it is on the left - exactly the sequence
above. This is why the reports come almost exclusively from laptop users with docking
stations, and why it is so hard for them to reproduce on demand.
What we would like
A fix in DoLoadPlacement so that a restored form is always reachable on a currently
connected monitor. In our view that means:
- Compare against the desktop's real edges, not its extents -
Screen.DesktopRect,
or DesktopLeft + DesktopWidth and DesktopTop + DesktopHeight.
- Better still, validate per monitor against
Screen.Monitors[i].WorkareaRect, so
the gaps between monitors of differing size are covered too.
- Clamp the size to the target monitor before adjusting the position.
- Run the check on the final scaled rectangle, immediately before
MoveWindow.
A useful additional safeguard, if the above is considered too large a behavioural
change for a maintenance release: keep the current logic but only apply a stored
placement when it is actually reachable, and fall back to the form's design-time
position when it is not. That alone would remove the invisible-window symptom.
We currently work around the problem in application code from the OnLoaded event
added in 1.5.0.0, which fires late enough to see the final rectangle. That event is
very useful and we are glad to have it - but the correction really belongs in the
component, since every application using TFormSize has the same exposure.