二进制时钟的设计与实现

二进制时钟是一种以二进制格式显示当前时间的时钟。在本文中,将创建一组图形化的LED灯,每个LED灯代表一个二进制位。每个LED灯有两种状态:亮起(表示1的值)或熄灭(表示0的值)。从右到左,LED灯将代表的值依次为1、2、4、8、16、32,因为将基于24小时制的时间进行转换,并且需要足够的位数来表示最大值为60的十进制数(用于分钟和秒)。

当前时间的每个部分(小时、分钟、秒)都将拥有自己的六LED灯行,以表示十进制值的二进制转换。例如,如果想要显示时间10:33:42,LED灯必须按照以下模式点亮:

XAML中的二进制时钟

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;
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485