二进制时钟是一种以二进制格式显示当前时间的时钟。在本文中,将创建一组图形化的LED灯,每个LED灯代表一个二进制位。每个LED灯有两种状态:亮起(表示1的值)或熄灭(表示0的值)。从右到左,LED灯将代表的值依次为1、2、4、8、16、32,因为将基于24小时制的时间进行转换,并且需要足够的位数来表示最大值为60的十进制数(用于分钟和秒)。
当前时间的每个部分(小时、分钟、秒)都将拥有自己的六LED灯行,以表示十进制值的二进制转换。例如,如果想要显示时间10:33:42,LED灯必须按照以下模式点亮:
在XAML页面中实现上述概念相当简单。需要创建三行矩形,它们的边框半径设置为50,以赋予它们圆形的外观。其他设置将涉及填充颜色、形状阴影等,以绘制想要的LED灯。在示例中,LED灯将根据以下XAML代码进行阴影处理和着色:
<Rectangle
HorizontalAlignment="Left"
Height="35"
Margin="211,40,0,0"
Stroke="#FF033805"
VerticalAlignment="Top"
Width="38"
RadiusX="50"
RadiusY="50"
>
<Rectangle.Effect>
<DropShadowEffect
BlurRadius="10"
ShadowDepth="10"
/>
</Rectangle.Effect>
<Rectangle.Fill>
<LinearGradientBrush
EndPoint="0.5,1"
StartPoint="0.5,0"
>
<GradientStop
Color="#FFFFFF1B"
Offset="0"
/>
<GradientStop
Color="#FF29B413"
Offset="0.568"
/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
一旦为UI创建了每一行,并进行了装饰,XAML页面将如下所示:
可以参考文章末尾的下载部分,获取上述代码片段的完整参考。
下面的部分解释了矩形如何被控制以显示当前时间的二进制表示。
在XAML窗口中,声明了一个事件调用——更确切地说,是一个在页面加载时必须触发的事件(Loaded事件)。在Window_Loaded例程的代码后台中,执行两个主要操作:第一个仅仅是图形化的,包括将每个矩形的不透明度设置为0.35,以给人一种LED灯关闭的印象。第二个是执行任务,该任务将执行计算并更新UI。稍后将对此进行更多说明。首先,让看看如何识别在XAML页面上声明的控件。
让来看一下将所有矩形的不透明度设置为0.35的循环:
// Sets all rectangles opacity to 0.35
foreach (
var r in LogicalTreeHelper.GetChildren(MainGrid)
)
{
if (r is Rectangle) (r as Rectangle).Fill.Opacity = 0.35;
}
谈到识别控件,WinForms和WPF之间的主要区别在于不能使用属性Controls()来引用容器的控件。WPF执行此类操作的方式是通过LogicalTreeHelper类。通过它,可以调用GetChildren方法,指定要检索子控件的主控件的名称。在案例中,在控制MainGrid(标识XAML页面的Grid对象的名称)上执行了LogicalTreeHelper.GetChildren。然后,在遍历控件数组时,检查该特定控件是否为Rectangle——如果是的话——将设置其不透明度为期望的值。
Window_Loaded事件的第二组指令是执行一个辅助任务,用于计算每个时间部分的二进制表示,并更新UI。代码如下:
Task.Factory.StartNew(() =>
{
// while the thread is running...
while (true)
{
// ...get the current system time
DateTime _now = System.DateTime.Now;
// Convert each part of the system time (i.e.: hour, minutes, seconds) to
// binary, filling with 0s up to a length of 6 char each
String _binHour = Convert.ToString(_now.Hour, 2).PadLeft(6, '0');
String _binMinute = Convert.ToString(_now.Minute, 2).PadLeft(6, '0');
String _binSeconds = Convert.ToString(_now.Second, 2).PadLeft(6, '0');
// For each digit of the binary hour representation
for (int i = 0; i <= _binHour.Length - 1; i++)
{
Dispatcher.Invoke(() =>
{
// Update the contents of the labels which use decimal h/m/s representation
lbHour.Content = _now.Hour.ToString("00");
lbMinute.Content = _now.Minute.ToString("00");
lbSeconds.Content = _now.Second.ToString("00");
// Search for a rectangle which name corresponds to the _binHour current char index.
// Then, set its opacity to 1 if the current _binHour digit is 1, or to 0.35 otherwise
(MainGrid.FindName("H" + i.ToString()) as Rectangle).Fill.Opacity =
_binHour.Substring(i, 1).CompareTo("1") == 0 ?
1 :
0.35;
(MainGrid.FindName("M" + i.ToString()) as Rectangle).Fill.Opacity =
_binMinute.Substring(i, 1).CompareTo("1") == 0 ?
1 :
0.35;
(MainGrid.FindName("S" + i.ToString()) as Rectangle).Fill.Opacity =
_binSeconds.Substring(i, 1).CompareTo("1") == 0 ?
1 :
0.35;
});
}
}
});
任务相当直观,它包含一个永不停止的循环,不断地检索当前系统时间。然后,它将其分解为三个主要部分(小时、分钟、秒),并通过使用Convert.ToString()函数将其转换为二进制表示,将传递转换的数字基数(在例子中是2)。由于需要三个长度等于六的字符串(每行有六个LED灯),需要将每个字符串填充到六个字符。因此,例如,如果正在转换值5,函数将产生101作为输出——将填充为000101。
第二个循环将工作到与小时相关的二进制字符串的长度(一个永远是六的值),将提供UI更新,使用Dispatcher属性Invoke在另一个线程上运行的对象的update方法(有关Invoke方法的更多详细信息,请参阅“VB.NET:Invoke方法从辅助线程更新UI”)。对于字符串中的每个数字,需要识别正确的Rectangle,以更新其不透明度值。
可以通过FindName()函数完成这类任务:给定一个父对象(在案例中是MainGrid),FindName将引用一个UI控件,如果传递的参数对应于一个存在的控件名称。由于为每个时间部分的Rectangles命名了一个递增的数字(从0到5,以H开头表示小时,M表示分钟,S表示秒),可以要求函数检索一个名称以特定字母开头,并继续与当前二进制字符串索引相等的索引的控件。
让看一个这些行中的一个,以清楚地说明:以下行:
(MainGrid.FindName("H" + i.ToString()) as Rectangle).Fill.Opacity =
_binHour.Substring(i, 1).CompareTo("1") == 0 ?
1 :
0.35;