Gyakorlati alapok IV.
Végre ablakozunk! (AWT, Swing, JavaFX)
The Final Countdowner
Ebben a fejezetben egy SWING-alapú alkalmazást fogunk implementálni, amely
képes a felhasználó által megadott időintervallumból visszaszámolni, majd a
végidő pillanatában vizuálisan és akusztikailag jelezni. Konkrétan: én szoktam
fizikai erősítő és egyéb gyakorlatokat is végezni és számomra fontos, hogy
adott gyakorlatot jól meghatározható ideig végezzek. Az alkalmazás tehát
visszaszámol és a végidő pillanatában határozottan jelez.
Ennek alapján az alapszintű specifikáció a következő:
-
épüljön már meglévő keretekre, nagyjából azonos szabályok szerint, főként oktatási, másrészt praktikus okokból (hiszen az elkészülési idő mindig fontos tényező),
-
a felhasználó által bevitt bemeneti adatokat minden szempontból validálni kell,
-
a felhasználó által bevitt bemeneti adat csakis egy időintervallum lehet 1 és 10 perc között (ez egyéni elvárás, természetesen a kódban átalakítható),
-
a lehetséges rossz adatbevitelről, hibákról a felhasználó üzenetablakokon keresztül értesüljön,
-
a program szabványos megjelenítésben számoljon vissza a felhasználó által megadott időintervallumtól kezdve, majd a végidő elérkeztét vizuálisan és akusztikailag jelezze.
A projektben a
Center, a Main és a
Hang (sound)
című fejezetben bemutatott Sound osztály nem
változik, ezért az ismertetést a Window
osztállyal kezdjük. Az osztály bevezető részében létrehozzuk a szükséges
objektumokat, inicializáljuk azokat, pozícióikat beállítjuk a
frame-en belül:
class Window extends JFrame {
JFrame frame = new JFrame();
JButton buttonOK = new JButton("START!");
JLabel labelTitle = new JLabel();
JLabel labelClock = new JLabel();
JTextField textFieldDuration = new JTextField();
ImageIcon icon = new ImageIcon("D:\\Java/Projects/SWING -
Countdowner/icon.jpg");
Window() {
super();
labelTitle.setBounds(105, 25, 200, 25);
labelTitle.setFont(new Font("Serif", Font.PLAIN, 20));
labelTitle.setText("The Final Countdowner");
labelClock.setBounds(100, 120, 200, 50);
labelClock.setFont(new Font("Serif", Font.PLAIN, 60));
labelClock.setText("0");
labelClock.setHorizontalAlignment(JLabel.CENTER);
textFieldDuration.setBounds(100, 60, 200, 50);
textFieldDuration.setFont(new Font("Serif", Font.PLAIN, 40));
textFieldDuration.setHorizontalAlignment(JTextField.CENTER);
textFieldDuration.setText("0");
buttonOK.setBounds(100, 190, 200, 25);
buttonOK.addActionListener(event -> buttonActionOK());
Image icon =
Toolkit.getDefaultToolkit().getImage("D:\\Java/Projects/SWING -
Countdowner/icon.jpg");
frame.setIconImage(icon);
frame.add(buttonOK);
frame.add(labelTitle);
frame.add(labelClock);
frame.add(textFieldDuration);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(420, 325);
frame.setTitle("PJP - Penzes Java Programming");
Center center = new Center();
center.setCenter(frame);
frame.setLayout(null);
frame.setVisible(true);
}
Már megszokott módon a buttonActionOK()
függvényben kezeljük le az eseménykezelés nagyobbik részét. Itt azonban
felmerül egy további probléma. Nyilvánvalóan az eseménykezelés validálással
kezdődik, amelyet egyébként a függvény túlnyomó részében láthatunk. Mivel a
validálás logikailag nézve különböző funkciót alkot, igazából illett volna
metódusait külön osztályba szerveznünk. Ezt most azért nem tettük meg, mert
egyrészről erre az egyszerű programra is már 5 osztályunk keletkezett,
másrészről a validálás ilyen közvetlen módon jóval egyszerűbb és átláthatóbb,
harmadrészről pedig a validálás külön osztályba szervezése esetén a
függvényben nem maradt volna más, mint csupán egy további osztályhivatkozás:
TimeThread timeThread = new TimeThread(labelClock,
duration);
timeThread.start();
Amint az sok esetben lenni szokott, a kód egyik legbonyolultabb része a
validálás. Konkrét kódja alább tanulmányozható, ezért itt csak a szempontokat
soroljuk fel. Rossz az adatbevitel, ha a felhasználó a beviteli szövegmezőt:
-
üresen hagyja,
-
szám helyett egyéb karaktereket ír be – érdekes, ugyanakkor nagyon hasznos programozástechnikai megoldás a kivétel “elkapása” (Kivételesen beszéljünk a kivételkezelésről című fejezetcsomag), azaz ha a beviteli karakter számmá való konvertálása nem sikerül, akkor annak hibajelzését, kivételét (Exception) mintegy “elkapjuk” és hibaüzenet generálására használjuk fel.
-
Nem elvárt számot ír be – ez többféleképpen is megoldható; a magam részéről kiindulópontként egy állandó méretű, integer típusú tömböt használtam fel, amely 1 és 10 között tartalmazza az elvárt számokat.
private void buttonActionOK() {
int duration = 0;
if(textFieldDuration.getText().equals ("")) {
JOptionPane.showMessageDialog(frame,
"Empty textfield!");
textFieldDuration.setText("0");
return;
}
try {
duration =
Integer.parseInt(textFieldDuration.getText());
} catch (Exception e) {
JOptionPane.showMessageDialog(frame,
"The text is not a number!");
textFieldDuration.setText("0");
return;
}
System.out.println(duration);
int[] array = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
boolean numberOK = false;
for(int i = 0; i < array.length; i++) {
if(duration != array[i]) {
System.out.println(numberOK);
continue;
}
if(duration == array[i]) {
numberOK = true;
System.out.println(numberOK);
break;
}
}
if(numberOK == false) {
JOptionPane.showMessageDialog(frame, "The number is not in
range!");
textFieldDuration.setText("0");
System.out.println(numberOK);
return;
}
TimeThread timeThread = new TimeThread(labelClock, duration);
timeThread.start();
}
}
Már említettük, hogy a függvény utolsó 2 sora egy másik,
TimeThread nevű osztályt hív meg. Ez voltaképpen a külön programszálon
futó visszaszámlálás rutinja és ezt mindenképpen így szükséges megtennünk. De
miért is? Próbáljuk meg azt, hogy a visszaszámláló rutint a
Window osztály
buttonActionOK() függvényébe tesszük. A visszaszámlálás ugyan beindul,
ám az indító gomb addig “benyomott” állapotban lesz és a kimeneti szövegmező
sem változik, mert ezen intervallumon belül folyamatosan a függvényben
maradunk, azaz a függvény nem adja vissza a vezérlést. A megoldás olyan
folyamat beindítása, amelyik a vezérlés átadása után különálló programszálként
indul. Pontosan ilyen lehetőségeket tanultunk a című fejezetcsomagban. Csupán
arra kell ügyelnünk, hogy a Window osztály, a
visszaszámlálás megjelenítéséért felelős JLabel
típusú, labelClock objektumát, valamint a
konkrét, már validált visszaszámláló értéket (int minute)
bemeneti paraméterekként az osztályba beküldjük. Ezt könnyen megtehetjük az
osztály konstruktorán keresztül:
class TimeThread extends Thread{
private JLabel labelClock;
private int minute;
public TimeThread(JLabel labelClock, int minute){
this.labelClock = labelClock;
this.minute = minute;
}
A visszaszámlálás kötelezően a programszál (thread)
run() metódusában van implementálva (A többszálú programozás alapjai (multithread)
című fejezetcsomag). Működtető kódja egy egybeázott for ciklus (Ágyazzunk
be a for ciklusnak! (az egybeágyazott for ciklusok)
című fejezet), amelyben a külső ciklus a percért, a belső ciklus pedig a
másodpercért felel. A programszálat 1000 milliszekundumonként altató
sleep() metódust szintén kötelezően
try-catch blokkba kell tennünk (A try-catch-finally
blokkok című fejezet); futási hibájáról névtelen osztállyal
megoldott üzenetablak értesíthet minket (A "névtelen" osztály
című fejezet):
public void run(){
for(int i = minute-1; i >= 0; i--) {
for(int j = 59; j >= 0; j--) {
labelClock.setText("" + i + ":" + j);
if(j < 10) {
labelClock.setText("" + i + ":" + "0" + j);
}
try {
this.sleep(1000);
}
catch(Exception e) {
JOptionPane.showMessageDialog(new Window().frame, "Something wrong with
countdowning!");
}
}
}
A visszaszámlálás végét névtelen osztállyal létrehozott üzenetablak és külön
osztályban menedzselt hang jelzi:
String audioFilePath = "D:/Java/Projects/SWING
- Countdowner/countdown.wav";
Sound sound = new Sound();
sound.play(audioFilePath);
JOptionPane.showMessageDialog(new Window().frame, "THE COUNTDOWNING IS
OVER!");
A kódban még észrevehetünk viszonylag sok System.out.println() függvényhívást. Ezek most is, mint mindig konzolos részeredménykijelzésre és ellenőrzésre szolgálnak, egyébiránt a program működésébe nem zavarnak bele.
Nézzük meg a futtatható Java-kódot:
Main.java
public class
Main {
public static void main(String[] args) {
Window window = new Window();
}
}
Window.java
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import javax.swing.*;
class Window extends JFrame {
JFrame frame = new JFrame();
JButton buttonOK = new JButton("START!");
JLabel labelTitle = new JLabel();
JLabel labelClock = new JLabel();
JTextField textFieldDuration = new JTextField();
ImageIcon icon = new ImageIcon("D:\\Java/Projects/SWING -
Countdowner/icon.jpg");
Window() {
super();
labelTitle.setBounds(105, 25, 200, 25);
labelTitle.setFont(new Font("Serif", Font.PLAIN, 20));
labelTitle.setText("The Final Countdowner");
labelClock.setBounds(100, 120, 200, 50);
labelClock.setFont(new Font("Serif", Font.PLAIN, 60));
labelClock.setText("0");
labelClock.setHorizontalAlignment(JLabel.CENTER);
textFieldDuration.setBounds(100, 60, 200, 50);
textFieldDuration.setFont(new Font("Serif", Font.PLAIN, 40));
textFieldDuration.setHorizontalAlignment(JTextField.CENTER);
textFieldDuration.setText("0");
buttonOK.setBounds(100, 190, 200, 25);
buttonOK.addActionListener(event -> buttonActionOK());
Image icon =
Toolkit.getDefaultToolkit().getImage("D:\\Java/Projects/SWING -
Countdowner/icon.jpg");
frame.setIconImage(icon);
frame.add(buttonOK);
frame.add(labelTitle);
frame.add(labelClock);
frame.add(textFieldDuration);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(420, 325);
frame.setTitle("PJP - Penzes Java Programming");
Center center = new Center();
center.setCenter(frame);
frame.setLayout(null);
frame.setVisible(true);
}
private void buttonActionOK() {
int duration = 0;
if(textFieldDuration.getText().equals ("")) {
JOptionPane.showMessageDialog(frame,
"Empty textfield!");
textFieldDuration.setText("0");
return;
}
try {
duration =
Integer.parseInt(textFieldDuration.getText());
} catch (Exception e) {
JOptionPane.showMessageDialog(frame,
"The text is not a number!");
textFieldDuration.setText("0");
return;
}
System.out.println(duration);
int[] array = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
boolean numberOK = false;
for(int i = 0; i < array.length; i++) {
if(duration != array[i]) {
System.out.println(numberOK);
continue;
}
if(duration == array[i]) {
numberOK = true;
System.out.println(numberOK);
break;
}
}
if(numberOK == false) {
JOptionPane.showMessageDialog(frame, "The number is not in
range!");
textFieldDuration.setText("0");
System.out.println(numberOK);
return;
}
TimeThread timeThread = new TimeThread(labelClock, duration);
timeThread.start();
}
}
TimeThread.java
import javax.swing.JLabel;
import javax.swing.JOptionPane;
class TimeThread extends Thread{
private JLabel labelClock;
private int minute;
public TimeThread(JLabel labelClock, int minute){
this.labelClock = labelClock;
this.minute = minute;
}
public void run(){
for(int i = minute-1; i >= 0; i--) {
for(int j = 59; j >= 0; j--) {
labelClock.setText("" + i + ":" + j);
if(j < 10) {
labelClock.setText("" + i + ":" + "0" + j);
}
try {
this.sleep(1000);
}
catch(Exception e) {
JOptionPane.showMessageDialog(new Window().frame, "Something wrong with
countdowning!");
}
}
}
String audioFilePath = "D:/Java/Projects/SWING -
Countdowner/countdown.wav";
Sound sound = new Sound();
sound.play(audioFilePath);
JOptionPane.showMessageDialog(new Window().frame, "THE
COUNTDOWNING IS OVER!");
}
}
Center.java
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import javax.swing.JFrame;
public class Center {
void setCenter(JFrame frame) {
Point center =
GraphicsEnvironment.getLocalGraphicsEnvironment().getCenterPoint();
int x = (int) center.getX() - (frame.getWidth() / 2);
int y = (int) center.getY() - (frame.getHeight() / 2);
Point ablakCenter = new Point(x, y);
frame.setLocation(ablakCenter);
}
}
Végeredmény (a monitor közepén / felbontástól függetlenül/):
az alkalmazás fenti szempontok szerint működik