January 9, 2013

Make sure a form is visible on an existing monitor


In most of my applications, I make all form size and position persistent: On startup (FormShow) I read the size and position back from an ini file and on close, I write the size and position back to the ini file.

But sometimes, the number of attached monitor is reduced, or I change the relative position of the monitors, or the screen resolution is lowered. This has the negative side effect that my forms are sometime positioned on an invisible part of the desktop!

The fix for this annoying issue is quite easy. #Delphi has a global variable “Screen” which holds an array of monitor descriptors. It is enough to iterate thru this array and check if the form position lies inside a visible monitor area. Actually the code below make sure at least 100 pixels are visible.

Here is the code to put in the FormShow event handler:

var
    I  : Integer;
    Ri : TRect;
const
    Margin = 100;  // Number of pixels to be seen, at least
begin
    I := 0;
    while I < Screen.MonitorCount do begin

        // Compute the intersection between screen and form
        Windows.IntersectRect(Ri, BoundsRect,

                              Screen.Monitors[I].BoundsRect);
        // Check the intersection is large enough
        if (Ri.Width > Margin) and (Ri.Height > Margin) then
            break;
        Inc(I);
    end;
    if I >= Screen.MonitorCount then begin
        // Form is outside of any monitor.

        // Move to center of main monitor
        Top  := (Screen.Height - Height) div 2;
        Left := (Screen.Width  - Width)  div 2;
    end;

end;
 
Suggested readings:
     Writing an iterator for a container
    Original method to iterate thru the bits of an integer
    Adding properties to a set
    Internet Component Suite (ICS)
    MidWare multi-tier framework

Follow me on Twitter

8 comments:

  1. You can replace the loop with TScreen.MonitorFromWindow():

    if Screen.MonitorFromWindow(Handle, mdNull) = null then begin
    // Form is outside of any monitor. Move to center of main monitor
    Top := (Screen.Height - Height) div 2;
    Left := (Screen.Width - Width) div 2;
    end;

    ReplyDelete
  2. And if the're two monitors - the form would launch split half between them ?

    And if monitors have different PPI resolution or there is taskbar in the middle - that would look weird...

    I believe you select soem monitor and arrange form to it's center, not to the virtual screen center

    ReplyDelete
  3. @Arioch,

    I thought the same, but that code is correct. TScreen.Width/Height actually returns the width/height of the primary monitor, not all of the monitors combined.

    ReplyDelete
  4. Use Screen.DesktopWidth and Screen.DesktopHeight for the dimensions of the combined monitors.

    ReplyDelete
  5. Thank you - just what I needed!

    ReplyDelete
  6. Alternative approach:

    procedure TMyForm.FormShow(Sender: TObject);
    var
    L, T, W, H : Integer;
    begin
    if LoadWindowRegPos(Name, L, T, W, H) then begin
    Left:=L;
    Top:=T;
    Width:=W;
    Height:=H;
    end;
    end;

    procedure TMyForm.FormHide(Sender: TObject);
    begin
    SaveWindowRegPos(Name, Left, Top, Width, Height)
    end;


    GLOBAL Functions

    //Adjust the left for Multiple Monitors
    Function AdjustLeftForMultipleMonitors(L : Integer; Screen : TScreen) : Integer;
    Var
    I : Integer;
    OurBoundsrect : TRect;
    begin
    Result:=10;
    OurBoundsrect.Left:=MaxInt; OurBoundsrect.Top:=0; OurBoundsrect.Right:=-MaxInt; OurBoundsrect.Bottom:=0;
    for I:= 0 to Screen.MonitorCount-1 do begin
    if Screen.Monitors[I].BoundsRect.Left < OurBoundsrect.Left then OurBoundsrect.Left:= Screen.Monitors[I].BoundsRect.Left;
    if Screen.Monitors[I].BoundsRect.Right > OurBoundsrect.Right then OurBoundsrect.Right:= Screen.Monitors[I].BoundsRect.Right;
    end;
    if L < OurBoundsrect.Left then Exit; //Left is Out of Bounds Rect
    if L > (OurBoundsrect.Right{-OurBoundsrect.Left}) then Exit; //Left is larger then the desktop width
    Result:=L;
    end;

    //Adjust the top for Multiple Monitors
    Function AdjustTopForMultipleMonitors(T : Integer; Screen : TScreen) : Integer;
    Var
    I : Integer;
    OurBoundsrect : TRect;
    begin
    Result:=10;
    OurBoundsrect.Left:=0; OurBoundsrect.Top:=MaxInt; OurBoundsrect.Right:=0; OurBoundsrect.Bottom:=-MaxInt;
    for I:= 0 to Screen.MonitorCount-1 do begin
    if Screen.Monitors[I].BoundsRect.Top < OurBoundsrect.Top then OurBoundsrect.Top:= Screen.Monitors[I].BoundsRect.Top;
    if Screen.Monitors[I].BoundsRect.Bottom > OurBoundsrect.Bottom then OurBoundsrect.Bottom:= Screen.Monitors[I].BoundsRect.Bottom;
    end;
    if T < OurBoundsrect.Top then Exit; //Top is Out of Bounds Rect
    if T > (OurBoundsrect.Bottom {- OurBoundsrect.Top}) then Exit; //Top is larger then the desktop width
    Result:=T;
    end;

    ///////////////////////////////////////////////////////////////////////////////////////////
    //Get Saved Window Position
    ///////////////////////////////////////////////////////////////////////////////////////////
    Function LoadWindowRegPos(WindowName : String; var L,T,W,H : Integer) : Boolean;
    Var
    R : TRegistry;
    RL, RT : Integer;
    begin
    R := TRegistry.Create(KEY_READ);
    R.RootKey := HKEY_CURRENT_USER;
    try
    Result:=True;
    if R.OpenKey(cStrPosition, False) then begin
    try
    if R.ValueExists(WindowName+'L') then L:=AdjustLeftForMultipleMonitors(R.ReadInteger(WindowName+'L'), Screen) else Result:=False;
    if R.ValueExists(WindowName+'T') then T:=AdjustTopForMultipleMonitors(R.ReadInteger(WindowName+'T'), Screen) else Result:=False;
    if R.ValueExists(WindowName+'W') then W:=R.ReadInteger(WindowName+'W') else Result:=False;
    if R.ValueExists(WindowName+'H') then H:=R.ReadInteger(WindowName+'H') else Result:=False;
    Except
    Result:=False;
    end;
    end else Result:=False;
    finally
    R.Free;
    end;
    end;


    ///////////////////////////////////////////////////////////////////////////////////////////
    //Save Window Pos to Registry
    ///////////////////////////////////////////////////////////////////////////////////////////
    Procedure SaveWindowRegPos(WindowName : String; L,T,W,H : Integer);
    Var
    R : TRegistry;
    begin
    R := TRegistry.Create(KEY_READ or KEY_WRITE);
    R.RootKey := HKEY_CURRENT_USER;
    try
    if R.OpenKey(cStrPosition, True) then begin
    R.WriteInteger(WindowName+'L', L);
    R.WriteInteger(WindowName+'T', T);
    R.WriteInteger(WindowName+'W', W);
    R.WriteInteger(WindowName+'H', H);
    end;
    finally
    R.Free;
    end;
    end;

    ReplyDelete
  7. For anyone searching for a newer Solution:

    procedure CorrectFormPosition(fForm: TForm);
    begin
    fForm.Left := MIN(MAX(-7,fForm.left-Screen.DesktopLeft),Screen.DesktopWidth-fForm.Width+7)+Screen.DesktopLeft;
    fForm.top := MIN(MAX(0,fForm.top),Screen.DesktopHeight-fForm.Height);
    fForm.Width := MIN(fForm.Width,Screen.DesktopWidth);
    fForm.Height := MIN(fForm.Height,Screen.DesktopHeight);
    end;

    This works for any Monitor-Setup with a higher priority on the left-right position of the given form.

    ReplyDelete
  8. 2 lines of code:

    if not Assigned(Screen.MonitorFromWindow(Handle, mdNull)) then
    MakeFullyVisible;

    ReplyDelete