Timer Component
The Timer component raises a Tick event at specified time intervals. The Tick event handler can then process a regularly occurring event, such as repainting the screen for animation or a clock display, updating a status report, or terminating a program based on elapsed time.
The interval between Tick events, specified by the Interval property, is measured in milliseconds, with valid values between 1 and 2,147,483,647, inclusive. The maximum value corresponds to approximately 597 hours, or a little over 24 days.
If there are heavy demands on the system running the application, either from the current or other applications, the Tick events may not be raised as often as specified by the Interval property, especially if the Interval is very short. Under any circumstances, the interval is not guaranteed to be accurate. If accuracy is required, the system clock should be checked as needed, especially for long intervals.
Although the Interval property is in milliseconds, the system clock generates only 18 ticks per second. Therefore, the true precision of the Timer is no better than one-eighteenth of a second, which is about 55 milliseconds.
Three types of timers are provided in the .NET Framework. The first is a member of the System.Threading namespace. It is used primarily for multi-threaded applications and will not be covered in this book. It is not represented in any Visual Studio .NET Toolbox.
The second is a member of the System.Timers namespace. It is primarily intended for server applications and also is designed for multithreaded applications. It is found on the Components tab of the Visual Studio .NET Toolbox. It too will not be covered in this book.
The third type of timer component, described in this book, is a member of the System.Windows.Form namespace. It is designed for the single-threaded environment of Windows Forms, where the UI thread controls processing. This single-threaded timer is available from the Windows Forms tab of the Visual Studio .NET Toolbox.
The Timer component is not a control, since it does not have a visual aspect. Because it is not a control, it does not have a Parent property and is not part of the Controls collection.
In Visual Studio .NET, a Timer component is added to the form by dragging it from the Toolbox onto the form. However, it doesn't stay or appear on the form, but displays in the component tray at the bottom of the design window, as shown in Figure 16-6.
Figure 16-6. Timer component in Visual Studio .NET
The Timer component has only two properties, listed in Table 16-18. The Enabled property must be set to true in order to turn the timer function on. This can be done in the Properties window of Visual Studio .NET, in the code in the constructor (which is effectively the same), or in some other part of the program, in the event handler for a button Click. Setting the Enabled property false turns the timer off.
Property |
Value type |
Description |
---|---|---|
Enabled |
Boolean |
Read/write. If true, the time is enabled. The default is false. |
Interval |
Integer |
Read/write. The number of milliseconds between timer ticks. |
The Timer component has two methods. The Start method starts the timer; it is equivalent to setting the Enabled property to true. The Stop method turns the timer off; it is equivalent to setting the Enabled property to false.
The Timer component has a single event, Tick. It is raised every time the number of milliseconds in the Interval property has passed. The Tick event has an event argument of type EventArgs, which means that no additional properties are associated with the event.
If the Enabled property is set to false in the Tick event handler, then the timer will be a one-shot deal: once the event is raised and handled, it will not be raised again until the Enabled property is toggled. If the Enabled property is not changed in the Tick event handler, then the timer will keep recurring until the property is set to false.
The first timer example, listed in Example 16-5 (in VB.NET only; the C# version is very similar) is a simple demonstration of a label control being used as a clock. The text value of the label is updated every 10 seconds. The result is shown in Figure 16-7.
Figure 16-7. Timer demo
Example 16-5. Timer example in VB.NET (Timers.vb)
Option Strict On imports System imports System.Drawing imports System.Windows.Forms namespace ProgrammingWinApps public class Timers : inherits Form dim lblTime as Label dim strFormat as String public sub New( ) Text = "Timer Demo" Size = new Size(300,100) strFormat = "dddd, MMMM d, yyyy h:mm:ss tt" lblTime = new Label( ) lblTime.Parent = me lblTime.Size = new Size(CInt(ClientSize.Width * .8), 25) lblTime.Location = new Point(CInt(ClientSize.Width * .1), _ CInt(ClientSize.Height * .4)) lblTime.BorderStyle = BorderStyle.FixedSingle lblTime.Text = DateTime.Now.ToString(strFormat) lblTime.TextAlign = ContentAlignment.MiddleCenter dim t as new Timer( ) t.Interval = 10000 ' 10 seconds t.Start( ) AddHandler t.Tick, AddressOf t_Tick end sub ' close for constructor public shared sub Main( ) Application.Run(new Timers( )) end sub private sub t_Tick(ByVal sender as object, _ ByVal e as EventArgs) lblTime.Text = DateTime.Now.ToString(strFormat) end sub end class end namespace
In this example, the Timer is instantiated in the constructor with an Interval property of 10,000 milliseconds, which is equivalent to 10 seconds. The Timer Start method is called so the timer will run as soon as the form is loaded.
In the Tick event handler, t_Tick, the Text property of the label is updated to display the current time, DateTime.Now, using the ToString method. The format of the label is controlled by an argument to the ToString method, a formatting string instantiated back in the constructor.
The next example is a countdown timer. It is similar to the previous example in that it displays a text string with the timein this case, updated every second. It also provides a DateTimePicker control for the user to enter a time interval to count down. The countdown begins when the user clicks the Start button, with the remaining time displayed. When the specified time elapses, a message is displayed in a label control. The resulting application looks like Figure 16-8 during countdown.
Figure 16-8. Countdown timer
In addition to using a different technique for displaying updated text strings, this example also demonstrates the use of TimeSpan objects, described earlier in this chapter.
The C# version of the CountDownTimer application is listed in Example 16-6, and the VB.NET version is listed in Example 16-7. As you will see in the analysis that follows the code listings, it was necessary to jump through some DateTime and TimeSpan hoops to get the times to display properly.
Example 16-6. CountDownTimer in C# (CountDownTimer.cs)
using System; using System.Drawing; using System.Windows.Forms; namespace ProgrammingWinApps { public class CountDownTimer : Form { DateTimePicker dtpTotalTime; Button btnStart; Button btnStop; bool boolStart = false; DateTime dtEndTime; Label lblTimesUp; Label lblTitle; public CountDownTimer( ) { Text = "CountDown Timer"; Size = new Size(500,400); FormBorderStyle = FormBorderStyle.FixedDialog; Font = new Font("Arial", 12); Timer t = new Timer( ); t.Interval = 1000; t.Start( ); t.Tick += new EventHandler(t_Tick); lblTitle = new Label( ); lblTitle.Parent = this; lblTitle.Font = new Font("Arial Black", 24); lblTitle.Text = "CountDown Timer"; lblTitle.TextAlign = ContentAlignment.MiddleCenter; lblTitle.Size = new Size((int)(lblTitle.Font.Height * .7) * lblTitle.Text.Length, 35); lblTitle.Location = new Point(ClientSize.Width / 2 - lblTitle.Width / 2, 25); Label lblTotalTime = new Label( ); lblTotalTime.Parent = this; lblTotalTime.Text = "Total Time (h:m:s):"; lblTotalTime.Size = new Size((int)(Font.Height * .5) * lblTotalTime.Text.Length, 25); lblTotalTime.Location = new Point(ClientSize.Width / 10, 100); dtpTotalTime = new DateTimePicker( ); dtpTotalTime.Parent = this; dtpTotalTime.Format = DateTimePickerFormat.Custom; dtpTotalTime.CustomFormat = "H:mm:ss"; dtpTotalTime.Value = DateTime.Parse("00:00:00"); dtpTotalTime.ShowUpDown = true; dtpTotalTime.Size = new Size((int)(Font.Height * .6) * dtpTotalTime.Value.ToString("t").Length, dtpTotalTime.PreferredHeight); dtpTotalTime.Location = new Point(lblTotalTime.Right, 100); btnStart = new Button( ); btnStart.Parent = this; btnStart.Text = "Start"; btnStart.Location = new Point(ClientSize.Width / 4, 300); btnStart.Click += new EventHandler(btnStart_Click); btnStop = new Button( ); btnStop.Parent = this; btnStop.Text = "Stop"; btnStop.Location = new Point(btnStart.Right + 10, 300); btnStop.Click += new EventHandler(btnStop_Click); lblTimesUp = new Label( ); lblTimesUp.Parent = this; lblTimesUp.Size = new Size(200, 35); lblTimesUp.Location = new Point(btnStart.Left, btnStart.Top - 75); lblTimesUp.Text = ""; lblTimesUp.Font = new Font("Times New Roman Bold", 20); } // close for constructor static void Main( ) { Application.Run(new CountDownTimer( )); } private void t_Tick(object sender, EventArgs e) { Invalidate( ); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g = e.Graphics; Brush b = new SolidBrush(ForeColor); StringFormat fmt = new StringFormat( ); fmt.Alignment = StringAlignment.Near; PointF pt = new PointF(ClientSize.Width / 10, 150); Font fnt = new Font("Arial", 12); String str = "Current Time: " + DateTime.Now.ToString("F") + " "; if (boolStart) { TimeSpan ts = new TimeSpan( ); ts = dtEndTime - DateTime.Now; DateTime dt = new DateTime( ); dt = DateTime.Parse(ts.ToString( )); str += "Remaining Time: " + dt.ToString("HH:mm:ss"); } else { str += "Remaining Time:"; } g.DrawString(str, fnt, b, pt, fmt); if (boolStart && (dtEndTime - DateTime.Now) <= TimeSpan.Zero) { TimesUp( ); } } private void btnStart_Click(object sender, EventArgs e) { lblTimesUp.Text = ""; boolStart = true; TimeSpan ts = new TimeSpan( ); ts = TimeSpan.Parse(dtpTotalTime.Value.Hour.ToString( ) + ":" + dtpTotalTime.Value.Minute.ToString( ) + ":" + dtpTotalTime.Value.Second.ToString( )); dtEndTime = DateTime.Now + ts; } private void btnStop_Click(object sender, EventArgs e) { boolStart = false; } private void TimesUp( ) { lblTimesUp.Text = "Times Up!"; boolStart = false; } } // close for form class } // close form namespace
Example 16-7. CountDownTimer in VB.NET (CountDownTimer.vb)
Option Strict On imports System imports System.Drawing imports System.Windows.Forms namespace ProgrammingWinApps public class CountDownTimer : inherits Form dim dtpTotalTime as DateTimePicker dim btnStart as Button dim btnStop as Button dim boolStart as Boolean = false dim dtEndTime as DateTime dim lblTimesUp as Label dim lblTitle as Label public sub New( ) Text = "CountDown Timer" Size = new Size(500,400) FormBorderStyle = FormBorderStyle.FixedDialog Font = new Font("Arial", 12) dim t as new Timer( ) t.Interval = 1000 t.Start( ) AddHandler t.Tick, AddressOf t_Tick lblTitle = new Label( ) lblTitle.Parent = me lblTitle.Font = new Font("Arial Black", 24) lblTitle.Text = "CountDown Timer" lblTitle.TextAlign = ContentAlignment.MiddleCenter lblTitle.Size = new Size(CInt(lblTitle.Font.Height * .7) * _ lblTitle.Text.Length, 35) lblTitle.Location = new Point(CInt(ClientSize.Width / 2 - _ lblTitle.Width / 2), 25) dim lblTotalTime as new Label( ) lblTotalTime.Parent = me lblTotalTime.Text = "Total Time (h:m:s):" lblTotalTime.Size = new Size(CInt(Font.Height * .5) * _ lblTotalTime.Text.Length, 25) lblTotalTime.Location = new Point( _ CInt(ClientSize.Width / 10), 100) dtpTotalTime = new DateTimePicker( ) dtpTotalTime.Parent = me dtpTotalTime.Format = DateTimePickerFormat.Custom dtpTotalTime.CustomFormat = "H:mm:ss" dtpTotalTime.Value = DateTime.Parse("00:00:00") dtpTotalTime.ShowUpDown = true dtpTotalTime.Size = new Size(CInt(Font.Height * .6) * _ dtpTotalTime.Value.ToString("t").Length, _ dtpTotalTime.PreferredHeight) dtpTotalTime.Location = new Point(lblTotalTime.Right, 100) btnStart = new Button( ) btnStart.Parent = me btnStart.Text = "Start" btnStart.Location = new Point(CInt(ClientSize.Width / 4), 300) AddHandler btnStart.Click, AddressOf btnStart_Click btnStop = new Button( ) btnStop.Parent = me btnStop.Text = "Stop" btnStop.Location = new Point(btnStart.Right + 10, 300) AddHandler btnStop.Click, AddressOf btnStop_Click lblTimesUp = new Label( ) lblTimesUp.Parent = me lblTimesUp.Size = new Size(200, 35) lblTimesUp.Location = new Point(btnStart.Left, _ btnStart.Top - 75) lblTimesUp.Text = "" lblTimesUp.Font = new Font("Times New Roman Bold", 20) end sub ' close for constructor public shared sub Main( ) Application.Run(new CountDownTimer( )) end sub private sub t_Tick(ByVal sender as object, _ ByVal e as EventArgs) Invalidate( ) end sub protected overrides sub OnPaint(ByVal e as PaintEventArgs) myBase.OnPaint(e) dim g as Graphics = e.Graphics dim b as new SolidBrush(ForeColor) dim fmt as new StringFormat( ) fmt.Alignment = StringAlignment.Near dim pt as new PointF(CInt(ClientSize.Width / 10), 150) dim fnt as new Font("Arial", 12) dim str as string = "Current Time: " + _ DateTime.Now.ToString("F") + vbCrLf + vbCrLf if boolStart then dim ts as new TimeSpan( ) ts = DateTime.op_Subtraction(dtEndTime, DateTime.Now) dim dt as new DateTime( ) dt = DateTime.Parse(ts.ToString( )) str += "Remaining Time: " + dt.ToString("HH:mm:ss") else str += "Remaining Time:" end if g.DrawString(str, fnt, b, pt, fmt) if (boolStart and _ (TimeSpan.op_LessThanOrEqual(DateTime.op_Subtraction( _ dtEndTime, DateTime.Now), TimeSpan.Zero))) then TimesUp( ) end if end sub private sub btnStart_Click(ByVal sender as object, _ ByVal e as EventArgs) lblTimesUp.Text = "" boolStart = true dim ts as new TimeSpan( ) ts = TimeSpan.Parse(dtpTotalTime.Value.Hour.ToString( ) + ":" + _ dtpTotalTime.Value.Minute.ToString( ) + ":" + _ dtpTotalTime.Value.Second.ToString( )) dtEndTime = DateTime.op_Addition(DateTime.Now, ts) end sub private sub btnStop_Click(ByVal sender as object, _ ByVal e as EventArgs) boolStart = false end sub private sub TimesUp( ) lblTimesUp.Text = "Times Up!" boolStart = false end sub end class end namespace
The FormBorderStyle is set in the constructor to FormBorderStyle.FixedDialog, which prevents the user from resizing the form. By doing so, there is no need to anchor any of the controls or otherwise worry about how the look of the form will be affected by user interaction. Also in the constructor, the default font for the form is set to 12-point Arial.
FormBorderStyle = F
ormBorderStyle.FixedDialog Font = new Font("Arial", 12)
The Timer is declared and instantiated in the constructor, with the Interval property set to one second (1,000 milliseconds) and the Start method called to enable the timer. An event handler is added for the Tick event:
Timer t = new Timer( ); t.Interval = 1000; t.Start( ); t.Tick += new EventHandler(t_Tick); dim t as new Ti
mer( ) t.Interval = 1000 t.Start( ) AddHandler t.Tick, AddressOf t_Tick
The DateTimePicker control is used here as a convenient way for the user to enter the time to count down in hours, minutes, and seconds. This use requires that the value initially be set to "00:00:00" (which is cast from a string to a DateTime object using the static DateTime.Parse method) and displayed using a custom format, "H:mm:ss." As you recall from Table 16-9, the leading uppercase H in that format string specifies a one- or two-digit hour in a 24-hour format.
dtpTotalTime.Format = DateTimePickerFormat.Custom dtpTotalTime.CustomFormat = "H:mm:ss" dtpTotalTime.Value = DateTime.Parse("00:00:00")
At first, this custom formatting may seem unnecessary, since the DateTimePickerFormat.Time format should provide what you are looking for. However, if the Time format is used, the DateTimePicker control displays 12:00:00, rather than the desired 00:00:00.
Notice that the horizontal component of the Size property of the DateTimePicker control is calculated using the Value property of the control in conjunction with the ToString method, taking a formatting argument to retrieve the number of characters. From Table 16-10, you saw that the "t" formatting string corresponds to a short time display, which is effectively how the custom format used by the control appears. As with all the Size calculations based on the Font.Height property, the ".6" factor is arrived at empirically:
dtpTotalTime.Size = new Size((int)(Font.Height * .6) * dtpTotalTime.Value.ToString("t").Length, dtpTotalTime.PreferredHeight);
dtpTotalTime.Size = new Size(CInt(Font.Height * .6) * _ dtpTotalTime.Value.ToString("t").Length, _ dtpTotalTime.PreferredHeight)
The TimesUp label is positioned, sized, and given a nice bold 20-point font, but the Text property is initially set to an empty string. The Text property will be set appropriately as necessary, as you will see in a moment.
Now turn your attention to the Start button. The Click event handler for the Start button first clears the TimesUp label.
lblTimesUp.Text = ""
Next it sets a flag, boolStart, to true. This flag was initialized to false as a class member variable.
boolStart = true
Now comes a tricky part. The value in the DateTimePicker control is a DateTime object. It must be converted to a TimeSpan object so that the ending time, dtEndTime, which is a DateTime object, can be calculated. This is necessary because the DateTime Addition operator (and the DateTime Add method, as well) can only add a TimeSpan to a DateTime, not add together two DateTimes.
The conversion of the DateTimePicker value to a TimeSpan is accomplished by using the static TimeSpan.Parse method, which takes a string argument. That string argument is built up by calling the ToString method against the Hour, Minute, and Second components of the DateTimePicker control's Value property.
Then the ending time can be calculated by adding the resulting TimeSpan object to the current time:
TimeSpan ts = new TimeSpan( ); ts = TimeSpan.Parse(dtpTotalTime.Value.Hour.ToString( ) + ":" + dtpTotalTime.Value.Minute.ToString( ) + ":" + dtpTotalTime.Value.Second.ToString( )); dtEndTime = DateTime.Now + ts;
dim ts as new TimeSpan( ) ts = TimeSpan.Parse(dtpTotalTime.Value.Hour.ToString( ) + ":" + _ dtpTotalTime.Value.Minute.ToString( ) + ":" + _ dtpTotalTime.Value.Second.ToString( )) dtEndTime = DateTime.op_Addition(DateTime.Now, ts)
Notice that the C# version allows the use of the + DateTime operator, and the VB.NET version does not. Instead, it uses the shared DateTime method DateTime.op_Addition.
|
Now that you have the boolStart flag and the ending time, dtEndTime, you can handle the Tick event.
The Tick event handler consists of a single line of code, which invalidates the form and causes the OnPaint method to be invoked. This OnPaint method has been overridden, so it draws the text strings containing the current time of day and the remaining time being counted down.
|
The overridden OnPaint method first chains up to the base class:
base.OnPaint(e);
myBase.OnPaint(e)
Next it declares and instantiates several objects, which will be used shortly in the Graphics DrawString method:
- A Graphics object.
- A Brush object with the foreground color (which defaults to black on most systems).
- A StringFormat object used to set the Alignment property. (StringAlignment.Near corresponds to Left in a Left-to-Right languagesee Tables 9-8 and 9-9 for a description of the StringAlignment enumeration.)
- A PointF object.
- A Font object specifying 12-point Arial.
- A string object containing the current time, formatted with the "F" formatting string:
Graphics g = e.Graphics; Brush b = new SolidBrush(ForeColor); StringFormat fmt = new StringFormat( ); fmt.Alignment = StringAlignment.Near; PointF pt = new PointF(ClientSize.Width / 10, 150); Font fnt = new Font("Arial", 12); String str = "Current Time: " + DateTime.Now.ToString("F") + " ";
dim g as Graphics = e.Graphics dim b as new SolidBrush(ForeColor) dim fmt as new StringFormat( ) fmt.Alignment = StringAlignment.Near dim pt as new PointF(CInt(ClientSize.Width / 10), 150) Font fnt = new Font("Arial", 12) dim str as string = "Current Time: " + _ DateTime.Now.ToString("F") + vbCrLf + vbCrLf
The specified string will be drawn with every timer ticki.e., every second. The contents of the next string, however, depend on whether the application is counting down. For this, it tests the boolStart flag, which was set in the Start button Click event handler.
If the boolStart flag is true, then another string is built up by subtracting the current time, DateTime.Now, from the ending time, dtEndTime, which was calculated in the Start Button event handler. This process is surprisingly tricky. You might think you could use the following code to display the remaining time, where the TimeSpan is calculated and then displayed using the TimeSpan ToString method.
TimeSpan ts = new TimeSpan( ); ts = dtEndTime - DateTime.Now; str += "Remaining Time: " + ts.ToString( );
This works, but it displays the time with hours, minutes, and fractional seconds, as in 01:01:01.1234567, with the seconds displaying seven decimal digits. You should, however, display only hours, minutes, and whole seconds, as in 01:01:01.
No problem, you think: I'll just add a formatting argument to the ToString method. However, this causes a compiler error. The DateTime.ToString method accepts a formatting argument, but the TimeSpan.ToString does not. So you need to convert the TimeSpan to a DateTime, using the static DateTime.Parse method. This method takes a string argument, so you give it the TimeSpan object converted to a string with ToString. Then the string for display can be built up using the DateTime ToString, which accepts the formatting argument.
The complete code section for testing the boolStart flag, constructing the line that displays the remaining time and drawing the two lines of text, is reproduced here:
if (boolStart) { TimeSpan ts = new TimeSpan( ); ts = dtEndTime - DateTime.Now; DateTime dt = new DateTime( ); dt = DateTime.Parse(ts.ToString( )); str += "Remaining Time: " + dt.ToString("HH:mm:ss"); } else { str += "Remaining Time:"; } g.DrawString(str, fnt, b, pt, fmt);
if boolStart then dim ts as new TimeSpan( ) ts = DateTime.op_Subtraction(dtEndTime, DateTime.Now) dim dt as new DateTime( ) dt = DateTime.Parse(ts.ToString( )) str += "Remaining Time: " + dt.ToString("HH:mm:ss") else str += "Remaining Time:" end if g.DrawString(str, fnt, b, pt, fmt)
The final piece of the OnPaint method tests to see if time has expired. If so, it calls the TimesUp helper method:
if (boolStart && (dtEndTime - DateTime.Now) <= TimeSpan.Zero) { TimesUp( ); }
if (boolStart and _ (TimeSpan.op_LessThanOrEqual(DateTime.op_Subtraction( _ dtEndTime, DateTime.Now), TimeSpan.Zero))) then TimesUp( ) end if
Again, as with the TimeSpan and DateTime operators used previously, the C# version uses the <= operator, while the VB.NET version must use the shared TimeSpan.op_LessThanOrEqual method.
The TimesUp method is simple. It sets the Text property of the lblTimesUp label and resets the boolStart flag to false.
The Stop button Click event handler is also simpleit just sets the boolStart flag to false. The next time the Tick event fires and the OnPaint method is called, the form will correctly display with the count down stopped.