Create a Screen Saver Using C# – Part 2

by Dave

in Articles, Operating System, Windows Forms

Create a Screen SaverIn this article – part two of creating a screen saver in C#, we are going to pick up from where we left off in part one, which I suggest you read before continuing with this article if you haven’t already done so.

So far (in part one) we have created a Settings class and a form where the user can edit the screen saver settings. The next part is creating the actual screen saver form.

The Screen Saver Form

The screen saver form has the following custom properties set:

  • this.BackColor = System.Drawing.Color.Black;
  • this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
  • this.Name = "ScreenSaverForm";
  • this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
  • this.WindowState = System.Windows.Forms.FormWindowState.Maximized;

The form also has a System.Windows.Forms.Timer control placed on it which will be used as the interval between drawing each shape.

Form Mouse Events

The form must also handle mouse down and mouse move events so that when the mouse is either moved or clicked the screen saver will exit. Below is the code for these events plus the declarations we will be needing for the screen saver:

// Graphics object to use for drawing
private Graphics graphics;

// Random object for randomizing drawings
private Random random;

// Settings object which contains the screensaver settings
private Settings settings;

// Flag used to for initially setting mouseMove location
private bool active;

// Used to determine if the Mouse has actually been moved
private Point mouseLocation;

// Used to indicate if screensaver is in Preview Mode
private bool previewMode = false;

private void ScreenSaverForm_MouseMove(object sender, MouseEventArgs e)
{
    if (!this.previewMode)   
    {
        // If the mouseLocation still points to 0,0, move it to its actual 
        // location and save the location. Otherwise, check to see if the user
        // has moved the mouse at least 10 pixels.
        if (!this.active)
        {
            this.mouseLocation = new Point(e.X, e.Y);
            this.active = true;
        }
        else
        {
            if ((Math.Abs(e.X - this.mouseLocation.X) > 10) ||
                (Math.Abs(e.Y - this.mouseLocation.Y) > 10))
            {
                // Exit the screensaver
                Application.Exit();
            }
        }
    }
}

private void ScreenSaverForm_MouseDown(object sender, MouseEventArgs e)
{
    if (!this.previewMode) 
    {
        // Exit the screensaver if not in preview mode
        Application.Exit();
    }
}

As you can see in both event handlers we are checking that the screen saver is not in preview mode before exiting. Preview mode is the small window where a screen saver is previewed on the Windows Screen Saver Settings dialog. Obviously, we do not want to close our screen saver application when the user moves the mouse while in preview mode, so that is why we are performing this check.

The ScreenSaverForm_MouseMove event handler is a little more complex. We are only closing the screen saver if the user has moved the mouse more than 10 pixels. This way we won’t exit just because the mouse sensed a one pixel move, maybe because of imperfections in the mouse surface or because of a faulty mouse (don’t you just hate faulty mice which wobble on screen when you’re not even touching them! :)).

The Form_Load Event

In the ScreenSaverForm_Load event we need to initialise our screen saver variables and also load the settings from the Settings class.

private void ScreenSaverForm_Load(object sender, EventArgs e)
{
    try
    {
        // Initialise private members
        this.random = new Random();
        this.settings = new Settings();
        this.settings.LoadSettings();
        this.active = false;

        // Hide the cursor
        Cursor.Hide();

        // Create the Graphics object to use when drawing.
        this.graphics = this.CreateGraphics();

        // Set the draw speed from the settings file
        switch (this.settings.DrawSpeed)
        {
            case Settings.Speed.Custom:
                tmrMain.Interval = this.settings.CustomSpeed;
                break;
            case Settings.Speed.Fast:
                tmrMain.Interval = 100;
                break;
            case Settings.Speed.Normal:
                tmrMain.Interval = 200;
                break;
            case Settings.Speed.Slow:
                tmrMain.Interval = 500;
                break;
        }

        // Enable the timer.
        tmrMain.Enabled = true;
    }
    catch (Exception ex)
    {
        MessageBox.Show(string.Format("Error loading screen saver! {0}", ex.Message), "Dave on C# Screen Saver", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

The Draw Method

The meat of this project is the method which draws the shapes on screen. We will be calling this method DrawShape(). To draw shapes we will be making use of Window’s Graphics Device Interface +, or GDI+ in short. GDI+ offers many cool features such as drawing with gradients, adding anti-aliasing, working with JPEG and PNG files, etc, but for the purpose of this example we only need to scratch the surface of GDI+.

The .NET Framework wraps GDI+ in a nice, easy to use, class which is called System.Drawing.Graphics. This Graphics class contains methods such as FillRectangle and FillEllipse which we will be using to draw our shapes. You can see how they are used within the DrawShape() method below:

private void DrawShape()
{
    try
    {
        // Rectangle object for painting shapes in
        Rectangle rect;

        // Color object used for colouring the shapes
        Color colour;

        // Generate random coordinates for drawing shapes
        int x1 = random.Next(0, this.Width);
        int x2 = random.Next(0, this.Width);
        int y1 = random.Next(0, this.Height);
        int y2 = random.Next(0, this.Height);

        // Get smaller coordinates
        int x = Math.Min(x1, x2);
        int y = Math.Min(y1, y2);

        // Get smallest dimension for use when drawing in rectangles of equal sides
        int smallerSide = Math.Min(Math.Abs(x1 - x2), Math.Abs(y1 - y2));

        // Generate a random shape to display when user selects All shapes
        int allId = random.Next(0, 4);

        if (this.settings.UseTransparency)
        {
            // Generate a random colour with a random level of transparency
            colour = Color.FromArgb(this.random.Next(255),
                                    this.random.Next(255),
                                    this.random.Next(255),
                                    this.random.Next(255));
        }
        else
        {
            // Generate a random colour with no transparency
            colour = Color.FromArgb(255,
                                    this.random.Next(255),
                                    this.random.Next(255),
                                    this.random.Next(255));
        }

        // Create a coloured brush
        SolidBrush brush = new SolidBrush(colour);

        // Display shapes according to settings
        switch (this.settings.AllowedShapes)
        {
            case Settings.Shapes.Circles:
                // Draw a circle
                rect = new Rectangle(x, y, smallerSide, smallerSide);
                graphics.FillEllipse(brush, rect);
                break;

            case Settings.Shapes.Ellipses:
                // Draw an ellipse
                rect = new Rectangle(x, y, Math.Abs(x1 - x2), Math.Abs(y1 - y2));
                graphics.FillEllipse(brush, rect);
                break;

            case Settings.Shapes.Rectangles:
                // Draw a rectangle
                rect = new Rectangle(x, y, Math.Abs(x1 - x2), Math.Abs(y1 - y2));
                graphics.FillRectangle(brush, rect);
                break;

            case Settings.Shapes.Squares:
                // Draw a square
                rect = new Rectangle(x, y, smallerSide, smallerSide);
                graphics.FillRectangle(brush, rect);
                break;

            case Settings.Shapes.All:
                // Draw all the different type of shapes
                if (allId == 0)
                {
                    // Draw a circle
                    rect = new Rectangle(x, y, smallerSide, smallerSide);
                    graphics.FillEllipse(brush, rect);
                }
                else if (allId == 1)
                {
                    // Draw an ellipse
                    rect = new Rectangle(x, y, Math.Abs(x1 - x2), Math.Abs(y1 - y2));
                    graphics.FillEllipse(brush, rect);
                }
                else if (allId == 2)
                {
                    // Draw a rectangle
                    rect = new Rectangle(x, y, Math.Abs(x1 - x2), Math.Abs(y1 - y2));
                    graphics.FillRectangle(brush, rect);
                }
                else
                {
                    // Draw a square
                    rect = new Rectangle(x, y, smallerSide, smallerSide);
                    graphics.FillRectangle(brush, rect);
                }

                break;
        }
    }
    catch
    {}
}

The comments in the code clearly explain what is being done. As you can see extensive use of the Random class is being made because we are randomly generating the size of the shapes, the position of the shapes, the colour and also the level of transparency if UseTransparency is enabled.

Previewing the Screen Saver

For the screen saver to run in the Windows Screen Saver Settings preview window we need to add some special code.

First of all we need to add some declarations to our form which are not normal .NET variables/classes, but they are actually calls to Window’s dlls – specifically the user32.dll.

// Changes the parent window of the specified child window
[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

// Changes an attribute of the specified window
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

// Retrieves information about the specified window
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

// Retrieves the coordinates of a window's client area
[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, out Rectangle lpRect);

Then we must overload the screen saver form’s constructor so that we can receive the handle to the Windows Screen Saver Settings preview window. This will be given to us by Windows and I’ll explain it in more detail soon. The overloaded constructor looks like this:

public ScreenSaverForm(IntPtr previewHandle)
{
    InitializeComponent();

    // Set the preview window of the screen saver selection 
    // dialog in Windows as the parent of this form.
    SetParent(this.Handle, previewHandle);

    // Set this form to a child form, so that when the screen saver selection 
    // dialog in Windows is closed, this form will also close.
    SetWindowLong(this.Handle, -16, new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000));

    // Set the size of the screen saver to the size of the screen saver 
    // preview window in the screen saver selection dialog in Windows.
    Rectangle ParentRect;
    GetClientRect(previewHandle, out ParentRect);
    this.Size = ParentRect.Size;

    this.Location = new Point(0, 0);

    this.previewMode = true;
}

In the above code we are receiving the handle as a parameter and passing it to the SetParent Windows method. That method is changing our screen saver’s parent window so that the screen saver will display in the preview window. The SetWindowLong and GetWindowLong Windows methods are being used to inform Windows to close our screen saver application if the user selects to close the Windows Screen Saver Settings dialog. If we do not add this code we can create a memory leak because our application will still be running in the background. The GetClientRect Windows method sets the size of our screen saver to fit the preview window. Then finally we are initialising the location of the form and setting the previewMode flag to true.

That all might seem pretty complicated but it’s not that bad if you take it a step at a time and understand what each call does.

So that’s basically it for our screen saver form. Now to put everything together.

Putting It All Together

To tie it all up we must edit our static void Main method which is found in the static class Program. But first you need to understand how Windows communicates with a screen saver.

Windows will control a screen saver by sending it one of three command line arguments. It will either send a “/p”, a “/s”, or a “/c”.

“/p” means the screen saver should run in preview mode.
“/s” means the screen saver should run normally – it stands for show screen saver.
“/c” means display the screen saver’s configuration – in our case the settings form we created in part one of this article.

So what we need to do is to check which parameter Windows passed us and then show the appropriate form. The code would look like this:

[STAThread]
static void Main(string[] args)
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // Check for any passed arguments
    if (args.Length > 0)
    {
        switch (args[0].ToLower().Trim().Substring(0, 2))
        {
            // Preview the screen saver
            case "/p": 
                // args[1] is the handle to the preview window
                ScreenSaverForm screenSaverForm = new ScreenSaverForm(new IntPtr(long.Parse(args[1])));
                screenSaverForm.ShowDialog();
                break;

            // Show the screen saver
            case "/s": 
                RunScreensaver();
                break;

            // Configure the screesaver's settings
            case "/c": 
                // Show the settings form
                SettingsForm settingsForm = new SettingsForm();
                settingsForm.ShowDialog();
                break;

            // Show the screen saver
            default: 
                RunScreensaver();
                break;
        }
    }
    else
    {
        // No arguments were passed so we show the screensaver anyway
        RunScreensaver();
    }
}

private static void RunScreensaver()
{
    ScreenSaverForm screenSaverForm = new ScreenSaverForm();
    screenSaverForm.ShowDialog();
}

As you can see when Windows sends us /p we are constructing the ScreenSaverForm using the overloaded constructor since we are sending the preview window handle as a parameter. The preview window handle was also given to us by Windows as a command line parameter, so all we had to do with it was pass it on.

When Windows sends us /s we are just calling the RunScreensaver method which calls the default constructor of our ScreenSaverForm.

And finally when Windows sends us /c we are launching the SettingsForm dialog.

And that’s it. Our screen saver is complete.

Installing the Screen Saver

When you compile the project you will end up with an exe file. To make that exe a screen saver just rename the exe extension to scr and you’re done.

Then to install it just copy the scr file to the Windows\System32 directory and then open the Windows Screen Saver Settings dialog and you should find our screen saver listed in the drop down list box. :)

I hope you enjoyed this two part article series. Please feel free to leave your comments below or maybe you want to share your screen saver designs by linking to them in a comment. Either way please leave me your feedback below.

You can download the source code for this screen saver at the end of the article but remember it requires Microsoft Visual Studio 2008, although you can easily copy the code to an older version of Visual Studio.

Stay tuned for more articles soon.

Dave

Download sourceDownload Screen Saver source – 21.9 KB

Screenshots

Squares Circles
Rectangles Ellipses
All All - No Transparency


{ 6 comments… read them below or add one }

Tom March 30, 2011 at 16:47

Dave,

This is a very good article with detailed comments. I am however having a problem with the preview. In VS 2010 when I put in /p I get the following error? What am I missing?

Thanks,
Tom

System.IndexOutOfRangeException was unhandled
  Message="Index was outside the bounds of the array."
  Source="Dave on C# Screensaver"
  StackTrace:
       at ScreenSaver.Program.Main(String[] args) in C:\Users\Name\Desktop\ss\Screensaver\Screensaver\Program.cs:line 27
       at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException:

Reply

Eric August 2, 2012 at 05:08

Thanks for the article! Very easy to follow.

I ran into one problem, however: the mouse move and mouse click events are not firing. I added a key down event as a workaround.

The form contains panels, which contain split containers and table layouts, which contain picture boxes. I tried added the events to the panels and picture boxes, but they are still not firing. The form’s topmost property is set to true, and the cursor is hidden, but reversing these properties doesn’t make a difference either. Any ideas???

Reply

Eric August 2, 2012 at 05:39

Another issue that I ran into: I added the call to SetWindowLong, but that app does not close when the screen saver settings dialog is closed, i.e. memory leak. I am running on Win7 x86

Reply

Eric August 2, 2012 at 06:25

Strange – the FormClosed event was firing, even though the app wasn’t closing. Adding ‘Application.Exit()’ to this event solved the problem. Something else that I don’t understand is that adding a call to MessageBox.Show() in this event also caused the app to close.

Reply

Daniel November 23, 2012 at 07:23

Great tutorial Dave,
I’d like to use this as the starting point for a side-project: screensaver that reports project dashboard information. You have a source download, so I hope this is ok, but I couldn’t find any license terms. Would you consider a BSD style license?

Thanks!

Reply

Dave November 23, 2012 at 10:42

Hi Daniel,

The code is licensed under a BSD license so you’re free to use it as you please. I’ve updated the website to include source code license information since I didn’t have this section before – Source Code Licensing.

Cheers,
Dave

Reply

Leave a Comment

Previous post:

Next post: