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

WPF: A 3D screensaver written in WPF

Rate me:
Please Sign up or sign in to vote.
4.95/5 (70 votes)
12 Oct 2008CPOL5 min read 169K   3.4K   152   49
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:

Image 1

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:

C#
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.

C#
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:

C#
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:

XML
<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:

XML
<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:

C#
/// <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.

C#
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:

XML
<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:

C#
/// <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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralGreat article Pin
Daytona_67526-Aug-13 18:52
Daytona_67526-Aug-13 18:52 
GeneralRe: Great article Pin
Sacha Barber26-Aug-13 19:55
Sacha Barber26-Aug-13 19:55 
QuestionWPF (XBAP) calling WCF in partial trust mode so that I can avoid user to install certificate Pin
Ajay Kewale6-Oct-09 6:08
Ajay Kewale6-Oct-09 6:08 
Generalstrange problems [modified] Pin
Seraph_summer5-Jul-09 8:19
Seraph_summer5-Jul-09 8:19 
GeneralRe: strange problems Pin
Sacha Barber9-Jul-09 21:47
Sacha Barber9-Jul-09 21:47 
GeneralError in Vista Visual Studio 2008 Pin
Member 565667325-Nov-08 15:42
Member 565667325-Nov-08 15:42 
GeneralRe: Error in Vista Visual Studio 2008 Pin
Sacha Barber25-Nov-08 21:57
Sacha Barber25-Nov-08 21:57 
GeneralCongratulations !!! Pin
Ashutosh Phoujdar24-Nov-08 23:17
Ashutosh Phoujdar24-Nov-08 23:17 
GeneralRe: Congratulations !!! Pin
Sacha Barber25-Nov-08 0:29
Sacha Barber25-Nov-08 0:29 
GeneralCongratulation Pin
Dr.Luiji24-Nov-08 21:04
professionalDr.Luiji24-Nov-08 21:04 
GeneralRe: Congratulation Pin
Sacha Barber24-Nov-08 22:24
Sacha Barber24-Nov-08 22:24 
GeneralAs usual Pin
Dr.Luiji21-Nov-08 1:48
professionalDr.Luiji21-Nov-08 1:48 
GeneralRe: As usual Pin
Sacha Barber21-Nov-08 2:25
Sacha Barber21-Nov-08 2:25 
GeneralNice work Pin
Rama Krishna Vavilala10-Nov-08 5:03
Rama Krishna Vavilala10-Nov-08 5:03 
GeneralRe: Nice work Pin
Sacha Barber10-Nov-08 9:40
Sacha Barber10-Nov-08 9:40 
GeneralRe: Nice work Pin
Rama Krishna Vavilala10-Nov-08 11:41
Rama Krishna Vavilala10-Nov-08 11:41 
GeneralRe: Nice work Pin
Sacha Barber10-Nov-08 12:02
Sacha Barber10-Nov-08 12:02 
GeneralGiven link to this from my one more article Pin
prasad0230-Oct-08 19:21
prasad0230-Oct-08 19:21 
Hi Sacha,

I have given link to your article from my article. for cube reference.

here is my second article in wpf Smile | :)

http://www.codeproject.com/KB/WPF/WITIYMIWYG.aspx[^]

Thanks and Regards,
Prasad
QuestionHow to save Model3D object to jpg file? Pin
wmjcomcn23-Oct-08 23:20
wmjcomcn23-Oct-08 23:20 
AnswerRe: How to save Model3D object to jpg file? Pin
Sacha Barber24-Oct-08 6:26
Sacha Barber24-Oct-08 6:26 
GeneralGiven link to this from my article Pin
prasad0223-Oct-08 2:37
prasad0223-Oct-08 2:37 
QuestionWhat is it with you and green women? Pin
Pete O'Hanlon22-Oct-08 10:02
subeditorPete O'Hanlon22-Oct-08 10:02 
AnswerRe: What is it with you and green women? Pin
Sacha Barber22-Oct-08 10:16
Sacha Barber22-Oct-08 10:16 
GeneralRe: What is it with you and green women? Pin
Pete O'Hanlon22-Oct-08 11:53
subeditorPete O'Hanlon22-Oct-08 11:53 
GeneralRe: What is it with you and green women? Pin
Sacha Barber23-Oct-08 11:34
Sacha Barber23-Oct-08 11:34 

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.