As some have pointed out in the comments, it is best to use the TimeSpan type for working with times.
To make a timer, you can use the Stopwatch class, which can be started and stopped and has an Elapsed (time) property.
When you subtract this elapsed time from a preset time, you basically get a countdown timer.
You also want to add/remove time, so you need at least one additional field to store this 'bonus' time, which can be positive or negative.
The stopwatch class does not have an event available that is raised at a certain time. Because of this I added a System.Timers.Timer field (_zeroTimer), which raises an elapsed event after a set time. The timer is given the remainingTime and will fire when the remainingTime reaches zero. The timer is reset with a new remainingTime whenever bonus time is added/removed and when the QuizTimer is started again after a pause. When paused, we want to pause this timer as well, but since System.Timers.Timer does not allow that, we destroy it and re-create it on start.
I took the liberty to make a crude QuizTimer class for you that does this. Use it as an inspiration and/or expand it to suit your needs.
It has a Start, Pause and Reset method, as well as add/remove Time methods.
You also have the option to allow negative times or not.
You can get the minutes, seconds, millis from the TimeRemaining (TimeSpan) property. Or expose them using additional properties in the class.
There is also TimeRemainingClamped, which is simply the TimeRemaining value, but if the time is negative, this will return TimeSpan.Zero.
When the time reaches zero, the TimeZeroed event is raised.
If 'AllowNegativeValues' is negative, this will also stop/pause the QuizTimer.
While testing I noticed that the TimeRemaining after a time-zeroed event will have a very small negative value (fraction of 1ms). To prevent showing this to the user, you can use TimeRemainingClamped value or just display in a resolution of 1ms or 1s.
It works, but it's crude and not thoroughly tested. I'll leave that up to you.
using System;
using System.Diagnostics;
using System.Timers;
namespace QuizTimerNs
{
public class QuizTimer
{
private Stopwatch _stopwatch;
private Timer _zeroTimer;
private TimeSpan _startTime;
private TimeSpan _bonusTime = TimeSpan.Zero;
public event EventHandler TimeZeroed;
public TimeSpan TimeRemaining => _startTime + _bonusTime - (_stopwatch?.Elapsed ?? TimeSpan.Zero);
public TimeSpan TimeRemainingClamped => TimeRemaining > TimeSpan.Zero ? TimeRemaining : TimeSpan.Zero;
public int TimeRemaining_ms => (int)Math.Floor(TimeRemaining.TotalMilliseconds);
public int TimeRemaining_s => (int)Math.Floor(TimeRemaining.TotalSeconds);
public int TimeRemaining_min => (int)Math.Floor(TimeRemaining.TotalMinutes);
public int TimeRemainingClamped_ms => (int)Math.Floor(TimeRemainingClamped.TotalMilliseconds);
public int TimeRemainingClamped_s => (int)Math.Floor(TimeRemainingClamped.TotalSeconds);
public int TimeRemainingClamped_min => (int)Math.Floor(TimeRemainingClamped.TotalMinutes);
public bool AllowNegativeTime { get; set; } = false;
public QuizTimer(TimeSpan startTime)
{
if (startTime == null) throw new ArgumentNullException(nameof(startTime));
_stopwatch = new Stopwatch();
_zeroTimer = null;
_startTime = startTime;
}
public void Reset()
{
_stopwatch.Reset();
_bonusTime = TimeSpan.Zero;
EndZeroTimer();
}
private void EndZeroTimer()
{
if (_zeroTimer != null)
{
_zeroTimer.Stop();
_zeroTimer.Elapsed -= OnZeroTimerElapsed;
_zeroTimer.Dispose();
_zeroTimer = null;
}
}
private void ResetZeroTimerForRemainingTime()
{
if (_zeroTimer != null)
EndZeroTimer();
_zeroTimer = new Timer(TimeRemaining.TotalMilliseconds);
_zeroTimer.Elapsed += OnZeroTimerElapsed;
_zeroTimer.AutoReset = false;
}
public void Start()
{
if (!_stopwatch.IsRunning && (AllowNegativeTime || TimeRemaining >= TimeSpan.Zero))
{
ResetZeroTimerForRemainingTime();
_stopwatch.Start();
_zeroTimer.Start();
}
}
private void OnZeroTimerElapsed(object sender, ElapsedEventArgs e)
{
if (!AllowNegativeTime)
Pause();
OnTimeZeroed();
}
public void AddTime(TimeSpan time)
{
_bonusTime = _bonusTime + time;
EndZeroTimer();
if (_stopwatch.IsRunning && TimeRemaining > TimeSpan.Zero)
{
ResetZeroTimerForRemainingTime();
_zeroTimer.Start();
}
}
public void RemoveTime(TimeSpan time)
{
_bonusTime = _bonusTime - time;
EndZeroTimer();
if (_stopwatch.IsRunning && TimeRemaining > TimeSpan.Zero)
{
ResetZeroTimerForRemainingTime();
_zeroTimer.Start();
}
if (_stopwatch.IsRunning && TimeRemaining <= TimeSpan.Zero && !AllowNegativeTime)
{
Pause();
}
}
public void Pause()
{
_stopwatch.Stop();
EndZeroTimer();
}
private void OnTimeZeroed()
{
TimeZeroed?.Invoke(this, EventArgs.Empty);
}
}
}
Optional
To test it, I made a small WPF application. Not according to proper design rules, but just as a quick rude testing mechanism :)
MainWindow.xaml:
<Window x:Class="QuizTimerNs.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:QuizTimerNs"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="200">
<StackPanel>
<Button Content="Start" Click="StartButton_Click"></Button>
<Button Content="Pause" Click="PauseButton_Click"></Button>
<Button Content="Reset" Click="ResetButton_Click"></Button>
<Button Content="Add 10 s" Click="AddButton_Click"></Button>
<Button Content="Remove 10 s" Click="RemoveButton_Click"></Button>
<TextBox x:Name="TimeRemainingTextBox"></TextBox>
<TextBox x:Name="TimeRemainingTextBox2"></TextBox>
</StackPanel>
</Window>
Code-behind: MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace QuizTimerNs
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private QuizTimer _quizTimer;
public MainWindow()
{
_quizTimer = new QuizTimer(TimeSpan.FromSeconds(20));
var timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(0.1) };
timer.Tick += (o, e) => Update();
timer.Start();
InitializeComponent();
}
private void StartButton_Click(object sender, RoutedEventArgs e) => _quizTimer.Start();
private void PauseButton_Click(object sender, RoutedEventArgs e) => _quizTimer.Pause();
private void ResetButton_Click(object sender, RoutedEventArgs e) => _quizTimer.Reset();
private void AddButton_Click(object sender, RoutedEventArgs e) => _quizTimer.AddTime(TimeSpan.FromSeconds(10));
private void RemoveButton_Click(object sender, RoutedEventArgs e) => _quizTimer.RemoveTime(TimeSpan.FromSeconds(10));
private void Update()
{
TimeRemainingTextBox.Text = _quizTimer.TimeRemaining.ToString();
TimeRemainingTextBox2.Text = _quizTimer.TimeRemainingClamped.ToString();
}
}
}