Click here to Skip to main content
Click here to Skip to main content

Countdown Reminders

, 24 Apr 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Create countdown timers to remind you of upcoming events.

CountDownReminder

Introduction

I've been wanting to write an applet that would count down to an event's date and time. This was originally inspired by the millennium countdown displays that you could find in the post office. Ten years later, I'm finally writing something. The idea here is that you can create multiple events, and the window shows the time remaining to that event.

There are many options and possibilities to how to go about doing this--I finally settled on something fairly simple, and yet I could probably spend a couple of weeks just doing feature enhancements. In fact, there are actually display features that I coded that I haven't even exposed as options in the user interface. Regardless, here's what it does:

  • Create multiple countdown events.
  • Events can repeat at hourly, daily, weekly, monthly, or yearly intervals. I figure hourly would be useful for when you have to take some antibiotic three times a day.
  • Events more than three days away are displayed in green.
  • Events between one and three days away are displayed in yellow.
  • Events less than 24 hours away are displayed in red.
  • Events that have passed are displayed in purple.
  • Events can be reset at a specified interval or cancelled.
  • Events can be manually organized by moving them up and down in the list.

Right off the bat, I can see a few things I'd like to add:

  • Create a UI for colors, fonts, background, etc.
  • Customize the warning level alert times
  • Remove counters from the display that are too far in the future
  • Alert sounds or other visual effects
  • Expose the count group display options
  • Automate the group display options--for example, something 20 days away doesn't need to show hours, minutes, and seconds
  • Mouse over for more event detail information
  • Synchronize with Outlook Calendar
  • Sort by event due date
  • Other display modalities: horizontal, stacked, carousel, etc.

The Program

I chose to completely ignore what is considered to be standard programming practices, meaning there is no MVC or MVVM pattern here. Most of the work is done in static methods of the Program class, and the only real objects are the UI components:

CounterFrame Class

So, we have a CounterFrame. The most interesting thing the frame does is handle the events from the popup menu when the user right-clicks on the timers:

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();
}

Purists will be rolling over in their graves at the static calls and the use of the global "counterTable" DataTable!

Counter Class

The Counter class is a user control that also maintains model information regarding the target event:

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);
  }
  ...

The counter display is configurable, in the sense that any combination of the four groups (days, hours, minutes, seconds) can be shown or not. This feature is not exposed in a configuration UI right now, but it does work.

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;
}

The event description is a Label added programmatically:

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);
}

Notice the mouse events are wired up to static method handlers in the Program class. This occurs several times in the code, both here and in the Group user control.

Every second, the Tick method of the Counter instance is called, which updates the display according to hard-coded logic:

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);
}

The interesting thing here is that the display format is adjusted based on whether the higher-order group is missing. Normally, except for days, all groups display as two digit values using the "D2" format. However, if a group is missing, the following group has to pick up the overflow. So if hours are not displayed, minutes has to display minutes + hours * 60. This exceeds the two digit format, so the code changes the display format to "G". At the moment, you can certainly exceed the width of the Group control, which is why I haven't made this feature "public" in the UI.

Group Class

The Group class is a user control that contains two controls: the Label describing the group and a Label for the counter value. The mouse events are wired up to capture mouse clicks that occur on these two controls.

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;
  }
}

The Data Model

A DataView, sorted on an Index column, is used for managing the counters. This fits well with working with the counter editor DataGridView:

The backing DataTable is created with the columns shown above (except for "Index", which is hidden):

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";
}

The table is serialized and deserialized using the DataTable's XML serialization feature:

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

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

Additional form-specific information (location and the always-on-top flag) are serialized to a different file:

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();
}

Program Startup

The program startup puts it all together. If there are no events on startup, the application brings up the event editor first and then launches the main application.

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);
  }
}

The Font

I've currently hardcoded an LED font that I found here, by Sizenko Alexander of "Style-7". It's a freeware font, and I've included it as part of the executable download.

Conclusion

What was interesting, for me at least, in writing this was where I decided to put effort into some architectural considerations and where I decided I wanted to keep things very simple. For example, the user controls and the internals on how the counters are configured is, I think, well architected. The choice to implement most of the control logic as static methods was because nothing more complicated was warranted. Before adding more complicated features, I'll probably go back and clean up the model-view entanglement.

Further Reading

Definitely off-topic, but given the screenshot, here's links to things I'm into lately.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
GeneralThe ICounter and The CounterConfiguration Namespaces PinmemberDr. Lamba26-May-11 2:05 
GeneralRe: The ICounter and The CounterConfiguration Namespaces PinprotectorMarc Clifton26-May-11 2:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141015.1 | Last Updated 24 Apr 2010
Article Copyright 2010 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid