65.9K
CodeProject 正在变化。 阅读更多。
Home

PersianDate 和一些 WPF 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (40投票s)

2009年11月4日

Ms-PL

7分钟阅读

viewsIcon

116805

downloadIcon

12768

波斯日期类型,以及两个用于处理波斯日期的 WPF 控件(波斯日历和波斯日期选择器)

引言

源代码包含三个主要项目:*PersianDate*,它是一个用于存储波斯日历值的类型(实际上是结构);*PersianDateControls*,它包含两个用于波斯日历的 WPF 控件:PersianCalendarPersianDatePicker(这些控件与 WPF 控件库中的 CalendarDatePicker 非常相似;这两个控件使用 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() 方法,该方法将作为参数提供的天数添加到实例中。现在,假设有一个名为 DateFieldPersianDate 类型字段的 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 类还有一个名为 datePropertyPersianDate 类型属性,并且它是自动实现的。

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 类型来处理波斯历,因此 DisplayDateSelectedDate 等属性都是此类型。

 我使用了 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 将 ButtonBackground 属性值绑定到模板的视觉树元素 BorderBackground 属性。

这个样式连同其中定义的模板,在创建按钮时被应用到按钮上。

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 控件,您应该将两个项目(PersianDatePersianDateControls)添加到您的解决方案中,或者同时生成它们并都引用它们,或者下载演示并引用 *PersianDate.dll* 和 *PersianDateControls.dll*。

历史

  • 版本 1.1
    • 删除了 PersianDateControl 对 WPFToolkit 程序集的依赖(实际上,它只从该程序集使用了 CalendarMode 枚举,该枚举现在在项目中的 *CalendarMode.cs* 代码文件中),以及其他一些小的改动(请注意,如果您想生成和运行演示项目,仍然需要 WPFToolkit)。
  • 版本 1.2
    • 进行了一些小的调整,包括解决了制表符顺序问题。
      请注意,此版本与先前版本不兼容,因为 TodayBackGround 已更改为 TodayBackgroundSelectedDateBackGround 已更改为 SelectedDateBackground
  • 版本 2.0
    • 现在以 .NET 4 为目标。
    • 修复了 PersianDatePicker 中的数据绑定 bug。
    • 添加了用于测试 PersianDate 和 PersianDateControls 的项目。
    • 更改了演示项目,使用数据绑定在 CalendarPersianCalendarDatePickerPersianDatePicker 之间绑定值。
    • 其他一些小的改动。
© . All rights reserved.