Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Win32
Article

Making a C# screensaver

Rate me:
Please Sign up or sign in to vote.
4.84/5 (37 votes)
30 Nov 2008CPOL10 min read 191.6K   4.8K   104   24
Multiple monitor support, preview, and more.

Introduction

First, let me clear one thing up. A screensaver is nothing more than a standard Windows executable with the .scr extension instead of the standard .exe extension. It has the same file format as an EXE, and of course, you can make a .NET screensaver. You could take any old application you have on your computer, and give it a .scr extension, and you would have a perfectly valid Windows screensaver. However, you probably wouldn't want to do that. That's because screensavers do have certain responsibilities:

When Windows runs a screensaver, it will pass certain arguments to it. These arguments are "/s" (show the screensaver), "/p" (preview the screensaver in the small little monitor on the screensaver selection dialog), and "/c" (show the screensaver's options, or configure it). Windows expects your application to read these arguments in your screensaver's Main method, and act on them accordingly.

A screensaver usually takes up the whole screen. Thus, you must have some sort of exit signal built into the screensaver. Traditionally, a screensaver will exit if a key is pressed, or if the mouse is moved.

A screensaver also will stay on top of all other windows, and it will hide the cursor.

Reading the arguments

The only place you can read the arguments in a C# application is in the automatically generated Main() method. Go to the Solution Explorer. There should be a code file called "Program.cs". Open it up in the code editor. It might look like this:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace YourApplicationNamespace
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new YourApplicationMainForm());
        }
    }
}

All we are concerned with in that is the Main method:

C#
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new YourApplicationMainForm());
}

By default, the Main() method will take no arguments. You must edit it so it will take an argument of "string[] args"; that way, you will be able to read the arguments passed to your application:

C#
static void Main(string[] args)
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new YourApplicationMainForm());
}

The args parameter will now hold all the arguments passed to your application. Now, you must interpret those arguments. Only one argument should be passed to a screensaver, the argument that tells the screensaver what Windows wants it to do. We could read the arguments easily with a simple if statement, like so:

C#
if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
{
    //show the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
    //preview the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
    //configure the screen saver
}

However, it is possible for no arguments to be passed to a screensaver. Thus, we should probably add another if statement:

C#
if (args.Length > 0)
{
    if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
    {
        //show the screen saver
    }
    else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
    {
        //preview the screen saver
    }
    else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
    {
        //configure the screen saver
    }
}
else
{
    //no arguments were passed (we should probably show the screen saver)
}

That way, you won't get an index out of range exception if you try to reference the first argument passed and there is nothing there.

At this stage, we must modify our Main method to look like this:

C#
static void Main(string[] args)
{
    if (args.Length > 0)
    {
        if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
        {
            //show the screen saver
        }
        else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
        {
            //preview the screen saver
        }
        else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
        {
        //configure the screen saver
        }
    }
    else
    {
        //no arguments were passed (we should probably show the screen saver)
    }
}

Setting up the screensaver

At this point, if we try to run our application, it would run through like a console application with no code, because we never invoke Application.Run() anywhere in our Main. After we do a few things here, that will be taken care of. Now, I have made several screensavers in C# that support multiple monitors, and I will explain to you the structure I use to make them. I'm not saying my way is the best, but it works, and it's organized. To have multiple monitor support, instead of simply making a main form and stretching it to fill up all the screens, I make the screensaver so it will show a different main form on every screen (by the way, whenever I refer to the main form in the code, I will call it "MainForm"). To do this, the System.Windows.Forms.Screen class comes in handy. Take a look at this code:

C#
foreach (Screen screen in Screen.AllScreens)
{
    MainForm screensaver = new MainForm(screen.Bounds);
    screensaver.Show();
}

The foreach loop you see is looping through all the screens (monitors) on the current computer. I then create a new main form for that monitor, passing it the bounds of that screen, and showing it. But, wait a minute, that will not work because there is no constructor method in our main form that takes an argument. So, let us make one. I added this exact constructor to my main form:

C#
public MainForm(Rectangle Bounds)
{
    InitializeComponent();
    this.Bounds = Bounds;
    Cursor.Hide();
}

As you can see, it takes an argument of System.Drawing.Rectangle (that's what bounds are), and in the constructor, it would set the form's bounds to the one passed in the constructor. It would also hide the cursor, because like I said, screensavers hide the cursor.

So, to run the screensaver, we could create a method like so:

C#
void ShowScreenSaver()
{
    foreach (Screen screen in Screen.AllScreens)
    {
        MainForm screensaver = new MainForm(screen.Bounds);
        screensaver.Show();
    }
}

Typically, I will make a method like that to show the screensaver, and put that in the class my main method is in (Program). So, your Program class would now look like:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace YourApplicationNamespace
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
                {
                    //show the screen saver
                }
                else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
                {
                    //preview the screen saver
                }
                else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
                {
                    //configure the screen saver
                }
            }
            else
            {
                //no arguments were passed (we should probably show the screen saver)
            }
        }

        static void ShowScreenSaver()
        {
            foreach (Screen screen in Screen.AllScreens)
            {
                MainForm screensaver = new MainForm(screen.Bounds);
                screensaver.Show();
            }
        }

    }
}

(Note: we must make the ShowScreenSaver() method static when we put it in the Program class, as it is a static class.)

Now, let's modify our Main() method so it will actually use that method if our application was passed /s, or if it was passed nothing.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace YourApplicationNamespace
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
                {
                    //show the screen saver
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    ShowScreenSaver(); //this is the good stuff
                    Application.Run();
                }
                else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
                {
                    //preview the screen saver
                }
                else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
                {
                    //configure the screen saver
                }
            }
            else
            {
                //no arguments were passed (we should probably show the screen saver)
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                ShowScreenSaver(); //this is the good stuff
                Application.Run();
            }
        }

        static void ShowScreenSaver()
        {
            foreach (Screen screen in Screen.AllScreens)
            {
                MainForm screensaver = new MainForm(screen.Bounds);
                screensaver.Show();
            }
        }

    }
}

Guess what, we now have a working screensaver. Working, but not complete. We still need to make our main() method allow for /p and /c. We will now set it up for a /c (configure) argument as that is quite easy. Do you want the user to be able to choose some of the options in the screensaver? If so, simply create a form where the user can configure the options, and have your application so it will store those options in the application's settings, an ini file, the Registry, or wherever. Now, we only need to modify the part of our main method that handles /c:

C#
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
    //configure the screen saver
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new ConfigureForm()); //this is the good stuff
}

(In this code, I referred to the configuration form as "ConfigureForm", but that can be changed.) Notice, I am using an Application.Run() call to start the application, and all I am doing is pass it the configuration form as the application's main form.

On the other hand, if you don't have any options the user can set, we can do something very easy, like so:

C#
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
    //nothing to configure
    MessageBox.Show("This screensaver has no options that you can set",
        "My Cool Screen Saver",
        MessageBoxButtons.OK,
        MessageBoxIcon.Information);
}

That will show a message telling the user the screensaver has no configurable options. Then, since we never invoked Appliction.Run(), the application will exit once the main method has run through, like a console application.

Now, we can handle the /p (preview) argument. The preview is when Windows wants your screensaver to preview itself in the little computer monitor on the screensaver dialog. There are two ways we can go about this. Generally, I choose to screw the preview all together. By the way, if you don't want a preview, you are done. You don't need any code in the section that handles the preview, so when Windows calls your application to preview, it will start, run through, and since the Application.Run() was never called, it will close in an inconceivably small amount of time like it never even ran. That was easy. However, if you so desire for your screensaver to have a preview, I will teach you how to do it.

Setting up the preview

This is a bit tricky. It takes a few APIs to do it. Windows will not automatically do some voodoo magic to make your application show perfectly in the preview window, you have to do it on your own. It does, however, lend you some help. When Windows passes your application the /p command, it will also pass another argument: the handle to the preview window on the Select screensaver dialog. With that, we can use a few APIs to make it the parent of our screensaver's main form. This is how it will work:

  1. The main function will see the first argument is /p. It then knows the second argument is the handle to the preview window.
  2. The main function will run the application with MainForm as the main form using Application.Run(). It will also pass the main form the handle to the preview window.
  3. The main form will configure itself so that it will show inside the preview window.
  4. They all live happily ever after.

Let's start to implement that. First, let me refresh your (and my) mind of what the section of our main routine that handles /p looks like:

C#
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
    //preview the screen saver
}

As it is now, it is set up in a way where there will be no preview since there is no code except for that lonely little comment. We must modify that to look like this:

C#
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
    //show the screen saver preview
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    //args[1] is the handle to the preview window
    Application.Run(new MainForm(new IntPtr(long.Parse(args[1]))));
}

Looks like that comment's not so lonely anymore. Anyway, that should be all you need to do to the Main function. However, see the line:

C#
Application.Run(new MainForm(new IntPtr(long.Parse(args[1]))));

That is passing MainForm the handle to the preview window in the form of an IntPtr. However, we have no constructor made for that form that will accept an IntPtr. Well, time to make one. Guess what, you don't have to do any work again because all you have to do is use this code I've already written:

C#
//This constructor is the handle to the select screen saver dialog preview window
//It is used when in preview mode (/p)
public MainForm(IntPtr PreviewHandle)
{
    InitializeComponent();

    //set the preview window as the parent of this window
    SetParent(this.Handle, PreviewHandle);

    //make this a child window, so when the select screensaver 
    //dialog closes, this will also close
    SetWindowLong(this.Handle, -16, 
          new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000));

    //set our window's size to the size of our window's new parent
    Rectangle ParentRect;
    GetClientRect(PreviewHandle, out ParentRect);
    this.Size = ParentRect.Size;

    //set our location at (0, 0)
    this.Location = new Point(0, 0);

    IsPreviewMode = true;
}

In case you haven't figured out, I am making a real screensaver as I am writing this tutorial. Anyway, that is exactly what you should use. But wait a minute, that is going to throw a whole bunch of errors at you if you try to use it right now. That's because there are four APIs I'm using in there that aren't in your code yet. Here they are:

C#
#region Preview API's

[DllImport("user32.dll")]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);

[DllImport("user32.dll")]
static extern bool GetClientRect(IntPtr hWnd, out Rectangle lpRect);

#endregion

I believe I did a fairly good job of explaining exactly what is going on, so I'll move on. Oh, and about that variable in the constructor I gave you, called IsPreviewMode. That is to let the application know weather or not it is in preview mode when it's running. If you are making a screensaver as you are reading this, make a global boolean called IsPreviewMode. You will need a variable like that because when you are in preview mode, you want to disable any mouse or key events that will make the screensaver exit. For example, if you had a key down event that had code in it to exit the application, you would want to put an if statement around that to make sure your application is not in preview mode. Why? When your screensaver is previewing in the preview window on the Select screensaver dialog, any time you move your mouse over the preview window, press a key, or click the preview window, you certainly don't want the preview to go away, and that is what will happen if we don't take these precautions. You can also use the IsPreviewMode variable to make it so that certain code won't run through when in preview mode, and certain code will run through in preview mode that won't run in standard mode. That can be very useful.

Putting it all together

OK, this is about what your Program.cs file should look like at this point:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace YourApplicationNamespace
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                if (args[0].ToLower().Trim().Substring(0, 2) == "/s") //show
                {
                    //run the screen saver
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    ShowScreensaver();
                    Application.Run();
                }
                else if (args[0].ToLower().Trim().Substring(0, 2) == "/p") //preview
                {
                    //show the screen saver preview
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    //args[1] is the handle to the preview window
                    Application.Run(new MainForm(new IntPtr(long.Parse(args[1]))));
                }
                else if (args[0].ToLower().Trim().Substring(0, 2) == "/c") //configure
                {
                    //Show the configuration dialog, or inform the user 
                    //there are no options with a message box here
                }
                else
                // an argument was passed, but it wasn't /s, /p,
                // or /c, so we don't care wtf it was
                {
                    //show the screen saver anyway
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    ShowScreensaver();
                    Application.Run();
                }
            }
            else //no arguments were passed
            {
                //run the screen saver
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                ShowScreensaver();
                Application.Run();
            }

        }

        //will show the screen saver
        static void ShowScreensaver()
        {
            //loops through all the computer's screens (monitors)
            foreach (Screen screen in Screen.AllScreens)
            {
                //creates a form just for that screen 
                //and passes it the bounds of that screen
                MainForm screensaver = new MainForm(screen.Bounds);
                screensaver.Show();
            }
        }
    }
}

And just to be clear, these are the two new constructors you should have in your MainForm (you will only have the top one if you chose not to do a preview):

C#
//This constructor is passed the bounds this form is to show in
//It is used when in normal mode
public Form1(Rectangle Bounds)
{
    InitializeComponent();
    this.Bounds = Bounds;
    //hide the cursor
    Cursor.Hide();
}

//This constructor is the handle to the select 
//screensaver dialog preview window
//It is used when in preview mode (/p)
public Form1(IntPtr PreviewHandle)
{
    InitializeComponent();

    //set the preview window as the parent of this window
    SetParent(this.Handle, PreviewHandle);

    //make this a child window, so when the select 
    //screensaver dialog closes, this will also close
    SetWindowLong(this.Handle, -16, 
      new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000));

    //set our window's size to the size of our window's new parent
    Rectangle ParentRect;
    GetClientRect(PreviewHandle, out ParentRect);
    this.Size = ParentRect.Size;

    //set our location at (0, 0)
    this.Location = new Point(0, 0);

    IsPreviewMode = true;
}

Now, with what we have thus far, we are pretty good to go, but there's still one more thing we need to do. We absolutely have to add in functions that will make the screensaver exit when a key is pressed, the screensaver is clicked, or if the mouse is moved significantly over the screensaver. All you really have to do is something like this:

C#
#region User Input

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    //** take this if statement out if your not doing a preview
    if (!IsPreviewMode) //disable exit functions for preview
    {
        Application.Exit();
    }
}

private void Form1_Click(object sender, EventArgs e)
{
    //** take this if statement out if your not doing a preview
    if (!IsPreviewMode) //disable exit functions for preview
    {
        Application.Exit();
    }
}

//start off OriginalLoction with an X and Y of int.MaxValue, because
//it is impossible for the cursor to be at that position. That way, we
//know if this variable has been set yet.
Point OriginalLocation = new Point(int.MaxValue, int.MaxValue);

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    //** take this if statement out if your not doing a preview
    if (!IsPreviewMode) //disable exit functions for preview
    {
        //see if originallocation has been set
        if (OriginalLocation.X == int.MaxValue & 
            OriginalLocation.Y == int.MaxValue)
        {
            OriginalLocation = e.Location;
        }
        //see if the mouse has moved more than 20 pixels 
        //in any direction. If it has, close the application.
        if (Math.Abs(e.X - OriginalLocation.X) > 20 | 
            Math.Abs(e.Y - OriginalLocation.Y) > 20)
        {
            Application.Exit();
        }
    }
}
#endregion

Now, you obviously can't just copy/paste that because you have to make those functions event handlers. Anyway, that won't be hard at all. Note that I used an Application.Exit() call instead of a this.close() because, though I call it MainForm, it really isn't a "Main Form" because if it is closed, the application will still keep on running. To fix that, we simply use the Application.Exit() call. Now, there's only one thing you have left to do: change the extension of your build application from .exe to .scr, and if you want to use it as a screensaver on your computer, copy it into the system32 directory and set it as your screensaver.

I said I was making a real screensaver as I was writing this tutorial, and it's only fair I give it to you as an example. Download it from the link at the top. The screensaver I made shows a fake, but highly authentic blue screen of death, like the BSOD screensaver on SysInternals. Anyway, that's not the point. The point is that it demonstrates everything I have said. It also has a preview.

License

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


Written By
United States United States
My real name is Jacob Jordan (no, really). I am currently 14 and have been programming in .NET for 3 years now. I am extremely skilled in both C# and VB.NET, and am learning other languages (C++, Java, and VB). I am also learning HTML and CSS, but those aren't my top priorities right now. I am NOT a nerd for the sole reason that nerds aren't cool.

Comments and Discussions

 
SuggestionHexadecimal addresses in the BSOD are always the same Pin
Polda1820-Sep-16 21:43
Polda1820-Sep-16 21:43 
BugMouse events seem not responding - no errors occured Pin
Polda1820-Sep-16 8:43
Polda1820-Sep-16 8:43 
QuestionPreview API dll's go where? Pin
Polda1819-Sep-16 11:16
Polda1819-Sep-16 11:16 
GeneralMy vote of 5 Pin
Imagiv2-Jun-16 15:13
Imagiv2-Jun-16 15:13 
QuestionCall files or pictures from a folder Pin
Member 101940194-Jul-14 18:07
Member 101940194-Jul-14 18:07 
SuggestionRe: Call files or pictures from a folder Pin
Polda1820-Sep-16 11:27
Polda1820-Sep-16 11:27 
GeneralThank you Pin
Mohib Sheth8-Aug-12 23:16
Mohib Sheth8-Aug-12 23:16 
QuestionScreen saver with custom password interface? Pin
Member 917429026-Jun-12 8:40
Member 917429026-Jun-12 8:40 
GeneralMy vote of 5 Pin
Keith Barrow9-May-12 22:05
professionalKeith Barrow9-May-12 22:05 
QuestionCool Pin
Peter Hawke13-Apr-12 15:00
Peter Hawke13-Apr-12 15:00 
GeneralWow Pin
d.castillo.messa6-Jul-09 8:30
d.castillo.messa6-Jul-09 8:30 
QuestionI have no words to describe the excellence of your explanations! But cant build from zip? Pin
Michael Lev25-Feb-09 0:06
Michael Lev25-Feb-09 0:06 
AnswerRe: I have no words to describe the excellence of your explanations! But cant build from zip? Pin
Michael Lev25-Feb-09 1:21
Michael Lev25-Feb-09 1:21 
GeneralVery Cool! But ... Pin
KinnMirr21-Jan-09 18:21
KinnMirr21-Jan-09 18:21 
GeneralRe: Very Cool! But ... Pin
jacobjordan24-Jan-09 9:45
jacobjordan24-Jan-09 9:45 
QuestionDual Screens Pin
Jaguire4-Dec-08 11:07
Jaguire4-Dec-08 11:07 
AnswerRe: Dual Screens Pin
jacobjordan6-Dec-08 16:23
jacobjordan6-Dec-08 16:23 
GeneralRe: Dual Screens Pin
jackwmc429-Jun-09 4:56
jackwmc429-Jun-09 4:56 
GeneralFix for "Dual Screens Overlay" Bug Pin
solariz22-Jul-09 11:04
solariz22-Jul-09 11:04 
GeneralMore simply... Pin
scottfe12-Dec-09 12:06
scottfe12-Dec-09 12:06 
GeneralVery nice Pin
kornakar2-Dec-08 2:00
kornakar2-Dec-08 2:00 
GeneralGreat work Pin
Claudio Nicora1-Dec-08 20:47
Claudio Nicora1-Dec-08 20:47 
GeneralCool article Pin
Sacha Barber30-Nov-08 21:58
Sacha Barber30-Nov-08 21:58 
Generalexcellent article, thanks ! Pin
BillWoodruff30-Nov-08 18:57
professionalBillWoodruff30-Nov-08 18:57 

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

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