Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Swing per principianti: il progetto FileZipper

In questo articolo utilizzeremo le conoscenze di base di swing per realizzare una semplice utilità per comprimere i file (FileZipper)
In questo articolo utilizzeremo le conoscenze di base di swing per realizzare una semplice utilità per comprimere i file (FileZipper)
Link copiato negli appunti

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:

Figura 1. esempio di flow layout
(clic per ingrandire)


esempio di flow layout

BoderLayout

I componenti sono disposti solamente in 5 posizioni specifiche che si ridimensionano automaticamente:

  • NORTH,SOUTH che si ridimensionano orizzontalmente
  • EAST, WEST che si ridimensionano verticalmente
  • CENTER che si ridimensiona orizzontalmente e verticalmente

Questo significa che un componente aggiunto in una certa area si ridimensionerà per occuparla interamente.

Figura 2. esempio di border layout
(clic per ingrandire)


esempio di border layout

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:

Figura 3. layout iniziale dell'applicazione
(clic per ingrandire)


layout iniziale dell'applicazione

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:

Figura 4. esempio di grid layout
(clic per ingrandire)


esempio di grid layout

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:

Figura 5. esempio di grid layout 3x2
(clic per ingrandire)


esempio di grid layout 3x2

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

Figura 6. file zipper: selezione dei file
(clic per ingrandire)


file zipper: selezione dei file

Ogni file aggiunto viene visualizzato nell'albero principale con l'intero percorso.

Figura 7. file zipper: creazione dell'archivio
(clic per ingrandire)


file zipper: creazione dell'archivio

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 grafica
  • it.html.swing.actions: Il package per le classi che gestiscono gli eventi
  • it.html.swing.jar: Il package che contiene la classe che si occupa di creare l'archivio
  • it.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 alla JTree.
  • it.html.swing.actions.CompressFileAction: recupererà i file dalla JTree 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.

Ti consigliamo anche