8)
Um mini-relógio para desktop completamente funcional, que usa a imagem do desktop do usuário e mantém um ícone no system tray (ao lado do relógio do Windows) com uma opção para sair da aplicação.
Contém inúmeras técnicas de animação, aceleração gráfica, processamento digital de imagens (pixel a pixel), entre outras coisas. Todas as imagens são geradas em tempo de execução (incluindo o ícone do system tray).
Eu demorei uns bons meses para reunir os diferentes conhecimentos necessários para isso, então pode servir de uma boa fonte de estudos para quem está pesquisando animação, processamento de imagens pixel a pixel (algo que dizem que não tem em Java, mas quem diz isso apenas não se aprofundou para saber) e programação de jogos em Java (embora aqui não haja controles via teclado ou mouse).
Segue o código e o jar auto-executável em anexo. Experimentem pra ver e postem quaisquer dúvidas aqui!
:wink:
obs: tomara que esse editor de texto não distorça alguns caracteres como às vezes faz. Por via das dúvidas segue o fonte em anexo também!
import java.awt.AWTException;
import java.awt.AlphaComposite;
import java.awt.BufferCapabilities;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.ImageCapabilities;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Robot;
import java.awt.SystemTray;
import java.awt.Transparency;
import java.awt.TrayIcon;
import java.awt.BufferCapabilities.FlipContents;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import javax.swing.JDialog;
import javax.swing.JFrame;
public class MiniClock extends JDialog implements Runnable {
private static final GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
private static final GraphicsDevice dev = env.getDefaultScreenDevice();
private static final GraphicsConfiguration conf = dev.getDefaultConfiguration();
private static final DisplayMode mode = dev.getDisplayMode();
private static final long serialVersionUID = 1L;
private static final Font font = new Font("Courier New", Font.BOLD, (int)(0.015 * mode.getHeight()));
private static final RenderingHints rh = RendHints.getInstance();
private static final int size = (int)(0.17 * (double)mode.getHeight());
private static final int deskPad = 10;
private static final double fps = 60;
public static void main(String... args) {
new MiniClock();
}
private volatile boolean running;
private Integer[] raiosMenores;
private Integer[] raiosMaiores;
private BufferedImage trayIcon;
private BufferedImage background;
private BufferedImage[] numbers;
private Calendar clock;
public MiniClock() {
config();
initRaios();
createImages();
init();
start();
}
private void config() {
this.setAlwaysOnTop(true);
this.setIgnoreRepaint(true);
this.setUndecorated(true);
this.setResizable(false);
this.setFocusable(true);
this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
MiniClock.this.stop();
}
});
this.addFocusListener(new FocusAdapter(){
public void focusLost(FocusEvent e) {
MiniClock.this.requestFocus();
}
});
this.pack();
this.setSize(size, size);
this.setLocation(deskPad, deskPad);
this.createBufferStrategy();
}
private void createBufferStrategy() {
try {
this.createBufferStrategy(2, new BufferCapabilities(new ImageCapabilities(true), new ImageCapabilities(true), FlipContents.UNDEFINED));
System.out.println("Flip Contents ON");
} catch (AWTException e) {
this.createBufferStrategy(2);
System.out.println("Flip Contents OFF");
}
}
private void initRaios() {
double raioMedio = size / 2.0;
double raioInc = 0.075 * raioMedio;
double maiorRaio = Math.pow(2 * Math.pow(raioMedio, 2), 0.5);
double menorRaio = 3 * raioInc;
double raioMostrador = raioMedio - raioInc;
List<Integer> raiosMaiores = new ArrayList<Integer>();
for(double raio = maiorRaio; raio >= raioMedio; raio -= raioInc) {
raiosMaiores.add((int)raio);
}
this.raiosMaiores = raiosMaiores.toArray(new Integer[]{});
List<Integer> raiosMenores = new ArrayList<Integer>();
for(double raio = raioMostrador; raio >= menorRaio; raio -= raioInc) {
raiosMenores.add((int)raio);
}
this.raiosMenores = raiosMenores.toArray(new Integer[]{});
}
private void createImages() {
createIconImage();
createNumberImages();
createBackgroundImage();
}
private void createIconImage() {
Dimension trayIconSize = SystemTray.getSystemTray().getTrayIconSize();
double posXCentro = trayIconSize.getWidth()/2.0;
double posYCentro = trayIconSize.getHeight()/2.0;
BufferedImage image = new BufferedImage(trayIconSize.width, trayIconSize.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = image.createGraphics();
g2.setRenderingHints(rh);
g2.setColor(Color.CYAN);
g2.drawRect(0, 0, image.getWidth()-1, image.getHeight()-1);
g2.drawLine((int)posXCentro, (int)posYCentro, (int)posXCentro+3, (int)posYCentro);
g2.drawLine((int)posXCentro, (int)posYCentro, (int)posXCentro, (int)posYCentro-3);
g2.dispose();
Raster raster = (Raster)image.getRaster();
SampleModel model = raster.getSampleModel();
double raio = (trayIconSize.getWidth() - 4.0) / 2.0;
int[] pixel = new int[]{0, 0xFF, 0xFF, 0xFF};
for(double angle = 270, ctrlAngle=0; ctrlAngle < 360; ctrlAngle += (360.0/12.0), angle = (angle + 360.0/12.0) % 360.0) {
double posX = posXCentro + raio * Math.cos(Math.toRadians(angle));
double posY = posYCentro + raio * Math.sin(Math.toRadians(angle));
model.setPixel((int)posX, (int)posY, pixel, raster.getDataBuffer());
}
// Acelerando
BufferedImage finalImage = conf.createCompatibleImage(image.getWidth(), image.getHeight(), Transparency.TRANSLUCENT);
g2 = finalImage.createGraphics();
g2.setRenderingHints(rh);
g2.drawImage(image, 0, 0, null);
g2.dispose();
this.trayIcon = finalImage;
}
private void createNumberImages() {
BufferedImage tmp = new BufferedImage(50, 50, BufferedImage.TYPE_INT_ARGB);
Graphics2D gTmp = tmp.createGraphics();
gTmp.setRenderingHints(rh);
gTmp.setFont(font);
FontMetrics fm = gTmp.getFontMetrics();
List<BufferedImage> list = new ArrayList<BufferedImage>();
for(int number = 12, ctrl=0; ctrl<12; ctrl++, number = (number + 1) % 13) {
if(number == 0) number = 1;
String sNumber = leftPadding(number, '0', 2);
BufferedImage image = new BufferedImage(fm.stringWidth(sNumber), fm.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = image.createGraphics();
g2.setRenderingHints(rh);
g2.setColor(ColorType.FONTE.getColor());
g2.drawString(sNumber, 0, fm.getAscent());
g2.dispose();
list.add(trimTransp(image));
}
this.numbers = list.toArray(new BufferedImage[]{});
gTmp.dispose();
}
public void createBackgroundImage() {
BufferedImage image = conf.createCompatibleImage(this.getWidth(), this.getHeight(), Transparency.TRANSLUCENT);
Graphics2D g2 = image.createGraphics();
g2.setRenderingHints(rh);
Composite comp = g2.getComposite();
try {
g2.drawImage(new Robot().createScreenCapture(new Rectangle(deskPad, deskPad, this.getWidth(), this.getHeight())), 0, 0, null);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.85F));
g2.setColor(ColorType.FUNDO.getColor());
g2.fillRect(-2, -2, image.getWidth()+2, image.getHeight()+2);
g2.setComposite(comp);
} catch (AWTException e) {
g2.setColor(ColorType.FUNDO.getColor());
g2.fillRect(-2, -2, image.getWidth()+2, image.getHeight()+2);
}
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3F));
g2.setColor(ColorType.MOLDURA.getColor());
double posXCentro = this.getWidth() / 2.0;
double posYCentro = this.getHeight() / 2.0;
double raioExterno = raiosMaiores[0];
double raioInterno = raiosMenores[raiosMenores.length-1];
for(double raio = raioInterno; raio <= raioExterno; raio += 0.1 * (this.getWidth() / 2.0)) {
g2.drawOval((int)(posXCentro - raio), (int)(posYCentro - raio), (int)(raio*2), (int)(raio*2));
}
raioExterno = raiosMenores[0];
for(double angle=270, angleCtrl=0; angleCtrl<=360.0; angleCtrl += (360.0/48.0), angle = (angle + (360.0/48.0)) % 360.0) {
double posXMenor = posXCentro + raioInterno * Math.cos(Math.toRadians(angle));
double posYMenor = posYCentro + raioInterno * Math.sin(Math.toRadians(angle));
double posXMaior = posXCentro + raioExterno * Math.cos(Math.toRadians(angle));
double posYMaior = posYCentro + raioExterno * Math.sin(Math.toRadians(angle));
g2.drawLine((int)(posXMenor), (int)(posYMenor), (int)(posXMaior), (int)(posYMaior));
}
raioInterno = raioExterno;
raioExterno = raiosMaiores[0];
for(double angle=270, angleCtrl=0; angleCtrl<=360.0; angleCtrl += (360.0/96.0), angle = (angle + (360.0/96.0)) % 360.0) {
double posXMenor = posXCentro + raioInterno * Math.cos(Math.toRadians(angle));
double posYMenor = posYCentro + raioInterno * Math.sin(Math.toRadians(angle));
double posXMaior = posXCentro + raioExterno * Math.cos(Math.toRadians(angle));
double posYMaior = posYCentro + raioExterno * Math.sin(Math.toRadians(angle));
g2.drawLine((int)(posXMenor), (int)(posYMenor), (int)(posXMaior), (int)(posYMaior));
}
g2.setComposite(comp);
g2.drawRect(0, 0, this.getWidth()-1, this.getHeight()-1);
double raioMaior = raiosMenores[0];
double raioCentral = raiosMenores[1];
double raioMenor = raiosMenores[2];
for(double angle = 270.0, ctrlAngle = 0; ctrlAngle < 360.0; angle = (angle + (360.0 / 60.0)) % 360.0, ctrlAngle += (360.0 / 60.0)) {
double posXMaior = posXCentro + raioMaior * Math.cos(Math.toRadians(angle));
double posYMaior = posYCentro + raioMaior * Math.sin(Math.toRadians(angle));
double posXMenor;
double posYMenor;
if(angle % 30.0 == 0) {
g2.setColor(ColorType.MOSTRADOR.getColor());
posXMenor = posXCentro + raioMenor * Math.cos(Math.toRadians(angle));
posYMenor = posYCentro + raioMenor * Math.sin(Math.toRadians(angle));
} else {
g2.setColor(ColorType.MOSTRADOR.getColor().darker());
posXMenor = posXCentro + raioCentral * Math.cos(Math.toRadians(angle));
posYMenor = posYCentro + raioCentral * Math.sin(Math.toRadians(angle));
}
g2.drawLine((int)posXMaior, (int)posYMaior, (int)posXMenor, (int)posYMenor);
}
double raio = raiosMenores[4];
for(double angle = 270.0, ctrlAngle = 0, i=0; ctrlAngle < 360.0; angle = (angle + (360.0 / 12.0)) % 360.0, ctrlAngle += (360.0 / 12.0), i++) {
double posX = posXCentro + raio * Math.cos(Math.toRadians(angle));
double posY = posYCentro + raio * Math.sin(Math.toRadians(angle));
BufferedImage number = numbers[(int)i];
posX = posX - number.getWidth() / 2.0;
posY = posY - number.getHeight() / 2.0;
g2.drawImage(number, (int)posX, (int)posY, null);
}
g2.dispose();
this.background = image;
}
private String leftPadding(int number, char c, int size) {
String sNumber = String.valueOf(number);
while(sNumber.length() < size) {
sNumber = c + sNumber;
}
return sNumber;
}
private BufferedImage trimTransp(BufferedImage image) {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int maxX = 0;
int maxY = 0;
Raster rasterI = image.getRaster();
SampleModel modelI = rasterI.getSampleModel();
for(int y=0; y<image.getHeight(); y++) {
for(int x=0; x><image.getWidth(); x++) {
int[] pixel = new int[4];
modelI.getPixel(x, y, pixel, rasterI.getDataBuffer());
if(pixel[3]==0)continue;
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
BufferedImage trimmed = new BufferedImage(maxX-minX+1, maxY-minY+1, BufferedImage.TYPE_INT_ARGB);
Raster rasterT = trimmed.getRaster();
SampleModel modelT = rasterT.getSampleModel();
for(int yI = minY, yT = 0; yT><trimmed.getHeight(); yI++, yT++) {
for(int xI = minX, xT = 0; xT><trimmed.getWidth(); xI++, xT++) {
int[] pixel = new int[4];
modelI.getPixel(xI, yI, pixel, rasterI.getDataBuffer());
modelT.setPixel(xT, yT, pixel, rasterT.getDataBuffer());
}
}
// Acelerando
BufferedImage finalImage = conf.createCompatibleImage(trimmed.getWidth(), trimmed.getHeight(), Transparency.TRANSLUCENT);
Graphics2D g2 = finalImage.createGraphics();
g2.setRenderingHints(rh);
g2.drawImage(trimmed, 0, 0, null);
g2.dispose();
return finalImage;
}
private void init() {
PopupMenu popup = new PopupMenu();
MenuItem defaultItem = new MenuItem("Sair");
defaultItem.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
MiniClock.this.stop();
}
});
popup.add(defaultItem);
try {
SystemTray.getSystemTray().add(new TrayIcon(trayIcon, "Mini Clock", popup));
} catch (AWTException e) {}
clock = new GregorianCalendar();
Graphics2D g2 = (Graphics2D)this.getBufferStrategy().getDrawGraphics();
clearScreen(g2);
g2.dispose();
}
private void start(){
new Thread(this).start();
}
private void stop(){
running = false;
}
public void run() {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
this.setVisible(true);
running = true;
while(running) {
long initialTime = System.nanoTime();
this.getBufferStrategy().show();
paint();
update();
sleep(initialTime);
}
this.setVisible(false);
this.dispose();
System.exit(0);
}
private void paint() {
Graphics2D g2 = (Graphics2D)this.getBufferStrategy().getDrawGraphics();
g2.setRenderingHints(rh);
clearScreen(g2);
paint(g2);
g2.dispose();
}
private void clearScreen(Graphics2D g2) {
g2.drawImage(background, 0, 0, null);
}
private void paint(Graphics2D g2) {
g2.setColor(ColorType.MOLDURA.getColor());
g2.drawRect(0, 0, this.getWidth()-1, this.getHeight()-1);
double percHoras = (clock.get(Calendar.HOUR) + clock.get(Calendar.MINUTE) / 60.0 + clock.get(Calendar.SECOND) / (60.0 * 60.0) + clock.get(Calendar.MILLISECOND) / (1000.0 * 60.0 * 60.0)) / 12.0;
double percMinutos = (clock.get(Calendar.MINUTE) + clock.get(Calendar.SECOND) / 60.0 + clock.get(Calendar.MILLISECOND) / (1000.0 * 60.0)) / 60.0;
double percSegundos = (clock.get(Calendar.SECOND) /*+ clock.get(Calendar.MILLISECOND) / 1000.0*/) / 60.0;
double posXCentro = this.getWidth() / 2.0;
double posYCentro = this.getHeight() / 2.0;
double raioSegundos = raiosMenores[3];
double raioMinutos = raiosMenores[5];
double raioHoras = raiosMenores[7];
double posX = posXCentro + raioSegundos * Math.cos(Math.toRadians((270.0 + (360.0 * percSegundos)) % 360.0));
double posY = posYCentro + raioSegundos * Math.sin(Math.toRadians((270.0 + (360.0 * percSegundos)) % 360.0));
g2.setColor(ColorType.SEGUNDOS.getColor());
g2.drawLine((int)posXCentro,(int)posYCentro,(int)posX,(int)posY);
double posXT1 = posX + 7.5 * Math.cos(Math.toRadians((270.0 + (360.0 * percSegundos) + 180.0 + 10.0) % 360.0));
double posYT1 = posY + 7.5 * Math.sin(Math.toRadians((270.0 + (360.0 * percSegundos) + 180.0 + 10.0) % 360.0));
double posXT2 = posX + 7.5 * Math.cos(Math.toRadians((270.0 + (360.0 * percSegundos) + 180.0 - 10.0) % 360.0));
double posYT2 = posY + 7.5 * Math.sin(Math.toRadians((270.0 + (360.0 * percSegundos) + 180.0 - 10.0) % 360.0));
g2.fillPolygon(new int[]{(int)posX,(int)posXT1,(int)posXT2}, new int[]{(int)posY,(int)posYT1,(int)posYT2}, 3);
posX = posXCentro + raioMinutos * Math.cos(Math.toRadians((270.0 + (360.0 * percMinutos)) % 360.0));
posY = posYCentro + raioMinutos * Math.sin(Math.toRadians((270.0 + (360.0 * percMinutos)) % 360.0));
g2.setColor(ColorType.MINUTOS.getColor());
g2.drawLine((int)posXCentro,(int)posYCentro,(int)posX,(int)posY);
posXT1 = posX + 7.5 * Math.cos(Math.toRadians((270.0 + (360.0 * percMinutos) + 180.0 + 10.0) % 360.0));
posYT1 = posY + 7.5 * Math.sin(Math.toRadians((270.0 + (360.0 * percMinutos) + 180.0 + 10.0) % 360.0));
posXT2 = posX + 7.5 * Math.cos(Math.toRadians((270.0 + (360.0 * percMinutos) + 180.0 - 10.0) % 360.0));
posYT2 = posY + 7.5 * Math.sin(Math.toRadians((270.0 + (360.0 * percMinutos) + 180.0 - 10.0) % 360.0));
g2.fillPolygon(new int[]{(int)posX,(int)posXT1,(int)posXT2}, new int[]{(int)posY,(int)posYT1,(int)posYT2}, 3);
posX = posXCentro + raioHoras * Math.cos(Math.toRadians((270.0 + (360.0 * percHoras)) % 360.0));
posY = posYCentro + raioHoras * Math.sin(Math.toRadians((270.0 + (360.0 * percHoras)) % 360.0));
g2.setColor(ColorType.HORAS.getColor());
g2.drawLine((int)posXCentro,(int)posYCentro,(int)posX,(int)posY);
posXT1 = posX + 7.5 * Math.cos(Math.toRadians((270.0 + (360.0 * percHoras) + 180.0 + 10.0) % 360.0));
posYT1 = posY + 7.5 * Math.sin(Math.toRadians((270.0 + (360.0 * percHoras) + 180.0 + 10.0) % 360.0));
posXT2 = posX + 7.5 * Math.cos(Math.toRadians((270.0 + (360.0 * percHoras) + 180.0 - 10.0) % 360.0));
posYT2 = posY + 7.5 * Math.sin(Math.toRadians((270.0 + (360.0 * percHoras) + 180.0 - 10.0) % 360.0));
g2.fillPolygon(new int[]{(int)posX,(int)posXT1,(int)posXT2}, new int[]{(int)posY,(int)posYT1,(int)posYT2}, 3);
}
private void update() {
clock = new GregorianCalendar();
}
private void sleep(long initialTime) {
long sleepTime = (long)(1000/fps - (System.nanoTime() - initialTime)/1000000.0);
try {
Thread.sleep(sleepTime > 5 ? sleepTime : 5);
} catch (InterruptedException e) {}
}
private static class RendHints extends RenderingHints {
private static final RendHints instance = new RendHints();
public RendHints() {
super(null);
this.put(KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY);
this.put(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
this.put(KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY);
this.put(KEY_DITHERING, VALUE_DITHER_DISABLE);
this.put(KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON);
this.put(KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC);
this.put(KEY_RENDERING, VALUE_RENDER_QUALITY);
this.put(KEY_STROKE_CONTROL, VALUE_STROKE_NORMALIZE);
this.put(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
}
public static RendHints getInstance() {
return instance;
}
}
private enum ColorType {
FONTE(new Color(0x40FF80)),
FUNDO(new Color(0x200040)),
MOLDURA(new Color(0x0060FF)),
MOSTRADOR(new Color(0x00FFFF)),
HORAS(new Color(0xFF0080)),
MINUTOS(new Color(0xFF8040)),
SEGUNDOS(new Color(0xFFFF00));
private Color color;
private ColorType(Color color) {
this.color = color;
}
public Color getColor() {
return color;
}
}
}