Draw an analog clock in C#

Datetime:2016-08-22 22:12:37          Topic: C#           Share

This example demonstrates several useful techniques including:

  • Context menus
  • Restricting a form to a region to give it a shape.
  • Moving a form with no title bar
  • Double buffering
  • Drawing with transformations
  • Drawing an analog clock

When you right-click the clock, the program displays the context menus shown on the right. The size menu items all invoke the following code.

// Set the form's size according to the menu item's caption.
private void SetSize(ToolStripMenuItem menu_item)
{
    // Resize the form.
    string text = menu_item.Text.Replace("&", "");
    int width = int.Parse(text.Split('x')[0]);
    int height = int.Parse(text.Split('x')[1]);
    ClientSize = new Size(width, height);

    // Set the form's region.
    GraphicsPath path = new GraphicsPath();
    path.AddEllipse(this.ClientRectangle);
    this.Region = new Region(path);

    // Redraw.
    Refresh();

    // Check the correct menu item.
    ToolStripMenuItem[] items =
    {
        ctxSize100x100,
        ctxSize150x150,
        ctxSize100x150,
        ctxSize200x200
    };
    foreach (ToolStripMenuItem item in items)
        item.Checked = (item == menu_item);
}

This code gets the menu item’s caption and parses it to see what dimensions it indicates. It sets the form’s ClientSize property and then creates a GraphicsPath holding an ellipse that fits the form’s ClientRectangle . It sets the form’s Region property to that path. That restricts all of the form’s drawing and events to the path. In this case that gives the clock an elliptical shape. It also means mouse events that are not within the ellipse are not processed by the program and fall through to whatever window lies beneath.

Next the code refreshes the form to make it redraw the analog clock. That code is described shortly.

The method finishes by looping through the size menu items and checking the one that was selected.

At design time I removed the form’s border and title bar. Without the title bar, the user cannot resize or move the form. The context menu lets the user resize the clock. The following code shows how the program lets the user move it.

private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        // Release the mouse capture started by the mouse down.
        this.Capture = false;

        // Create and send a WM_NCLBUTTONDOWN message.
        const int WM_NCLBUTTONDOWN = 0x00A1;
        const int HTCAPTION = 2;
        Message msg =
            Message.Create(this.Handle, WM_NCLBUTTONDOWN,
                new IntPtr(HTCAPTION), IntPtr.Zero);
        this.DefWndProc(ref msg);
    }
}

When the user presses a mouse button down on the form, this code executes. If the left mouse button is pressed, the code sets this.Capture to false to end the mouse capture that automatically starts when you press the mouse down. It then sends the WM_NCLBUTTONDOWN message to the form to indicate that the user pressed a button down on a form’s non-client area such as the title bar or border. It includes the parameter HTCAPTION to indicate that the mouse press was on the title bar. This message makes the window start moving exactly as if the user had actually pressed on the title bar.

The next item described here is double buffering. If a program does a lot of complicated drawing very quickly, the user may see the form flicker. In this example, about twice per second, the program clears its form and redraws the analog clock face and hands. That’s enough drawing to cause flicker.

To prevent that flicker, the form’s Load event handler sets the form’s DoubleBuffer property to true . If that property is true , then whenever the form needs to redraw itself, it does so in a memory buffer first and it displays the result only when the redraw is complete. Because it is displaying the completed image all at once, the user doesn’t see the flicker. To see the difference in this program, comment out the code that sets DoubleBuffer and see what happens.

The final item I’ll mention in this post is drawing with transformations. To draw points on a circle (or the analog clock tick marks and hands in this example), you need to use the sine and cosine functions to figure out where the points belong on the circle. You would then need to translate those points so the result was centered on the form. While that isn’t terribly hard, it’s more convenient to draw the clock centered at the origin.

The following code shows the program’s main Paint event handler. Before drawing, it uses the TranslateTransform method to make the Graphics object translate all drawing so it is centered on the form. That lets the code draw objects centered at the origin and the objects are transformed to fill the form automatically.

// Draw the clock.
private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.Clear(BackColor);
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.TextRenderingHint =
        TextRenderingHint.AntiAliasGridFit;

    // Translate to center the drawing.
    e.Graphics.TranslateTransform(
        ClientSize.Width / 2,
        ClientSize.Height / 2);

    // Draw the face including tick marks.
    DrawClockFace(e.Graphics);

    // Show the time digitally.
    ShowDigitalTime(e.Graphics);

    // Draw the hands.
    DrawClockHands(e.Graphics);

    // Draw the center.
    e.Graphics.FillEllipse(Brushes.Blue, -5, -5, 10, 10);
}

The Paint method calls other methods to draw the analog clock face, the digital time (if desired), and the clock’s hands. You could do all of that in this one event handler, but that would make the code harder to read.

Note also that you could use other Graphics transformations to make drawing the clock somewhat easier. For example, you could use a scale transformation to scale the clock to the desired size and then draw it at some convenient scale such as 100×100 pixels or even 1×1 units centered at the origin. That would complicate the drawing slightly, however, because line widths would also be scaled. If you draw with pens that have a thickness of 0, the Graphics object knows to make the lines as thin as possible and not scale them. In this example, however, I use a few thicker lines for the clock’s hands, so I didn’t think this simplification was worth it.

The rest of the program is interesting but relatively straightforward. For example, drawing the analog clock face and hands is just a matter of performing a bunch of drawing operations, using sines and cosines to figure out where things should be drawn. Download the example to see the details.





About List