PersianDate 和一些 WPF 控件






4.83/5 (40投票s)
波斯日期类型,以及两个用于处理波斯日期的 WPF 控件(波斯日历和波斯日期选择器)
引言
源代码包含三个主要项目:*PersianDate*,它是一个用于存储波斯日历值的类型(实际上是结构);*PersianDateControls*,它包含两个用于波斯日历的 WPF 控件:PersianCalendar
和 PersianDatePicker
(这些控件与 WPF 控件库中的 Calendar
和 DatePicker
非常相似;这两个控件使用 PersianDate
类型来处理波斯日历的值);第三个项目是一个简单的演示,展示了如何使用这些控件。
关于波斯历
波斯历是一种太阳历,与公历相似,但存在一些差异。一个差异是历法的起始点不同,波斯历的起始点比公历晚约 621 年;另一个差异是波斯历的每年第一天是 3 月 21 日;而可能最重要的一点是,波斯历年的平均长度与公历不同:波斯历每 33 年有 8 个闰年(即比普通年份多一天的年份),而公历每 32 年有 8 个闰年。这个微小的差异意味着波斯日期无法直接从公历日期计算得出。
PersianDate 结构
PersianDate
结构存储波斯历的日期。它在某种程度上类似于 .NET Framework 类库中的 DateTime
结构,不同之处在于 PersianDate
只存储日期,而不存储时间。该结构只有一个字段,用于存储自波斯历第一年第一天(1/1/1)以来经过的总天数。
uint n; //the only field, stores the number of days passed 1/1/1
年份、月份和日期的计算都基于这个单一值。private yearMonthDay()
方法接收这个数字,并返回由该数字表示的日、月、年。为此,日期被划分为一些组。
const int period33y = 365 * 33 + 8;
const int p33p1 = 366;
const int p33p2 = 365 * 20 + 4;
const int p33p3 = 366;
const int p33p4 = 365 * 11 + 2;
第一个常量(period33y
)是每 33 年的总天数;这被分为四组:每组的年天数相同。例如,如果 n % period33y
是 400,这些类别意味着该日期属于 p33p2
组,因为它大于 p33p1
且小于等于 p33p1+p33p2
,这意味着该日期不在闰年,通过一些相对简单的计算,就可以提取出该日期的年份。之后,日期中的日和月部分在该方法中被提取出来。
在根据年份、月份和日期计算 n 时,也使用了这些常量。days()
方法就是做这个的。该方法用于结构的一个构造函数中,该构造函数以年、月、日作为参数。
关于 PersianDate 的几点注意事项
PersianDate
是一个结构,而不是类,这是出于性能原因。如前所述,该结构只有一个 4 字节的字段,因此将其设为结构而不是类更为合理。由于类对象的访问是间接的,而对象的引用至少有 4 字节长,这与数据本身的大小相同,因此将此类型设为引用类型似乎不合理。
该类型是不可变的,这是推荐的结构创建方式。这是因为可变结构在某些情况下会有奇怪的行为。例如,假设该类型是可变的,并且有一个 AddDays()
方法,该方法将作为参数提供的天数添加到实例中。现在,假设有一个名为 DateField
的 PersianDate
类型字段的 Foo
类。
Foo foo=new Foo();
foo.DateField=new PersianDate(1376,2,22);
foo.DateField.AddDays(12);
System.Console.WriteLine(foo.dateField.ToString());
输出将是 1376/3/2,这可能是您预期的。现在,假设这个 Foo
类还有一个名为 dateProperty
的 PersianDate
类型属性,并且它是自动实现的。
class Foo{
public PersianDate DateProperty{get; set;}
...
}
如果您编写的代码与您为 DateField
编写的代码类似……
Foo foo=new Foo();
foo.DateProperty=new PersianDate(1376,2,22);
foo.DateProperty.AddDays(12);
System.Console.WriteLine(foo.DateProperty.ToString());
……输出将是 1376/2/22!原因在于,由于 PersianDate
是值类型,dateProperty
的 getter 将返回存储值的副本,而 AddDays
方法将修改这个副本,而不是实际的后备字段;这就是为什么属性的值没有改变。
所以,这就是为什么 PersianDate
是不可变的原因。
PersianCalendar 类
PersianCalendar
是一个 WPF 用户控件。在 WPF 中,用户控件继承自 System.Windows.Controls.UserControl
基类。这个 WPF 控件代表波斯历,与 Calendar
控件(System.Windows.Controls.Calendar
)非常相似。与 WPF 的 Calendar
一样,它也有一个名为 DisplayMode
的属性,可以用来选择日历的显示方式:显示年份的十年、年份的月份,还是月份的日期。
此控件使用 PersianDate
类型来处理波斯历,因此 DisplayDate
或 SelectedDate
等属性都是此类型。
我使用了 WPF 的 UniformGrid
控件作为容器控件,在 PersianCalendar
中排列日期按钮。
<UniformGrid Margin="3,26,3,2" Name="monthUniformGrid"
Rows="7" Columns="7" FlowDirection="RightToLeft"/>
<UniformGrid Margin="3,26,3,2" Name="yearUniformGrid"
Columns="3" Rows="4" FlowDirection="RightToLeft"/>
<UniformGrid Margin="3,26,3,2" Name="decadeUniformGrid"
Columns="3" Rows="4" FlowDirection="RightToLeft"/>
其中一个用于 DisplayMode
的三个值(月、年、十年)。例如,当 DisplayMode
设置为 Month
时,将显示 monthUniformGrid
,而其他两个则会折叠(即隐藏,不占用空间)。
private void setMonthMode() { this.decadeUniformGrid.Visibility = this.yearUniformGrid.Visibility = Visibility.Collapsed; this.monthUniformGrid.Visibility = Visibility.Visible; ... }
使用样式和模板自定义控件的外观
为了使 PersianCalendar
正常工作,这些 UniformGrid
必须填充显示日期(或月份或十年)的控件。Label
不是一个好的选择,因为它没有 Click
事件,所以我使用了 Button
;但 Button
的外观似乎不太适合此目的,所以我使用样式和模板来更改它。
<Style x:Key="InsideButtonsStyle" TargetType="Button">
...
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Border" CornerRadius="2"
BorderThickness="0"
Background="{TemplateBinding Background}"
BorderBrush="{StaticResource NormalBorderBrush}">
<ContentPresenter Margin="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
...
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="Border"
Property="Background"
Value="{StaticResource HoverBackgroundBrush}" />
<Setter Property="Foreground"
Value="{StaticResource HoverForegroundBrush}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="false">
<Setter Property="Foreground"
Value="{StaticResource HoverForegroundBrush}" />
</Trigger>
...
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
在 ControlTemplate
内部,使用 Trigger
来改变按钮的外观,例如,当鼠标悬停在按钮上时,等等。
我使用 Template Binding 将 Button
的 Background
属性值绑定到模板的视觉树元素 Border
的 Background
属性。
这个样式连同其中定义的模板,在创建按钮时被应用到按钮上。
Button newControl()
{
var element = new Button
{
...
Style = (Style)this.FindResource("InsideButtonsStyle"),
...
};
return element;
}
依赖属性和路由事件
PersianCalendar
类中定义的所有属性都是依赖属性,事件都是 RoutedEvent
;这是 WPF 推荐的用户控件创建方式。将属性设置为依赖属性需要其 get 和 set 访问器中不包含额外的代码,因此每当需要对 PersianCalendar
对象进行任何验证或修改时,都会使用 PropertyMetaData
。例如,以 SelectedDate
属性为例。
public PersianDate SelectedDate
{
get { return (PersianDate)GetValue(SelectedDateProperty); }
set { SetValue(SelectedDateProperty, value); }
}
public static readonly DependencyProperty SelectedDateProperty;
如您所见,setter 只做的是为后备字段(当然是 DependencyProperty
类型)设置值。额外的逻辑放在属性的元数据中(通过在我的代码中使用 lambda 表达式)在类的 static
构造函数中。
static PersianCalendar()
{
...
PropertyMetadata selectedDateMetaData = new PropertyMetadata(
(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
{
PersianCalendar pc = d as PersianCalendar;
pc.selectedDateCheck((PersianDate)e.OldValue);
}
);
SelectedDateProperty=
DependencyProperty.Register("SelectedDate",
typeof(PersianDate), typeof(PersianCalendar), selectedDateMetaData);
...
}
PersianDatePicker 类
它实际上并没有做什么神奇的事情。这个控件只是使用一个 TextBox
和一个 PersianCalendar
,并将 PersianCalendar
放置在 Popup
控件中,以便在单击相应的按钮时在另一个窗口中显示它。但这个类有一个值得注意的地方,那就是使用数据绑定将该类的属性与 PersianCalendar
的属性连接起来。以下代码展示了如何使用此功能来绑定 SelectedDate
属性。
Binding selectedDateBinding = new Binding
{
Source = this,
Path = new PropertyPath("SelectedDate"),
Mode = BindingMode.TwoWay,
};
this.persianCalendar.SetBinding(PersianCalendar.SelectedDateProperty, selectedDateBinding);
使用这种技术使我无需编写所有混乱的代码来保持这些属性同步。
如何使用代码
如果您只想使用 PersianDate
,您可以将其项目添加到您的解决方案中,或者生成它并引用程序集,或者下载演示并引用 *PersianDate.dll*。
如果您想使用 WPF 控件,您应该将两个项目(PersianDate
和 PersianDateControls
)添加到您的解决方案中,或者同时生成它们并都引用它们,或者下载演示并引用 *PersianDate.dll* 和 *PersianDateControls.dll*。
历史
- 版本 1.1
- 删除了
PersianDateControl
对 WPFToolkit 程序集的依赖(实际上,它只从该程序集使用了CalendarMode
枚举,该枚举现在在项目中的 *CalendarMode.cs* 代码文件中),以及其他一些小的改动(请注意,如果您想生成和运行演示项目,仍然需要 WPFToolkit)。 - 版本 1.2
- 进行了一些小的调整,包括解决了制表符顺序问题。
请注意,此版本与先前版本不兼容,因为TodayBackGround
已更改为TodayBackground
,SelectedDateBackGround
已更改为SelectedDateBackground
。 - 版本 2.0
- 现在以 .NET 4 为目标。
- 修复了
PersianDatePicker
中的数据绑定 bug。 - 添加了用于测试 PersianDate 和 PersianDateControls 的项目。
- 更改了演示项目,使用数据绑定在
Calendar
和PersianCalendar
、DatePicker
和PersianDatePicker
之间绑定值。 - 其他一些小的改动。