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.

Sunday, November 19, 2006

Settings and Persisting Window State

The next thing I hooked up was the settings. I've seen a number of ways to do this, and I decided to use the XML serialization services build into .NET. It's simple, although somewhat slow. Still, it's very easy to understand and can be utilized with just a few lines of code. Basically, it takes a data type object and generates an XML file from the read/write properties of the class. I have one class per logical group of settings which translates to one file per class: "Network", "Lan", "Global" and "Other". Around that, I have a singleton instance called "AppSettings" that acts as a container for the different settings classes. The AppSettings class is what the application interfaces with and is where the saving and loading of the program settings happens as the application is started and shut down.

When the AppSettings instance is created, the private "init" method is called. This checks if the "Settings" directory is there and creates it if it isn't. Then, it loads the settings.


///
/// Initializes the settings path and creates the "Settings" directory if it doesn't exist.
///

private void init()
{
_fFirstTimeLoad = false;
_strSettingsPath = Path.Combine(Environment.CurrentDirectory, SettingsDirectory);
if (!Directory.Exists(_strSettingsPath) )
{
_fFirstTimeLoad = true;
Directory.CreateDirectory(_strSettingsPath);
}

// Or if there's no files in the settings directory, treat it like a first time run
if (Directory.GetFiles(_strSettingsPath).Length == 0)
{
_fFirstTimeLoad = true;
}

loadSettings();
}


The loadSettings() method just calls the static "load" method on each of the components of the settings.



  private void loadSettings()
{
_globalSettings = GlobalSettings.load(_strSettingsPath);
_networkSettings = NetworkSettings.load(_strSettingsPath);
_lanSettings = LanSettings.load(_strSettingsPath);
_otherSettings = OtherSettings.load(_strSettingsPath);
}


Each of the settings classes derive from a base class that handles the XML serialization to and from disk.

In a nutshell it opens a FileStream, creates a serializer of the correct type and then calls Deserialize. Viola! Your settings are restored! Here's the guts of it. I removed the exception handling code just to keep it brief here.




protected static AbstractSettingsItem read(Type type, string strPath)
{
FileStream fs;
fs = File.Open(strPath, FileMode.Open, FileAccess.Read);
XmlSerializer xs = new XmlSerializer(type);
object objData;
objData = xs.Deserialize(fs);
fs.Close();
return (objData as AbstractSettingsItem);
}

The code to save the settings is very similar except, "Serialize" is called instead. This makes for a very easy way to blast settings to disk and restore them later. Provided all settings are initialized with proper defaults, adding new settings are accommodated easily as the program grows. I don't know if this is the best way of doing this, but it seems to work pretty well.

I think one way to improve it would be to extract an interface to pass to and from the subclasses and the base class, so that reading doesn't pass back an object of type "AbstractSettingsItem" - it just seems awkward to me. Not sure what methods would be defined in the interface, though. I'll have to think about that more.

For now, if I want to add a new settings class, I just derive from "AbstractSettings", add the data members to hold the settings data, and write the properties that are to be saved and restored between sessions. Then add an instance to the AppSettings class and add the calls to "load" and "save".

I realized as I was writing this, that the current implementation is pretty much limited to simple types: boo, int, string, etc. The default XML serialization handles arrays and collections pretty well, and also nested objects, as long as they have public read and write properties. Beyond that, I think I'd have to provide a custom serializer or handle writing out my own XML document. For the most part, I think settings fall into the "simple types" category. At least I haven't run into anything that required something more complex.

Next up, saving and restoring the main window size and state with a system tray icon.

Wednesday, November 15, 2006

Starting Fresh

I was converting the Game-Tracker project to the .NET 2.0 framework to make use of the new project structure and better resource handling. Even though it's not necessary, I wanted to convert the forms so they had the seperate Designer classes and resources. Pretty much the only way to do that (that I know of) is to create a new form, and then copy the code over from the 1.1 form. So far so good. I create a new project, create a new MainForm and start carrying over code. In the process, I'm thinking about all the stuff I did wrong and don't like about Game-Tracker. First of all, I realize I don't like the name. Time for a new name. Also, I think about all the crap that I don't like about the source code. Since I was learning .NET/C#, I did a lot of experimenting and made a lot of mistakes, having to re-do parts of it. Parts of it were terribly confusing and difficult to work in. Hmm.

Time to scrap the old and start fresh! w00t! It's one of the benefits of being the only developer on a free project that you do in your own time that has never been released. :-) That freed me up to think about what I didn't like about Game-Tracker and either remove it or fix it. One thing was that the UI was plain and somewhat cluttered - one of the first things I didn't like about the GameSpy UI. I really like a clean UI and an app that does one thing well. What I would like this app to do well, is monitor and find public Internet game servers. And, like everyone else, I want a slick, nice-looking UI.

I think the game protocol plugins are still good, as are a number of the non-UI components. The game template and networking code is ok. I still needed to optimize the networking code a bit, so that when querying a lot of servers the data gets pulled off the socket as quickly as possible. I can also carry over the multicast chat utility, which already works great on a lan. I plan to extend this to a true "friends" type feature. So, quite a bit is reusable and "good", in my opinion. (I still have my doubts about using a blocking socket call in the plugins, but I'll talk about that in a different post)



I created a new project in VS2005, created the main window and started with a simple ToolStrip at the top and StatusStrip at the bottom. I won't be using the .NET 1.1 Menus or StatusBars at all. The new ToolStrip components are much more flexible, and just look a lot nicer. As a plus, the ToolStrip components use a bit of the .NET 3.0 component structure with their "Renderer" property, so it seems like a good choice moving forward. I also started a custom control to handle the display of the game icons installed on the users' system. I have a rough idea of how I'd like it to loook, but we'll see how it turns out. Here's a screenshot of what I have so far. Not very interesting, but with a little imagination, I think you'll see what I'm striving for.



The red squares at the top will be where the game icons are. When the app starts up for the first time, it reads the templates I've defined for the games that it has support for, scans the user's system looking for the games, extracts the appropriate icon and will display them in that control at the top. That's the idea any way. I have to admit, I've never had the "gift of great UI", so any suggestions/feedback will be listened to. I know that pretty much everyone is better at it than me! :-)