In questo articolo verrà realizzata una semplice applicazione swing per comprimere file, che chiameremo FileZipper. L'applicazione sarà molto semplice e ci consentirà di riutilizzare i componenti di base, già introdotti nei precedenti articoli sui JFrame e componenti e sulla gestione di layout ed eventi, e di introdurre anche elementi un po' più avanzati.
Quale layout manager utilizzare?
I layout manager sono gestori della disposizione dei componenti e il loro ruolo è quindi principalmente quello di dare organizzazione all'interfaccia. Ne esistono diversi tipi che possono essere usati anche in modo congiunto per la creazione di interfacce grafiche più o meno complesse.
L'abitudine ad utilizzare l'impostazione di swing (ed i suoi patterns) porta per lo sviluppatore anche benefici indiretti, come la possibilità di acquisire rapidamente competenza sui framework presentazionali per il web quali zk, extjs, o gwt, oppure framework html5 per le RIA, che riutilizzano gran parte dei concetti d'uso comune in swing.
FlowLayout
Con questo gestore i componenti vengono aggiunti in un flusso ordinato che va da destra a sinistra, con un allineamento che va verso l'alto all'interno del container che li ospita:
BoderLayout
I componenti sono disposti solamente in 5 posizioni specifiche che si ridimensionano automaticamente:
NORTH
,SOUTH
che si ridimensionano orizzontalmenteEAST
,WEST
che si ridimensionano verticalmenteCENTER
che si ridimensiona orizzontalmente e verticalmente
Questo significa che un componente aggiunto in una certa area si ridimensionerà per occuparla interamente.
Prima di proseguire vediamo un esempio utilizzando il container JPanel
. Utilizziamo per il container principale(ottenuto con getContentPane()
) il BorderLayout
e per ciascun JPanel
aggiunto al container principale, nelle zone scelte, il FlowLayout
.
Creiamo la seguente classe
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class BorderLayoutDemo {
public static void main(String[] args) {... }
}
All'interno del metodo main costruiamo un JFrame
per la schermata generale e recuperiamo su di di esso il container, per poi aggiungere tre differenti JPanel
(ciascuno con un FlowLayout
):
JFrame jFrame = new JFrame("BorderLayout");
jFrame.setSize(400,400);
Container c = jFrame.getContentPane();
// ...
JPanel north = new JPanel(new FlowLayout());
JPanel south = new JPanel(new FlowLayout());
JPanel center = new JPanel(new FlowLayout());
Aggiungiamo poi due JButton
al JPanel
north, una JLabel
al JPanel
center, ed Un JButton
al JPanel
south:
north.add(new JButton("Open"));
north.add(new JButton("Save"));
// ...
center.add(new JLabel("This is the center"));
// ...
south.add(new JButton("Exit"));
A questo punto desideriamo disporre i JPanel
secondo il layout BorderLayout
all'interno del container principale. Tutto ciò che dobbiamo fare è utilizzare il metodo add()
del Container, specificando come primo parametro l'oggetto da posizionare, nel nostro caso un JPanel
, e come secondo la posizione tra le 5 disponibili:
c.add(north,BorderLayout.NORTH);
c.add(center,BorderLayout.CENTER);
c.add(south,BorderLayout.SOUTH);
Infine gestiamo la chiusura e visibilità del JFrame:
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setVisible(true);
Eseguendo il programma, a schermo vedremo:
GridLayout
I componenti vengono disposti da sinistra verso destra e dall'alto verso il basso all'interno di una griglia. Tutte le celle della griglia hanno la stessa dimensione che si preserva anche se il frame viene ridimensionato. I componenti all'interno di esse occuperanno tutto lo spazio possibile:
Vediamo un esempio.
Definiamo una griglia di 3 righe e 2 colonne. Le celle della prima riga conterranno delle label, le celle della seconda dei campi di input, mentre la prima cella della 3 riga un JButton
. In questo caso aggiungiamo i componenti ad un JPanel
al quale abbiamo associato un GridLayout
come gestore.
Realizziamo anche in questo caso una classe di prova (all'interno del main definiamo un JFrame
come fatto precedentemente):
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class GridLayoutDemo {
public static void main(String[] args) {
JFrame jFrame = new JFrame("GridLayout");
jFrame.setSize(400,100);
Container c = jFrame.getContentPane();
// ...
}
}
Per esempio vogliamo istanziare un JPanel
ed assegnamo un layout manager di tipo GridLayout
, con 3 righe e due colonne:
JPanel jPanel1 = new JPanel();
jPanel1.setLayout(new GridLayout(3,2));
// aggiungiamo i componenti:
jPanel1.add(new JLabel("Name"));
jPanel1.add(new JLabel("Surname"));
jPanel1.add(new JTextField(""));
jPanel1.add(new JTextField(""));
jPanel1.add(new JButton("My button"));
jPanel1.setBackground(Color.CYAN);
// ricordiamoci di aggiungere il JPanel al container principale:
c.add(jPanel1);
// va poi gestita anche la chiusura e visualizzazione del JFrame
jFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
jFrame.setVisible(true);
Eseguiamo il codice e visualizziamo la seguente schermata:
GridBagLayout
Il GridBagLayout
può organizzare interfacce grafiche complesse da solo. Infatti divide come il GridLayout
il container in una griglia ma, a differenza di esso, può disporre i componenti in modo che si estendano anche su più di una cella. L'utilizzo di questo layout è argomento avanzato e non viene trattato in questo articolo.
L'applicazione: FileZipper
Utilizziamo adesso tutte le nozioni esposte per creare un semplice File Zipper: una applicazione in grado di farci selezionare un insieme di file, per creare a partire da essi un file jar in un percorso da noi specificato. Si parte quindi da una schermata principale nella quale è possibile selezionare file da aggiungere all'archivio attraverso il pulsante "Open a file":
Ogni file aggiunto viene visualizzato nell'albero principale con l'intero percorso.
Una volta aggiunti tutti i file, possiamo utilizzare il pulsante "Make archive" per creare l'archivio jar dei file selezionati.
Il progetto Swing
Suddividiamo il progetto in 4 package:
it.html.swing.gui
: Il package per la classi che disegnano l'interfaccia graficait.html.swing.actions
: Il package per le classi che gestiscono gli eventiit.html.swing.jar
: Il package che contiene la classe che si occupa di creare l'archivioit.html.swing.app
: Infine il package che contiene la classe di avvio dell'applicazione.
la classe MainWindow
Iniziamo dalla classe it.html.swing.gui.MainWindow
. Questa classe estende JFrame
ed ha come obiettivo quello di agevolarci nella costruzione della gui, fornendoci costruttori che ricevono le dimensioni ed il layout per la form.
Aggiungiamo il costruttore che permette di realizzare una finestra specificando titolo e dimensioni con layout manager di default, un altro che invece permette di specificare anche il layout manager, ed infine un metodo per agevolare l'aggiunta di componenti al container principale:
package it.html.swing.gui;
import java.awt.Component;
import java.awt.LayoutManager;
import javax.swing.JFrame;
public class MainWindow extends JFrame {
// ...
public MainWindow(String title, int width, int height) {
super(title);
setSize(width, height);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public MainWindow(String title, int width, int height, LayoutManager layout) {
this(title,width,height);
getContentPane().setLayout(layout);
}
public void addComponent(Component component) {
getContentPane().add(component);
}
}
Come visto precedentemente, l'interfaccia che intendiamo realizzare è composta da due pulsanti ed una JTree
per la visualizzazione ad albero dei file selezionati. Le classi che gestiscono l'evento di click sui pulsanti "Open a file"
e "Make archive"
sono:
it.html.swing.actions.OpenFileAction
: permetterà di aggiungere file allaJTree
.it.html.swing.actions.CompressFileAction:
recupererà i file dallaJTree
e costruirà un archivio jar, usando la classe seguente.it.html.swing.jar.Zipper
: la classe che effettuerà la compressione vera e propria.it.html.swing.app.Application
: l'applicazione.
Creiamo con la nostra MainWindow un JFrame 600x400 con layout GridLayout a 2 righe ed una colonna. Il container del JFrame conterrà due JPanel: uno per il banner ed il JTree,l'altro per i due pulsanti:
MainWindow mainWindow = new MainWindow("File Zipper", 600, 400, new GridLayout(2, 1));
Definiamo il primo JPanel, e aggiungiamo l'immagine del banner:
JPanel headerPnl = new JPanel();
headerPnl.setLayout(new GridLayout(2, 1));
// ...
JLabel banner = new JLabel();
ImageIcon ii = new ImageIcon("banner.png");
banner.setIcon(ii);
// ...
headerPnl.add(banner);
Procediamo con il secondo pannello. Definiamo la JTree
costruendo il modello dei dati da fornire in input attraverso le classi DefaultMutableTreeNode
e DefaultTreeModel
. Decoriamo poi la JTree
con uno scroller verticale:
DefaultMutableTreeNode rootFileTree = new DefaultMutableTreeNode("Files");
DefaultTreeModel dtm = new DefaultTreeModel(rootFileTree, false);
JTree fileListTree = new JTree(dtm);
JScrollPane qPane = new JScrollPane(fileListTree,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
headerPnl.add(qPane);
mainWindow.addComponent(headerPnl);
Abbiamo quindi completato la parte superiore che contiene il banner e l'albero di visualizzazione dei file. Implementiamo adesso la parte inferiore dell'interfaccia che conterrà i plusanti per la selezione dei file e la creazione dell'archivio:
JPanel footerPnl = new JPanel();
footerPnl.setBackground(Color.white);
// Definiamo il JButton per aprire i file:
JButton openBtn = new JButton("Open a file");
// Definiamo il JButton per comprimere i file selezionati:
JButton compressionBtn = new JButton("Make archive");
Aggiungiamo la gestione degli eventi
A questo punto prima di aggiungere i pulsanti al loro JPanel gestiamo la logica di registrazione degli eventi di click su essi. Registriamo i pulsanti presso i loro ascoltatori:
OpenFileAction openFileAction = new OpenFileAction(mainWindow);
openBtn.addActionListener(openFileAction);
CompressFileAction compressFileAction = new CompressFileAction(mainWindow);
compressionBtn.addActionListener(compressFileAction);
OpenFileAction
e CompressFileAction
,come vedremo più avanti nell'articolo, sono classi che implementano l'interfaccia ActionListener
. L'implementazione del metodo actionPerformed conterrà, nel caso della classe OpenFileAction
, la logica per la selezione dei file, mentre per CompressFileAction
la logica di creazione dell'archivio.
Aggiungiamo quindi i pulsanti al secondo JPanel(quello inferiore):
footerPnl.add(openBtn);
footerPnl.add(compressionBtn);
// ...
mainWindow.addComponent(footerPnl);
mainWindow.setVisible(true);
Nella prossima parte aggiungeremo finalmente la compressione dei file, con la creazione dell'archivio
La classe ZIpper
La realizzazione dell'interfaccia è quindi completata, la classe Application
usa MainWindow
per creare l'interfaccia grafica e, successivamente, applica i concetti visti per creare pannelli ed aggiungere componenti. Trattiamo preliminarmente la classe Zipper
responsabile della creazione dell'archivio Jar e vediamo successivamente come i listener associati ai pulsanti ci permettono di costruire una lista di file e di comprimerli in un file jar:
public class Zipper {
public static void makeJarFile(File destinationPathFile, File... sourcePathFile) {
// ...
}
}
La classe ha un solo metodo che prende in input il percorso di salvataggio dell'archivio ed un numero variabile di file input da includere nell'archivio. Un primo controllo di coerenza sull'input:
if ((sourcePathFile == null || destinationPathFile == null)) {
throw new IllegalArgumentException("Arguments null");
}
Successivamente creiamo il file dell'archivio ed un buffer di lettura e scrittura dei byte relativi ai file:
destinationPathFile.createNewFile();
byte buffer[] = new byte[1024];
FileOutputStream fileStream = new FileOutputStream(destinationPathFile);
JarOutputStream jarStream = new JarOutputStream(fileStream, new Manifest());
// TODO: aggiunta dei file nell'archivio
jarStream.close();
fileStream.close();
Il ciclo di lettura dei file da aggiungere all'archivio:
for (File sFile : sourcePathFile) {
// Se il file non è una directory lo aggiungiamo
if (!sFile.isDirectory()) {
// Creaimo un entry con il suo nome
JarEntry file = new JarEntry(sFile.getName());
file.setTime(sFile.lastModified());
// La inseriamo nello stream
jarStream.putNextEntry(file);
// Apertura di uno stream di lettura sul file corrente
FileInputStream in = new FileInputStream(sFile);
int bytes = 0;
// Scriviamo nello stream relativo a questa entry i byte del file corrente
while ((bytes = in.read(buffer, 0, buffer.length)) != -1) {
jarStream.write(buffer, 0, bytes);
}
}
}
il metodo per la creazione dei jar
La classe utilizza il package java.util.jar
per creare l'archivio. Il metodo makeJarFile
prende in input il file di destinazione e la lista di file da archiviare. Questa lista viene letta e per ciascun file viene creata una entry nel file jar. Lo stream associato a questa entry viene riempito con i byte del file corrente. Procediamo con il vedere la selezione dei file e la loro aggiunta al JTree
. La classe che si occupa di questa operazione è, come anticipato, l'ascoltatore di eventi it.html.swing.actions.OpenFileActions
:
package it.html.swing.actions;
public class OpenFileAction implements ActionListener {
private File fileOpened;
private MainWindow window;
public OpenFileAction(MainWindow window) {
this.window = window;
}
@Override
public void actionPerformed(ActionEvent arg0) {
// ...
}
private void addToListTree(String filePath) {
// ...
}
// ...
}
la classe ha due varibili di istanza, una è fileOpened
, che rappresenta il file correntemente selezionato, e l'altra è il riferimento alla finestra principale dell'applicazione che contiene il JTree
sul quale si deve agire. Il metodo actionPerformed
si occupa fondamentalmente di gestire l'apertura del file affidando successivamente l'inserimento nella JTree
al metodo ausiliario addToListTree()
. Il codice in actionPerformed
è quindi il seguente:
JFileChooser fileChooser = new JFileChooser();
int returnValue = fileChooser.showOpenDialog(null);
// in caso di errore visualizziamo un messaggio di errore
// altrimenti invochiamo il metodo addToListTree() per l'aggiunta di un entry nel JTree per il file selezionato:
if (returnValue == JFileChooser.ERROR_OPTION) {
JOptionPane.showMessageDialog(window, "Unexpected error", "Error", JOptionPane.ERROR_MESSAGE);
} else if (returnValue == JFileChooser.APPROVE_OPTION) {
// E' stato selezionato un file
fileOpened = fileChooser.getSelectedFile();
// La stringa del suo path vienne aggiunta alla JTree
addToListTree(fileOpened.getAbsolutePath());
}
il metodo per selezionare i File
Vediamo adesso come il metodo addToListTree
realizza l'aggiunta del file alla JTree
. Viene recuperato il container generale del JFrame
e su questo il primo JPanel
aggiunto che ha quindi indice 0. I componenti aggiunti ad un Container sono indicizzati partendo da zero in relazione al Container che li ospita:
JPanel jPanel = (JPanel) window.getContentPane().getComponent(0);
Su questo JPanel abbiamo aggiunto una JLabel per il banner (indice 0 rispetto al JPanel che la contiene) ed il JScrollPane che ha quindi indice 1:
JScrollPane jScrollPane = (JScrollPane) jPanel.getComponent(1);
Grazie alla classe JViewPort
possiamo recuperare il JTree incapsulato:
JViewport viewport = jScrollPane.getViewport();
JTree fileListTree = (JTree) viewport.getView();
Definiamo il nodo figlio da aggiungere al nodo root per il file selezionato, recuperiamo il riferimento al nodo root, e aggiungiamo finalmente il nodo figlio relativo al file appena selezionato:
DefaultMutableTreeNode child = new DefaultMutableTreeNode(filePath);
// ...
DefaultTreeModel dtm = (DefaultTreeModel) fileListTree.getModel();
DefaultMutableTreeNode root = (DefaultMutableTreeNode)
dtm.getRoot();
// ...
root.add(child);
// refresh del JTree:
dtm.reload(root);
// JTree completamente aperto:
fileListTree.expandRow(0);
Ricapitolando: la classe riceve il riferimento alla classe MainWindow
della finestra principale dell'applicazione.
Grazie a questo riferimento è in grado di recuperare i riferimenti ai componenti in essa contenuti. Attraverso il JFileChooser
viene recuperato il file selezionato. Successivamente viene esegutio tutto il codice per aggiungere il path del file al JTree
della finestra principale dell'applicazione. Come si può vedere dal codice, il punto chiave è sfruttare il metodo getComponent(index)
invocato su un determinato container.In questo modo riusciamo a recuperare i riferimenti dei componenti all'interno di un container grazie all'indice ad essi associato rispetto al container che invece li contiene. Una volta ottenuto il riferimento al JTree
, l'aggiunta di un elemento all'albero si ottiene attraverso le classi DefaultTreeModel
e DefaultMutableTreeNode
. Una volta aggiunti tutti i file possiamo fare click sul pulsante "Make archive"
e creare l'archivio jar.
CompressFileAction
è invece la classe responsabile di elaborare il JTree
e creare l'archivio:
public class CompressFileAction implements ActionListener {
private MainWindow window;
public CompressFileAction(MainWindow window) {
this.window = window;
}
@Override
public void actionPerformed(ActionEvent arg0) {
// ...
}
// ...
}
il codice per la creazione del jar
All'interno del metodo actionPerformed
risiede la logica di creazione del jar. Si inizia con il recuperare il riferimento al JPanel
che contiente il JTree
e poi attraverso la classe JViewPort
il JTree
stesso.
la lista dei File: VIEW (gestione del JTree)
JPanel header = (JPanel) window.getContentPane().getComponent(0);
JScrollPane jScrollPane = (JScrollPane) header.getComponent(1);
JViewport viewport = jScrollPane.getViewport();
JTree fileListTree = (JTree) viewport.getView();
A questo punto possiamo recuperare la root dell'albero che ci permetterà, attraverso un'iterazione, di acquisire tutti i file da inserire nell'archivio:
DefaultTreeModel dtm = (DefaultTreeModel) fileListTree.getModel();
DefaultMutableTreeNode root = (DefaultMutableTreeNode) dtm.getRoot();
la lista dei File: MODEL (array dei File)e
Decidiamo di immagazzinare i riferimenti ai file in un array di oggetti java.io.File
della dimensione pari al numero di elementi figli del nodo root della JTree
. Otteniamo successivamente un'enumerazione dall'oggetto DefaultMutableTreeNode
ed iteriamo per il recupero di tutti i figli del nodo root, ovvero i nostri file:
File[] files = new File[root.getChildCount()];
Enumeration filesSelected = root.children();
int i = 0;
while (filesSelected.hasMoreElements()) {
DefaultMutableTreeNode fl = (DefaultMutableTreeNode) filesSelected.nextElement();
files[i] = new File(fl.toString());
i++;
}
la lista dei File: CONTROLLER (action performed)
Una volta recuperati i file, proseguiamo con la creazione del file jar che li deve contenere. Attraverso la classe JFileChooser
apriamo una finestra di salvataggio del file jar attraverso la quale possiamo specificare nome del file, compreso di estensione jar (es. miofile.jar), e percorso nel quale salvare. Facciamo poi i controlli del caso per verificare che il ritorno dal FileChooser
abbia avuto esito positivo. Nel caso di esito positivo recuperiamo il file del nostro archivio, non ancora salvato fisicamente, altrimenti visualizziamo un messaggio di errore:
File jarFile = null;
JFileChooser fileChooser = new JFileChooser();
int returnValue = fileChooser.showSaveDialog(window);
if (returnValue == JFileChooser.ERROR_OPTION) {
JOptionPane.showMessageDialog(window, "Unexpected error", "Error", JOptionPane.ERROR_MESSAGE);
return;
} else if (returnValue == JFileChooser.APPROVE_OPTION) {
jarFile = fileChooser.getSelectedFile();
}
Se tutto è andato bene possiamo utilizzare la nostra classe Zipper
per creare fisicamente l'archivio. Il codice che segue effettua la creazione dell'archivio jar, vengono fatti una serie di controlli di validità e viene effettuata l'invocazione del metodo makeJarFile
, al quale passiamo il riferimento al file jar e la lista di File recuperati dal JTree, salverà fisicamente il file sul disco nel percorso e con il nome da noi indicato precedentemente:
// ...
try {
if ( (jarFile != null) && files != null) {
String path = jarFile.getAbsolutePath();
if(!path.endsWith(".jar")){
JOptionPane.showMessageDialog(window,
"File extension must be jar", "Error",
JOptionPane.ERROR_MESSAGE);
return;
}
Zipper.makeJarFile(jarFile, files);
} else {
return;
}
} catch (FileNotFoundException e) {
JOptionPane.showMessageDialog(window,
"File not found error", "Error",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
return;
} catch (IOException e) {
JOptionPane.showMessageDialog(window,
"IO Error", "Error",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
return;
}
JOptionPane.showMessageDialog(window,
"Archive created with successful", "Info",
JOptionPane.INFORMATION_MESSAGE);
}
}
Conclusioni
Abbiamo visto le basi per la costruzione di semplici interfacce grafiche tramite Swing. Sono stati evidenziati gli aspetti fondamentali che vanno dal design alla gestione degli eventi dei nelle nostre GUI,dando una possibile soluzione architetturale, e introducendo anche alcuni componenti generalmente non introdotti in una prima trattazione, ma estremamente utili, e con l'obiettivo di cercare di rendere il tutto più interessante.