views:

2892

answers:

3

Many posts around about restoring a WinForm position and size.

Examples:

But I have yet to find code to do this with multiple monitors.

That is, if I close my .NET Winform app with the window on monitor 2, I want it to save the windows size, location, and state to the application settings, so it could later restore to monitor 2 when I restart the app. It would be nice if, like in the codeproject example above, it includes some sanity checks, as in if the saved location is mostly off-screen it "fixes" it. Or if the saved location is on a monitor that is no longer there (e.g. my laptop is now by itself without my second monitor) then it correctly moves it to monitor 1.

Any thoughts?

My environment: C#, .NET 3.5 or below, VS2008

A: 

If you have multiple monitors, I believe the screen UI dimensions are simply larger. So the normal "1 monitor" approach of storing and restoring the location will just work. I haven't tried this because I am away from my second monitor but it shouldn't be hard to test. The way you asked the Question is seems like you haven't tested it.

Your second requirement will mean you will have to check the max sceen dimensions when restoring the app, and then reposition as necessary. To do this latter bit, I use this code:

    private System.Drawing.Rectangle ConstrainToScreen(System.Drawing.Rectangle bounds)
    {
        Screen screen = Screen.FromRectangle(bounds);
        System.Drawing.Rectangle workingArea = screen.WorkingArea;
        int width = Math.Min(bounds.Width, workingArea.Width);
        int height = Math.Min(bounds.Height, workingArea.Height);
        // mmm....minimax            
        int left = Math.Min(workingArea.Right - width, Math.Max(bounds.Left, workingArea.Left));
        int top = Math.Min(workingArea.Bottom - height, Math.Max(bounds.Top, workingArea.Top));
        return new System.Drawing.Rectangle(left, top, width, height);
    }

I call this method when restoring the form. I store the screen geometry in the registry on form close, and then read the geometry on form open. I get the bounds, but then constrain the restored bounds to the actual current screen, using the method above.

Save on close:

      // store the size of the form
      int w = 0, h = 0, left = 0, top = 0;
      if (this.Bounds.Width < this.MinimumSize.Width || this.Bounds.Height < this.MinimumSize.Height)
      {
          // The form is currently minimized.  
          // RestoreBounds is the size of the window prior to last minimize action.
          w = this.RestoreBounds.Width;
          h = this.RestoreBounds.Height;
          left = this.RestoreBounds.Location.X;
          top = this.RestoreBounds.Location.Y;
      }
      else
      {
          w = this.Bounds.Width;
          h = this.Bounds.Height;
          left = this.Location.X;
          top = this.Location.Y;
      }
      AppCuKey.SetValue(_rvn_Geometry,
        String.Format("{0},{1},{2},{3},{4}",
              left, top, w, h, (int)this.WindowState));

Restore on form open:

    // restore the geometry of the form
    string s = (string)AppCuKey.GetValue(_rvn_Geometry);
    if (!String.IsNullOrEmpty(s))
    {
        int[] p = Array.ConvertAll<string, int>(s.Split(','),
                         new Converter<string, int>((t) => { return Int32.Parse(t); }));
        if (p != null && p.Length == 5)
            this.Bounds = ConstrainToScreen(new System.Drawing.Rectangle(p[0], p[1], p[2], p[3]));
    }
Cheeso
+8  A: 

Try this code. Points of interest:

  • Checks if the window is (partially) visible on any screen's working area. E.g. dragging it behind the task bar or moving it completely offscreen resets the position to windows default.
  • Saves the correct bounds even if the Form is minimized or maximized (common error)
  • Saves the WindowState correctly. Saving FormWindowState.Minimized is disabled by design.

The bounds and state are stored in the appsettings with their corresponding type so there's no need to do any string parsing. Let the framework do its serialization magic.

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        // this is the default
        this.WindowState = FormWindowState.Normal;
        this.StartPosition = FormStartPosition.WindowsDefaultBounds;

        // check if the saved bounds are nonzero and visible on any screen
        if (Settings.Default.WindowPosition != Rectangle.Empty &&
            IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
        {
            // first set the bounds
            this.StartPosition = FormStartPosition.Manual;
            this.DesktopBounds = Settings.Default.WindowPosition;

            // afterwards set the window state to the saved value (which could be Maximized)
            this.WindowState = Settings.Default.WindowState;
        }
        else
        {
            // this resets the upper left corner of the window to windows standards
            this.StartPosition = FormStartPosition.WindowsDefaultLocation;

            // we can still apply the saved size
            this.Size = Settings.Default.WindowPosition.Size;
        }
    }

    private bool IsVisibleOnAnyScreen(Rectangle rect)
    {
        foreach (Screen screen in Screen.AllScreens)
        {
            if (screen.WorkingArea.IntersectsWith(rect))
            {
                return true;
            }
        }

        return false;
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);

        // only save the WindowState if Normal or Maximized
        switch (this.WindowState)
        {
            case FormWindowState.Normal:
            case FormWindowState.Maximized:
                Settings.Default.WindowState = this.WindowState;
                break;

            default:
                Settings.Default.WindowState = FormWindowState.Normal;
                break;
        }

        // reset window state to normal to get the correct bounds
        // also make the form invisible to prevent distracting the user
        this.Visible = false;
        this.WindowState = FormWindowState.Normal;

        Settings.Default.WindowPosition = this.DesktopBounds;
        Settings.Default.Save();
    }
}

The settings file for reference:

<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ScreenTest" GeneratedClassName="Settings">
    <Profiles />
    <Settings>
        <Setting Name="WindowPosition" Type="System.Drawing.Rectangle" Scope="User">
            <Value Profile="(Default)">0, 0, 0, 0</Value>
        </Setting>
        <Setting Name="WindowState" Type="System.Windows.Forms.FormWindowState" Scope="User">
            <Value Profile="(Default)">Normal</Value>
        </Setting>
    </Settings>
</SettingsFile>
VVS
+3  A: 

The answer provided by VVS was a great help! I found two minor issues with it though, so I am reposting the bulk of his code with these revisions:

(1) The very first time the application runs, the form is opened in a Normal state but is sized such that it appears as just a title bar. I added a conditional in the constructor to fix this.

(2) If the application is closed while minimized or maximized the code in OnClosing fails to remember the dimensions of the window in its Normal state. (The 3 lines of code--which I have now commented out--seems reasonable but for some reason just does not work.) Fortunately I had previously solved this problem and have included that code in a new region at the end of the code to track window state as it happens rather than wait for closing.


With these two fixes in place, I have tested:

A. closing in normal state--restores to same size/position and state

B. closing in minimized state--restores to normal state with last normal size/position

C. closing in maximized state--restores to maximized state and remembers its last size/position when one later adjusts to normal state.

D. closing on monitor 2--restores to monitor 2.

E. closing on monitor 2 then disconnecting monitor 2--restores to same position on monitor 1

David: your code allowed me to achieve points D and E almost effortlessly--not only did you provide a solution for my question, you provided it in a complete program so I had it up and running almost within seconds of pasting it into Visual Studio. So a big thank you for that!

public partial class MainForm : Form
{
    bool windowInitialized;

    public MainForm()
    {
        InitializeComponent();

        // this is the default
        this.WindowState = FormWindowState.Normal;
        this.StartPosition = FormStartPosition.WindowsDefaultBounds;

        // check if the saved bounds are nonzero and visible on any screen
        if (Settings.Default.WindowPosition != Rectangle.Empty &&
            IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
        {
            // first set the bounds
            this.StartPosition = FormStartPosition.Manual;
            this.DesktopBounds = Settings.Default.WindowPosition;

            // afterwards set the window state to the saved value (which could be Maximized)
            this.WindowState = Settings.Default.WindowState;
        }
        else
        {
            // this resets the upper left corner of the window to windows standards
            this.StartPosition = FormStartPosition.WindowsDefaultLocation;

            // we can still apply the saved size
            // msorens: added gatekeeper, otherwise first time appears as just a title bar!
            if (Settings.Default.WindowPosition != Rectangle.Empty)
            {
                this.Size = Settings.Default.WindowPosition.Size;
            }
        }
        windowInitialized = true;
    }

    private bool IsVisibleOnAnyScreen(Rectangle rect)
    {
        foreach (Screen screen in Screen.AllScreens)
        {
            if (screen.WorkingArea.IntersectsWith(rect))
            {
                return true;
            }
        }

        return false;
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);

        // only save the WindowState if Normal or Maximized
        switch (this.WindowState)
        {
            case FormWindowState.Normal:
            case FormWindowState.Maximized:
                Settings.Default.WindowState = this.WindowState;
                break;

            default:
                Settings.Default.WindowState = FormWindowState.Normal;
                break;
        }

        # region msorens: this code does *not* handle minimized/maximized window.

        // reset window state to normal to get the correct bounds
        // also make the form invisible to prevent distracting the user
        //this.Visible = false;
        //this.WindowState = FormWindowState.Normal;
        //Settings.Default.WindowPosition = this.DesktopBounds;

        # endregion

        Settings.Default.Save();
    }

    # region window size/position
    // msorens: Added region to handle closing when window is minimized or maximized.

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        TrackWindowState();
    }

    protected override void OnMove(EventArgs e)
    {
        base.OnMove(e);
        TrackWindowState();
    }

    // On a move or resize in Normal state, record the new values as they occur.
    // This solves the problem of closing the app when minimized or maximized.
    private void TrackWindowState()
    {
        // Don't record the window setup, otherwise we lose the persistent values!
        if (!windowInitialized) { return; }

        if (WindowState == FormWindowState.Normal)
        {
            Settings.Default.WindowPosition = this.DesktopBounds;
        }
    }

    # endregion window size/position
}
msorens
For those interested in actually using this solution, I have taken the final step of converting the example into a library with a documented API (http://cleancode.sourceforge.net/api/csharp/CleanCode.Forms.WindowRestorer.html). With the library, you need only a couple lines to load/save settings, and a couple one-line event handlers--a total of 7 lines of code for everything! Download area is at http://cleancode.sourceforge.net/wwwdoc/download.html.
msorens
Any Idea why I would get a StackOverflow error on the `base.OnMove()`???
Refracted Paladin
Perhaps the problem with my code is that I shouldn't make the window invisible before changing the state. Perhaps I just should test it instead of guessing :)
VVS