Introduction
Screensavers are nice. They don't serve much of a practical purpose anymore now that monitors don't "burn" images onto the screen, but they do turn your computer into a pretty decoration when you're not around, and it's a lot more healthy to be staring at screensavers all day than to be hooked on MMORPGs.
What's not so nice about them is the way a lot of them behave, and the way they have to be written. Screensavers seem easy enough to set up; on Windows, all you have to do is to rename an .exe to .scr, and voila. But there's a bunch of quirks along the way. Here are some of the most bothersome ones for users:
- Multiple monitor support is often flakey, especially when monitors aren't of equal sizes.
- Previews often don't show up properly in the Display Properties dialog box.
- There's often no Settings box, not even a really simple one.
- Some screensavers use the "game loop" design, crippling background applications and causing a lot of modern CPUs to overheat.
- Behavior in response to mouse and keyboard input is inconsistent; some screensavers don't snap out until you actually click on the mouse.
And here are some of the troubles for developers:
- You decide you want to use a timer to avoid pinning the CPU, maybe
System.Timers.Timer
, but on Windows 9x/2K/XP, it doesn't provide enough precision to run a screensaver at even 30 frames per second.
- Debugging screensavers can be a nightmare when they're running in full screen.
- You've written your whole screensaver in a Windows Form... only to realize that you have to use a native handle in order for it to show up in the preview window.
- When you're writing a screensaver, you want to get right down to the fun stuff while the inspiration's still fresh in your mind. You don't want to deal with boring initialization and shutdown code.
Screensavers are luxury items, not necessities. Any one of these problems is enough to discourage people from using them. There are so many awesome screensavers out there, yet screensavers as a whole category of apps is becoming less and less popular as computer setups become more diverse and problems are becoming more exposed.
The Screensaver
base class attempts to lessen these problems and make screensaver development easier by providing a backbone that handles the mechanical aspects of writing screensavers. Its aim is not to create a whole new API, but simply to help make things work properly.
Using the code
A basic example
To avoid cluttering deployment, the Screensaver
class and all of its satellite objects are contained in a single file, Screensaver.cs, which you can drop right in your project. Compiled with the default settings using Visual Studio or Microsoft's C# compiler (csc.exe), it adds up to about 32 KB. Though some neat freaks may say that this is "bloated", I feel it's a fair price to pay for consistent behavior and reusable code, two things that too many screensavers lack.
Here is a basic example screensaver using the Screensaver
class:
class MyCoolScreensaver : Screensaver
{
public MyCoolScreensaver()
{
Initialize += new EventHandler(MyCoolScreensaver_Initialize);
Update += new EventHandler(MyCoolScreensaver_Update);
Exit += new EventHandler(MyCoolScreensaver_Exit);
}
void MyCoolScreensaver_Initialize(object sender, EventArgs e)
{
}
void MyCoolScreensaver_Update(object sender, EventArgs e)
{
Graphics0.Clear(Color.Black);
Graphics0.DrawString(
DateTime.Now.ToString(),
SystemFonts.DefaultFont, Brushes.Blue,
0, 0);
}
void MyCoolScreensaver_Exit(object sender, EventArgs e)
{
}
[STAThread]
static void Main()
{
Screensaver ss = new MyCoolScreensaver();
ss.Run();
}
}
And that's it! This example prints the current time in the top left corner of the primary monitor, in blue. The Initialize
event is fired right after the windows are created. Update
is fired 30 times per second, by default; this speed can be changed by changing the value of Framerate
. Exit
is called just before the windows are closed.
Run mode
The run mode is determined automatically by Screensaver.Run()
. Without any parameters, the screensaver will show the Settings window if the file extension is .scr; if it is .exe, it will show the screensaver in a window 9/10th the size of the screen.
You can change this by sending a ScreensaverMode
value to Run()
: Normal
will start the screensaver in full screen, Settings
will start the Settings dialog, and Windowed
will start the screensaver in windowed mode. The value can't be Preview
; preview mode only works when the Windows Display Properties gives it the right parameters (the handle to the preview control to be specific). The run mode is overridden by the command line arguments which are passed to the screensaver by the Display Properties.
Rendering with System.Drawing
Graphics0
provides the System.Drawing.Graphics
object for the primary window. To paint in the other windows, you can use Windows[n].Graphics
, where n
is the number of the monitor. Note that you are responsible for clearing the screen first.
Double buffering is enabled, by default, but to avoid excess overhead, it doesn't actually kick in until the Window.Graphics
object or the Window.DoubleBuffer
property is first used. It's a good idea to turn it off explicitly if you are using other rendering methods, such as DirectX. Double buffering is not as efficient in the .NET 1.1 version as it is in the .NET 2.0 version.
Installing the screensaver
To install a screensaver on Windows XP, all you have to do is to change the screensaver executable's file extension to .scr, right click in the shell, and click Install. On older versions of Windows, the .scr goes into the Windows System32 folder. The same can be done on XP to have it always appear in the screensaver menu, but I prefer not to touch the Windows folder.
You can have Visual Studio automatically make a .scr copy of the executable, by adding this command to the post-build event under the project properties:
copy "$(TargetFileName)" "$(TargetName).scr" /y
A slightly more complex example with Direct3D
Any of the available rendering technologies can be used with the Screensaver
class -- DirectX, OpenGL, WPF (Avalon) etc.. Here is an example of using Managed Direct3D with the Screensaver
class.
Setting up Managed Direct3D is not much trouble, assuming you have some knowledge of DirectX. If not, there are plenty of good tutorials out there that you can find with a Google search. Skim through a few, and study one that you like.
class MyCoolScreensaver : Screensaver
{
public MyCoolScreensaver()
: base(FullscreenMode.MultipleWindows)
{
Initialize += new EventHandler(MyCoolScreensaver_Initialize);
Update += new EventHandler(MyCoolScreensaver_Update);
Exit += new EventHandler(MyCoolScreensaver_Exit);
}
Device device;
Microsoft.DirectX.Direct3D.Font font;
int updates;
void MyCoolScreensaver_Initialize(object sender, EventArgs e)
{
PresentParameters pp = new PresentParameters();
if (this.Mode != ScreensaverMode.Normal)
pp.Windowed = true;
else
{
pp.Windowed = false;
pp.BackBufferCount = 1;
pp.BackBufferWidth =
Manager.Adapters[Window0.DeviceIndex].CurrentDisplayMode.Width;
pp.BackBufferHeight =
Manager.Adapters[Window0.DeviceIndex].CurrentDisplayMode.Height;
pp.BackBufferFormat =
Manager.Adapters[Window0.DeviceIndex].CurrentDisplayMode.Format;
}
pp.SwapEffect = SwapEffect.Flip;
device = new Device(
Window0.DeviceIndex, DeviceType.Hardware,
Window0.Handle, CreateFlags.HardwareVertexProcessing, pp);
Window0.DoubleBuffer = false;
font = new Microsoft.DirectX.Direct3D.Font(
device, System.Drawing.SystemFonts.DefaultFont);
}
void MyCoolScreensaver_Update(object sender, EventArgs e)
{
System.IO.StringWriter writer = new System.IO.StringWriter();
writer.WriteLine("Time: " + DateTime.Now);
writer.WriteLine("Achieved framerate: " + this.AchievedFramerate);
writer.WriteLine("Update count: " + updates++);
writer.WriteLine("Device: " + Window0.DeviceIndex);
device.Clear(ClearFlags.Target, System.Drawing.Color.Black, 0, 0);
device.BeginScene();
font.DrawText(null, writer.ToString(), 0, 0,
System.Drawing.Color.Blue.ToArgb());
device.EndScene();
device.Present();
}
void MyCoolScreensaver_Exit(object sender, EventArgs e)
{
device.Dispose();
}
[STAThread]
static void Main()
{
Screensaver ss = new MyCoolScreensaver();
ss.Run();
}
}
To compile this example, you need the Managed DirectX Runtime. You can get the latest (as of May 2006) version here. Add references to Microsoft.DirectX
, Microsoft.DirectX.Direct3D
, and Microsoft.DirectX.Direct3DX
.
Initializing DirectX for screensavers is, actually, somewhat easier than for a typical application, because you don't have to worry about device resets and resizes and such. Just be sure to dispose the device at the end, or you'll get erratic shutdowns.
Fullscreen mode
An overridden constructor of the Screensaver
class takes in a FullscreenMode
value: SingleWindow
to cover all screens with one single window, and MultipleWindows
to cover each screen with a window of its own. For DirectX, we want to use MultipleWindows
, and draw in just one window. The default value is MultipleWindows
, but in this example, I specify it explicitly just to demonstrate.
Note that ScreensaverMode.Normal
is the only mode in which the screensaver will run in full screen mode. Though you can skip most of that full screen initialization and just run it as a windowed app, it's a good idea to initialize full screen properly to get the extra performance boost.
The Window class
Just as Graphics0
is an alias to Windows[0].Graphics
, Window0
is an alias to Windows[0]
. The Screensaver.Window
class encapsulates either a Windows form or just a window handle if the screensaver is running in preview mode. Various properties related to the graphical aspects are available in these objects.
Miscellaneous functionality
- Settings dialog - you can either use the default Settings dialog, which simply shows a message box with some text gathered from the assembly information, or you can show your own dialog box by overriding
ShowSettingsDialog()
. If you choose to stick with the default dialog, you can enter some additional text by setting the SettingsText
property. In this example, I set it to my e-mail address.
- Keyboard and mouse events - the full array of Windows Forms keyboard and mouse events are available in the
Window
class. Additionally, you can use the Form
property to access the underlying Windows Form
, but this value will not necessarily be set, so these are provided as well for convenience.
- Frame rate settings - the
AchievedFramerate
property retrieves the actual number of frames processed in the past second, while the Framerate
property sets the target frame rate. The Screensaver
class uses the multimedia timer, so you can expect it to be quite precise. Note, however, that there is no frame skipping.
- CloseOnClick, CloseOnMouseMove, CloseOnKeyboardInput - these properties can be set to change the conditions on which to close the screensaver. By default, in normal mode, all three of these properties are set to
true
.
- Debug mode differences - to make life easier, certain behaviors are slightly different when the app is compiled in debug mode. Here are the differences for the current version:
- Windows are not topmost in debug mode. This is so that you can see your debugger.
Run()
offers to launch a debugger when the screensaver is started in preview mode.
PixieSaver sample application
PixieSaver is a simple, fully functional screensaver, written using the Screensaver
base class, in around 150 lines of code including whitespace. It uses the SingleWindow
full screen mode, and draws using System.Drawing
. Each pixie starts at the bottom, flickering as it makes its way to the top. For cuteness value, each one has its own tendency to drift a little to the left or to the right. This is the screensaver that I'm using right now. I'm quite fond of it since I have my computer in my room, and flashy screensavers tend to keep me awake.
Let me know if you have any questions or suggestions, and let me know if you make any cool screensavers.
History
- 2006.05.12 - First release.
- 2006.05.16 - Uploaded .NET 1.1 compatible version of the code. Minor change in shutdown code to match the .NET 1.1 version; should not have any real effect.