WPF:用 WPF 编写的 3D 屏幕保护程序






4.95/5 (65投票s)
一个用WPF编写的3D屏保。
引言
今天我不得不等一台新洗衣机送货,所以有几个小时的空闲时间。因此,我决定尝试创建一个漂亮的WPF屏保。这篇文章就是由此产生的。
目录
本文将介绍以下内容:
它看起来怎么样?
嗯,运行时它看起来是这样的:
并且可以通过配置屏幕进行配置。配置方式与普通屏保一样。
配置屏幕允许用户选择一个包含图片的文件夹列表。当用户关闭配置屏幕时,一个文件将被写入Environment.SpecialFolder.MyPictures
文件夹。该文件仅包含用户选择的所有目录的列表。当用户关闭此窗体时,还会更新一个内部的IList<FileInfo>
,其中包含在这些目录中找到的任何有效图像文件的实例。
使用以下扩展方法可以找到有效文件,这些方法与IEnumerable<FileInfo>
类型配合使用:
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;
}
}
为了更好地理解,我将展示配置屏幕的完整列表。代码量不多,所以不用担心。
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
}
}
我认为这段代码相当自explanatory。这个屏幕的XAML没什么特别之处;有几个用于ScrollViewer
和Button
类型的模板,但这些都是标准的东西。所以,留给读者作为练习。在配置屏幕中使用前面提到的扩展方法的部分是这里:
try
{
foreach (var file in
new DirectoryInfo(folderName).GetFiles().IsImageFile())
{
Globals.Files.Add(file);
}
tw.WriteLine(folderName);
}
catch (DirectoryNotFoundException dex)
{
}
catch (ArgumentException ax)
{
}
在上面显示的DealWithLocationFile()
方法中。你一定会喜欢扩展方法,它们让做一些很酷的事情变得如此容易。
屏保模板
WPF模板我不敢 claiming anything,我在互联网上找到了它,网址是:http://scorbs.com/2006/12/21/wpf-screen-saver-template。
如果您想尝试创建自己的屏保,可以在此链接找到如何使用的完整说明。
代码设计
我已经讨论了配置屏幕,所以不再赘述。这样就只剩下主窗口,也就是实际的屏保。基本思路如下:
- 有一个3D立方体,其表面显示着图像。这些图像是从一组正在使用的图像中随机挑选的。
- 实际使用的图像集是通过从用户在配置屏幕上选择的目录中检索到的所有图像中随机选择一定数量的图像来确定的。
- 如果没有足够的图像来创建一组正在使用的图像,则用嵌入在实际程序集资源中的一个图像来填充该组。我为此选择了一个漂亮的She-Hulk图像。
- 使用一个计时器来发出信号,表明应该为3D立方体选择一张新图像。操作方法如下:
- 从正在使用的图像集中随机选择一个索引,然后用于在3D立方体上显示。
- 一个计数器被递增。当计数器达到正在使用的图像集的大小与此计数器值相同时,就会生成一个新的图像集。
- 屏幕底部有一个小的区域,代表当前正在使用的图像集。当前图像集中的当前图像将获得一个小的
IsSelected
指示器。每当创建一个新的图像集时,这个区域也会被刷新。
我现在将展示其中一些是如何工作的。
3D立方体
在XAML中定义如下:
<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>
然后,3D立方体使用以下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>
图像集
图像集选择如下:
/// <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);
}
}
可以看出,它实际上并没有使用图像来添加到底部的ItemsControl
,而是使用一个SelectableImageUrl
对象。让我们来看看其中一个对象。由于实现了INotifyPropertyChanged
接口,它们是一个简单的可绑定对象。
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
}
}
这意味着我们可以为这种类型的对象创建一个漂亮的XAML DataTemplate
。所以,我正是这样做的,以显示当前选中的一个。下面是添加到代表当前正在使用的窗口对象的ItemsControl
中的这种类型的对象的一个DataTemplate
:
<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>
生成新的图像集
正如我之前所说,有一个动画计时器在运行,当它触发时,将使用图像集中的新图像来显示3D立方体的表面。但这个计时器触发也决定了是否创建新的图像集。如下所示:
/// <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;
}
}
如何在家里使用它
要在家里使用它,只需以Release模式构建附加项目,然后执行以下操作:
- 将生成的EXE复制到方便的位置
- 将EXE重命名为SCR
- 右键单击SCR文件
- 选择“安装”
好了,您现在将拥有一个可用的WPF屏保。尽情享受吧。
一个警告
你们中的一些人可能在“我的图片”文件夹中有成千上万张照片。我从来没有打算让这个屏保需要处理成千上万张图片。尤其是5-7兆像素的相机照片,它们的文件可能非常大。如果你想在这种情况下的屏保中使用它,我强烈建议你修改代码中获取所选目录中所有照片的部分,并将它们存储在全局List<FileInfo>
中。这部分代码位于配置屏幕逻辑中。你可以这样做,比如只选择前100张图片。你可以为此使用一些不错的LINQ。
这篇文章更多地是关于如何用WPF创建屏保。我有大约200张PNG/JPG图片(虽然不是照片),它们加载速度快得惊人。
就是这样
这次就这么多,希望对你们有所帮助。如果喜欢这篇文章,能否请您为它投票?