經典類比時鐘是圖形程式設計入門佳作。本文提供生產級 AnalogClock:雙緩衝無閃爍、平滑秒針動畫 (每秒重繪)、時/分/秒針幾何計算、系統時間同步、可配置外觀 (顏色、粗細、字型)、資源正確釋放、打包為可執行 JAR。
package com.example.swing;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Line2D;
import java.time.LocalTime;
import java.time.ZoneId;
public class AnalogClock extends JFrame {
private static final int SIZE = 400;
private static final int MARGIN = 20;
private static final int RADIUS = (SIZE - 2 * MARGIN) / 2;
private static final int CENTER = SIZE / 2;
private final ClockPanel clockPanel = new ClockPanel();
private final Timer timer;
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {}
new AnalogClock().setVisible(true);
});
}
public AnalogClock() {
super("類比時鐘");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setResizable(false);
add(clockPanel);
pack();
setLocationRelativeTo(null);
// 每秒觸發重繪
timer = new Timer(1000, e -> clockPanel.repaint());
timer.start();
// 視窗關閉時停止 Timer
addWindowListener(new java.awt.event.WindowAdapter() {
@Override public void windowClosing(java.awt.event.WindowEvent e) {
timer.stop();
}
});
}
private class ClockPanel extends JPanel {
public ClockPanel() {
setPreferredSize(new Dimension(SIZE, SIZE));
setBackground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setStroke(new BasicStroke(2f));
// 1. 外圓
g2.setColor(new Color(0x333333));
g2.drawOval(MARGIN, MARGIN, 2 * RADIUS, 2 * RADIUS);
// 2. 刻度與數字
drawTicksAndNumbers(g2);
// 3. 取得當前時間
LocalTime now = LocalTime.now(ZoneId.systemDefault());
int hour = now.getHour();
int minute = now.getMinute();
int second = now.getSecond();
int milli = now.getNano() / 1_000_000;
// 4. 計算角度 (12 點鐘方向 = -90° = -PI/2)
// 秒針:平滑移動 (含毫秒)
double secAngle = Math.toRadians(6 * second + 6.0 * milli / 1000 - 90);
// 分針:含秒數微調
double minAngle = Math.toRadians(6 * minute + 0.1 * second - 90);
// 時針:含分數微調 (12 小制)
double hourAngle = Math.toRadians(30 * (hour % 12) + 0.5 * minute - 90);
// 5. 繪製指針 (中心點 CENTER, CENTER)
drawHand(g2, hourAngle, RADIUS * 0.5, 6f, new Color(0x2C3E50)); // 時針:短、粗、深藍
drawHand(g2, minAngle, RADIUS * 0.75, 4f, new Color(0xE74C3C)); // 分針:中、中、紅
drawHand(g2, secAngle, RADIUS * 0.9, 1.5f, new Color(0xE67E22)); // 秒針:長、細、橙
// 6. 中心圓
g2.setColor(new Color(0x2C3E50));
g2.fillOval(CENTER - 6, CENTER - 6, 12, 12);
// 7. 數位時間顯示 (左上角)
g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 18));
g2.setColor(Color.BLACK);
String digital = String.format("%02d:%02d:%02d", hour, minute, second);
g2.drawString(digital, MARGIN + 5, MARGIN + 20);
}
private void drawTicksAndNumbers(Graphics2D g2) {
g2.setFont(new Font(Font.SERIF, Font.BOLD, 18));
g2.setColor(Color.BLACK);
for (int i = 1; i <= 12; i++) {
double angle = Math.toRadians(30 * i - 90); // 30° per hour
int outer = RADIUS - 10;
int inner = RADIUS - 25;
int xOuter = CENTER + (int) (outer * Math.cos(angle));
int yOuter = CENTER + (int) (outer * Math.sin(angle));
int xInner = CENTER + (int) (inner * Math.cos(angle));
int yInner = CENTER + (int) (inner * Math.sin(angle));
// 粗細刻度
g2.setStroke(i % 3 == 0 ? new BasicStroke(3f) : new BasicStroke(1.5f));
g2.draw(new Line2D.Double(xInner, yInner, xOuter, yOuter));
// 數字 (僅 3,6,9,12 或全部)
if (i % 3 == 0) {
String num = String.valueOf(i);
FontMetrics fm = g2.getFontMetrics();
int tx = CENTER + (int) ((RADIUS - 35) * Math.cos(angle)) - fm.stringWidth(num) / 2;
int ty = CENTER + (int) ((RADIUS - 35) * Math.sin(angle)) + fm.getAscent() / 2;
g2.drawString(num, tx, ty);
}
}
}
private void drawHand(Graphics2D g2, double angle, double length, float thickness, Color color) {
int x = CENTER + (int) (length * Math.cos(angle));
int y = CENTER + (int) (length * Math.sin(angle));
g2.setStroke(new BasicStroke(thickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setColor(color);
g2.drawLine(CENTER, CENTER, x, y);
}
}
}
| 技術 | 說明 |
|---|---|
Timer(1000, ...) | Swing Timer 在 EDT 執行,安全更新 UI;1000ms 精度足夠秒針動畫。 |
| 平滑秒針 | 6 * second + 6.0 * milli / 1000 讓秒針每毫秒微動,非跳躍式。 |
| 角度基準 | 數學慣例 0° = 3 點鐘方向;12 點鐘 = -90°,故全部 - 90。 |
| 雙緩衝 | Swing JPanel 預設雙緩衝,無需手動 BufferStrategy。 |
BasicStroke.CAP_ROUND | 指針末端圓潤,視覺更專業。 |
LocalTime.now(ZoneId) | JDK 8+ 時間 API,執行緒安全、不可變、時區明確。 |
JComboBox<ZoneId> 選擇時區,clockPanel.repaint() 即時更新。ScheduledExecutorService 排程觸發 Toolkit.getDefaultToolkit().beep() 或彈窗。ClockTheme 物件,支援深色/淺色模式。setUndecorated(true); setExtendedState(MAXIMIZED_BOTH);。# Maven
mvn clean package -DskipTests
java -jar target/analog-clock-1.0.jar
# 或直接編譯執行
javac com/example/swing/AnalogClock.java
java com.example.swing.AnalogClock
timer.start() 已呼叫,且未在 paintComponent 執行耗時操作阻塞 EDT。ZoneId.of("UTC") 或使用者設定時區。-Dsun.java2d.uiScale.enabled=true;或自行縮放 g2.scale(scale, scale)。timer.stop()。