4
\$\begingroup\$

I've written a little clock implementation in Java Swing. It now seems to work flawlessly after having terrible Swing specific timing problems. Please indicate if this is the way to draw the clock and if the passive / active combination (i.e. drawing from timer and drawing from e.g. paintComponent) has been programmed correctly.

I've left some question marks in the source code comments for things that I'm unsure of. It has been quite some time since my last Swing application, so any other comment is welcome too. Once it has been reviewed I can extend it to an alarm clock...

This is fully runnable code, programmed for Java 13 (it won't compile for Java 8).

package digiclock;

import static digiclock.ClockUtil.convertFromDimensionInMM;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JFrame;

public class DigiClock extends JFrame {

    private static final long serialVersionUID = 1L;

    private static final Dimension DIMENSION_IN_MM = new Dimension(60, 20);
    private static final Dimension MINIMUM_DIMENSION_IN_MM = new Dimension(40, 20);

    /**
     * Inner class that allows for dragging and closing the clock frame.
     */
    private class ClockMouseAdapter extends MouseAdapter {
        private int pointerLocationX;
        private int pointerLocationY;

        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() == 2) {
                DigiClock.this.dispose();
            }
        }
        
        @Override
        public void mousePressed(MouseEvent me) {
            pointerLocationX = me.getX();
            pointerLocationY = me.getY();
        }

        @Override
        public void mouseDragged(MouseEvent me) {
            DigiClock.this.setLocation(DigiClock.this.getLocation().x + me.getX() - pointerLocationX,
                    DigiClock.this.getLocation().y + me.getY() - pointerLocationY);
        }
    }

    public static void main(String[] args) {
        var clock = new DigiClock();
        clock.setVisible(true);
    }
    
    public DigiClock() {
        setTitle("24-hour clock");
        // --> resizing hard to implement? setUndecorated(true); <--
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        // reduces flickering during resize if set to same color as ClockPanel
        setBackground(Color.DARK_GRAY);
        setSize(convertFromDimensionInMM(DIMENSION_IN_MM));
        setMinimumSize(convertFromDimensionInMM(MINIMUM_DIMENSION_IN_MM));
        
        var clockFont = loadFont();
        var clockPanel = new ClockPanel(ClockPrecision.SECOND, Color.GREEN, Color.DARK_GRAY, clockFont);
        add(clockPanel);
        
        var clockMouseAdapter = new ClockMouseAdapter();
        addMouseListener(clockMouseAdapter);
        addMouseMotionListener(clockMouseAdapter);
    }
    
    private static Font loadFont() {
        Font font;
        try {
            var is = ClockUtil.class.getResourceAsStream("E1234.ttf");
            font = Font.createFont(Font.TRUETYPE_FONT, is);
        } catch(@SuppressWarnings("unused") Exception e) {
            // choose a default font if the font cannot be loaded for some reason or other
            font = new Font("Sans-Serif", Font.BOLD, 20);
        }
        return font;
    }
}

the one and only panel, the magic happens here.

package digiclock;

import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JPanel;

public class ClockPanel extends JPanel {

    private static final long serialVersionUID = 1L;

    private static final String MAX_SIZED_TIME_STRING = "00:00:00";

    // i.e. 10 percent on both sides
    private static final float MARGIN_PART = 0.1F;

    public class ClockTimerTask extends TimerTask {

        @Override
        public void run() {
            // --> is this how we should render? <---
            ClockPanel.this.drawDigitalClock(ClockPanel.this.getGraphics());
            var delay = calculateDelay(precision);
            timer.schedule(new ClockTimerTask(), delay);
        }
    }

    public class Repainter implements Runnable {

        @Override
        public void run() {
            ClockPanel.this.drawDigitalClock(getGraphics());
        }
    }

    private final Timer timer;
    private final ClockPrecision precision;
    private final Color foregroundColor;
    private final Font clockFont;

    private Font currentClockFont;
    private boolean sizeChanged = true;

    public ClockPanel(ClockPrecision precision, Color foregroundColor, Color backgroundColor, Font clockFont) {
        this.precision = precision;
        this.foregroundColor = foregroundColor;
        this.clockFont = clockFont;
        this.currentClockFont = clockFont;

        setBackground(backgroundColor);

        addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                sizeChanged = true;
            }
        });

        // note that the Swing timer is so imprecise that it's usefulness is basically reduced to zero
        this.timer = new Timer("ClockTimer");
        var initialDelay = calculateDelay(precision);
        this.timer.schedule(new ClockTimerTask(), initialDelay);

        setIgnoreRepaint(true);
    }

    @Override
    public void paintComponent(Graphics graphics) {
        drawDigitalClock(graphics);
    }

    /**
     * Draws the digital clock using the font field and given a graphics context.
     * 
     * @param graphics the graphics contents
     */
    // --> is synchronized sufficient to prevent deadlock in paintComponent e.g. during resizing? <--
    public synchronized void drawDigitalClock(Graphics graphics) {
        super.paintComponent(graphics);

        var dimension = getSize();
        var marginSizeX = dimension.width * MARGIN_PART;
        var marginSizeY = dimension.height * MARGIN_PART;
        var marginSizeMin = Math.min(marginSizeX, marginSizeY);

        if (sizeChanged) {
            var innerDimension = new Dimension((int) (dimension.width - 2 * marginSizeMin),
                    (int) (dimension.height - 2 * marginSizeMin));

            currentClockFont = resizeFontForDimension(graphics, clockFont, MAX_SIZED_TIME_STRING, innerDimension);
            sizeChanged = false;
        }

        graphics.setColor(foregroundColor);

        // --> would it be a good idea to create my own graphics rendering here? <--
        // e.g. var clockGraphics = graphics.create();

        if (graphics instanceof Graphics2D) {
            ((Graphics2D) graphics).setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
        }

        graphics.setFont(currentClockFont);

        var metrics = graphics.getFontMetrics(currentClockFont);
        float xCenter = dimension.width * 0.5F;
        float yCenter = dimension.height * 0.5F;
        String timeString = getTimeString(precision, LocalTime.now());

        int x = (int) (xCenter - metrics.stringWidth(MAX_SIZED_TIME_STRING) / 2.0F);
        int y = (int) (yCenter + metrics.getHeight() / 2 - metrics.getDescent() / 2);

        graphics.drawString(timeString, x, y);

        Toolkit.getDefaultToolkit().sync();
    }

    private static int calculateDelay(ClockPrecision precission) {
        var delayNow = Instant.now();
        return precission.getInMilli() - (int) (delayNow.toEpochMilli() % precission.getInMilli());
    }

    private static String getTimeString(ClockPrecision precision, LocalTime time) {
        // TODO take care of precision

        // pattern for 24 hrs
        var pattern = DateTimeFormatter.ofPattern("HH:mm:ss");
        return time.format(pattern);
    }

    /**
     * Calculates the best font size to fit in a certain dimension (in pixels) given a graphics context, a font-type and
     * a maximum sized text; then returns the font.
     * 
     * @param graphics     the graphics for which to compute the dimensions
     * @param font         the font for which to compute the dimension
     * @param text         the (max sized) text to display
     * @param forDimension the size in which the text needs to be displayed, excluding margins
     * @return the correctly sized font
     */
    private static Font resizeFontForDimension(Graphics graphics, Font font, String text, Dimension forDimension) {
        // get metrics from the graphics
        var metrics = graphics.getFontMetrics(font);
        var lineMetrics = metrics.getLineMetrics(MAX_SIZED_TIME_STRING, graphics);

        int orgTextWidth = metrics.stringWidth(text);
        float ascent = lineMetrics.getAscent();

        // calculate font size
        float difTextWidth = (float) forDimension.width / (float) orgTextWidth;
        float difTextHeigth = (float) forDimension.height / ascent;
        float minDif = Math.min(difTextWidth, difTextHeigth);

        return font.deriveFont(font.getSize2D() * minDif);
    }
}

a single utility class

package digiclock;

import java.awt.Dimension;
import java.awt.Toolkit;

public final class ClockUtil {
    
    private static final float MM_IN_INCH = 0.0393701F;
    private static final int RES = Toolkit.getDefaultToolkit().getScreenResolution();

    private ClockUtil() {
        // avoid instantiation
    }

    public static Dimension convertFromDimensionInMM(Dimension dimInMM) {
        return new Dimension(mmToPixels(dimInMM.width), mmToPixels(dimInMM.height));
    }

    public static int mmToPixels(int mm) {
        return (int) (mm * MM_IN_INCH * RES);
    }
}

and finally the little enum

package digiclock;

public enum ClockPrecision {
    
    DECISECOND(100), SECOND(1000), MINUTE(60_000);

    private int inMS;

    private ClockPrecision(int inMS) {
        this.inMS = inMS;
    }

    public int getInMilli() {
        return inMS;
    }
}

Things I already know:

  • JavaDoc is incomplete;
  • Swing Timer / repaint doesn't work.

Things I am curious about:

  • the use of Graphics my code;
  • how to center the text in the Panel (current height calculation is mainly adjusted until it looked right);
  • possibility of deadlock.
\$\endgroup\$
2
  • \$\begingroup\$ Is there a reason you're drawing the time yourself instead of using a (or multiple) labels? Also, I guess you're aware that your mm to pixel conversion only works for certain screens correctly. \$\endgroup\$ – Bobby Nov 27 '20 at 16:56
  • \$\begingroup\$ @Bobby Honestly I've just thought how to draw the time and wanting to add graphical elements later. Those might not be as easily captured in a label. I'm also not planning to use any kind of LayoutManager although I guess you could always use a null layout... As for the mm, sure I know that, but it is at least more useful than pixels in many cases. \$\endgroup\$ – Maarten Bodewes Nov 28 '20 at 1:18

Your Answer

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

Browse other questions tagged or ask your own question.