Persisting Form Settings
Created: 15 September 2007
Get the code here.
When working with Windows applications users expect that the application will remember
its state when it is closed and restore that state when it is opened. This
allows the user to customize the layout of the application to fit their needs.
If Bill only works with a single application at a time he might like to have the
application take up the whole screen so he can maximize his view. Julie, on
the other hand, runs several applications at once. She wants to keep windows
laid out in a certain order so she can efficiently copy and paste data between applications.
It doesn't make sense to require Julie and Bill to share the same window layouts
when their individual settings can be stored easily.
In the early betas of .NET v2 this functionality was available. It seems to
have been removed before the final release. This article will
discuss how to add in form state persistence to your WinForms applications. This article
will only deal with a single form and the basic form attributes but it can be extended
to multiple forms with many different properties including tool windows and docking
state.
Where to Store the Data
For persisting data on a per-user basis you basically have three common choices:
registry, isolated storage and settings file. The registry is commonly used
for old-school programming. The HKEY_CURRENT_USER key is designed for storing
per-user settings. It works a lot like a file system. The registry is
generally not recommended anymore. It is designed for small pieces of data
and has limited support for data formats. It is, however, secure and requires more than a passing knowledge of Windows to use properly. Therefore it is
a good choice for settings that should generally be protected but not to big in
size. A big limitation of the registry is that it can't be used in applications
that don't have registry access (like network applications or smart clients).
Isolated storage is a step up the file system hierarchy. It looks like a file
system (and actually resides in the system somewhere) but its actual location is
hidden from applications. Isolated storage allows any application to store
data per-user. The downside to isolated storage is that it can be a little
confusing to use. Additionally, since the actual path and file information
is hidden, it can be hard to clean up corrupt data if something were to go wrong.
Finally there is the settings file. We are talking about the user settings
file here, not the application settings file. Each user can have their own
settings file. This file works similar to the application property settings
file that you can use for application-wise settings. The difference is that
each user has their own copy and it is store in the user's profile directory.
Before moving on it is important to consider versioning of the settings. If
you want a user running v1 of your application to be able to upgrade to v2 and not
lose any of their settings then you must be sure to chose a persistence location
that is independent of the version of the application. The registry is a good
choice here as isolated storage and user settings are generally done by application
version. Still it doesn't make sense in all cases to be backward compatible
with the settings file.
You will have to decide on a case by case basis.
FormSettings
Let's start with the basic class we'll use. Ultimately, since we might have
quite a few forms to persist we want to create a base class (FormSettings)
that will take care of the details. We can derive from this class for custom
form settings as needed. In this article we will use the user's setting file
so we derive from ApplicationSettingsBase. If you want to
use a different storage mechanism then you'll need to make the appropriate changes.
Since we want our class to work with multiple forms we need to make each form unique.
We will use the SettingsKey to make each form unique. Each
form must specify the key it will use. Here is the start of our class.
public class FormSettings : ApplicationSettingsBase
{
public FormSettings ( string prefix )
{
SettingsKey = prefix;
}
public void Load ( Form target )
{
//Load
}
public void Save ( Form target )
{
//Save
}
}
When the form is loaded it will call Load to load its settings.
When the form is closed it will call Save. Here is sample
code for our form.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
m_Settings.Load(this);
}
protected override void OnFormClosing ( FormClosingEventArgs e )
{
base.OnFormClosing(e);
if (!e.Cancel)
m_Settings.Save(this);
}
private FormSettings m_Settings = new FormSettings("MainForm");
Persisting Properties
At a minimum a user would expect to be able to move the window around and resize
it to fit their needs. Therefore we need to load and save the following properties:
DesktopLocation, Size and WindowState.
DesktopLocation specifies the position, relative to the top-left
corner of the desktop, of the top-left corner of the form.
The Size property indicates the width and height of the form.
Finally the WindowState is used to track when a window is minimized
or maximized. We will discuss this property shortly.
To load and save properties using the settings file we need to define a property
for each item we want to save. We need to mark the property as user-scoped
and we need to get the value from and set the value to the settings file.
This is pretty straightforward so we will not dwell on the details. Here is
the code for getting and setting the property values. One point of interest
is that we use special values when we can not find the property values. This
will come back later when we talk about loading the settings.
public class FormSettings : ApplicationSettingsBase
{
...
[UserScopedSetting()]
public Point Position
{
get
{
object value = this["Position"];
return (value !=
null) ? (Point)value : Point.Empty;
}
set { this["Position"] = value; }
}
[UserScopedSetting()]
public Size Size
{
get
{
object value = this["Size"];
return (value != null) ? (Size)value
: Size.Empty;
}
set { this["Size"] = value; }
}
[UserScopedSetting()]
public FormWindowState
State
{
get
{
object value = this["State"];
return (value != null) ? (FormWindowState)value
: FormWindowState.Normal;
}
set { this["State"] = value; }
}
}
Saving the Settings
Saving the settings is really easy. All we have to do is set each of the user-scoped
property values and then call Save on the base settings class.
This will flush the property values to the user's settings file.
public void Save ( Form target )
{
//Save the values
Position = target.DesktopLocation;
Size = target.Size;
State = target.WindowState;
//Save the settings
Save();
}
Loading the Settings
Loading the settings requires a little more work. On the surface it is similar
to the save process: get each property value and assign it to the form. The
only issue that comes up is what to do when no settings have been persisted (or
they are corrupt). In this case I believe the best option is to not modify
the form's properties at all and, therefore, let it use whatever settings were defined
in the designer. Let's do that now and see what happens.
public void Load ( Form target )
{
//If the saved position isn't empty we will use it
if (!Position.IsEmpty)
target.DesktopLocation = Position;
//If the saved size isn't empty we will use it
if (!Size.IsEmpty)
target.Size = Size;
target.WindowState = State;
}
It seems to work. This is too easy, right? Now try minimizing the form
and then closing it. Open the form again. Can't restore it can you?
Minimizing and Maximizing Forms
The problem is that for a minimized/maximized form the DesktopLocation
and Size properties are not reliable. Instead we need to
use the RestoreBounds property which tracks the position and size
of the form in its normal state. If we persist this property when saving then
when we load we can restore the normal position and size and then set the state
to cause the form to minimize or maximize properly. But there is another problem.
RestoreBounds isn't valid unless the form is minimized or maximized.
Therefore our save code has to look at the state of the form and use DesktopLocation
when normal and RestoreBounds when minimized/maximized. Note
that RestoreBounds.Size is valid in both cases although whether
this is by design or not is unknown. The load code remains unchanged as we
will set the values based upon the form's normal state and then tell the form to
minimize or maximize. Here is the updated code.
public void Save ( Form target )
{
//Save the values
if (target.WindowState == FormWindowState.Normal)
Position = target.DesktopLocation;
else
Position = target.RestoreBounds.Location;
Size = target.RestoreBounds.Size;
State = target.WindowState;
//Save the settings
Save();
}
Disappearing Windows
The final problem with our code is the problem of disappearing windows. We've
all seen it happen. You start an application and the window shows up in the
Task Bar but not on the screen. Windows doesn't realize that the application's
window is off the screen. This often occurs when using multiple monitors and
we switch the monitors around or when changing the screen resolution. Fortunately
we can work around it.
During loading we need to verify that the window is going to be in the workspace
of the screen (which includes all monitors). If it isn't then we need to adjust
the window to appear (at least slightly) on the screen. We can do this by
doing a quick check to make sure the restore position is valid and update it if
not.
What makes this quite a bit harder is the fact that screen coordinates are based
off the primary monitor. Therefore it is possible to have negative screen
coordinates. We also don't want the Task Bar or other system windows to overlap
so we will use the working area of the screen which is possibly smaller than the
screen size itself. .NET has a method to get the working area given a control
or point but
it returns the closest match. In this case we don't want a match.
.NET also has SystemInformation.VirtualScreen which gives us the
upper and lower bounds of the entire screen but it doesn't take the working area
into account.
For this article we'll take the approach of calculating the working area manually
by enumerating the monitors on the system and finding the smallest and largest working
areas. Once we have the work area we need to determine if the caption of the
form fits inside this area. The caption height is fixed by Windows but the
width will match whatever size the form is. We do a little math and viola.
If the caption is visible then we will set the position otherwise, in this case,
we simply let the form reset to its initial position. Here is the load code.
public void Load ( Form target )
{
//If the saved position isn't empty we will use it
if (!Position.IsEmpty)
{
//Verify the position is visible (at least
partially)
Rectangle rcArea = GetWorkingArea();
//We want to confirm that any portion
of the caption is visible
//The caption is the same width as the window but the
height is fixed
//from the top-left of the window
Size sz = (Size.IsEmpty) ? target.Size
: this.Size;
Rectangle rcForm = new Rectangle(Position, new Size(sz.Width, SystemInformation.CaptionHeight));
if (rcArea.IntersectsWith(rcForm))
target.DesktopLocation = Position;
};
//If the saved size isn't empty we will use it
if (!Size.IsEmpty)
target.Size = Size;
target.WindowState = State;
}
private Rectangle GetWorkingArea
()
{
int minX, maxX, minY, maxY;
minX = minY = Int32.MaxValue;
maxX = maxY = Int32.MinValue;
foreach (Screen scr in Screen.AllScreens)
{
Rectangle area = scr.WorkingArea;
if (area.Bottom < minY) minY = area.Bottom;
if (area.Bottom > maxY) maxY = area.Bottom;
if (area.Top < minY) minY = area.Top;
if (area.Top > maxY) maxY = area.Top;
if (area.Left < minX) minX = area.Left;
if (area.Left > maxX) maxX = area.Left;
if (area.Right < minX) minX = area.Right;
if (area.Right > maxX) maxX = area.Right;
};
return new Rectangle(minX, minY, (maxX - minX), (maxY - minY));
}
|