Tuesday, November 21, 2006

Maintaining Window State

Remembering the window size and position between sessions sounds easy enough, doesn't it? I thought it would be, too until I tried to do it. Things get more complex when you offer the ability to "hide when minimized" and have a system tray icon. One of the first problems is when the application is shutdown with the window hidden. I wanted the app to start in that state, but also remember the last known position and size of the main window. I also wanted to delay creation of any windows until they were needed. In other words, if the app was shutdown with only the system tray visible, it should be able to start up in that state and not have to create any of the UI until it is needed.

After several rounds of trial and error, I settled on a "WindowStateManager" class that adds event handlers for window size and position changes, as well as saving them to settings. In addition, this class would be responsible for knowing when to create the main window and would fire an event when the window was created. The application entry point class listens to that event so it can do whatever it needs to do when the main form is created. This is a departure from what the typical Windows Forms application looks like in that the Application.Run method doesn't receive the MainForm instance.

So, here's the application entry point:


[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.ThreadException +=
new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
_splash = new AppSplash();
_splash.Closed += new EventHandler(splash_Closed);
_splash.Show();

initApplication();
startSystemTrayIcon();
Application.Run();
}


First the usual stuff generated by Visual Studio. Then, it registers some top-level exception handlers, deal with the splash screen (I'll post my implementation of that in a later post). That's followed by a call to "initApplication()", which at the moment deals with the WindowStateManager and the creation of the tray icon. Finally, Application.Run() is called to start running the application. The main window may or may be created depending on the how the app was shut down. If it was shut down with the main window visible, it will be created by the WindowStateManager. Otherwise, no UI is created at all.



private static void initApplication()
{
_windowStateManger.MainFormCreated += new WindowStateManager.MainFormCreatedHandler(windowStateManager_MainFormCreated);
_windowStateManger.init();
}

The WindowStateManager.init() method checks the settings that were read from disk, and then creates the main window if necessary:


public void init()
{
GlobalSettings global = AppSettings.Instance.Global;
if (global.MainWindowState != FormWindowState.Minimized)
{
createMainForm();
}
}
"createMainForm()" creates the MainForm instance, fires the "MainFormCreated" event, registers event handlers for the "SizeChanged" and "PositionChanged" events, and sets the size and state of the window.


private void createMainForm()
{
Debug.Assert(_form == null);
createTheForm();
registerEvents();
restoreWindowState();
}

private void createTheForm()
{
_form = new MainForm();
if (MainFormCreated != null)
{
MainFormCreated(_form);
}
}

private void restoreWindowState()
{
GlobalSettings global = AppSettings.Instance.Global;
if (global.MainWindowSize.IsEmpty)
{
_form.DesktopBounds = getDefaultDesktopBounds();
}
else
{
_form.Size = global.MainWindowSize.Size;
_form.Location = global.MainWindowLocation;
}

_form.WindowState = global.MainWindowState;
if(_form.WindowState != FormWindowState.Minimized)
{
_form.Show();
}
}

private void registerEvents()
{
_form.SizeChanged += new EventHandler(sizeChanged);
_form.LocationChanged += new EventHandler(locationChanged);
}

private void sizeChanged(object sender, EventArgs e)
{
if (_form.WindowState != FormWindowState.Maximized && _form.WindowState != FormWindowState.Minimized)
{
_lastSize = _form.DesktopBounds;
}
else if (_form.WindowState == FormWindowState.Minimized)
{
_form.Visible = false;
}
}

private void locationChanged(object sender, EventArgs e)
{
if (_form.WindowState == FormWindowState.Normal)
{
_lastLocation = _form.Location;
}
}

I'm not sure if accessing the AppSettings directly from within this class is the best thing to do (perhaps the relevant data should be passed in?), but it works. I may change that yet. At least now, the application starts in exactly the same state and position it was when it was shut down. This even works on multimonitor configurations, although, I didn't test it with the second monitor on the left.

No comments: