![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Beginner
License: The Code Project Open License (CPOL)
WPF : A 3D screensaver written in WPFBy Sacha BarberWPF : A 3D screensaver written in WPF |
C# (C#3.0), .NET (.NET3.5), WPF, LINQ, Architect, Dev, Design
|
||||||||
|
Advanced Search Add to IE Search |
|
|
||||||||||||||||||
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 screen saver. This article is the outcome of this.
Here is what I will be covering in this article:
Well it looks like this when running.

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

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 for 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.
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 screen saver.
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 screen saver. The basic idea is as follows:
I'll now show you how some of this works:
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 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. Lets 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>
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;
}
}
All you have to do to us this at home, it build the attached project in RELEASE mode and then do the following:
That's it, you will then have a working WPF screen saver. Enjoy.
Some of you may actually have 1000nds of photos in your "My Pictures" folder. It was never my intention that this screen saver would need to work with 1000nds of images. Especially not 5-7 Megapixel camera photos, which could be very large files indeed. If you would like to use this for a screen saver in this situation, I would strongly recommend you modify the code in the part that gets all the photos for the selected directories, and stores 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 screen saver in WPF. I have about 200 png/jpg images (though not photos) and they load like lightning.
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.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 12 Oct 2008 Editor: Deeksha Shenoy |
Copyright 2008 by Sacha Barber Everything else Copyright © CodeProject, 1999-2010 Web22 | Advertise on the Code Project |