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

倒计时提醒

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (29投票s)

2010 年 4 月 24 日

CPOL

5分钟阅读

viewsIcon

68937

downloadIcon

2995

创建倒计时器,提醒您即将到来的事件。

CountDownReminder

引言

我一直想写一个小程序,可以倒计时到某个事件的日期和时间。这最初的灵感来自邮局里曾经流行的千年倒计时显示。十年后,我终于开始写了。这里的想法是,你可以创建多个事件,窗口会显示距离该事件还剩多少时间。

有很多选项和可能性可以做到这一点——我最终选择了一个相当简单的方法,但我自己可能需要花两周时间来做功能增强。事实上,我编写的一些显示功能甚至没有在用户界面中暴露为选项。尽管如此,它还是可以做到以下几点:

  • 创建多个倒计时事件。
  • 事件可以按每小时、每天、每周、每月或每年间隔重复。我认为每小时的间隔对于每天需要服三次抗生素的情况很有用。
  • 距离事件三天以上的事件显示为绿色。
  • 距离事件一到三天之间的事件显示为黄色。
  • 距离事件不到 24 小时的事件显示为红色。
  • 已经过去的事件显示为紫色。
  • 事件可以按指定间隔重置或取消。
  • 事件可以通过在列表中上下移动来手动排序。

我立即想到了一些我想要添加的功能:

  • 创建用于颜色、字体、背景等的 UI。
  • 自定义警告级别警报时间。
  • 移除显示中距离未来太远的计数器。
  • 警报声音或其他视觉效果。
  • 公开计数分组显示选项。
  • 自动化分组显示选项——例如,距离 20 天的事件不需要显示小时、分钟和秒。
  • 鼠标悬停可查看更多事件详细信息。
  • 与 Outlook 日历同步。
  • 按事件截止日期排序。
  • 其他显示模式:水平、堆叠、轮播等。

程序

我选择完全忽略公认的标准编程实践,这意味着这里没有 MVC 或 MVVM 模式。大部分工作都在 Program 类的静态方法中完成,而唯一真正的对象是 UI 组件。

CounterFrame 类

所以,我们有一个 CounterFrame。这个框架最有趣的功能是处理当用户右键单击计时器时弹出的菜单事件。

private void setupToolStripMenuItem_Click(object sender, EventArgs e)
{
  new CounterEditor().ShowDialog();
}

private void cancelToolStripMenuItem_Click(object sender, EventArgs e)
{
  Program.RemoveCurrentCounter();
  Program.counterTable.AcceptChanges();
  Program.SaveTable();
  Program.LoadCounters();
}

private void resetToolStripMenuItem_Click(object sender, EventArgs e)
{
  Program.ResetCurrentCounter();
  Program.counterTable.AcceptChanges();
  Program.SaveTable();
}

private void closeApplicationToolStripMenuItem_Click(object sender, EventArgs e)
{
  Close();
}

严格遵循模式的程序员看到静态调用和全局“counterTableDataTable 的使用,一定会气得跳脚!

Counter 类

Counter 类是一个用户控件,它还维护有关目标事件的模型信息。

public partial class Counter : UserControl, ICounter
{
  protected CounterConfiguration config;
  protected DiagnosticDictionary<CounterConfiguration.DisplayOptions, Group> counterGroupMap;
  protected bool showDays;
  protected bool showHours;
  protected bool showMinutes;
  protected bool showSeconds;
  protected bool expired;

  public DateTime TargetEvent { get; set; }
  public CounterConfiguration Config { get { return config; } }

  public Counter(DateTime targetEvent, string descr)
  {
    TargetEvent = targetEvent;
    config = new CounterConfiguration();
    InitializeComponent();
    counterGroupMap = new DiagnosticDictionary<CounterConfiguration.DisplayOptions, Group>();
    CreateCounterGroups();
    CreateDescription(descr);
  }
  ...

计数器的显示是可配置的,它可以显示或不显示四组(天、小时、分钟、秒)的任意组合。此功能目前未在配置 UI 中公开,但它确实有效。

protected void CreateCounterGroups()
{
  int pos = 0;
  int groups=0;
  showDays = ((config.CounterDisplayOptions & 
               CounterConfiguration.DisplayOptions.ShowDays) 
    == CounterConfiguration.DisplayOptions.ShowDays);
  showHours = ((config.CounterDisplayOptions & 
                CounterConfiguration.DisplayOptions.ShowHours) 
    == CounterConfiguration.DisplayOptions.ShowHours);
  showMinutes = ((config.CounterDisplayOptions & 
                  CounterConfiguration.DisplayOptions.ShowMinutes) 
    == CounterConfiguration.DisplayOptions.ShowMinutes);
  showSeconds = ((config.CounterDisplayOptions & 
                  CounterConfiguration.DisplayOptions.ShowSeconds) 
    == CounterConfiguration.DisplayOptions.ShowSeconds);

  foreach (CounterConfiguration.DisplayOptions option in 
    Enumerator<CounterConfiguration.DisplayOptions>.Items().OrderByDescending(x => x))
  {
    Group group = null;

    if ((config.CounterDisplayOptions & option) == option)
    {
      string descr = EnumHelper.GetDescription(option);
      group = new Group(descr);
    }
    else
    {
      group = new EmptyGroup();
    }

    group.Location = new Point(pos, 0);
    pos += group.Width;
    Controls.Add(group);
    counterGroupMap[option] = group;
    ++groups;
  }

  Width = 6 * Group.GroupWidth;
}

事件描述是程序化添加的 Label

protected void CreateDescription(string descr)
{
  Label lblDescr = new Label();
  lblDescr.Text = descr;
  lblDescr.Height = Height;
  lblDescr.Width = Group.GroupWidth * 2;
  lblDescr.Dock = DockStyle.Right;
  lblDescr.BackColor = Color.Black;
  lblDescr.ForeColor = Color.White;
  lblDescr.Font = new System.Drawing.Font("Verdana", 7F, 
                  System.Drawing.FontStyle.Regular, 
                  System.Drawing.GraphicsUnit.Point, ((byte)(0)));
  lblDescr.TextAlign = ContentAlignment.MiddleCenter;

  lblDescr.MouseDown += new MouseEventHandler(Program.OnMouseDown);
  lblDescr.MouseUp += new MouseEventHandler(Program.OnMouseUp);
  lblDescr.MouseMove += new MouseEventHandler(Program.OnMouseMove);

  Controls.Add(lblDescr);
}

请注意,鼠标事件已连接到 Program 类中的静态方法处理程序。这在代码中多次出现,包括在此处和 Group 用户控件中。

每秒钟,都会调用 Counter 实例的 Tick 方法,该方法根据硬编码的逻辑更新显示。

public void Tick()
{
  DisplayStruct disp = GetGroupValues();
  TimeSpan when = TargetEvent - DateTime.Now;
  Color color = Color.Red;

  if (when.TotalDays > 3)
  {
    color = Color.Green;
  }
  else if (when.TotalDays > 1)
  {
    color = Color.Yellow;
  }
  else if (when.TotalSeconds < 0)
  {
    color = Color.Purple;
  }

  Group dayGroup = counterGroupMap[CounterConfiguration.DisplayOptions.ShowDays];
  dayGroup.Value = disp.days;
  dayGroup.UpdateDisplay("G", color);

  Group hourGroup = counterGroupMap[CounterConfiguration.DisplayOptions.ShowHours];
  hourGroup.Value = disp.hours;
  hourGroup.UpdateDisplay(disp.hourFormat, color);

  Group minuteGroup = counterGroupMap[CounterConfiguration.DisplayOptions.ShowMinutes];
  minuteGroup.Value = disp.minutes;
  minuteGroup.UpdateDisplay(disp.minuteFormat, color);

  Group secondGroup = counterGroupMap[CounterConfiguration.DisplayOptions.ShowSeconds];
  secondGroup.Value = disp.seconds;
  secondGroup.UpdateDisplay(disp.secondFormat, color);
}

这里有趣的是,显示格式会根据是否缺少更高阶的组进行调整。通常,除了天数之外,所有组都使用“D2”格式显示为两位数。但是,如果缺少一个组,则下一个组必须处理溢出。因此,如果小时不显示,分钟就必须显示分钟 + 小时 * 60。这会超出两位数的格式,因此代码会将显示格式更改为“G”。目前,你肯定会超出 Group 控件的宽度,这就是为什么我没有将此功能“公开”到 UI 中。

Group 类

Group 类是一个用户控件,包含两个控件:描述组的 Label 和计数器值的 Label。鼠标事件已连接以捕获发生在这些控件上的鼠标点击。

public partial class Group : UserControl
{
  public static int GroupWidth = 50;

  public int Value { get; set; }

  public Group(string header)
  {
    InitializeComponent();
    lblGroup.Text = header;
    Width = GroupWidth;

    lblGroup.MouseDown += new MouseEventHandler(Program.OnMouseDown);
    lblGroup.MouseUp += new MouseEventHandler(Program.OnMouseUp);
    lblGroup.MouseMove += new MouseEventHandler(Program.OnMouseMove);

    lblCounter.MouseDown += new MouseEventHandler(Program.OnMouseDown);
    lblCounter.MouseUp += new MouseEventHandler(Program.OnMouseUp);
    lblCounter.MouseMove += new MouseEventHandler(Program.OnMouseMove);
  }

  public virtual void UpdateDisplay(string format, Color foreColor)
  {
    lblCounter.Text = Value.ToString(format);
    lblCounter.ForeColor = foreColor;
  }
}

数据模型

使用一个按 Index 列排序的 DataView 来管理计数器。这与使用计数器编辑器 DataGridView 非常契合。

后备 DataTable 是用上面显示的列创建的(除了“Index”,它是隐藏的)。

private static void CreateTable()
{
  counterTable = new DataTable("Counters");
  counterTable.Columns.Add("Index", typeof(Int32));
  counterTable.Columns.Add("TargetDate", typeof(DateTime));
  counterTable.Columns.Add("Repeat", typeof(bool));
  counterTable.Columns.Add("RepeatInterval", typeof(Interval.IntervalOption));
  counterTable.Columns.Add("IntervalAmount", typeof(Int32));
  counterTable.Columns.Add("Description", typeof(string));

  counterView = new DataView(Program.counterTable);
  counterView.Sort = "Index";
}

该表使用 DataTable 的 XML 序列化功能进行序列化和反序列化。

private static void LoadTable()
{
  if (File.Exists("counters.xml"))
  {
    counterTable.ReadXml("counters.xml");
  }
}

public static void SaveTable()
{
  counterTable.WriteXml("counters.xml", XmlWriteMode.WriteSchema);
}

其他特定于窗体的附加信息(位置和始终置顶标志)将序列化到另一个文件中。

public static void LoadFormPosition()
{
  if (File.Exists("CounterConfig.txt"))
  {
    StreamReader sr = new StreamReader("CounterConfig.txt");
    string onTop = sr.ReadLine();
    string x = sr.ReadLine();
    string y = sr.ReadLine();
    sr.Close();

    alwaysOnTop = Convert.ToBoolean(onTop);
    counterFrame.Location = new Point(Convert.ToInt32(x), Convert.ToInt32(y));
    counterFrame.TopMost = alwaysOnTop;
  }
}

public static void SaveFormPosition()
{
  StreamWriter sw = new StreamWriter("CounterConfig.txt");
  sw.WriteLine(alwaysOnTop.ToString());
  sw.WriteLine(counterFrame.Location.X);
  sw.WriteLine(counterFrame.Location.Y);
  sw.Close();
}

程序启动

程序启动将所有内容整合在一起。如果在启动时没有事件,应用程序将首先打开事件编辑器,然后启动主应用程序。

static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  counterFrame = new CounterFrame();
  CreateTable();
  LoadTable();

  Timer timer = new Timer();
  timer.Tick += new EventHandler(OnTick);
  timer.Interval = 1000;
  timer.Start();

  if (counterTable.Rows.Count == 0)
  {
    new CounterEditor().ShowDialog();

    if (counterTable.Rows.Count > 0)
    {
      LoadCounters();
      Application.Run(counterFrame);
    }
  }
  else
  {
    LoadCounters();
    Application.Run(counterFrame);
  }
}

字体

我目前硬编码了一个 LED 字体,我在 这里找到的,作者是 Sizenko Alexander,来自“Style-7”。这是一款免费字体,我已将其包含在可执行文件下载中。

结论

写这个项目对我来说,有趣之处在于我在哪些地方投入了精力进行一些架构方面的考虑,以及在哪些地方我决定保持事物非常简单。例如,用户控件和计数器配置的内部机制,我认为架构得很好。选择将大多数控件逻辑实现为静态方法是因为不需要更复杂的东西。在添加更复杂的功能之前,我可能会回去清理模型-视图之间的纠缠。

延伸阅读

这绝对是题外话,但鉴于截图,这里是我最近喜欢的一些事物的链接。

© . All rights reserved.