|
|||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionI 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. ContentsHere is what I will be covering in this article:
What Does It Look LikeWell 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 The valid files are found using the following extension methods, that work with 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 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 ScreenSaver TemplateI 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. The Design Of The CodeI 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: The 3D CubeThis 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 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 ImagesThe 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 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 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 ImagesAs 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 HomeAll 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. A Word Of WarningSome 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 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 itThat'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.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||