Monday, March 21, 2011

How to display an analog clock ?

The previous article demonstrated a digital clock. An obvious follow-up question is how to display an analog clock.


To display an analog clock, the first thing is of course to draw a circle.

The class Graphics2D contains a method for drawing circular arc.

java.awt.Graphics2D gr=(java.awt.Graphics2D) graphics;    
gr.drawArc(0,0,200,200,0,360);

That means to draw a elliptical arc in the bounding rectangle (0,0,200,200).
Since the bounding rectangle is a square, it will actually draw a circular arc.
The arc starts from 0 degree and ends at 360 degree. That would draw a full circle.
The radius of the circle is 100 and the center is at (100,100)

The following is the first version of the 'analog clock'.

/******************************************************************************
* File : AnalogClock.java (version 1)
* Author : http://java.macteki.com/
* Description :
*   Display an analog clock. (Draw a circle only)
* Tested with : JDK 1.6
******************************************************************************/

class AnalogClock extends javax.swing.JPanel
{
  public static AnalogClock getInstance()
  {
    AnalogClock panel=new AnalogClock();
    panel.setPreferredSize(new java.awt.Dimension(300,300));
    return panel;
  }

  public void paintComponent(java.awt.Graphics graphics)
  {
    super.paintComponent(graphics);
 
    java.awt.Graphics2D gr=(java.awt.Graphics2D) graphics;    

    gr.drawArc(0,0,200,200,0,360);
  }

  public static void main(String[] args) throws Exception
  {
    // create frame
    javax.swing.JFrame frame=new javax.swing.JFrame();
    frame.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);    

    frame.setTitle("Macteki AnalogClock");

    AnalogClock clockPanel=AnalogClock.getInstance();
   
    frame.add(clockPanel);  
    frame.pack();  // set the window size big enough to hold its components
    frame.setVisible(true); 

  }
}

The next thing to do is to draw a clock arm. Let's start with the 'seconds' arm since it is moving every second and its motion is obvious.

This arm rotates a circle every 60 seconds, hence it should move 6 degree per second.

We can easily define this arm using polar coordinates.

Two variables are defined :

radius = length of arm
degree = angle between the arm and the positive x-axis


First read the 'seconds' value from the current time, then calculate the degree it should have moved, then transform the polar coordinates to screen coordinates.

The transformation is done by :
x = radius*cos(angle);
y = radius*sin(angle);
where 'angle' is in radian.


Note that the java coordinates system is 'upside down', whereas the Cartesian coordinates has the positive y-axis pointing upwards. Hence a transformation need to be applied :
y = -y;


The following sample is an analog clock with one moving arm.

/******************************************************************************
* File : AnalogClock.java (version 2)
* Author : http://java.macteki.com/
* Description :
*   Display an analog clock with a moving arm (seconds)
* Tested with : JDK 1.6
******************************************************************************/

class AnalogClock extends javax.swing.JPanel
{
  public static AnalogClock getInstance()
  {
    AnalogClock panel=new AnalogClock();
    panel.setPreferredSize(new java.awt.Dimension(300,300));
    return panel;
  }

  public void paintComponent(java.awt.Graphics graphics)
  {
    super.paintComponent(graphics);
 
    java.awt.Graphics2D gr=(java.awt.Graphics2D) graphics;    

    gr.drawArc(0,0,200,200,0,360);

    int xCenter=100,yCenter=100;

    java.util.Calendar calendar=java.util.Calendar.getInstance();
    int second=calendar.get(java.util.Calendar.SECOND);

    // polar coordinate of the 'second' arm
    // This arm start at 90 degree and it should move 6 degree per second
    int degree = 90-second*6;
    int radius = 80;
    double pi=3.141592654;

    int x=(int) (radius*Math.cos(degree*pi/180));
    int y=(int) (radius*Math.sin(degree*pi/180));

    y=-y;  // positive y-axis should point upwards.

    // translate the whole coordinate system.
    x+=xCenter;
    y+=yCenter;

    gr.drawLine(xCenter,yCenter,x,y);
  }

  public static void main(String[] args) throws Exception
  {
    // create frame
    javax.swing.JFrame frame=new javax.swing.JFrame();
    frame.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);    

    frame.setTitle("Macteki AnalogClock");

    AnalogClock clockPanel=AnalogClock.getInstance();
   
    frame.add(clockPanel);  
    frame.pack();  // set the window size big enough for its components.
    frame.setVisible(true); 

    while (true)
    {
      clockPanel.repaint();
      Thread.sleep(500);
    }

  }
}


Next we add a digital clock for comparison. The previous article has explained how to do this.

/******************************************************************************
* File : AnalogClock.java (version 3)
* Author : http://java.macteki.com/
* Description :
*   Display an analog clock with a digital label
* Tested with : JDK 1.6
******************************************************************************/

import javax.swing.JLabel;

class AnalogClock extends javax.swing.JPanel
{
  public static AnalogClock getInstance()
  {
    AnalogClock panel=new AnalogClock();
    panel.setPreferredSize(new java.awt.Dimension(300,300));
    return panel;
  }

  public void paintComponent(java.awt.Graphics graphics)
  {
    super.paintComponent(graphics);
 
    java.awt.Graphics2D gr=(java.awt.Graphics2D) graphics;    

    gr.drawArc(0,0,200,200,0,360);

    int xCenter=100,yCenter=100;

    java.util.Calendar calendar=java.util.Calendar.getInstance();
    int second=calendar.get(java.util.Calendar.SECOND);

    // polar coordinate of the 'second' arm
    // This arm start at 90 degree and it should move 6 degree per second
    int degree = 90-second*6;
    int radius = 80;
    double pi=3.141592654;

    int x=(int) (radius*Math.cos(degree*pi/180));
    int y=(int) (radius*Math.sin(degree*pi/180));

    y=-y;  // positive y-axis should point upwards.

    // translate the whole coordinate system.
    x+=xCenter;
    y+=yCenter;

    gr.drawLine(xCenter,yCenter,x,y);
  }

  public static void main(String[] args) throws Exception
  {
    // create frame
    javax.swing.JFrame frame=new javax.swing.JFrame();
    frame.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);    

    frame.setTitle("Macteki AnalogClock");

    AnalogClock clockPanel=AnalogClock.getInstance();

    // create a image label
    JLabel digitalLabel=new JLabel("", JLabel.CENTER); 
    digitalLabel.setBounds(0,250,300,30);
    clockPanel.setLayout(null);
    clockPanel.add(digitalLabel);

    frame.add(clockPanel);  
    frame.pack();  // set the window size big enough for its components
    frame.setVisible(true); 

    while (true)
    {
      java.util.Date date=new java.util.Date();
      digitalLabel.setText(""+date);
      clockPanel.repaint();
      Thread.sleep(500);
    }

  }
}


Finally we add the minute and the hour arm.

They are similar to the 'second' arm.

The minute arm rotates a full circle every 60 minutes, that is 3600 seconds.
That means it moves 360 degree in 3600 seconds.
That is 0.1 degree per second.

The hour arm rotates a full circle every 43200 seconds.
That is 0.008333 degree per second.

Full sample below :

/******************************************************************************
* File : AnalogClock.java
* Author : http://java.macteki.com/
* Description :
*   Display an analog clock.
* Tested with : JDK 1.6
******************************************************************************/

import javax.swing.JLabel;

class AnalogClock extends javax.swing.JPanel
{
  public static AnalogClock getInstance()
  {
    AnalogClock panel=new AnalogClock();
    panel.setPreferredSize(new java.awt.Dimension(300,300));
    return panel;
  }

  public void paintComponent(java.awt.Graphics graphics)
  {
    super.paintComponent(graphics);
 
    java.awt.Graphics2D gr=(java.awt.Graphics2D) graphics;    

    gr.drawArc(0,0,200,200,0,360);

    int xCenter=100,yCenter=100;

    java.util.Calendar calendar=java.util.Calendar.getInstance();
    int second=calendar.get(java.util.Calendar.SECOND);

    // polar coordinate of the 'second' arm
    // This arm start at 90 degree and it should move 6 degree per second
    double degree = 90-second*6;
    int radius = 80;
    double pi=3.141592654;

    int x=(int) (radius*Math.cos(degree*pi/180));
    int y=(int) (radius*Math.sin(degree*pi/180));

    y=-y;  // positive y-axis should point upwards.

    // translate the whole coordinate system.
    x+=xCenter;
    y+=yCenter;

    gr.drawLine(xCenter,yCenter,x,y);


    // draw the minute arm, it should move 0.1 degree every second.
    int minute=calendar.get(java.util.Calendar.MINUTE);
    double secondPassedInThisHour = minute*60+second;
    degree = 90.0 - secondPassedInThisHour*(0.1);
    radius = 80;

    x=(int) (radius*Math.cos(degree*pi/180));
    y=(int) (radius*Math.sin(degree*pi/180));

    y=-y;  // positive y-axis should point upwards.

    // translate the whole coordinate system.
    x+=xCenter;
    y+=yCenter;
    
    gr.drawLine(xCenter,yCenter,x,y);    


    // draw the hour arm, since this arm should move 360 degree in 12 hour
    // 12 hour = 43200 seconds
    // it should move 360/43200 degree every second.
    int hour=calendar.get(java.util.Calendar.HOUR);
    if (hour>12) hour-=12;
    double secondPassed = hour*3600+minute*60+second;
    degree = 90.0 - secondPassed*(360.0/43200.0);
    radius = 60;  // make it shorter

    x=(int) (radius*Math.cos(degree*pi/180));
    y=(int) (radius*Math.sin(degree*pi/180));

    y=-y;  // positive y-axis should point upwards.

    // translate the whole coordinate system.
    x+=xCenter;
    y+=yCenter;
    
    gr.drawLine(xCenter,yCenter,x,y);    

  }

  public static void main(String[] args) throws Exception
  {
    // create frame
    javax.swing.JFrame frame=new javax.swing.JFrame();
    frame.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);    

    frame.setTitle("Macteki AnalogClock");

    AnalogClock clockPanel=AnalogClock.getInstance();

    // create a image label
    JLabel digitalLabel=new JLabel("", JLabel.CENTER); 
    digitalLabel.setBounds(0,250,300,30);
    clockPanel.setLayout(null);
    clockPanel.add(digitalLabel);

    frame.add(clockPanel);  
    frame.pack();  // set the window size big enough to hold its component
    frame.setVisible(true); 

    while (true)
    {
      java.util.Date date=new java.util.Date();
      digitalLabel.setText(""+date);
      clockPanel.repaint();
      Thread.sleep(500);
    }

  }
}


This is not a pretty clock but it serves the purpose of a simple demonstration. You may draw a clock with better appearance if you wish.

Thanks for reading. Comments are welcome.

6 comments:

  1. Replies
    1. My pleasure. Glad to know that it is useful to some readers.

      Delete
  2. I am trying to make a calendar program and want to add this clock to it. How would I implement the clock to my main class? I'm kinda new to programming so I apologize if the answer is rather simple.

    ReplyDelete
    Replies
    1. 1.Put AnalogClock.java to the same path as your main class

      2. If your main class is contained inside a package, (e.g. package projectx), then "package projectx" must be added at the beginning of AnalogClock.java

      3. In getInstance(), just before returning, add this :

      new Thread(new Runnable() {
        public void run()
        {
          while (true)
          {
            java.util.Date date=new java.util.Date();
            panel.repaint();
            try { Thread.sleep(500); } catch (Exception ignore) {}
          }
        }
      }).start();

      4. In the main program, after creating the main window (e.g. JFrame frame), just write:

      AnalogClock clockPanel=AnalogClock.getInstance();
      frame.add(clockPanel);

      Delete
  3. It is giving me an error on the "panel.repaint();" line and tells me that "local variable panel is accessed from within inner class; needs to be declared final."
    (If this is of any concern, I am using BlueJ to code)

    ReplyDelete
    Replies
    1. The error has told you what to do, just declare panel as final :
        final AnalogClock panel=new AnalogClock();

      Delete