3
\$\begingroup\$

This is a Graphical Timer control I made for C# Winforms, which taught me quite a bit (it is my first Custom Control). The code below is a fully working application with the Graphical Timer, some buttons to control it, and a listbox showing the sweeping angle of the pie (for debugging purposes). Being my first control I have no idea how messy the code is, if it conforms to guidelines, etc. Hopefully I will get some good feedback about improvements that can be made, but other than that, it works perfectly in all my tests!

using System;
using System.Drawing;
using System.Windows.Forms;

namespace TestCustomControl
{
    class GraphicalTimer : Control
    {
        public event EventHandler Timeout;
        public Color TimerBack { get; set; }
        public Color TimerFront { get; set; }
        private Color TimerEmptying { get; set; }
        public Color TimerFull { get; set; }
        public Color TimerHalfEmpty { get; set; }
        public Color TimerQuarterEmpty { get; set; }
        public Color BorderColor { get; set; }
        private Timer t;
        public int MaxTime { get; set; }
        public bool AutoReset { get; set; }
        private int timeElapsed = 0;
        public GraphicalTimer()
        {
            DoubleBuffered = true;

            //Default values for properties
            TimerBack = Color.White;
            TimerFront = Color.Black;
            TimerEmptying = Color.LightGreen;
            TimerFull = Color.LightGreen;
            TimerHalfEmpty = Color.Orange;
            TimerQuarterEmpty = Color.Red;
            BorderColor = Color.Black;
            MaxTime = 60;
            AutoReset = true;

            SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            BackColor = Color.Transparent;

            t = new Timer();
            t.Interval = 1000;
            t.Tick += t_Tick;
        }

        protected virtual void OnTimeout(EventArgs e)
        {
            if (Timeout != null)
                Timeout(this, e);
        }

        public void Start()
        {
            t.Start();
        }

        public void Stop()
        {
            t.Stop();
        }

        public void Reset()
        {
            timeElapsed = 0;
            Invalidate();
        }

        void t_Tick(object sender, EventArgs e)
        {
            timeElapsed += 1;
            if (timeElapsed >= MaxTime*0.75)
            {
                TimerEmptying = TimerQuarterEmpty;
            }
            else if (timeElapsed >= MaxTime * 0.5)
            {
                TimerEmptying = TimerHalfEmpty;
            }
            else
            {
                TimerEmptying = TimerFull;
            }
            if (timeElapsed == MaxTime)
            {
                t.Dispose();
            }
            Invalidate();
            if (timeElapsed == MaxTime)
            {
                OnTimeout(new EventArgs());
                if (AutoReset)
                    Reset();
            }
        }

        private float getAngleFromTime()
        {
            if (timeElapsed == 0)
            {
                return 0;
            }
            else
            {
                MainWindow.lb.Items.Add((360.0 / ((float)MaxTime / (float)timeElapsed)).ToString());
                return ((360 * timeElapsed) / MaxTime);
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            Graphics g = e.Graphics;
            Rectangle rc = ClientRectangle;

            g.FillEllipse(new SolidBrush(TimerBack), (int)rc.Left + 5, (int)rc.Top + 5, (int)rc.Width - 10, (int)rc.Height - 10);
            g.FillPie(new SolidBrush(TimerEmptying), (int)rc.Left + 5, (int)rc.Top + 5, (int)rc.Width - 10, (int)rc.Height - 10, -90, getAngleFromTime());
            g.DrawEllipse(new Pen(BorderColor, 4), (int)rc.Left + 5, (int)rc.Top + 5, (int)rc.Width - 10, (int)rc.Height - 10);

            Font font = new Font("Arial", (float)rc.Height * 0.4f, FontStyle.Bold, GraphicsUnit.Pixel);
            StringFormat sf = new StringFormat();
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;
            g.DrawString((MaxTime - timeElapsed).ToString("D2"), font, new SolidBrush(TimerFront), ClientRectangle, sf);
        }
    }

    class MainWindow : Form
    {
        GraphicalTimer gt;
        Button startButton;
        Button stopButton;
        Button resetButton;
        Button setButton;
        TextBox timerValue;
        public static ListBox lb;
        public MainWindow()
        {
            this.Text = "Test Application";
            gt = new GraphicalTimer();
            gt.MaxTime = 30;
            gt.Top = 10;
            gt.Left = 10;
            gt.Width = 50;
            gt.Height = 50;
            gt.AutoReset = false;
            gt.Timeout += gt_Timeout;
            this.Controls.Add(gt);

            startButton = new Button();
            startButton.Top = 10;
            startButton.Left = gt.Right + 10;
            startButton.AutoSize = true;
            startButton.Text = "Start Timer";
            startButton.Click += startButton_Click;
            this.Controls.Add(startButton);

            stopButton = new Button();
            stopButton.Top = 10;
            stopButton.Left = startButton.Right + 10;
            stopButton.AutoSize = true;
            stopButton.Text = "Stop Timer";
            stopButton.Click += stopButton_Click;
            this.Controls.Add(stopButton);

            resetButton = new Button();
            resetButton.Top = startButton.Bottom + 10;
            resetButton.Left = gt.Right + 10;
            resetButton.AutoSize = true;
            resetButton.Text = "Reset Timer";
            resetButton.Click += resetButton_Click;
            this.Controls.Add(resetButton);

            timerValue = new TextBox();
            timerValue.Top = startButton.Bottom + 10;
            timerValue.Left = resetButton.Right + 10;
            timerValue.Font = new Font("arial", 12);
            timerValue.Width = (stopButton.Width / 2);
            this.Controls.Add(timerValue);

            setButton = new Button();
            setButton.Top = stopButton.Bottom + 10;
            setButton.Left = timerValue.Right + 10;
            setButton.Text = "Set";
            setButton.Click += setButton_Click;
            this.Controls.Add(setButton);

            lb = new ListBox();
            lb.Top = resetButton.Bottom + 10;
            lb.Left = 10;
            lb.Width = this.ClientSize.Width - 20;
            lb.Height = this.ClientSize.Height - lb.Top - 10;
            this.Controls.Add(lb);
        }

        void gt_Timeout(object sender, EventArgs e)
        {
            MessageBox.Show("Time has run out");
        }

        void setButton_Click(object sender, EventArgs e)
        {
            gt.MaxTime = int.Parse(timerValue.Text);
        }

        void resetButton_Click(object sender, EventArgs e)
        {
            gt.Reset();
        }

        void stopButton_Click(object sender, EventArgs e)
        {
            gt.Stop();
        }

        void startButton_Click(object sender, EventArgs e)
        {
            gt.Start();
        }
    }

    class StartClass
    {
        static void Main()
        {
            MainWindow form = new MainWindow();
            Application.EnableVisualStyles();
            Application.Run(form);
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

Some comments:

I would recommend making sure that the base constructor is called:

public GraphicalTimer()
    : base()
{
    ...
}

I would recommend doing the following in your OnTimeout method:

protected virtual void OnTimeout()
{
    // Make a temporary copy of the event to avoid possibility of 
    // a race condition if the last subscriber unsubscribes 
    // immediately after the null check and before the event is raised.
    EventHandler timeout = Timeout;

    if (timeout != null)
    {
        timeout (this, new EventArgs());
    }
}

I prefer only passing relevant data to the OnEvent() methods and then constructing the event argument object within these methods. This makes it easy to find where event argument objects are created. However, this is just my preference.

I would not make a reference to MainWindow from inside your own control. You should try and make your control self contained and not reliant on external objects. It would be better to define a listbox property such as this:

public ListBox ListBoxDisplay { get; set; }

Now in your getAngleFromTime method you should use the following:

if (ListBoxDisplay != null)
{
    ListBoxDisplay.Items.Add((360.0 / ((float)MaxTime / (float)timeElapsed)).ToString());
}

This way your control will work with or without a listbox and does not need to know about MainWindow. You can now also add a new constructor:

public GraphicalTimer(ListBox lb)
    : this()
{
    ListBoxDisplay = lb;
}

Note: Brush and Pen objects implement IDisposable and so if you use them in the OnPaint then you will want to wrap them in a using statement. Alternatively, you might like to consider making the brush and pen objects outside of the paint method, as class properties. For example:

// set the class property as:
public Brush TimerBack
{
    get { return _timerBack; }
    set
    {
        if (_timerBack != null) { _timerBack.Dispose(); }
        _timerBack = (Brush)value.Clone();
    }
}
private Brush _timerBack = null;

public Pen Border
{
    get { return _border; }
    set
    {
        if (_border!= null) { _border.Dispose(); }
        _border= (Pen)value.Clone();
    }
}
private Pen _border = null;

// default values (in constructor):
// (avoid making a clone)
_timerBack = new SolidBrush(Color.White);
_border = new Pen(Color.Black, 4)

// in the OnPaint method:
g.FillEllipse(TimerBack, ...
g.DrawEllipse(Border, ...

Now users of your class have more flexibility to customize your control, for example:

gt = new GraphicalTimer();

using (Brush myTimerBack = new LinearGradientBrush(new Point(0, 10), new Point(200, 10), Color.FromArgb(255, 255, 0, 0), Color.FromArgb(255, 0, 0, 255)))
{
    gt.TimerBack = myTimerBack;
}

using (Pen myBorder = new Pen(Color.Gray, 2))
{
    gt.Border = myBorder;
}

You should now also override the Dispose method of your control and dispose of the brush and pen objects in your class properties.

protected override void Dispose(bool disposing)
{
    // dispose of my objects first
    try { t.Dispose(); } catch {}
    try { TimerBack.Dispose(); } catch {}
    try { Border.Dispose(); } catch {}
    ...

    // dispose of base objects 
    base.Dispose(disposing);
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Welcome to Code Review! This is a well formed answer, thank you for taking the time to help others by reviewing their code, we look forward to seeing more of your answers and hopefully some of your questions as well! \$\endgroup\$ – Malachi Jul 16 '14 at 13:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.