Introduction
This is a simple drop-in component that will retain the bounds and state of a window between sessions of an application.
Background
A common task in developing desktop applications is remembering where the user put their forms in the last application run. Some users spend a great deal of time setting up their workspace exactly right, and it is quite an unpleasant experience to realise the application doesn't remember their hard work.
Using the Code
To use the code, follow these steps:
- Simply add a reference to the RememberFormPosition assembly, or include the source in your own project
- Open the designer for your Windows Form of choice
- From the toolbox, add a
RememberFormPosition
component
And that is all there is to it.
Points of Interest
Where Data is Stored
The position and state information for each form is stored in the Registry in the Application.UserAppDataRegistry
key. The following Registry entries will be created:
FormName_Left
FormName_Top
FormName_Right
FormName_Bottom
FormName_State
Where FormName
is the name of the form or the custom name that is provided to the control.
When the Data is Read in
The RememberFormPosition
component implements the ISupportInitialize
interface, and we do the reading of the form state in the EndInit()
implementation.
Doing this in EndEdit()
ensures that
- The form is not yet visible, so no update flickering can take place. For example, if we did this on the
FormLoad
event instead, we would see the window move around as it has already been made visible.
- The
Form
property of the RememberFormPosition
component is set up.
public void EndInit()
{
_form.StartPosition = FormStartPosition.Manual;
RememberFormPositionUtils.RestoreFormPlacement(Application.UserAppDataRegistry,
Form,
UseFormName ? _form.Name : StorageName);
}
When is the Information Written
During the initialization of the RememberFormPosition
component, we attach an event handler to our form's FormClosing
event. During this, we write out the form's position and state. This is done because it ensures the form has not been disposed yet and all the information we need is in a valid state.
How are Minimized and Maximized Windows Handled
The special cases of a form being maximized or minimized needs to be handled carefully, because using the ordinary Control.Bounds
will not achieve the desired result. In the case of being maximized, it will be the extends of the current Screen.WorkingArea
, and in the case of minimized, it may be very small and will not be useful.
So when our Form.WindowState
is not FormWindowState.Normal
, we do the following:
- Look at the
Form.RestoreBounds
property
- This
Form.RestoreBounds
property is relative to Screen.PrimaryScreen.WorkingArea
, so we need to compute the desktop bounds for this
- We then write out the current
WindowState
The function that handles this is RemeberFormPositionUtils.SaveFormPlacement()
.
public static void SaveFormPlacement(RegistryKey key, Form form, string name)
{
if (form.WindowState == FormWindowState.Normal)
{
key.SetRectangleValue(name, form.DesktopBounds);
}
else
{
Rectangle workingArea = Screen.PrimaryScreen.WorkingArea;
Rectangle restoreBounds = form.RestoreBounds;
restoreBounds.X -= workingArea.X;
restoreBounds.Y -= workingArea.Y;
key.SetRectangleValue(name, restoreBounds);
}
key.SetWindowStateValue(name, form.WindowState);
}
When reading these back in, we:
- Set
Form.StartPosition
to FormStartPosition.Manual
- Set the
WindowState
to FormWindowState.Normal
- We then set form bounds by using
Form.SetDesktopBounds()
- After this, we change
Form.WindowState
to the state stored in the Registry
The code that handles this is primarily in RemeberFormPositionUtils.RestoreFormPlacement()
.
public static void RestoreFormPlacement(RegistryKey key, Form form, string name)
{
Rectangle? rect = key.GetRectangleValue(name);
if (rect.HasValue)
{
Rectangle formBounds = rect.Value;
if (!form.IsMdiChild)
{
formBounds = EnsureFitsInDesktop(formBounds);
}
form.WindowState = FormWindowState.Normal;
form.SetDesktopBounds(formBounds.X, formBounds.Y,
formBounds.Width, formBounds.Height);
}
FormWindowState? state = key.GetWindowStateValue(name);
if (state.HasValue)
{
form.WindowState = state.Value;
}
}
Extension Methods
A small portion of the code makes use of Extension Methods to read and write values from the Registry. It is hardly a necessary thing, but it was just something to experiment with.
If you wish to use the code in a C# 2.0 compiler, it should be a trivial task to remove the extra this
keywords and just refer to the static classes explicitly.
Multiple Monitors
Multiple monitors has not been tested, but preliminary code has been put in place to handle it. If all four corners of the form are out of an available Screen, then we place the form in the middle of the screen. Also, if the form is too big to fit on the screen, we resize it so it is just under the size of the working area.
Things Learned About Components and WinForms in General
Designer Serialized Properties
When exposing properties that are [Browsable(true)]
, ensure that if you have a [DefaultValue]
attribute, you also set up this default value in your constructor! If you do not, then if these conditions are true:
- Your property value is the same as your
DefaultValue
.
- Your
DefaultValue
is different from the default compiler value.
Then your property value will not get serialized and it will very confusingly lose your value. This happens because in designer serialized code, if a property value equals a DefaultValue
, then the value is not written out as part of serialization. And hence next time the designer appears and your object is instantiated, it will revert to the compiler generated value.
Discovering the Owner of a Component
This was not as straightforward a task as I would have imagined. Components to not have direct knowledge of the Component or Control they are a part of.
In order to achieve this, a simple ComponentDesigner
needed to be created in order to initialise our RememberFormPosition.Form
property to the current Form
. Inside ComponentDesigner.Initialize()
, we have the ability to enquire what component we are designing via the ComponentDesigner.ParentComponent
property.
History
- 21/05/2008 - First article version.