Click here to Skip to main content
Click here to Skip to main content

Write a Screensaver that Actually Works

, 24 May 2006 CPOL
Rate this:
Please Sign up or sign in to vote.
A base class for managing screensaver initialization, timing, preview view, and multiple monitor support, the proper way.

Sample image

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

Default settings dialog box

  • 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Rei Miyasaka

Canada Canada
The cows are here to take me home now...

Comments and Discussions

 
GeneralMy vote of 5 PinsitebuilderAndy Brummer21-Jul-13 8:27 
QuestionGreat class PinsitebuilderAndy Brummer21-Jul-13 8:26 
GeneralMy vote of 5 Pinmembersteveo204213-Oct-12 3:56 
QuestionTechnical question PinmemberMember 823079018-Sep-11 13:54 
AnswerRe: Technical question Pinmemberreinux18-Sep-11 19:49 
GeneralGood article BUT... PinmemberJohnny J.8-Jun-10 4:22 
GeneralRe: Good article BUT... Pinmemberreinux9-Jun-10 11:32 
GeneralSmall bug in RunNormalMultipleWindows() PinmemberTobias Hertkorn15-May-10 16:39 
Hi,
 
first of all thanks for your awesome work.
 
I found a small bug in private void RunNormalMultipleWindows();
 
form.FormBorderStyle = FormBorderStyle.None; is set after Location and Size are set, which results in a form that is too small on the secondary screens. Set FormBorderStyle BEFORE setting Location and Size to get a correct fullscreen form. In other words:
 
Move
form.FormBorderStyle = FormBorderStyle.None;
up a couple of lines - e.g. right after
form.Owner = primary;
 
That should do the trick.
 
Cheers,
Tobi
---
My Blog: Tobi + C# = T#

GeneralCloseOnClick PinmemberBillK127657-Feb-10 16:43 
GeneralRe: CloseOnClick PinmemberBillK127658-Feb-10 1:07 
GeneralRe: CloseOnClick Pinmemberreinux9-Feb-10 11:08 
GeneralSettings Dialog PinmemberBillK127654-Feb-10 6:45 
GeneralRe: Settings Dialog PinmemberBillK127654-Feb-10 6:50 
GeneralRe: Settings Dialog Pinmemberreinux4-Feb-10 9:34 
GeneralSingle Window vs Multiple Windows PinmemberDavid Lind14-Dec-09 7:05 
GeneralRe: Single Window vs Multiple Windows Pinmemberreinux14-Dec-09 7:24 
GeneralMemory usage [modified] PinmemberDavid Lind2-Dec-09 14:55 
GeneralRe: Memory usage Pinmemberreinux2-Dec-09 21:16 
GeneralRe: Memory usage PinmemberDavid Lind3-Dec-09 6:31 
GeneralRe: Memory usage Pinmemberreinux3-Dec-09 21:24 
GeneralCreated a screen saver that wipes out Tortoise SVN authentication data Pinmemberbrunzefb5-Jun-09 8:34 
GeneralRe: Created a screen saver that wipes out Tortoise SVN authentication data Pinmemberreinux29-Aug-09 16:41 
Generalblack bar? [modified] Pinmemberdextrous112-Sep-08 8:55 
GeneralRe: black bar? Pinmemberreinux12-Sep-08 11:37 
GeneralRe: black bar? Pinmemberdextrous112-Sep-08 12:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 24 May 2006
Article Copyright 2006 by Rei Miyasaka
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid