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ő:

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:

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:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 



 

 

 

 

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


www.informatika-programozas.hu - The Final Countdowner