24

It seems like a standard requirement: next time the user launches the application, 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.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286

8 Answers8

18

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, 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.

Update: I added some static methods to simplify the boilerplate code that you add to each form. Once you've followed the instructions for adding the WindowSettings class to your project and creating an application setting, here's an example of the code that has to be added to each form whose position you want to record and restore.

    private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        Settings.Default.CustomWindowSettings = WindowSettings.Record(
            Settings.Default.CustomWindowSettings,
            this, 
            splitContainer1);
    }

    private void MyForm_Load(object sender, EventArgs e)
    {
        WindowSettings.Restore(
            Settings.Default.CustomWindowSettings, 
            this, 
            splitContainer1);
    }
Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
  • 1
    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 Apr 07 '09 at 07:56
  • 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 Apr 08 '09 at 00:12
  • 1
    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 Apr 13 '09 at 22:38
  • This looks really cool, but when I try to add the setting and type in Mynamespace.WindowSettings for the type field it says Type 'Mynamespace.WindowSettings' is not defined. This is in .NET 4.0 - any ideas? – Charlie Skilbeck Apr 11 '11 at 13:48
  • @cskilbeck, try doing a compile after adding the WindowSettings class to your project and before adding the new application setting. Did you follow all the instructions in WindowSettings.cs, including changing the namespace to match your project? Can you compile my sample project in your environment? If none of this helps, I suggest you post a new question and link it back to this one. – Don Kirkby Apr 11 '11 at 18:35
  • 1
    OK, I'll give it a go. I really want this to work as I can extend the class to save my listview column widths etc. Very neat idea. – Charlie Skilbeck Apr 12 '11 at 07:36
  • Aha, it works! It even shows the type in the drop down now. I did compile it but perhaps quitting and restarting VS2010 might have caused it to work. Thanks so much for this, very nice solution. – Charlie Skilbeck Apr 12 '11 at 07:43
6

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
  • 122,218
  • 32
  • 205
  • 338
  • I tried this, and had trouble with the UserPreferencesManager reference. Google indicates this is a a Java class, not c#! – Tom Bushell Dec 09 '09 at 15:44
  • 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 Dec 09 '09 at 16:16
  • The code comment says "Fit to the current screen size..." but the actual code uses Screen.PrimaryScreen: the current screen may not be the PrimaryScreen in a multi-monitor setup! – onedaywhen Feb 18 '15 at 16:36
  • @onedaywhen - good point, but in mitigation this was written for an environment where all screens were the same size, and the start position wasn't persisted. – Joe Feb 18 '15 at 18:02
5

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
  • 53,582
  • 27
  • 205
  • 286
  • 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 Sep 19 '08 at 22:13
  • 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 Sep 19 '08 at 22:18
5

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,171
  • 2
  • 17
  • 10
3

Based on the accepted answer by Don Kirkby and the WindowSettings class he wrote, you could derive a CustomForm from the standard one to reduce the amount of identical code written for each and every form, maybe like this:

using System;
using System.Configuration;
using System.Reflection;
using System.Windows.Forms;

namespace CustomForm
{
  public class MyCustomForm : Form
  {
    private ApplicationSettingsBase _appSettings = null;
    private string _settingName = "";

    public Form() : base() { }

    public Form(ApplicationSettingsBase settings, string settingName)
      : base()
    {
      _appSettings = settings;
      _settingName = settingName;

      this.Load += new EventHandler(Form_Load);
      this.FormClosing += new FormClosingEventHandler(Form_FormClosing);
    }

    private void Form_Load(object sender, EventArgs e)
    {
      if (_appSettings == null) return;

      PropertyInfo settingProperty = _appSettings.GetType().GetProperty(_settingName);
      if (settingProperty == null) return;

      WindowSettings previousSettings = settingProperty.GetValue(_appSettings, null) as WindowSettings;
      if (previousSettings == null) return;

      previousSettings.Restore(this);
    }

    private void Form_FormClosing(object sender, FormClosingEventArgs e)
    {
      if (_appSettings == null) return;

      PropertyInfo settingProperty = _appSettings.GetType().GetProperty(_settingName);
      if (settingProperty == null) return;

      WindowSettings previousSettings = settingProperty.GetValue(_appSettings, null) as WindowSettings;
      if (previousSettings == null)
        previousSettings = new WindowSettings();

      previousSettings.Record(this);

      settingProperty.SetValue(_appSettings, previousSettings, null);

      _appSettings.Save();
    }
  }
}

To use this, pass your application settings class and setting name in the constructor:

CustomForm.MyCustomForm f = new CustomForm.MyCustomForm(Properties.Settings.Default, "formSettings");

This uses Reflection to get/set the previous settings from/to the settings class. It may not be optimal to put the Save call into the Form_Closing routine, one could remove that and save the settings file whenever the main app exits.

To use it as a regular form, just use the parameterless constructor.

takrl
  • 6,356
  • 3
  • 60
  • 69
  • That's an interesting idea to reduce the boilerplate code. I might adapt it to a static method on the WindowSettings class that takes the WindowSettings object as a ref parameter. I think that could avoid the use of reflection. – Don Kirkby Apr 07 '11 at 18:15
  • Thanks for the comment. I'm not sure how that would work to get around using reflection though. You have to pass some type that represents a settings object. Since the object containing the settings varies from project to project, I didn't know how to set and extract the settings without using reflection, since the property names differ for each case. For simplification I also tried using _this.Name_ to construct a more generic setting name, but that didn't work since the form name is set within the InitializeComponent call, which happens after the call to the base constructor. – takrl Apr 12 '11 at 08:07
  • OK, I added the static methods to the sample code: http://code.google.com/p/donkirkby/source/browse/trunk/WindowSettings/WindowSettings.cs You can see how they're called in the snippet on the accepted answer above. Thanks for the idea. – Don Kirkby Apr 12 '11 at 20:02
  • Right, I misunderstood the way you were going with this. That simplifies things even more ... – takrl Apr 13 '11 at 09:27
  • ... but makes it almost impossible to do this within a derived CustomForm. To handle the setting object being null, it would have to be passed by ref to the constructor. This is not possible for a property. The only thing I found that works in this situation is passing a delegate invokes the setter like this: `public frm() : base(Properties.Settings.Default.test, v => Properties.Settings.Default.test = v)` ... and I don't like the look of this I must say. – takrl Apr 13 '11 at 12:02
  • Sorry, @takrl, I didn't notice your comment before. In my snippet on the accepted answer, you can see how I deal with the null setting. It is essentially `setting = Record(setting, form)`. That way if setting is null, I create a new one and save it. If the form is minimized, I just keep the old settings. – Don Kirkby May 19 '11 at 15:30
  • Yes, I noticed how your code handles this. Looks pretty elegant to me, but the way I went (before you modified your solution based on my post) was to build a custom form derived from the standard one that does this automatically. And to handle the null case you have to pass something that's able to set the property within the Settings class. This is one of the cases where a pointer to a property setter would've come in handy, but that's just not possible in c#. Of course the desired result could be achieved using - again- reflection. The delegate solution works for this, but I think it's ugly. – takrl May 20 '11 at 08:13
  • 1
    It just came to my mind ... since this is something I'll probably use in each and every future project, it could be a good use-case for extension methods. I'll think about that for a while ... – takrl May 20 '11 at 08:20
2

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

Dror Helper
  • 30,292
  • 15
  • 80
  • 129
2

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
  • 37,735
  • 14
  • 62
  • 96
2

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
  • 1,147
  • 1
  • 17
  • 27
  • 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 Sep 19 '08 at 22:14