Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Christian and James' Code Project Screensaver

0.00/5 (No votes)
16 May 2002 1  
Our attempt at a screen saver with a Code Project theme, written in C#

Summary

This article will document various aspects of the development of our Code Project screensaver. I started it when the contest was announced, it fit in perfectly with my plans to find a project to help me learn more C#. Although, on the way, I got to meet James and he became involved. This article will cover the stuff I wrote, and as James writes articles to cover his contribution, I will put links to them from here also. It's not quite as cut and dried as that though, James helped me all over the place.

Writing a Screensaver in C#

I expected the main challenge to simply be the creation of a screensaver. I quickly found an online sample (which would not compile) that simply created a topmost window in C# and responded to a command line parameter to run the screensaver. I made some changes to this and was quickly under way. The switches you need to respond to are as follows:

Switch Purpose
/p Passes a HWND for the preview
/c Show options dialog
/s Start screen saver
/a Password dialog

Additionally, James added code so that running the saver as an EXE (which, of course, it is), simply runs it. We did not get around to supporting passwords, although I doubt it would be hard. My thanks go to Neil Van Note, who wrote the code which finally made sense of the /p option, without him, we would not be providing a preview.

Multi Monitor Support

Having established that it was easy to get a screensaver up in C# at all, the next step was to make sure we supported multi monitors. I have two monitors myself, and so it was quickly apparent to me that my screensaver would not be complete if it only ran on one of them.

Windows Forms quickly came to the rescue, with a property called System.Windows.Forms.Screen. This nifty little thing gives me the PrimaryScreen, as well as an array of screens called, funny enough, AllScreens. From this, I was able to build a Rectangle which presented all the screens, as well as my own array. In order to make the bouncing Bobs pixel perfect (i.e., bouncing off the edges of each monitor, no matter what the sizes), I needed to keep track of what screen they were on, and figure out when they were about to cross an edge. The tricky bit is that the screens are stored relative to the Primary, in other words, if my monitor setup is such that my (smaller) secondary screen is to the left of my primary, its rectangle comes out as -1024, 0 to 0, 768, but drawing at 0,0 draws on the leftmost screen, not the one that reports its size as 0,0. Therefore, I needed to create an offset point in order to do my checking.

Screen [] scr = Screen.AllScreens;

rcScreens = new Rectangle[scr.Length];

rcScreen = Screen.PrimaryScreen.Bounds;
nTextPos = rcScreen.Bottom;

for (int i = 0; i < Screen.AllScreens.Length; ++ i)
{
    rcScreens[i] = scr[i].Bounds;

    if (rcScreens[i] != rcScreen)
    {
        if (rcScreens[i].Right == rcScreen.Left)
        {
            rcScreen.X = rcScreens[i].Left;
            rcScreen.Width += rcScreens[i].Width;
            rcScreen.Height = Math.Max(rcScreens[i].Bottom, rcScreen.Bottom);
        }
        else if (rcScreens[i].Left == rcScreen.Right)
        {
            rcScreen.Width += rcScreens[i].Width;
            rcScreen.Height = Math.Max(rcScreens[i].Bottom, rcScreen.Bottom);
            nTextPos = Math.Min(rcScreens[i].Bottom, rcScreen.Bottom);;
        }
        else if (rcScreens[i].Top == rcScreen.Bottom)
        {
            rcScreen.Y = rcScreens[i].Top;
            rcScreen.Height += rcScreens[i].Height;
            rcScreen.Width = Math.Max(rcScreens[i].Right, rcScreen.Right);
        }
        else if (rcScreens[i].Bottom == rcScreen.Top)
        {
            rcScreen.Height += rcScreens[i].Height;
            rcScreen.Width = Math.Max(rcScreens[i].Right, rcScreen.Right);
        }
    }
}

Actor.rcScreen = rcScreen;
Actor.rcScreens = rcScreens;
Bounds = rcScreen;

Adding Some Action

This project is an example of how things can get a little ugly if proper design is not done. It soon outgrew my original plans, and there are places where it shows. A good design should always work, if code starts being added to cover special cases, it means more design was needed.

Having said that, the system we've ended up with works pretty well. A base class, called Actor, serves simply as a common parent to two classes called BitmapActor and TextActor. I also hand wrote array classes for both, which simply wrap the ArrayList class, to avoid all that ugly casting, and provide some other methods that worked best at the container level.

BitmapActor

The common methods to both classes are Create, Move and Draw. The Create method in the Bitmap class uses the string passed in as a name to load an image from resources, and makes it transparent with a magenta color key. Draw and Move are fairly obvious. The two things that are worth mentioning are the rotating Bobs and the color keyed CPians. Because the class structure came late in the project, the Bitmap Actors do not actually use it, they are drawn in the main class. This is bad, and will be fixed, although it does not alter the behavior of the screensaver at all.

The CPian behavior is quite simple. A counter that goes from 0 to 255 and back is maintained as the screensaver runs, and this is used in conjunction with an ImageAttributes object. The code looks like this:

 if (BitmapActor.nCPian >=0 && nMode!= 3)
{        
        BitmapActor actor = (BitmapActor)arCPians[BitmapActor.nCPian];     
        ImageAttributes ia = new ImageAttributes(); ia.SetColorKey(Color.Black,
            
    Color.FromArgb(BitmapActor.nCPianAlpha, BitmapActor.nCPianAlpha, 
        BitmapActor.nCPianAlpha));
        grScreen.DrawImage(actor.actor, new Rectangle(actor.pt.X,
        actor.pt.Y, actor.actor.Width, actor.actor.Height), 0, 0, 
        actor.actor.Width, actor.actor.Height, GraphicsUnit.Pixel, ia);
    ia.Dispose();
}

Basically, this means we make a range of colors transparent, the bottom being black, and the range increasing until it covers the whole range. It's very simple to achieve, and I hope you agree that it looks very cool indeed.

The rotating Bobs are also very simple - they simply use a DrawImage method that specifies the rectangle to draw into, and shrink it gradually, then flip Bob as they expand again. The trick was to use the same code in the Move method, so that the Bobs only bounce when they touch the sides, not just when their maximum rectangle does.

for (int i = 0; i < arActors.Count; ++i)
{
    BitmapActor a = (BitmapActor)arActors[i];
    if (bRotate)
    {
        Rectangle rcDraw = new Rectangle(a.pt, a.actor.Size);

        if (a.bRotateHorz)
        {
            rcDraw.Inflate(0, -a.actor.Height * a.nRotate/200);
                    
            if (a.bFlip)
                grScreen.DrawImage(a.actor, rcDraw, 0, 
                    a.actor.Height, a.actor.Width, -a.actor.Height, 
                    GraphicsUnit.Pixel);
            else
                grScreen.DrawImage(a.actor, rcDraw, 0, 0, a.actor.Width, 
                    a.actor.Height, GraphicsUnit.Pixel);
        }
        else
        {
            rcDraw.Inflate(-a.actor.Width * a.nRotate/200, 0);

            if (a.bFlip)
                grScreen.DrawImage(a.actor, rcDraw, a.actor.Width, 0, 
                    -a.actor.Width, a.actor.Height, GraphicsUnit.Pixel);
            else
                grScreen.DrawImage(a.actor, rcDraw, 0, 0, 
    `                a.actor.Width, a.actor.Height, GraphicsUnit.Pixel);
        }                
    }
    else
        grScreen.DrawImageUnscaled(a.actor, a.pt);
}

TextActor

The TextActor was originally just an array of strings, which were drawn in the main class. However, Chris posted to the screensaver page, suggesting four different effects for text:

  • fading text in random positions
  • text in a block that bounced around the screen
  • 'some sort of swirly effect'
  • a crossword puzzle effect

I had high hopes of doing them all, but in the end, only the first three made the grade in time. It was at this point that I decided to move the actors into classes, so the TextActor class ended up more self-contained than the Bitmap one.

I guess the most interesting text mode is the rotating one. The Create method, the first time it is called, populates an array with points that form a swirling pattern on the primary screen, paired up in a class with font sizes. Then as the string is drawn, when it hits the middle of the screen, it starts incrementing an index to tell it how much string to keep drawing, and creates an instance of a class that contains a string (of one character, but making it a string translates to DrawString nicely), and an index into the array. When we stop moving text into the array, we start moving in a new string. The items keep getting incremented, which moves them in the spiral, until they have an index bigger than the array, and they are deleted.

The other cool thing with the text is that some of our modes implement parsing of HTML, which was James' doing, his article on it can be found here. Basically, I parse the information he returns and build an array, so the text is drawn a bit at a time, depending on where the tags are. I did not get to implement superscript and subscript, although all I really need to do is halve the font size and offset my drawing position for subscripts. Overall, I'd say the most satisfying part of the TextActors is how easy each new mode was to add to the framework, and I'd say it would be almost trivial to add other text modes based on the different options provided.

Arrays

The Bitmap and Text actor classes both have a hand written array class. If there's one thing I hate about C# (and there's more than one, I promise you), it's how much casting I need to do in dumb situations. Having to cast the return value from a container is the pits, so I do the cast within a class that wraps the array. The TextActor especially has additional methods, because some modes draw as much text as will fit on the screen, and others draw only one item at a time. A method called DrawOnce takes care of this - it sets up everything needed including marking all but one item to not be drawn.

Links

The screensaver also provides a Bob cursor, which is just a bitmap I draw up. When it is over a scrolling or fading text item, the item is drawn with a bitmap brush (also of Bob) instead of plain, which fact denotes that it is a hot link. Clicking on the item at that point closes the screensaver and launches the default browser.

Drawing Speed

The screensaver contains remnants of code put in to try and speed up the drawing, this is why the Move methods take a number of ticks, the idea was to use a timer to call the Move methods and use the ticks to increment by a number based on time, so it always ran the same speed but was jerky on slower machines. Sadly, we could find no method to substantially speed up the screensaver, and slow is better than how jerky it got when I turned that on. C# simply isn't up to the task at hand, not with the amount of animation we do, on two monitors, 1280x1024 and 1024x768, which is my development machine.

GDI+

The screensaver uses a lot of different GDI+ stuff, including four of the five available brushes (HatchBrush did not get a look in), scaled and flipped drawing, transparency masks, accurate text measurement, and lots more. I'd encourage anyone who wants to see lots of GDI+ examples to buy the Petzold book, but if you can't afford it, you may find some stuff to help you along in our code.

Last Minute Update !!!

I was keen to add a last minute feature, so I have set up a new option - clear screen. Basically, if you uncheck this option, instead of clearing the screen, the screensaver takes a snapshot of the desktop and makes that the background, creating the illusion that the screen is never cleared. This really slows things down, drawing an image is much slower than filling a bitmap with one color. However, if you have a fast enough machine, or run at a low enough resolution, it looks pretty cool.

Implementing screen capture was a bit of a hassle - I basically had to import half of GDI to do it:

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr HDC, int X, int Y);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr HDC);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr HDC, int Top, int Left, 
                                 int Width, int Height, IntPtr SourceHDC, 
                                 int X, int Y, int ROP);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hMemDC, IntPtr hObject);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteDC(IntPtr hMemDC);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nMetrics);

Having done this, I essentially capture the screen the old fashioned way. I don't need GetSystemMetrics to do this, because I've already used the AllScreens array to figure out the dimensions of my total drawing area.

IntPtr ScreenDC = GetDC(IntPtr.Zero);
IntPtr NewDC = CreateCompatibleDC(ScreenDC);
IntPtr BMScreen = CreateCompatibleBitmap(ScreenDC, rcScreen.Width, 
                                         rcScreen.Height);
IntPtr OldBitmap = SelectObject(NewDC, BMScreen);
BitBlt(NewDC, 0, 0, rcScreen.Width, rcScreen.Height, ScreenDC, 
       rcScreen.Left, rcScreen.Top, 0xCC0020); // SRCCOPY
SelectObject(NewDC, OldBitmap);
DeleteDC(NewDC);
bmScreen = Bitmap.FromHbitmap(BMScreen);

The Rest

The rest was pretty much James' handiwork, so I'll leave it to him to cover the details, and provide links as he does. I'm hoping to at least add one more cool mode sometime soon, but with code freeze on Monday to give us time to write the article and check for bugs, I just ran out of time. It's been a lot of fun working with James, and having this project to kick along my C# learning process - I hope you enjoy the screensaver as much as we enjoyed making it.

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.

A list of licenses authors might use can be found here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here