WPF 中的二进制时钟 3D 屏幕保护程序






4.88/5 (18投票s)
3D 二进制时钟屏保 - 展示 WPF 的布局可能性、样式/模板化和数据绑定

目录
引言
虽然您可能在网上找到许多二进制时钟项目,但本文将不仅向您展示如何创建二进制时钟,还将展示如何将其托管在太空中并用作 3D 旋转屏保 - 所有这些都利用了 Windows Presentation Foundation 及其强大的概念,如数据绑定、数据模板、样式、自动布局、动画和 3D 支持。
这个二进制时钟项目最初只是我为了帮助自己理解上述 WPF 概念而进行的一次实验。因此,您在这里看到的也是我 WPF 学习过程的一部分,我认为这对于该技术和其他爱好者也可能很有用。
背景
也许您已经知道 二进制时钟 的概念:它是一种时钟,您将当前时间的 6 位数字读取为 6 列,每列由一些点亮的和未点亮的 LED 组成。实际上,我们最好称之为伪二进制时钟,因为它以 BCD(二进制编码的十进制)表示法显示数字。如果您喜欢这个概念,但还没有在您的桌面上拥有它,那么至少可以通过下载附件中的二进制文件将其作为您的屏保。
核心 - 二进制时钟
首先,二进制时钟有一个简单的任务:逐一显示其六位数字。其次,数字本身甚至不需要了解它们的视觉表示,因此,要表示一个 BCD 数字,只需有一个纯数据类就足够了。显示数据类的想法是应用一个 DataTemplate
。这样,我们就可以简单地将 Digit
类的六个实例放入我们窗口的某个项目容器中,并将其余的外观工作留给我们的 DataTemplate
来完成。
这看起来是该模板的基本结构
<DataTemplate DataType="{x:Type local:Digit}">
<DataTemplate.Resources>
...
</DataTemplate.Resources>
<!-- BCD Digits consist of 4 bits -->
<StackPanel Orientation="Vertical">
<Rectangle Style="{StaticResource fourthBit}" />
<Rectangle Style="{StaticResource thirdBit}" />
<Rectangle Style="{StaticResource secondBit}" />
<Rectangle Style="{StaticResource firstBit}" />
</StackPanel>
</DataTemplate>
每个 BCD 数字包含四个二进制数字/位。它们堆叠在一起,最低有效位位于底部,最高有效位位于顶部。
应用于特定位的样式会告诉该位在特定时间是否需要点亮或熄灭。例如,最低有效位需要点亮,如果 BCD 数字的模 2 余数非零(也就是说,每隔一秒)。同样,最低有效位上方的位需要点亮,如果 BCD 数字的模 4 余数大于等于 2,依此类推。这个逻辑可以通过使用 WPF 值转换器来概括,该转换器内部包含一些简单的模运算。DigitModuloConverter
类将告诉我们任何整数值的指定模数是否大于提供的参数。
class DigitModuloConverter : DependencyObject, IValueConverter
{
public static DependencyProperty ModuloModeProperty = ...
public int ModuloMode { ... }
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
int source = (int)value;
int param = 0;
Int32.TryParse(parameter as string, out param);
return (source % this.ModuloMode > param);
}
...
}
有了这个转换器,我们就可以直接将 BCD 数字的值转换为其位的开/关状态。这是通过 WPF 的数据触发器实现的:每当指定的 DigitModuloConverter
告知时,每个位将使用高亮填充和高亮描边颜色。否则,它将保持或切换回基本的暗色。
<DataTemplate.Resources>
<!-- Digit converters -->
<local:DigitModuloConverter x:Key="moduloTwoConverter" ModuloMode="2" />
<local:DigitModuloConverter x:Key="moduloFourConverter" ModuloMode="4" />
<local:DigitModuloConverter x:Key="moduloEightConverter" ModuloMode="8" />
<local:DigitModuloConverter x:Key="moduloSixteenConverter" ModuloMode="16" />
...
<!-- Appearance of 1st bit in Digit (the least significant one) -->
<Style TargetType="Rectangle" x:Key="firstBit"
BasedOn="{StaticResource bitsCommonStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Value,
Converter={StaticResource moduloTwoConverter}}"
Value="True">
<Setter Property="Stroke"
Value="{DynamicResource fullRectBorderBrush}" />
<Setter Property="Fill" Value="{DynamicResource fullRectBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
...
</DataTemplate.Resources>
设置好所有这些之后,只需要一个定时器即可每秒更新我们的 BCD 数字值。BCD 数字 - 即 Digit
类的实例 - 在 XAML 中声明,并在 setTimeToDigits()
函数中进行更新。
private void setTimeToDigits()
{
System.DateTime now = System.DateTime.Now;
this.hourUpper.Value = (int)now.Hour / 10;
this.hourLower.Value = now.Hour % 10;
this.minUpper.Value = (int)now.Minute / 10;
this.minLower.Value = now.Minute % 10;
this.secUpper.Value = (int)now.Second / 10;
this.secLower.Value = now.Second % 10;
}
Digit 实例需要正确设置其 IsUpperDigit
和 IsHourDigit
属性。位的样式会读取这些属性并根据它们隐藏一些不必要的位。例如,“小时”的“十位”BCD 数字将隐藏最高两位,因为一天只有 24 小时。同样,“分钟”的“十位”BCD 数字将隐藏最高有效位,因为即使显示 59 分钟,我们只需要 3 位来表示“十位”。
<StackPanel ...>
<StackPanel ...>
<ContentControl>
<local:Digit x:Name="hourUpper" IsUpperDigit="True" IsHourDigit="True"/>
</ContentControl>
<ContentControl>
<local:Digit x:Name="hourLower" IsHourDigit="True" />
</ContentControl>
</StackPanel>
<StackPanel ...>
<ContentControl>
<local:Digit x:Name="minUpper" IsUpperDigit="True" />
</ContentControl>
<ContentControl>
<local:Digit x:Name="minLower" />
</ContentControl>
</StackPanel>
<StackPanel ...>
<ContentControl>
<local:Digit x:Name="secUpper" IsUpperDigit="True" />
</ContentControl>
<ContentControl>
<local:Digit x:Name="secLower" />
</ContentControl>
</StackPanel>
</StackPanel>
3D 屏保和带实时预览的配置
Windows 屏保基本上需要一个接受一些定义良好的命令行参数的 *.exe 模块。屏保主要需要显示一个全屏窗口,但也应该支持预览模式并提供一个可选的设置窗口进行自定义。
本项目更进一步:它不仅支持简单的预览模式,还在设置窗口中提供了实时预览,以便交互式地显示每个更改将如何影响输出。
这可以通过重用 BinaryClockPage
类来实现,该类提供了二进制时钟的核心功能(请参阅上一节)。

时钟的 3D 效果是通过将 Digit
类实例添加到 ViewPort3D
对象来实现的。这样,我们将 2D Rectangles
投影到空间中的 3D 曲面上。
<Viewport3D>
...
<!-- 2D elements on 3D surface -->
<Viewport2DVisual3D>
<!-- Give the plane a slight rotation -->
<Viewport2DVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="planeRotation"
Angle="0"
Axis="0, -1, -0.17" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Viewport2DVisual3D.Transform>
<!-- The Geometry, Material, and Visual for the Viewport2DVisual3D -->
<Viewport2DVisual3D.Geometry>
<MeshGeometry3D ... />
</Viewport2DVisual3D.Geometry>
<Viewport2DVisual3D.Material>
<DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True"
Brush="White"/>
</Viewport2DVisual3D.Material>
...
</Viewport2DVisual3D>
...
</Viewport3D>
二进制时钟绕垂直轴进行旋转动画。可以通过设置窗口中的简单滑块调整此旋转速度。另一个滑块用于设置相机距离,从而使二进制时钟看起来更近或更远。
每次移动旋转速度滑块时,都需要重新启动旋转动画以应用新的动画持续时间。
多显示器支持
屏保将以非常简单的方式处理您系统中的多个显示器:它将在每个显示器上复制相同的内容。如果您注意到这些不同实例中的时钟并不总是同步,那么是的,您是完全正确的。这种视觉上的副作用是由于每个时钟实例都使用自己的计时器,该计时器仅在创建前一个实例后启动。这个问题将来可能会在文章的更新中得到解决。
皮肤支持
BinaryClock 屏保的皮肤化利用了 WPF 的 ResourceDictionary
概念。这允许我们在运行时更改皮肤。每个“皮肤”都放置在一个单独的 XAML 文件中,其中包含一个 ResourceDictionary
,因此我们可以在运行时切换使用其中任何一个 XAML 文件。每个 ResourceDictionary
定义了相同的画笔对象,但颜色设置不同。因此,通过在运行时加载不同的 ResourceDictionary
,我们的二进制时钟颜色将自动切换到新皮肤(这对我们来说是完全透明的,因为我们将这些画笔引用为 DynamicResource
对象)。
定义“蓝色”皮肤的 XAML 文件代码片段
<ResourceDictionary ...>
<!-- Unlit LED border color -->
<SolidColorBrush x:Key="emptyRectBorderBrush" Color="#FF00087F" />
...
</ResourceDictionary>
BinaryClock 如何使用皮肤元素
<!-- Basic appearance of all Digits -->
<Style TargetType="Rectangle" x:Key="bitsCommonStyle">
<Setter Property="Stroke" Value="{DynamicResource emptyRectBorderBrush}" />
...
</Style>
未来的增强
如果您喜欢这个屏保并对改进有一些建议,请随时告知我,因为我仍然计划在未来添加一些新功能。这些功能可能包括用于调整 3D 效果的新设置、显示可读十进制提示的可能性,以及错误修复等。
历史
- 2009 年 1 月 18 日:皮肤支持
- 2008 年 12 月 23 日:根据读者评论,对多显示器系统的小错误修复
- 2008 年 12 月 22 日:初始修订