Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Implementing Screen Savers in .NET


May04: Implementing Screen Savers in .NET

Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at [email protected].


The first Windows program I ever wrote was a screen saver because I wanted to write some code that would let me do some graphical manipulation, and a screen saver seemed the ideal project. Since then, I have written many other screen savers to exercise new facilities as they are released for Windows. With this in mind, when .NET was first released, I took that as an opportunity to write yet another screen saver.

.NET has a library called "Windows Forms" that provides basic windowing features. In terms of windowing, there really isn't much new in Windows Forms. Sure, there are some new controls, and it makes existing controls easier to use, but it has the same look and feel as Visual Basic Forms and is based on Win32 Windows messages, handles, and device contexts. If you are comfortable with Visual Basic, you'll be comfortable with Windows Forms. However, I came to .NET as a C++ Win32 programmer, so Windows Forms was alien to me, especially since it was not obvious how to do many of the things that came as second nature to me as a C++ programmer. In this article, I describe the screen-saver library I developed to do most of the work of a screen saver—all you have to provide is the graphics code.

Windows Screen Savers

Typically when you create Win32 screen savers, you link to scrnsave.lib. The library does much of the work that your screen saver should do and ensures that the window is created in the right style. My first task was to reverse engineer this library using DUMPBIN. Table 1 lists the basic properties of a screen saver gleaned from this library. Notice that screen savers can be started in one of three distinct modes:

  • Full-Screen Mode. This is when the screen saver is started by the system and it covers the entire screen. The screen-saver process runs until the mouse is moved or a key is pressed
  • Preview Mode. The user starts the screen saver using the Screen Saver tab of the Display Properties dialog. When the Screen Saver tab is shown, the current screen saver covers the small preview window in the center of the tab, as in Figure 1. The process runs until the user moves to another tab or closes the dialog.

  • Configuration Mode. The user has clicked on the Settings button on the Screen Saver.

The screen-saver process is informed about the mode through the command line passed to the process; see Table 2. I decided that I should write a .NET library that can be used to do most of the work of a screen saver using a Windows Forms class. Users of the library only have to derive from this class and provide the drawing routine. One of the first problems to solve was keeping check of the instance that is run. When the Screen Saver tab is shown, the dialog runs the screen-saver process with /p, but when users click the Preview button, another instance of the screen saver is started full screen with /s. When the full-screen mode ends, the Screen Saver tab is shown again and another instance of the process is started with /p. I had to find a way of identifying other instances of the process, then telling those instances to shut down.

Forms and Windows

The System.Windows.Forms.Control class, at first glance, appears to let you create an object based upon an arbitrary windows handle through the Control.FromHandle method. However, if you try to pass the handle of a window that was not created by a Windows Forms process, this method fails—even if the window was created by a Windows Forms process, the method may still fail. The reason lies in the way that Windows Forms implements its event-based message handling scheme.

Win32 processes get a message from the user interface thread's message queue with a call to GetMessage. This message is then handled by the appropriate message handler procedure (the windows procedure) by passing information about the message to DispatchMessage. This information includes the handle of the window and DispatchMessage uses this handle to get the windows class, and from the class it can obtain the address of the windows procedure for the window type. Thus, in Win32 the windows class is very important.

In contrast, the Control class shows no outward appearance that a windows class has been registered, nor do you see the GetMessage/DispatchMessage message pump. Closer inspection suggests that the call to Application.Run that all graphical .NET applications make implements the message pump. The Control class, and its child class Form, handle messages through events: Your code provides an event handler for each of the messages that you want handled. Under the covers, Control does register a windows class. This does not occur when an instance is created, so when a class's constructor is called, there is no window. The first time that a Control object is told to show itself (Control.Visible set to True, which occurs when the form object is passed to Application.Run), a call is made to NativeWindow.CreateHandle. This method registers a windows class and creates the window.

CreateHandle does this through a WindowClass nested class and, specifically, the RegisterClass method. DispatchMessage calls the windows procedure, and so it expects to receive a function pointer to a native function. The WindowClass.RegisterClass method passes a structure to the Win32 RegisterClass function and the managed signature of this structure has a delegate called WndProc for the windows procedure member. This is important because, when the managed structure is marshaled to the unmanaged world, .NET automatically marshals the delegate as a thunk to the managed method contained in the delegate. The thunk is native code that performs the conversion from the unmanaged context to the managed context and, in this way, DispatchMessage gets access to what appears to be a native function pointer.

The important part of the managed RegisterClass method is that, usually, only a single windows class is registered and its name takes the form:

<windowsformsversion>.<windowstyle>.

app<hexappdomainhash>

Here, <windowsformsversion> is the version of the library and is a fixed string "WindowsForms10." <windowstyle> is in the form of Window.<hexstyle>, where <hexstyle> is the style of the window in hex. Finally, <hexappdomainhash> is the hash code of the current app domain given in hex. It is not clear how the hash code for an AppDomain is calculated but it appears to return the same value of 2 for the first application domain created in a process. This means that windows with the same style can have the same class name regardless of the process that created the window. Once the windows class is registered, the window is created with a call to the Win32 CreateWindowEx, the Control object is then added to a table indexed by its Windows handle (HWND). Control.FromHandle only returns values from this table because it guarantees that the windows will have been created in the current process.

My initial idea to ensure that only one instance of the screen-saver process runs was to use the Win32 FindWindowEx to search for windows with the class name of the screen saver's window, but the earlier discussion suggests that I would get back windows created in other .NET applications. Then I thought that I could use some identification string stored, for example, in the Form.Tag property, but I would only be able to create a Form from a HWND if the window was created in the current process. So, instead, I used a different approach.

.NET Process Class

The System.Diagnostics.Process class gives access to information about all processes running on the current machine through the static GetProcesses method. This returns an array of Process objects and each one has properties identifying the process. Listing One, my first attempt, iterates through all the processes and checks the full path of the main module of each process to see if it is the same as the full path to the main module of the current process. If this path is the same, then either the process is another instance or it is the running process. So that I don't attempt to close the running process, I check the process ID. In this code, the Win32 class is a class that I use to import Win32 functions through platform invoke, and in this listing I use it to send the WM_CLOSE message using the SendMessage function.

However, this routine fails when the process is either the System process or the Idle process because these are special "processes"—the Idle process is not a process at all, and the System process refers to several processes running in kernel mode. Attempting to get the MainModule for either of these throws an exception. Consequently, I use the altered routine in Listing Two, which performs an initial filtering based on the name of the process. Whenever the screen saver is run, this routine is called to make sure that the running instance is the only instance of the screen-saver process.

The Display Properties Dialog

A screen saver is chosen and configured through the Display Properties dialog; see Figure 1. This dialog shows a preview of the selected screen saver in a small window in the center. My first attempt was to place my window over the dialog's preview window, but this failed because if the user moved the dialog, the window would remain in the same place. Then I wrote some code to track the position of the preview window and moved my window appropriately, but then I got into difficulties determining the Z order of my window and rejected the whole idea.

As mentioned, Windows Forms is a veneer over Win32 windowing. Windows Forms uses device contexts to draw on a window, and it wraps a device context in a class called System.Drawing.Graphics. An instance of this class can be created from a call to the static method Graphics.FromHwnd and it does not matter if the window is in another process. Thus, I can draw on the device context of the preview window by passing the value passed by the /p switch to FromHwnd. I still have a Form window, which I initialize with the properties of the preview window so that these are available to the developer; however, I have to make sure that this Form window is not visible, otherwise it appears as an unpainted window on the screen.

To let the screen saver be animated, I create a timer to fire repeatedly with the intention of calling the user-defined paint routine. However, this can cause a flicker, so I remedy this with double buffering by creating a separate buffer based on the Graphics object created from the preview window, and passing this buffer to the painting routine.

However, if users click on another tab or close the dialog, the screen-saver process is not informed about this and continues to run. To detect this, I create a second timer to fire repeatedly and when this happens, I test to see if the preview window still exists (by calling the Win32 function IsWindow). If the window no longer exists, it means that the dialog has closed. If the user has clicked on another tab, the preview window will still exist, but it will be invisible, and I test this by calling the Win32 GetWindowInfo and test the result. In both of these cases (the preview window is invisible or no longer exists), I close the screen-saver process because the next time users show the Screen Saver tab, a new instance of the process is started by the system.

Configuration

Many screen savers have configuration settings. You alter these settings by clicking on the Settings button on the Display Properties dialog and the dialog starts the screen-saver process with the /c switch. In my screen-saver library, I obtain the configuration dialog by calling the virtual method GetConfigForm—your screen-saver class should override this method appropriately. This dialog is responsible for obtaining configuration settings when it is created and to save the settings when the dialog is closed and the user has clicked on the OK button.

There are several ways to store configuration data: For example, you could save it in the registry or in a file. The registry is a good place if the changes must be read by a process that is already running because the registry supports read and write access by multiple threads. You do not need simultaneous read and write access with my screen-saver library because only one instance of the screen-saver process can run at any time. Since my screen-saver process is a .NET process, I could use the .NET configuration file; however, I decided not to use this because in the current version of .NET, there is no supported mechanism to write to a configuration file. I also wanted to provide a generic way to read and write settings and if I used the configuration file, the screen saver would have to make multiple reads from the file. Instead I decided to write a class that is intended to be serialized.

Listing Three shows this base class. Your screen saver derives from this class and adds fields to the derived class for each of the settings you want to save. When you call Serialize, the code serializes the whole object using the standard SOAP formatter. This is then saved as an XML file in the user's local application data folder. The Deserialize method initializes the object's fields using the data in the XML file. Because the formatter Deserialize method creates a new object, the ConfigurationBase.Deserialize method uses reflection to access all of the serialized fields on the current object and initializes them with the same named fields on the deserialized object.

Writing Your Own Screen Saver

The source code for the library (available electronically; see "Resource Center," page 5) documents the code that you should write. There are also examples illustrating how to write your own screen saver.

Once you have compiled the screen-saver library, writing your own screen saver is straightforward. Create a Windows Application in Visual Studio.NET, add the screen-saver library to the project references, and then derive the project's main form from the Screensaver class. The screen saver should be installed in the %systemroot% or %systemroot%\system32 folder and should have an extension of .scr. To achieve this with a C# project in VS.NET, I add a post build event:

copy /y $(TargetPath) %systemroot%\$

(TargetName).scr

The screensaver.dll library should also be copied to this folder. In the example project, I achieve this with a similar post build event. If you do not add the screen-saver library as a project to the solution, you can copy the library from the output folder where it is automatically added by the build process. The additional line that you should add to the post build event should look like this:

copy /y $(TargetDir)\screensaver.dll

%systemroot%

I also change the wizard-generated file so that I have a two-stage construction, as in Listing Four. The important point here is that the same process should show the configuration dialog, show the window full screen, or show it in preview mode. The inherited InitScreenSaver method determines the mode and shows the configuration dialog if required. This method returns a Boolean, which is True if the process requires a message pump. At this point, the screen saver must read its configuration before starting and this is the purpose of the derived class method, Initialize, called in Listing Four.

In the derived class in Listing Five, the class must call the base class constructor, so that in the full-screen mode the window has the correct style. It also must implement a PaintHandler method, which draws the screen saver. The Graphics object passed to this method is the device context where the drawing should occur, which is either the full screen, or the preview window. Double buffering is enabled by default, but if you do not want this (for example, you only want to update part of the device context on each call to PaintHandler), you can set ScreenSaver.DoubleBuffering to False. In this example, I supply a configuration dialog, so I have to provide a GetConfigForm method that returns an instance of this class.

DDJ

Listing One

Process[] processes = Process.GetProcesses();
// Get the name of the file of this process
string thisProcess = Process.GetCurrentProcess().MainModule.FileName;
foreach (Process process in processes)
{
   // Check the file name of the processes main module
   if (thisProcess.CompareTo(process.MainModule.FileName) != 0) 
      continue;
   // Found an instance of the process
   if (Process.GetCurrentProcess().Id == process.Id) 
   {
      // We don't want to commit suicide
      continue;
   }
   // Tell the other instance to die
   Win32.SendMessage(
      process.MainWindowHandle, Win32.WM_CLOSE, 0, 0);
}

Back to Article

Listing Two

Process[] processes = Process.GetProcesses();
string thisProcess = Process.GetCurrentProcess().MainModule.FileName;
string thisProcessName = Process.GetCurrentProcess().ProcessName;
foreach (Process process in processes)
{
   // Compare process name, this will weed out most processes
   if (thisProcessName.CompareTo(process.ProcessName) != 0) continue;
   // Check the file name of the processes main module
   if (thisProcess.CompareTo(process.MainModule.FileName) != 0) continue;
   if (Process.GetCurrentProcess().Id == process.Id) 
   {
     // We don't want to commit suicide
     continue;
   }
   // Tell the other instance to die
   Win32.SendMessage(process.MainWindowHandle, Win32.WM_CLOSE, 0, 0);
}

Back to Article

Listing Three

[Serializable]
public abstract class ConfigurationBase
{
   public ConfigurationBase(string name);
   // Call this on the derived object to write the config data to disk
   public void Serialize();
   // Call this on the derived object to read the config data into the
   // derived object
   public void Deserialize();
}

Back to Article

Listing Four

[STAThread]
static void Main() 
{
   Saver saver = new Saver();
   // Test to see if the screen saver is full screen, preview, or started for
   // the config or password dialogs
   bool animate = saver.InitScreenSaver();
   // Preview or full screen
   if (animate)
   {
      saver.Initialize();
      Application.Run(saver);
   }
}

Back to Article

Listing Five

public class Saver : ScreenSaver
{
   public Saver() : base();
   protected void Initialize();
   protected override void PaintHandler(Graphics g);
   protected override Form GetConfigForm();
}







Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.