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

WPF: A 3D screensaver written in WPF

0.00/5 (No votes)
12 Oct 2008 1  
A 3D screensaver written in WPF.

Introduction

I had to wait in today for a new washing machine to be delivered, so had a few hours to spare. So decided it might be fun to try and create a nice WPF screensaver. This article is the outcome of this.

Contents

Here is what I will be covering in this article:

What Does it Look Like

Well, it looks like this when running:

And it can be configured using the configuration screen. Configuring is done just as for a normal screensaver.

config.gif

The configuration screen simply allows a user to select a list of folders that should contain images. When the user closes the configuration screen, a file is written to the Environment.SpecialFolder.MyPictures folder. This file simply contains a list of all the directories the user picked. Another thing that happens when the user closes this form is that an internal IList<FileInfo> is updated to hold an instance of any valid image file found in these directories.

The valid files are found using the following Extension Methods that work with IEnumerable<FileInfo> types:

public static IEnumerable<FileInfo> IsImageFile(
       this IEnumerable<FileInfo> files,
       Predicate<FileInfo> isMatch)
{
    foreach (FileInfo file in files)
    {
        if (isMatch(file))
            yield return file;
    }
}

public static IEnumerable<FileInfo> IsImageFile(
              this IEnumerable<FileInfo> files)
{
    foreach (FileInfo file in files)
    {
        if (file.Name.EndsWith(".jpg") || 
            file.Name.EndsWith(".png") || 
            file.Name.EndsWith(".bmp"))
            yield return file;
    }
}

To put this into context, I'll show you the entire listing for the configuration screen. There is not that much code for it, so don't worry.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.IO;
using System.Windows.Forms;

namespace WPF_ScreenSaver
{
    /// <summary>
    /// Allows user to pick directories of images for use with
    /// the screen saver
    /// </summary>
    public partial class Settings : System.Windows.Window
    {
        #region Ctor
        public Settings()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(Settings_Loaded);
            this.Closing += 
        new System.ComponentModel.CancelEventHandler(Settings_Closing);
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Populate the listbox by reading the file on disk if it exists
        /// </summary>
        private void Settings_Loaded(object sender, RoutedEventArgs e)
        {
            String fullFileName = System.IO.Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
                Globals.TempFileName);

            //populate the listbox by reading the file on disk if it exists
            String line;
            try
            {
                using (StreamReader reader = File.OpenText(fullFileName))
                {
                    line = reader.ReadLine();
                    while (line != null)
                    {
                        lstFolders.Items.Add(line);
                        line = reader.ReadLine();
                    }
                    reader.Close();
                }
            }
            catch (FileNotFoundException fex)
            {
            }
        }

        /// <summary>
        /// Persist selected directories to file on close
        /// </summary>
        private void Settings_Closing
        (object sender, System.ComponentModel.CancelEventArgs e)
        {
            DealWithLocationFile();
        }

        /// <summary>
        /// Pick another image location to use within the screen saver
        /// </summary>
        private void btnPick_Click(object sender, RoutedEventArgs e)
        {
            FolderBrowserDialog fd = new FolderBrowserDialog();
            fd.SelectedPath = Environment.GetFolderPath
                (Environment.SpecialFolder.MyPictures);
            if (fd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                if (fd.SelectedPath != String.Empty)
                {
                    if (!lstFolders.Items.Contains(fd.SelectedPath))
                        lstFolders.Items.Add(fd.SelectedPath);
                }
            }
        }        

        /// <summary>
        /// Delete directory file on disk if it exists, and recreate
        /// the file based on the new listbox folders that the user
        /// picked
        /// </summary>
        private void DealWithLocationFile()
        {
            String fullFileName = System.IO.Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), 
                Globals.TempFileName);

            //Delete existing file if it exists
            if (File.Exists(fullFileName))
            {
                File.Delete(fullFileName);
            }

            //re-create file, and the in memory collection of images
            using (TextWriter tw = new StreamWriter(fullFileName))
            {
                Globals.Files.Clear();
                //process each foldername, extracting the image files
                foreach (String folderName in lstFolders.Items)
                {
                    try
                    {
                        foreach (var file in
                            new DirectoryInfo(folderName).GetFiles().IsImageFile())
                        {
                            Globals.Files.Add(file);
                        }
                        tw.WriteLine(folderName);
                    }
                    catch (DirectoryNotFoundException dex)
                    {
                    }
                    catch (ArgumentException ax)
                    {
                    }
                }
                tw.Close();
            }
        }
        #endregion
    }
}

I think this code is fairly self-explanatory. The XAML for this screen is not that exciting; there are a few Templates for things like ScrollViewer and Button types, but this is all standard stuff. So I'll leave that as an exercise to the reader. The part within the configuration screen that makes use of the previously mentioned Extension Methods is this part:

try
{
    foreach (var file in
        new DirectoryInfo(folderName).GetFiles().IsImageFile())
    {
        Globals.Files.Add(file);
    }
    tw.WriteLine(folderName);
}
catch (DirectoryNotFoundException dex)
{
}
catch (ArgumentException ax)
{
}

within the DealWithLocationFile() method shown above. You got to love Extension Methods, they make it so easy to do neat things like this.

Screensaver Template

I can claim nothing for the WPF Template, I found that on the Internet at the following URL: http://scorbs.com/2006/12/21/wpf-screen-saver-template.

Full instructions of how to use this can be found at this link, should you wish to try and create your own screensaver.

The Design of the Code

I have already discussed the configuration screen so I will not go over that again. So that really only leaves the main window, which is the actual screensaver. The basic idea is as follows:

  1. There is a 3D cube that has images shown on its surfaces. These images are randomly picked from a working set of images.
  2. The actual working set is picked by randomly picking a number of images from the total images available that were retrieved based on the directories the user picked on the configuration screen.
  3. If there are not enough images to create a working set, the working set is padded with a single image that is embedded in the actual assembly resources. I have chosen a nice fetching She-Hulk image for this purpose.
  4. A timer is used to signal that a new image should be picked for the 3D cube. This is done as follows:
    1. A random index is picked from the working set which is then used to show on the 3D cube.
    2. A counter is incremented. When the counter reaches the same size as the working set of images, a new working set of images is generated.
  5. There is a small area at the bottom of the screen which represents the current working set. The current image within this set will get a small IsSelected indicator applied to it. This area is also refreshed when a new working set is created.

I'll now show you how some of this works.

The 3D Cube

This is defined in XAML as shown below:

<Viewport3D x:Name="myViewport">    
    <Viewport3D.Resources>
        <MeshGeometry3D x:Key="plane1" 
          Normals="0,-1,0 0,-1,0 0,-1,0 0,-1,0" 
          Positions="-0.5,0,0.5 0.5,0,-0.5 0.5,0,0.5 -0.5,0,-0.5" 
          TextureCoordinates="0,1 1,0 1,1 0,0" 
          TriangleIndices="0 1 2 1 0 3"/>
        <MeshGeometry3D x:Key="plane2" 
          Normals="0,0,1 0,0,1 0,0,1 0,0,1" 
          Positions="-0.5,0,0.5 0.5,0,0.5 0.5,1,0.5 -0.5,1,0.5" 
          TextureCoordinates="0,1 1,1 1,0 0,0" 
          TriangleIndices="0 1 2 2 3 0"/>
        <MeshGeometry3D x:Key="plane3" 
          Normals="0,0,-1 0,0,-1 0,0,-1 0,0,-1" 
          Positions="-0.5,0,-0.5 0.5,1,-0.5 0.5,0,-0.5 -0.5,1,-0.5" 
          TextureCoordinates="0,1 1,0 1,1 0,0" 
          TriangleIndices="0 1 2 1 0 3"/>
        <MeshGeometry3D x:Key="plane4" 
          Normals="1,0,0 1,0,0 1,0,0 1,0,0" 
          Positions="0.5,0,0.5 0.5,0,-0.5 0.5,1,-0.5 0.5,1,0.5" 
          TextureCoordinates="0,1 1,1 1,0 0,0" 
          TriangleIndices="0 1 2 2 3 0"/>
        <MeshGeometry3D x:Key="plane5" 
          Normals="-1,0,0 -1,0,0 -1,0,0 -1,0,0" 
          Positions="-0.5,0,0.5 -0.5,1,-0.5 -0.5,0,-0.5 -0.5,1,0.5" 
          TextureCoordinates="0,1 1,0 1,1 0,0" 
          TriangleIndices="0 1 2 1 0 3"/>
        <MeshGeometry3D x:Key="plane6" 
          Normals="0,1,0 0,1,0 0,1,0 0,1,0" 
          Positions="-0.5,1,0.5 0.5,1,0.5 0.5,1,-0.5 -0.5,1,-0.5" 
          TextureCoordinates="0,1 1,1 1,0 0,0" 
          TriangleIndices="0 1 2 2 3 0"/>
    </Viewport3D.Resources>
    
    <Viewport3D.Camera>
        <PerspectiveCamera x:Name="Camera" 
          FieldOfView="45" 
          FarPlaneDistance="20" LookDirection="5,-2,-3" 
          NearPlaneDistance="0.1" Position="-5,2,3" 
          UpDirection="0,1,0"/>
    </Viewport3D.Camera>

    <ModelVisual3D>
        <ModelVisual3D.Content>
            <Model3DGroup x:Name="Scene" 
                   Transform="{DynamicResource SceneTR8}">
                <AmbientLight Color="#333333" />
                <DirectionalLight Color="#C0C0C0" 
                  Direction="5,0,-1" />
                <DirectionalLight Color="#C0C0C0" 
                  Direction="1,0,-2.22045e-016" />
                <DirectionalLight Color="#C0C0C0" 
                  Direction="-1,0,-2.22045e-016" />
                <DirectionalLight Color="#C0C0C0" 
                  Direction="-2.44089e-016,0,1" />
            </Model3DGroup>
        </ModelVisual3D.Content>
    </ModelVisual3D>

    <ModelVisual3D x:Name="topModelVisual3D">
        <ModelVisual3D.Transform>
            <Transform3DGroup>
                <TranslateTransform3D OffsetX="0" 
                     OffsetY="0" OffsetZ="0"/>
                <ScaleTransform3D ScaleX="1" 
                     ScaleY="1" ScaleZ="1"/>
                <RotateTransform3D>
                    <RotateTransform3D.Rotation>
                       <AxisAngleRotation3D Angle="1" Axis="0,1,0"/>
                    </RotateTransform3D.Rotation>
                </RotateTransform3D>
                <TranslateTransform3D OffsetX="0" 
                  OffsetY="0" OffsetZ="0"/>
                <TranslateTransform3D OffsetX="0" 
                  OffsetY="0" OffsetZ="0"/>
            </Transform3DGroup>
        </ModelVisual3D.Transform>
        
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <DirectionalLight Color="#FFFFFFFF" 
                Direction="0.717509570032485,-0.687462205666443,
                            -0.112141574324722"/>
            </ModelVisual3D.Content>
                
        </ModelVisual3D>

        <!-- Plane1-->
        <Viewport2DVisual3D Geometry="{StaticResource plane1}">
            <Viewport2DVisual3D.Material>
                <DiffuseMaterial 
           Viewport2DVisual3D.IsVisualHostMaterial="True" 
                    Brush="CornflowerBlue"/>
            </Viewport2DVisual3D.Material>
            <Image x:Name="img1" 
              Source="Images/NoImage.jpg" Stretch="Fill"/>
        </Viewport2DVisual3D>

        <!-- Plane2-->
        <Viewport2DVisual3D Geometry="{StaticResource plane2}">
            <Viewport2DVisual3D.Material>
                <DiffuseMaterial 
           Viewport2DVisual3D.IsVisualHostMaterial="True" 
                    Brush="CornflowerBlue"/>
            </Viewport2DVisual3D.Material>
            <Image x:Name="img2" 
              Source="Images/NoImage.jpg" Stretch="Fill"/>
        </Viewport2DVisual3D>

        <!-- Plane3-->
        <Viewport2DVisual3D Geometry="{StaticResource plane3}">
            <Viewport2DVisual3D.Material>
                <DiffuseMaterial 
           Viewport2DVisual3D.IsVisualHostMaterial="True" 
                    Brush="CornflowerBlue"/>
            </Viewport2DVisual3D.Material>
            <Image x:Name="img3"
              Source="Images/NoImage.jpg" Stretch="Fill"/>
        </Viewport2DVisual3D>

        <!-- Plane4-->
        <Viewport2DVisual3D Geometry="{StaticResource plane4}">
            <Viewport2DVisual3D.Material>
                <DiffuseMaterial 
           Viewport2DVisual3D.IsVisualHostMaterial="True" 
                    Brush="CornflowerBlue"/>
            </Viewport2DVisual3D.Material>
            <Image x:Name="img4"
               Source="Images/NoImage.jpg" Stretch="Fill"/>
        </Viewport2DVisual3D>

        <!-- Plane5-->
        <Viewport2DVisual3D Geometry="{StaticResource plane5}">
            <Viewport2DVisual3D.Material>
                <DiffuseMaterial 
           Viewport2DVisual3D.IsVisualHostMaterial="True" 
                    Brush="CornflowerBlue"/>
            </Viewport2DVisual3D.Material>
            <Image x:Name="img5"
              Source="Images/NoImage.jpg" Stretch="Fill"/>
        </Viewport2DVisual3D>

        <!-- Plane6-->
        <Viewport2DVisual3D Geometry="{StaticResource plane6}">
            <Viewport2DVisual3D.Material>
                <DiffuseMaterial 
           Viewport2DVisual3D.IsVisualHostMaterial="True" 
                    Brush="CornflowerBlue"/>
            </Viewport2DVisual3D.Material>
            <Image x:Name="img6"
              Source="Images/NoImage.jpg" Stretch="Fill"/>
        </Viewport2DVisual3D>
    </ModelVisual3D>
</Viewport3D>

And from there, the 3D cube is animated, using the following StoryBoard:

<Storyboard x:Key="sbLoaded" RepeatBehavior="Forever" 
         AutoReverse="True" Duration="00:00:02.5000000">
    <Rotation3DAnimationUsingKeyFrames BeginTime="00:00:00" 
        Storyboard.TargetName="topModelVisual3D" 
        Storyboard.TargetProperty="(Visual3D.Transform).
        (Transform3DGroup.Children)[2].(RotateTransform3D.Rotation)">
        <SplineRotation3DKeyFrame KeyTime="00:00:00.5000000">
            <SplineRotation3DKeyFrame.Value>
                <AxisAngleRotation3D Angle="46.567463442210148" 
                Axis="0.447213595499955,0.774596669241484,
                        0.44721359549996"/>
            </SplineRotation3DKeyFrame.Value>
        </SplineRotation3DKeyFrame>
        <SplineRotation3DKeyFrame KeyTime="00:00:01">
            <SplineRotation3DKeyFrame.Value>
                <AxisAngleRotation3D Angle="78.477102851225609" 
                Axis="0.250562807085731,0.93511312653103,
                        0.250562807085732"/>
            </SplineRotation3DKeyFrame.Value>
        </SplineRotation3DKeyFrame>
        <SplineRotation3DKeyFrame KeyTime="00:00:01.5000000">
            <SplineRotation3DKeyFrame.Value>
                <AxisAngleRotation3D Angle="180" 
                Axis="-6.12303176911192E-17,
                    2.8327492261615E-16,1"/>
            </SplineRotation3DKeyFrame.Value>
        </SplineRotation3DKeyFrame>
        <SplineRotation3DKeyFrame KeyTime="00:00:02">
            <SplineRotation3DKeyFrame.Value>
                <AxisAngleRotation3D Angle="148.600285190081" 
                Axis="-0.678598344545847,-0.28108463771482,
                        -0.678598344545847"/>
            </SplineRotation3DKeyFrame.Value>
        </SplineRotation3DKeyFrame>
        <SplineRotation3DKeyFrame KeyTime="00:00:02.5000000">
            <SplineRotation3DKeyFrame.Value>
                <AxisAngleRotation3D Angle="338.81717773037957" 
                Axis="-0.704062592219638,-0.704062592219635,
                        0.0926915987235715"/>
            </SplineRotation3DKeyFrame.Value>
        </SplineRotation3DKeyFrame>
    </Rotation3DAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
    Storyboard.TargetName="topModelVisual3D" 
        Storyboard.TargetProperty="(Visual3D.Transform).
    (Transform3DGroup.Children)[1].(ScaleTransform3D.ScaleX)">
        <SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:01" Value="2"/>
        <SplineDoubleKeyFrame KeyTime="00:00:01.5000000" Value="1.5"/>
        <SplineDoubleKeyFrame KeyTime="00:00:02" Value="1.5"/>
        <SplineDoubleKeyFrame KeyTime="00:00:02.5000000" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
    Storyboard.TargetName="topModelVisual3D" 
        Storyboard.TargetProperty="(Visual3D.Transform).
    (Transform3DGroup.Children)[1].(ScaleTransform3D.ScaleY)">
        <SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:01" Value="2"/>
        <SplineDoubleKeyFrame KeyTime="00:00:01.5000000" Value="1.5"/>
        <SplineDoubleKeyFrame KeyTime="00:00:02" Value="1.5"/>
        <SplineDoubleKeyFrame KeyTime="00:00:02.5000000" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
    Storyboard.TargetName="topModelVisual3D" 
        Storyboard.TargetProperty="(Visual3D.Transform).
    (Transform3DGroup.Children)[1].(ScaleTransform3D.ScaleZ)">
        <SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:01" Value="2"/>
        <SplineDoubleKeyFrame KeyTime="00:00:01.5000000" Value="1.5"/>
        <SplineDoubleKeyFrame KeyTime="00:00:02" Value="1.5"/>
        <SplineDoubleKeyFrame KeyTime="00:00:02.5000000" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

The Working Set of Images

The working set of images is picked as follows:

/// <summary>
/// Creates a window of n-many images from the total list of
/// images available. If none are available create a working
/// set of place holder (she-hulk images)
/// </summary>
private void CreateWorkingSetOfFiles()
{
    //grab n-many random images
    Int32 currentSetIndex = 0;
    Globals.WorkingSetOfImages.Clear();

    if (Globals.Files.Count > 0)
    {
        while (currentSetIndex < Globals.WorkingSetLimit)
        {
            Int32 randomIndex = rand.Next(0, Globals.Files.Count);
            String imageUrl = Globals.Files[randomIndex].FullName;
            if (!Globals.WorkingSetOfImages.Contains(imageUrl))
            {
                Globals.WorkingSetOfImages.Add(imageUrl);
                currentSetIndex++;
            }
        }
    }
    else
    {
        for (int i = 0; i < Globals.WorkingSetLimit; i++)
        {
            Globals.WorkingSetOfImages.Add("Images/NoImage.jpg");
        }
    }

    //create ItemsControl
    itemsCurrentImages.Items.Clear();
    foreach (String imageUrl in Globals.WorkingSetOfImages)
    {
        SelectableImageUrl selectableImageUrl = new SelectableImageUrl();
        selectableImageUrl.ImageUrl = imageUrl;
        selectableImageUrl.IsSelected = false;
        itemsCurrentImages.Items.Add(selectableImageUrl);
    }
}

It can be seen that this is not actually using images to add to the ItemsControl at the bottom, but rather a SelectableImageUrl object. Let's have a look at one of these objects. They are a simple bindable object, thanks to the INotifyPropertyChanged interface.

using System.ComponentModel;
using System;

namespace WPF_ScreenSaver
{
    /// <summary>
    /// A simple SelectableImageUrl bindable object
    /// </summary>
    public class SelectableImageUrl : INotifyPropertyChanged
    {
        #region Data
        private String imageUrl;
        private Boolean isSelected;
        #endregion

        #region Public Properties

        public String ImageUrl
        {
            get { return imageUrl; }
            set
            {
                if (value == imageUrl)
                    return;

                imageUrl = value;
                this.OnPropertyChanged("ImageUrl");
            }
        }

        public Boolean IsSelected
        {
            get { return isSelected; }
            set
            {
                if (value == isSelected)
                    return;
                isSelected = value;
                this.OnPropertyChanged("IsSelected");
            }
        }
        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                this.PropertyChanged
        (this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}

Which means that we can create a nice XAML DataTemplate for this type of object. So this is exactly what I do to show the currently selected one. Here is the DataTemplate for one of these objects that are added to the ItemsControl representing the current working window objects:

<DataTemplate DataType="{x:Type local:SelectableImageUrl}">
    <Grid  Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="15"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <Rectangle x:Name="rect" Grid.Column="0" 
                   Grid.Row="0" Fill="Transparent" 
                   Width="10" Height="10" 
                   HorizontalAlignment="Center" 
                   VerticalAlignment="Center"/>
        <Border Grid.Column="0" 
                Grid.Row="1" Margin="2"
                Background="White">
            <Image  
               Source="{Binding Path=ImageUrl}" 
               Width="40" Height="40" 
               Stretch="Fill" Margin="2"/>

        </Border>
    </Grid>
    <DataTemplate.Triggers>
        <DataTrigger 
               Binding="{Binding Path=IsSelected}" 
               Value="True">
            <Setter TargetName="rect" 
               Property="Fill" Value="Orange" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

Generating a New Working Set of Images

As I previously stated, there is an animation timer that runs, and when it ticks, a new image from the working set is used for the 3D cube surfaces. But this timer tick also works out whether to create a new working set of images. This is shown below:

/// <summary>
/// Assign new image, and if at end of working set of images
/// get a new working set of images
/// </summary>
private void timer_Tick(object sender, EventArgs e)
{

    Int32 randomIndex = rand.Next(0, Globals.WorkingSetOfImages.Count);
    String imageUrl = Globals.WorkingSetOfImages[randomIndex];

    foreach (SelectableImageUrl selectableImageUrl in itemsCurrentImages.Items)
    {
        if (selectableImageUrl.ImageUrl == imageUrl)
            selectableImageUrl.IsSelected = true;
        else
            selectableImageUrl.IsSelected = false;
    }
    
    //update 3d cube images
    img1.Source = new BitmapImage(new Uri(imageUrl, UriKind.RelativeOrAbsolute));
    img2.Source = new BitmapImage(new Uri(imageUrl, UriKind.RelativeOrAbsolute));
    img3.Source = new BitmapImage(new Uri(imageUrl, UriKind.RelativeOrAbsolute));
    img4.Source = new BitmapImage(new Uri(imageUrl, UriKind.RelativeOrAbsolute));
    img5.Source = new BitmapImage(new Uri(imageUrl, UriKind.RelativeOrAbsolute));
    img6.Source = new BitmapImage(new Uri(imageUrl, UriKind.RelativeOrAbsolute));
    //do we need to create a new working set of images
    currentChangeCount++;
    if (currentChangeCount == Globals.WorkingSetLimit)
    {
        CreateWorkingSetOfFiles();
        currentChangeCount = 0;
    }
}

How to Use it at Home

All you have to do to us this at home is build the attached project in Release mode and then do the following:

  • Copy the EXE produced to somewhere convenient
  • Rename the EXE to SCR
  • Right click the SCR file
  • Select Install

That's it, you will now have a working WPF screensaver. Enjoy.

A Word of Warning

Some of you may actually have thousands of photos in your "My Pictures" folder. It was never my intention that this screensaver would need to work with thousands of images. Especially not with 5-7 megapixel camera photos, which could be very large files indeed. If you would like to use this for a screensaver in this situation, I would strongly recommend you modify the code in the part that gets all the photos for the selected directories, and store these in the global List<FileInfo>. This is within the configuration screen logic. You could do something like, maybe take only the top 100 picked images. You could use some nice LINQ for this.

This article was more about how to go about creating a screensaver in WPF. I have about 200 PNG/JPG images (though not photos) and they load like lightning.

That's it

That's all I wanted to say this time, I hope it helps some of you. Could I just ask, if you liked this article, please vote for 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