Search code examples
windowsdelphimultiple-monitors

How can I prevent form position size changes after a Windows session unlock?


Description

I have a Delphi XE2 application with one of its forms stretched across two monitors. When I lock Windows, wait until the screen saver gets activated und then unlock windows, all of my application's forms will be resized/ repositioned to fit onto each monitor (which is obviously a default Windows behavior and applies to most applications).

Intention

Whenever this locking scenario occurs, I either want to restore my forms position or prevent my form from being resized beforehand.

Steps to reproduce

These steps work for me on Windows 7 x64.
I'm setting a blankscreen saver to be activated after 1 minute. I open my app and the appropriate stretched form. I lock my account and wait for the screen saver to pop up. After logging in I can see the form resized.

On other machines locking is enough to reproduce the behavior. On some machines the activated screen saver is enough.

Additional Info

What I have done and observed so far:

  • Using Spy++ I've seen my app receiving a WM_SETTINGCHANGE message with WParam = SPI_SETWORKAREA. At this point my form already has its new size.
  • I have registered session notifications to react on session locks, unlocks, logoff etc.
    Receving a session change when locking, my form's size seems to be okay. When receiving the WM_SETTINGCHANGE later, the form size is already altered and shrinked to one monitor.
  • Trying to resize my form to its former size when I receive an unlock event does not succeed (the form stays shrinked although its properties have been changed). I used the form's position and size properties as well as SetWindowPos.
  • The affected form's window state is wsNormal. I stretch the form programmatically above two monitors but don't touch its window state.
  • Trying to restore the old (internally saved) position/ size on WM_WTSSession_Change unlock messages, I have tried to call
    SetWindowPos(Handle, HWND_NOTOPMOST, FFormSizePos.Left, FFormSizePos.Top, FFormSizePos.Width, FFormSizePos.Height, SWP_NOACTIVATE or SWP_NOMOVE);
    or set the size properties by hand like Self.Left := FFormSizePos.Left;

Can anybody help to resolve my intention?


Solution

  • I found a solution and am posting demo code (XE2) as a Delphi solution to this problem.

    It is a combination of the answer here and solution 1 from delphiDabbler.

    Basically I am registering for Windows session state change events (WM_WTSSESSION_CHANGE). In the provided example (based on a naked VCL Form Application) I am using the WM_EXITSIZEPOS message to save the current form sizepos.

    Windows was showing a varying behavior regarding the moment the position change message was fired. This is why I had to revise my first draft and am now using two variables. I am preventing position changes when the session is locked and prevent the first position change after the session is unlocked. The position changes are intercepted using the WM_WINDOWPOSCHANGING message.

    But to not restore the normal position if the form is maximized I am using the FRestoreNormalRect field.

    unit Unit1;
    
    interface
    
    uses
      Winapi.Windows,
      Winapi.Messages,
      Vcl.Forms;
    
    type
      TForm1 = class(TForm)
      private
        FSessionIsLocked: Boolean;
        FSessionWasUnlocked: Boolean;
        FRestoreNormalRect: Boolean;
        FLeft: Integer;
        FTop: Integer;
        FWidth: Integer;
        FHeight: Integer;
    
        procedure WMWTSSessionChange(var Msg: TMessage); message WM_WTSSESSION_CHANGE;
    
      protected
    
        procedure CreateWnd; override;
        procedure DestroyWnd; override;
    
        procedure WMExitSizeMove(var Msg: TMessage); message WM_EXITSIZEMOVE;
        procedure WMPosChanging(var Msg: TWmWindowPosChanging); message WM_WINDOWPOSCHANGING;
        procedure WMSize(var Msg: TWMSize); message WM_SIZE;
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    //--------------------------------------------------------------------------------------------------
    
    procedure TForm1.CreateWnd;
    begin
      inherited;
    
      WTSRegisterSessionNotification(WindowHandle, NOTIFY_FOR_THIS_SESSION);
    end;
    
    //--------------------------------------------------------------------------------------------------
    
    procedure TForm1.DestroyWnd;
    begin
      WTSUnRegisterSessionNotification(WindowHandle);
    
      inherited;
    end;
    
    //--------------------------------------------------------------------------------------------------
    
    procedure TForm1.WMExitSizeMove(var Msg: TMessage);
    var
      WP: TWindowPlacement;
      NormalRect: TRect;
    
    begin
      WP.Length := SizeOf(TWindowPlacement);
      GetWindowPlacement(Self.Handle, @WP);
      NormalRect := WP.rcNormalPosition;
    
      FLeft := NormalRect.Left;
      FTop := NormalRect.Top;
      FWidth := NormalRect.Right - NormalRect.Left;
      FHeight := NormalRect.Bottom - NormalRect.Top;
    end;
    
    //--------------------------------------------------------------------------------------------------
    
    procedure TForm1.WMPosChanging(var Msg: TWmWindowPosChanging);
    begin
      { Sizepos changes might occur due to locks or unlocks. We need do prohibit both.
        While the session is locked we ignore all position changes.
        When the session has been unlocked we will ignore the next PosChanging message. }
      if FSessionIsLocked or FSessionWasUnlocked then
      begin
        { overwrite with the old settings }
        if FRestoreNormalRect then
        begin
          Msg.WindowPos.x := FLeft;
          Msg.WindowPos.y := FTop;
          Msg.WindowPos.cx := FWidth;
          Msg.WindowPos.cy := FHeight;
    
          Msg.Result := 0;
        end;
    
        { reset the variable, otherwise a manual resize would not be possible }
        if FSessionWasUnlocked then
          FSessionWasUnlocked := False;
      end;
    end;
    
    //--------------------------------------------------------------------------------------------------
    
    procedure TiQForm.WMSize(var Msg: TWMSize);
    begin
      inherited;
    
      { We need to restore our normal rect only if the form is not maximized. Because
        if it is maximized it only positioned on one monitor and will not be moved
        by windows. If we do not repsect this case we would be restoring the normal
        rect unintentionally in WMPosChanging for every maximized form. }
      FRestoreNormalRect := not (Msg.SizeType = SIZE_MAXIMIZED);
    end;
    
    //--------------------------------------------------------------------------------------------------
    
    procedure TForm1.WMWTSSessionChange(var Msg: TMessage);
    begin
      case Message.WParam of
        WTS_CONSOLE_DISCONNECT, WTS_REMOTE_DISCONNECT, WTS_SESSION_LOCK, WTS_SESSION_LOGOFF:
          begin
            FSessionIsLocked := True;
          end;
        WTS_CONSOLE_CONNECT, WTS_REMOTE_CONNECT, WTS_SESSION_UNLOCK, WTS_SESSION_LOGON:
          begin
            FSessionIsLocked := False;
            FSessionWasUnlocked := True;
          end;
      end;
    
      inherited;
    end;
    
    //--------------------------------------------------------------------------------------------------
    
    end.