Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Photo Screensaver

Everyone loves screensavers!  Photo screensavers are fun because you can show off your own photos in creative ways.  This Coding 4 Fun article will step you through creating a Windows screensaver to show off your photo collection.  Learn about GDI+ and what it takes to build a custom screensaver.
Arian's Blog

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Visual Basic or Visual C# Express Editions, Windows Desktop Search 3.0 for XP (optional, built-in to Vista)
Hardware: None
Download:

Introduction

In trying to bite off new things, I decided it would be fun to create a new screensaver.  Visual Studio Express comes with an RSS screensaver starter kit, but I never looked at it too closely.  It turns out that screensavers are pretty easy to make, though you'll need to do some work with graphics.  If you're not afraid to think in terms of bitmaps and drawing operations, you can do some fun things!

Use the download links at the top of the article to grab the C# or VB source, then if you haven't already, download the appropriate version of Visual Studio 2005 Express Edition.  The sample will run on either XP or Vista.  It will take advantage of Windows Desktop Search if available.  On XP this is a stand-alone download.

Goals

I've always like the Picasa Photo Pile effect:

Image 1 - The Picasa Photo Pile effect (not a screensaver) 

This is pretty cool looking, but the Picasa screensaver uses a different effect, removing the "Polaroid"-style photo frame:

Image 2 - The Picasa screensaver collage effect

Instead of just settling for what they offer, I decided to try my hand at recreating the Photo Pile myself.  It can work from a folder of images, optionally searching subfolders as well.  Instead of working from a single color background, I thought that it would be cute to layer the pile over the actual desktop background.  Of course, this might not be ideal if you have sensitive data on your screen, but if you walk away letting the screensaver kick in, you probably aren't as concerned about that anyway!

Image 3 - Coding 4 Fun screensaver overlaid on my desktop

 

Basic Setup

My first step was to crack open the starter kit.  It's nice and easy-to-follow.  The biggest thing to notice is that a screensaver is just an application.  There is no API.  You should do some kind of visual thing, but there's nothing requiring you to.  If the application is launched with the "/c" parameter, you should allow configuration.  The "/p" parameter is invoked along with the a window handle to allow you to create a preview (as seen in the Display Settings dialog).  I don't actually implement preview, but it's not all that difficult to do.  The final parameter is "/s".  This is the "show time!" flag telling you to display the screensaver itself.

At this point, you can do anything that you want.  What you should do is create a bitmap the same size as the screen.  It should be a top-most, borderless form.  The entire client area is the actual screensaver imagery.  With no extra work, you'd have a nice "screen blank" screensaver.  You'll probably you'll want to take it farther though!

You could simply draw images to cover the surface.  Perhaps a simple first stab would be to use the PictureBox control filling the entire form (Dock = Fill).  For that matter, you could do something without graphics at all.  It's just a form, so you have a lot of freedom at this point.

For most screensavers, you will want to turn on double-buffering, set the form dimensions to the screen dimensions, and enable/disable several other properties/settings.  To make this easy, I created a base class for the screensaver form: BaseScreenSaverForm (pretty original name, huh!).  Any screensaver can just extend from this form.  There is also an abstract class for the screensaver logic itself, ScreenSaverBase.   This is called to initialize it (in our case, reading a list of files), then it will raise an event whenever there is a new bitmap available at which point the form can redraw itself appropriately.  It makes it all pretty painless really.

Working with GDI+ Graphics

Once the basic framework is setup, you need to do some work to actually create an image in the derived screensaver object.  I actually created a very simple example, CircleScreenSaver that demonstrates this.  It just randomly draws circles on the screen every few seconds.  It's got much less going on, so it might be a good place to start to see how it all works.  If you want to switch screensavers, you'll have to make a small code modification in the ShowScreenSaver() method of Program.cs/vb.

Image 4 - The simple "Circles" screensaver (two for the money!)

Drawing in .NET using GDI+ take place using Graphics objects.  Graphics instances are created using a factory method attached to the Image class.  Think of the Bitmap/Image object as representing a snapshot of the pixels in a compact form, while the Graphics object provides methods to manipulate those pixels.  The DrawEllipse() method creates a circle when height and width are equals.  You can also DrawRectangle(), DrawString(), and much more.  All drawing requires pens or brushes, and you can set color (32-bit), width, and an alpha blend amount (0-255) as seen in Image 4.

In order to randomly draw circles around the screen, you start by obtaining a Graphics object.  The _workingImage object is a Bitmap of the actual desktop.  This is taken using the GrabPrimaryScreen() method of the ScreenSaverBase class.

Visual Basic

Public Function GrabPrimaryScreen() As Bitmap
    Dim rc As Rectangle = Screen.PrimaryScreen.Bounds

    Dim screenBitmap As New Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb)

    Using g As Graphics = Graphics.FromImage(screenBitmap)
        ' Copy the contents of the screen
        g.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy)
    End Using

    Return screenBitmap
End Function

Visual C#

public Bitmap GrabPrimaryScreen()
{
    Rectangle rc = rc = Screen.PrimaryScreen.Bounds;

    Bitmap screenBitmap = new Bitmap(rc.Width, rc.Height,
      PixelFormat.Format32bppArgb);

    using (Graphics g = Graphics.FromImage(screenBitmap))
    {
        // Copy the contents of the screen
        g.CopyFromScreen(rc.X, rc.Y,
           0, 0, rc.Size, CopyPixelOperation.SourceCopy);
    }

    return screenBitmap;
}

 

Next you generate lots of random numbers for the size of the circle, the location, and the color.  The Color.FromArgb() method takes RGB (red, green, blue) values from 0-255 to create a 32-bit color.  You can also optionally specify an alpha value (also from 0-255) -- in this case 128.  That affects how the color is blended with any existing color in the bitmap.  128 equates to a 50% transparency.  This is performed in the UpdateWorkingImageBubbles() method of the CircleScreenSaver class.  Building up an image from primitives such as rectangles, lines, and ellipses can be tedious, but isn't very difficult.

Visual Basic

Using g As Graphics = Graphics.FromImage(_workingImage)
    Dim s As Integer = rnd.Next(200)
    Dim x As Integer = rnd.Next(_workingImage.Width - s)
    Dim y As Integer = rnd.Next(_workingImage.Height - s)

    Dim c As Color = Color.FromArgb(128, rnd.Next(255), rnd.Next(255), rnd.Next(255))

    Using b As Brush = New SolidBrush(c)
        g.FillEllipse(b, x, y, s, s)
    End Using
End Using

Visual C#

using (Graphics g = Graphics.FromImage(_workingImage))
{
    int s = rnd.Next(200);
    int x = rnd.Next(_workingImage.Width - s);
    int y = rnd.Next(_workingImage.Height - s);

    Color c = Color.FromArgb(128, rnd.Next(255), rnd.Next(255), rnd.Next(255));
    
    using (Brush b = new SolidBrush(c))
    {
        g.FillEllipse(b, x, y, s, s);
    }
}
 

Rendering the Photos

Beyond the primitive drawing operations, you also have control over settings such as scaling, transformation, and rendering quality.  This is important since you want the image to be drawn smaller than the original in most cases (scaling).  You can choose trade-off's between fast and high quality.  There's also the compositing of the images -- how various graphic elements are drawn atop each other.  There are different quality levels for that as well.  Finally, transformation allows you to setup a matrix to affect how an image is warped as it is drawn.  This can allow for some interesting effects, though we just use it for performing the random rotation that you see in the final image.

The logic for rendering the photos is found in the PhotoScreenSaver class.  By factoring out the screensaver's custom rendering (using the ScreenSaverBase class) separate form the underlying form (BaseScreenSaverForm class), you can concentrate on just drawing.  The fact that it's a screensaver doesn't affect how to render to the background image.

The steps taken to draw the image are as follows (CreateSnapshotImage() method):

  1. Load the bitmap from the file
  2. Create a new bitmap the size of a 240x240 image plus adequate borders (this is our "canvas")
  3. Draw a white rectangle for the photo frame
  4. Draw an outline for the frame
  5. Compute the new size for the image (ConstrainSize method)
  6. Draw the image to fit in a 240x240 frame
  7. Draw an outline for the image
  8. Draw the caption

Drawing the caption alone is actually a lot of steps.  Use can invoke the DrawString() method of the Graphics class, but you don't get a whole lot of control.  I ended up using the GraphicsPath class instead of using its AddString method to create the paths for the caption string.  It allows me to define a rectangle and to left/center/right justify horizontally and/or vertically, and even to add automatic ellipses as necessary.  All without need to measure the width of the string first.  For better efficiency, the StringFormat object should just be reused between calls.  For bonus points, see what else you can reuse between calls!

Visual Basic

' Extract and draw caption
Dim f As Font = My.Settings.CaptionFont
Dim rect As New Rectangle(2, CInt(imageHeight + (imageBuffer * 2)), trueImageWidth - 4, CInt(imageBuffer * 2.5))

Dim path As New GraphicsPath()
Dim format As New StringFormat()
format.Alignment = StringAlignment.Center
format.LineAlignment = StringAlignment.Center
format.Trimming = StringTrimming.EllipsisCharacter

path.AddString(originalPhoto.Caption, f.FontFamily, CInt(f.Style), f.Height, rect, format)

g.FillPath(Brushes.Black, path)

Visual C#

// Extract and draw caption
Font f = Properties.Settings.Default.CaptionFont;
Rectangle rect = 
    new Rectangle(  2, (int)(imageHeight + (imageBuffer * 2)),
                    trueImageWidth - 4, (int)(imageBuffer * 2.5));

GraphicsPath path = new GraphicsPath();
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
format.Trimming = StringTrimming.EllipsisCharacter;

path.AddString(originalPhoto.Caption, f.FontFamily, (int)f.Style, f.Height, rect, format);

g.FillPath(Brushes.Black, path);

 

The complete process is as follows:

  1. In the Main method, the screensaver form and screensaver instances are created
  2. The Init() method is invoked on the screensaver object -- this starts a background thread
  3. In Init():
    1. Grab the desktop background (primary desktop) using GrabPrimaryScreen()
    2. Retrieve the list of images from Windows Desktop Search or the given folder as a collection of PhotoInfo objects
    3. Call GetNextPhoto() to figure out the next image to show
    4. If the WorkingImage property is NULL, generate the photo as previously covered
    5. With the WorkingImage value, render the photo onto the working background image with rotation and shadow (DrawImageRotated method)
    6. Raise an event (subscribed to by the form) to indicate a new image is available
    7. Sleep and repeat
  4. Once the event is received by the form, it dispatches the event to the UI thread and calls Refresh()
  5. Refresh() causes the Form_Render method to be raised.  The Render event handler calls GetWorkingPhoto() on the screensaver, then updates the screen.

After every twenty seconds, the working image is reverted back to that cached screen image.  With the images cached, memory usage goes up a bit, but performance improves.  Doing the heavy rendering work on a background thread means that the screensaver can still exit immediately when a keypress or mouse action is detected (remember that the screensaver is responsible for exiting gracefully when user activity is detected).  Otherwise, a very large image might take a few seconds, and it would be unresponsive in the meantime.

Final Touches

If you've read some of my past articles (Searching the Desktop, Creating an Enhanced File Search Dialog), you're already familiar with the power of Windows Desktop Search.  Adding the ability to search for screensaver images from the WDS index was trivial.  It makes sense too.  The alternative is to add multiple folders and filename matching keywords.  Searching the index is fast and it just works.

Adding the caption was more difficult than I expected.  It seems like an obvious need to show the caption, but I just couldn't find it.  The Image class exposes lists of embedded metadata properties, but it's very tedious to work with.  Worse still, modern OS's like XP and Vista don't really use EXIF anymore for metadata.  The new hotness if XML-formatted XMP data.  This is stored within the file as plain-text XML.  It's fairly easy-to-read, but I was frustrated that data isn't completely consistent since different vendors can extend it how they want (after all, it's just XML).  Feel free to uncomment code in the PhotoInfo class in the GetPhotoCaption() method if you want to get EXIF properties, but you may or may not get any use out of it.  If the list of images comes from WDS, it's moot anyway -- the System.Title property brings back the image's title/caption with no extra work at all, regardless of how it's stored!

One place that I do use the EXIF property is to detect image rotation (orientation).  It's only any good if your camera supports the "Orientation" flag (0x0112), or if your photo software does (Picasa doesn't update that flag as far as I can tell).  It's simple to rotate based on this flag.  If it's not updated, you may end up with some sideways images.  Notice the call to ExifSupport.GetExifShort().  I threw together the ExifSupport class based on some articles that I found.  The property values are all stored as byte arrays and they're really a pain.  In .NET 3.0, you can use the System.Windows.Media.Imaging namespace to get much better access to properties.  I wanted this project completely 2.0, so I had to do it the lower-level way.

Visual Basic

Dim rotation As Short = ExifSupport.GetExifShort(originalPhoto.SourceBitmap, 274, 1)
If rotation = 3 Then
    resizedPhoto.RotateFlip(RotateFlipType.Rotate180FlipNone)
ElseIf rotation = 6 Then
    resizedPhoto.RotateFlip(RotateFlipType.Rotate90FlipNone)
ElseIf rotation = 8 Then
    resizedPhoto.RotateFlip(RotateFlipType.Rotate270FlipNone)
End If

Visual C#

short rotation = ExifSupport.GetExifShort(originalPhoto.SourceBitmap, 0x0112, 1);
if (rotation == 3) resizedPhoto.RotateFlip(RotateFlipType.Rotate180FlipNone);
else if (rotation == 6) resizedPhoto.RotateFlip(RotateFlipType.Rotate90FlipNone);
else if (rotation == 8) resizedPhoto.RotateFlip(RotateFlipType.Rotate270FlipNone);

 

One thing to remember for a screensaver, is that you must rename the final EXE to the SCR extension.  You can do this automatically in a Post-Build event.  I actually make a copy of the file with the new name.  In C#, use the Build Events tab of the project properties, while in Visual Basic, look for the Build Events button in the Compile tab of My Project.  There you can define actions to take before or after building.  To perform this copy/rename, enter this command:

copy $(TargetFileName) $(TargetName).scr

The syntax is pretty self-explanatory.  You can do lots of cool things with build events.  You could even automate the deployment to the C:\Windows\System32 folder, but I chose not to make it quite that easy.  If I add a bug to the code, I don't want it in the system32 folder to kick in when I go to get my coffee!

A final note: To test the preview mode from Visual Studio, you'll need to set the command line argument to "/p".  You do this in the Debug tab in your project properties.  Just enter it in Command line arguments.  This is much easier than launching it from a command prompt, and also keeps everything within Visual Studio.

Next Steps

Originally, I wasn't going to add search to the application, but it was so painless that I decided that it was worth the little extra effort.  Another feature that seemed like a logical extension was working with RSS feeds with image enclosures (Flickr, Zooomr).  This is actually really easy too, but I chose to leave well enough alone for now!  Use the Feeds Manager classes included with Internet Explorer 7 to simplify the retrieval and caching of the feed items.

Adding the ability to dynamically change the dimensions of the photo frame would make it possible to avoid cropping images arbitrarily, though it would lose that "Polaroid" appearance.  It's certainly not a difficult change, I just didn't like the look personally.

Another big thing is supporting multi-monitor displays and preview (in the Screen Saver properties window).  The extra work of creating multiple bitmaps and rendering to the preview pane is left as an exercise to the reader!

The screensaver does mostly work in Vista, but I noticed that sometimes it doesn't seem to do anything.  I've found that pictures in the Public Pictures folder return the wrong path.  The path works fine in Windows Explorer, but it isn't the real path (there is no real "Public Pictures" folder).  If I find a fix, I'll update this.  If it hits such an image, no error will occur (unless you are debugging it -- I added some code to show errors then), it just doesn't show it.  If all images are in such as location, nothing will appear to happen.

Conclusion

Now that I know, I'll probably do more with screensavers in the future (on my blog or in an article).  Working with graphics was fun, though it's definitely not my best skill.  I'm just not that visual kind of guy!  Have fun with the code and as usual, contact me through my blog for comments, complaints, or questions.


Arian Kulp is an independent software developer and writer working in the Midwest.  He has been coding since the fifth grade on various platforms, and also enjoys photography, nature, and spending time with his family.  Arian can be reached through his web site at http://www.ariankulp.com.

Tags:

Follow the Discussion

  • LeighLeigh

    The Rnd.Next(int) is not working in Visual Studo 2008 (VB.NET).... What might be the problem here?

  • Clint RutkasClint I'm a "developer"

    @Leigh:  That is since we don't do full source posting in our articles to reduce their length.  the Random object was declared in a global view.

    If you download the full source, you'll see this.

    In your application, you'll need to do something like Random rnd = new Random(DateTime.Now.Ticks);

    The ticks will seed the random number generator to be random.

  • NickNick

    I like this code very much, but I'm curious how I would go about having it work on both (dual) monitors?  I just don't think I'm familiar enough with the code itself to see where/what I need to change.  I want the screen saver to spread across both of them...

  • GregGreg

    @Nick: Check out http://www.simbolic.net/software/picturescreensaver/ for a similar project (with source) that supports multiple monitors.

  • BrockBrock

    Why does nobody implement the /p preview functionality?!  If you actually *try* - there are some issues that you run into.  Namely - windows keeps re-running the screensaver (with the /p flag) each time you enter and exit the preview.  Your app needs to be smart enough to kill the previous one (or update it) and make sure that only one is running at any given time.

  • Clint RutkasClint I'm a "developer"

    @Brock:  Sorry, we provide base code examples that are aimed at people beefing them out.

    Ping Arian, he may have a fully finished application.

  • joepadmiraaljoepadmiraal

    Thanks for the example.

    I compiled it with VS2008 and it runs fine.

    One thing I noticed is a small problem with the memory usage. It is growing continuously while the screensaver runs. The problem seems to be in the ImageUpdater function and the ConstrainSize function.

    An example:

    int sourceHeight = sourcePhoto.SourceBitmap.Height;

    does create an bitmap through the get method, which is never released.

    It can be solved by creating a local bitmap object to which you assign sourcePhoto.SourceBitmap.

    At the end of the ConstraintSize function you can use the Dispose() method to release the memory of the local bitmap object.

    I hope this helps someone.

    The adjusted PhotoshowScreenSaver.cs file can be downloaded from my site.

  • robaultrobault

    Yeah, Joe is right, and there are plenty of places to manually handle the clean up in memory to keep the app lean. I watched task manager before and after making a few changes to both methods Joe mentioned. It kept the memory usage down.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.