views:

3730

answers:

7

It seems like a standard requirement: next time the user launches the app, open the window in the same position and state as it was before. Here's my wish list:

  • Window position same as it was
    • Unless the screen has resized and the old position is now off screen.
  • Splitters should retain their position
  • Tab containers should retain their selection
  • Some dropdowns should retain their selection
  • Window state (maximize, minimize, normal) is the same as it was.
    • Maybe you should never start minimized, I haven't decided.

I'll add my current solutions as an answer along with the limitations.

+2  A: 

The simplest solution I've found is to use data binding with the application settings. I bind the location and clientSize properties on the window along with the splitterDistance on the splitter.

Drawbacks:

  • If you close the window while minimized, it opens hidden the next time. It's really hard to get the window back.
  • If you close the window while maximized, it opens filling the whole screen, but not maximized (minor issue).
  • Resizing the window using the top-right corner or the bottom-left corner is just ugly. I guess the two databound properties are fighting each other.

If you'd like to experiment with the strange behaviour, I posted a sample solution using this technique.

Don Kirkby
There is a WindowState property you can reference to check for minimized or maximized windows. However, I cannot comment on what the window size is when it's in one of those two states.
Austin Salonen
Yes, I experimented with the WindowState property. It behaved very strangely. Perhaps a similar issue to the resizing one, where changing more than one databound property at the same time causes contention and flickering.
Don Kirkby
+1  A: 

A hack you can use Settings to store that information. All you have to do is bind the desired property (ex. form.Size) to a specific setting and it get saved and updated automatically.

Dror Helper
+4  A: 

My other option is to write more custom code around the application settings and execute it on formLoad and formClosed. This doesn't use data binding.

Drawbacks:

  • More code to write.
  • Very fiddly. The order you set the properties on formLoad is confusing. For example, you have to make sure you've set the window size before you set the splitter distance.

Right now, this is my preferred solution, but it seems like too much work. To reduce the work, I created a WindowSettings class that serializes the window location, size, state, and any splitter positions to a single application setting. Then I can just create a setting of that type for each form in my application, save on close, and restore on load.

I posted the source code, as well as a sample application including the WindowSettings class and some forms that use it. Instructions on adding it to a project are included in the WindowSettings.cs file. The trickiest part was figuring out how to add an application setting with a custom type. You choose Browse... from the type dropdown, and then manually enter the namespace and class name. Types from your project don't show up in the list.

Don Kirkby
Great sample code!
Cookey
Unfortunately, the license of the donkirkby project under which the sample is bound may not allow for a simple free-will use of the code. Consider re-publishing it here.
Atif Aziz
I've now changed to the MIT license; I didn't mean to restrict use of the code. Of course, attribution is appreciated. The code's a bit long to post here.
Don Kirkby
Good move, Don. Appreciate the prompt response. Needless to say, credit goes where its due and the MIT License should take care of that.
Atif Aziz
A: 

You can use the application settings to set which control properties will be persisted, in the Form_closed event you have to use the save method on the application settings to write these to disk:

Properties.Settings.Default.Save();
benPearce
+1  A: 

Here is an example of a few I use myself. It only takes into consideration the primary monitor, so it might be better to handle it differently if used on multiple monitors.

Size size;
int x;
int y;
if (WindowState.Equals(FormWindowState.Normal))
{
    size = Size;
    if (Location.X + size.Width > Screen.PrimaryScreen.Bounds.Width)
        x = Screen.PrimaryScreen.Bounds.Width - size.Width;
    else
        x = Location.X;
    if (Location.Y + Size.Height > Screen.PrimaryScreen.Bounds.Height)
        y = Screen.PrimaryScreen.Bounds.Height - size.Height;
    else
        y = Location.Y;
}
else
{
size = RestoreBounds.Size;
x = (Screen.PrimaryScreen.Bounds.Width - size.Width)/2;
y = (Screen.PrimaryScreen.Bounds.Height - size.Height)/2;
}
Properties.Settings.Position.AsPoint = new Point(x, y); // Property setting is type of Point
Properties.Settings.Size.AsSize = size;                 // Property setting is type of Size
Properties.Settings.SplitterDistance.Value = splitContainer1.SplitterDistance; // Property setting is type of int
Properties.Settings.IsMaximized = WindowState == FormWindowState.Maximized;    // Property setting is type of bool
Properties.Settings.DropDownSelection = DropDown1.SelectedValue;
Properties.Settings.Save();
Geir-Tore Lindsve
Yes, that's the kind of custom code I'm talking about in my second answer. As I said, it's fiddly to get right. I'm hoping there's an easier way. Thanks, though, I didn't know about the AsSize and AsPoint properties. I'll check those out.
Don Kirkby
+1  A: 

I make a Setting for each value I want to save, and use code like this:

private void MainForm_Load(object sender, EventArgs e) {
  RestoreState();
}

private void MainForm_FormClosing(object sender, FormClosingEventArgs e) {
  SaveState();
}

private void SaveState() {
  if (WindowState == FormWindowState.Normal) {
    Properties.Settings.Default.MainFormLocation = Location;
    Properties.Settings.Default.MainFormSize = Size;
  } else {
    Properties.Settings.Default.MainFormLocation = RestoreBounds.Location;
    Properties.Settings.Default.MainFormSize = RestoreBounds.Size;
  }
  Properties.Settings.Default.MainFormState = WindowState;
  Properties.Settings.Default.SplitterDistance = splitContainer1.SplitterDistance;
  Properties.Settings.Default.Save();
}

private void RestoreState() {
  if (Properties.Settings.Default.MainFormSize == new Size(0, 0)) {
    return; // state has never been saved
  }
  StartPosition = FormStartPosition.Manual;
  Location = Properties.Settings.Default.MainFormLocation;
  Size = Properties.Settings.Default.MainFormSize;
  // I don't like an app to be restored minimized, even if I closed it that way
  WindowState = Properties.Settings.Default.MainFormState == 
    FormWindowState.Minimized ? FormWindowState.Normal : Properties.Settings.Default.MainFormState;
  splitContainer1.SplitterDistance = Properties.Settings.Default.SplitterDistance;
}

Keep in mind that recompiling wipes the config file where the settings are stored, so test it without making code changes in between a save and a restore.

Wonko
+2  A: 

The sample below shows how I do it

  • SavePreferences is called when closing the form and saves the form's size, and a flag indicating if it's maximized (in this version I don't save if it's minimized - it will come back up restored or maximized next time).

  • LoadPreferences is called from OnLoad.

  • First save the design-time WindowState and set it to Normal. You can only successfully set the form size if it's WindowState is Normal.

  • Next restore the Size from your persisted settings.

  • Now make sure the form fits on your screen (call to FitToScreen). The screen resolution may have changed since you last ran the application.

  • Finally set the WindowState back to Maximized (if persisted as such), or to the design-time value saved earlier.

This could obviously be adapted to persist the start position and whether the form was minimized when closed - I didn't need to do that. Other settings for controls on your form such as splitter position and tab container are straightforward.

private void FitToScreen()
{
    if (this.Width > Screen.PrimaryScreen.WorkingArea.Width)
    {
     this.Width = Screen.PrimaryScreen.WorkingArea.Width;
    }
    if (this.Height > Screen.PrimaryScreen.WorkingArea.Height)
    {
     this.Height = Screen.PrimaryScreen.WorkingArea.Height;
    }
}   
private void LoadPreferences()
{
    // Called from Form.OnLoad

    // Remember the initial window state and set it to Normal before sizing the form
    FormWindowState initialWindowState = this.WindowState;
    this.WindowState = FormWindowState.Normal;
    this.Size = UserPreferencesManager.LoadSetting("_Size", this.Size);
    _currentFormSize = Size;
    // Fit to the current screen size in case the screen resolution
    // has changed since the size was last persisted.
    FitToScreen();
    bool isMaximized = UserPreferencesManager.LoadSetting("_Max", initialWindowState == FormWindowState.Maximized);
    WindowState = isMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
}
private void SavePreferences()
{
    // Called from Form.OnClosed
    UserPreferencesManager.SaveSetting("_Size", _currentFormSize);
    UserPreferencesManager.SaveSetting("_Max", this.WindowState == FormWindowState.Maximized);
    ... save other settings
}

x

Joe
I tried this, and had trouble with the UserPreferencesManager reference. Google indicates this is a a Java class, not c#!
Tom Bushell
I wasn't very clear was I. In this sample, UserPreferencesManager is a class I wrote, that does the work of loading and saving settings to a persistent medium. This was for .NET 1.1, these days you'd use the .NET 2.0 Settings architecture for persistence. Note that the focus of this sample was the order in which properties are set when loading settings, rather than the details of how they're saved / restored.
Joe