Java Swing, appunti

Il modo più semplice di gestire gli eventi di una finestra creata da Swing è quello di assegnare ad un componente un listener:

public class MyFrame extends JFrame {
	private JButton bottone = new JButton("Bottone");
	Ascoltatore listener = new Ascoltatore();

	public MyFrame() {
			...
			Container c = this.getContentPane();
			c.add(bottone);
			// Assegno l' ascoltatore
			bottone.addActionListener(listener);
	}
}

public class Ascoltatore implements ActionListener {
	public void actionPerformed(ActionEvent event) {
		// Accedo all' oggetto che ha scatenato l' evento
		JButton b = (JButton) event.getSource();
		JOptionPane.showMessageDialog(null, "E` stato premuto" + b.getText());
	}
}

ma il problema di questa tecnica è che non si può accedere ad un elemento diverso da quello che ha scatenato l’ evento: con la classe che implementa l’ interfaccia ActionListener posta “esternamente” alla classe del frame, è impossibile accedere agli altri oggetti del frame.

Una prima soluzione è quella di scrivere il listener internamente al frame, così da poter accedere ai suoi membri:

public class MyFrame extends JFrame {
	private JButton bottone = new JButton("Bottone");

	public MyFrame() {
			...
			Container c = this.getContentPane();
			c.add(bottone);
			// Assegno l' ascoltatore in modo anonimo
			bottone.addActionListener(new Ascoltatore());
	}

	class Ascoltatore implements ActionListener {
		public void actionPerformed(ActionEvent event) {
			// Accedo all' oggetto che ha scatenato l' evento
			// semplicemente accedendo al metodo "bottone" della classe
			JButton b = bottone;
			JOptionPane.showMessageDialog(null, "E` stato premuto" + b.getText());
		}
	}
}

Lo svantaggio di questa soluzione è che la classe del frame può diventare eccessivamente grande al crescere della complessita della GUI, e si avrà anche un codice poco leggibile con 2 classi eterogenee fuse tra loro.

La soluzione migliore è quella di definire un costruttore del listener che prende come parametro un riferimento alla finestra che contiene i vari componenti.

public class MyFrame extends JFrame {
	// Componenti della GUI come membri della classe
	JPanel centro = new JPanel();
	JPanel sud = new JPanel();
	JTextField txt = new JTextField(20);
	JButton button = new JButton("Premi");
	// Inizializzazione del listener
	Listen ascolt=new Listen(this);

	public MyFrame() {
		super("Esempio");
		centro.add(txt);
		...
		button.addActionListener(ascolt);
		txt.addActionListener(ascolt);
		...
	}
}

class Listen implements ActionListener {
	MyFrame frame;
	// Costruttore che riceve il riferimento al frame
	public Listen(MyFrame aFrame)
		{ frame = aFrame; }

	// Da qui accediamo ai metodi del frame !
	public void actionPerformed(ActionEvent e) {
	JTextField text = frame.txt;
	JOptionPane.showMessageDialog(null,text.getText());
	}
}

A questo punto bisogna dire che non si può pensare di creare un listener per ogni componente: si può invece raggruppare vari componenti ed assegnargli un unico listener condiviso, con lo stessa funzione actionPerformed condivisa tra essi.

Dunque, in actionPerformed bisogna distinguere quale oggetto ha generato l’ evento; si può fare in 2 modi:

  • con il metodo getSource() della classe ActionEvent. Questa tecnica è fattibile se si può confrontare il riferimento dato da getSource() con il riferimento memorizzato nella classe del frame: è, in generale, il caso della classe interna vista prima.
  • con la proprietà actionCommand dei componenti di swing. Con questa tecnica, in actionPerformed() non è più necessario prendere i riferimenti agli oggetti, ma basta leggere l’ actionCommand e chiamare la funzione appropriata.
    Questa tecnica è utile anche nel caso in cui ci siano diversi componenti che condividono la stessa azione: ad esempio un bottone e una voce di menu possono fare la stessa cosa, quindi basta scrivere una sola funzione e farla chiamare da actionPerformed tramite un actionCommand.
    Rimane il fatto che per accedere ai componenti del frame sia necessario passare al costruttore del listenere il riferimento al frame che contiene quei componenti come metodi della classe.

    public class MyFrame extends JFrame {
    	...
    	JMenuItem UpOpt = new JMenuItem("Up");
    	JMenuItem DownOpt = new JMenuItem("Down");
    	JMenuItem RandomOpt = new JMenuItem("Random");
    	Listener ascolt = new Listener();
    	public MyFrame() {
    		...
    		UpOpt.addActionListener(ascolt);
    		UpOpt.setActionCommand(ascolt.UPOPT);
    		DownOpt.addActionListener(ascolt);
    		DownOpt.setActionCommand(ascolt.DOWNOPT);
    		RandomOpt.addActionListener(ascolt);
    		RandomOpt.setActionCommand(ascolt.RANDOMOPT)
    		...
    	}
    }
    
    public class Listener implements ActionListener {
    	public final static String UPOPT = "up";
    	public final static String DOWNOPT = "down";
    	public final static String RANDOMOPT = "random";
    	// Eventualmente scrivere un costruttore
    	// per avere un riferimento al frame
    
    	public void actionPerformed(ActionEvent e) {
    		String com = e.getActionCommand();
    		if (com == UPOPT)
    			upOpt();
    		else if (src == DOWNOPT)
    			downOpt();
    		else if (src == RANDOMOPT)
    			randomOpt();
    	}
    
    private void upOpt()
    { ... }
    private void randomOpt()
    { ... }
    private void downOpt()
    { ... }
    }
    
    

Java Swing livello avanzato: http://home.cogeco.ca/~ve3ll/jatutorc.htm
MVC: http://www.macs.hw.ac.uk/guidebook/?name=Using%20The%20GUI&page=1

SQLite con Android

Il SO Android mette a disposizione gli strumenti per operare con database SQLite: per gestirli è sufficiente implementare una classe “helper”.

Per prima cosa creiamo un’ interfaccia in cui memorizzare i nomi delle colonne e delle tavole del database, per non doverli scrivere ogni volta: estendiamo l’ interfaccia BaseColumns

public interface ProvinciaTable extends BaseColumns {
	String TABLE_NAME = "province";
	String CODICE = "codice";
	String NOME = "nome";

	String[] COLUMNS = new String[] {_ID, CODICE, NOME};
}

si ricordi che in una interfaccia una costante è implicitamente public static final.

Creazione e popolamento del database

L’ SDK ci fornisce la classe astratta SQLiteOpenHelper per gestire “facilmente” i database SQLite: i suoi metodi onCreate e onUpgrade sono astratti, quindi siamo obbligati ad implementarli.
La classe si prenderà cura di aprire il database (se esiste), di crearlo (se non esiste), e di aggiornarlo: userà le transazioni per avere il database sempre in uno stato sensibile.

Vediamo il costruttore:

public class DatabaseHelper extends SQLiteOpenHelper {

	private static final String DATABASE_NAME = "devAPP.db";
	private static final int SCHEMA_VERSION = 1;

	public DatabaseHelper(Context context) {
		super(context, DATABASE_NAME, null, SCHEMA_VERSION);
	}
	//...
}

Lo schema_version serve per gestire gli aggiornamenti del database.
Vediamo ora il callback onCreate, che viene chiamato soltanto quando un database deve essere creato:

public void onCreate(SQLiteDatabase db) {
	// formatto una stringa sql, come si fa con printf in C, con i comandi per creare il database
	String sql = "CREATE TABLE {0} ({1} INTEGER PRIMARY KEY AUTOINCREMENT, {2} TEXT NOT NULL,{3} TEXT NOT NULL);";
	db.execSQL(MessageFormat.format(sql, ProvinciaTable.TABLE_NAME, ProvinciaTable._ID, ProvinciaTable.CODICE, ProvinciaTable.NOME));

	inserisciProvincia(db, "Agrigento", "AG");
	inserisciProvincia(db, "Alessandria", "AL");
	inserisciProvincia(db, "Ancona", "AN");
	//...
	inserisciProvincia(db, "Viterbo", "VT");
}

Per inserire i valori usiamo questo metodo:

private void inserisciProvincia(SQLiteDatabase db, String nome, String codice) {
	ContentValues v = new ContentValues();
	v.put(ProvinciaTable.CODICE, codice);
	v.put(ProvinciaTable.NOME, nome);
	db.insert(ProvinciaTable.TABLE_NAME, null, v);
}

il metodo insert della classe SQLiteDatabase richiede il terzo parametro di tipo ContentValues: questo serve a “mappare” un dato nella rispettiva colonna.
Il secondo parametro invece è opzionale e riguarda la gestione dell’ inserimento di una riga di dati nulli: se posto a true, fa si che vengano inseriti dei NULL nel database invece di restituire errore.

Esecuzione di una query

I metodi finora visti servono a creare e popolare il database, ma ancora non abbiamo incontrato un evento che possa chiamarli.

La classe SQLiteOpenHelper offre due metodi per accedere al database:

  • public synchronized SQLiteDatabase getReadableDatabase ()
  • public synchronized SQLiteDatabase getWritableDatabase ()

Il primo metodo crea o apre il database e restituisce lo stesso oggetto che restituirebbe getWritableDatabase, a meno che non sorga un problema che costringe ad aprire il database in sola lettura (ad esempio un disco pieno).
Il secondo metodo è più restrittivo: crea o apre il database, ma richiede esclusivamente un accesso in scrittura, e lancia una SQLiteException se ciò non è possibile.

Dunque, per accedere al database (eventualmente dopo averlo creato ex-novo), è necessario chiamare uno dei due metodi ora visti.
Implemento allora un nuovo metodo che chiamerò dalla Activity principale:

public Cursor getProvince() {
	return (getReadableDatabase().query(
		ProvinciaTable.TABLE_NAME, ProvinciaTable.COLUMNS, null, null, null, null, ProvinciaTable.NOME));
}

dentro questo metodo avviene la chiamata a getReadableDatabase() che ci assicura un accesso al database (sarebbe più raffinato creare un blocco try/catch SQLiteException per controllare eventuali errori), ed effettuo una query sul SQLiteDatabase restituito.

Tra i vari metodi query uso

public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)

i cui parametri sono:

  • il nome della tabella
  • l’ array con i nomi delle colonne (vedere interfaccia creata all’ inizio)
  • 4 parametri per definire la clausola WHERE, argomenti per il bind, la clausola GROUP BY, e la clausola HAVING
  • la clausola ORDER BY (che nello snippet ho impostato a ProvinciaTable.NOME)

L’ oggetto restituito dalla query è Cursor (che ricorda il database handler del PHP): infatti nella Activity dovremo ciclare su di esso per estrarre le righe.
Vediamo come si estraggono i dati:

// siamo nella Activity principale
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	// istanziamo l' helper della connessione
	DatabaseHelper databaseHelper = new DatabaseHelper(this);
	// eseguiamo la query
	Cursor c = databaseHelper.getProvince();

	try {
		while (c.moveToNext()) {
			Log.d("devAPP", c.getLong(0) + " " + c.getString(1) + " " + c.getString(2));
		}
	} finally {
		c.close();
	}
}

il codice è abbastanza semplice ed autoesplicativo.
Faccio solo notare i metodi moveToNext, getLong, e getString: il primo serve a muovere il cursore tra il recordset restituito dalla query (vedere la classe Cursor per altri metodi simili); gli altri due leggono i dati rispettivamente di tipo numerico (Long) e di tipo String.
Infatti la query viene eseguita sulle colonne ProvinciaTable.COLUMNS (vedere funzione getProvince()) che sono definite nell’ interfaccia (vedere all ‘inizio) ProvinciaTable come

String[] COLUMNS = new String[] {_ID, CODICE, NOME};

Conclusione

Al lancio dell’ applicazione viene istanziato il nostro DatabaseHelper, poi viene eseguita la query tramite databaseHelper.getProvince(): questa funzione chiama getReadableDatabase() che, se non trova il database, lo crea ex-novo, e lo fa chiamando il callback onCreate di DatabaseHelper.

Nella Activity ho usato la funzione Log.d(…) della classe Log: questa scrive nel log di Eclipse; per vederlo andare nel menu Window->Show View->Other e in “Android” selezionare Log Cat.

Interfaccia utente, Activity, ed Intent

Interfaccia utente

L’ interfaccia grafica si basa su una gerarchia di oggetti di tipo View e ViewGroup.
Un LinearLayout è una sottoclasse di ViewGroup: al suo interno si può inserire un oggetto View, come un EditText o un Button.

Un importante attributo delle View è layout_weight: esso permette di specificare una quota dello spazio a disposizione delle View contigue.
Ad esempio: se impostassimo a 2 il peso di una View e ad 1 il peso di un’ altra, avremmo che la prima occuperà 2/3 dello spazio (infatti la somma dei pesi è 3); se ad una terza View si desse il peso 1, la prima avrebbe 2/4=1/2 dello spazio, e le altre ne avrebbero 1/4 ciascuna.
Il valore di default per ogni View è zero: quindi se si desse un peso maggiore di 0 ad una sola vista, questa occuperebbe tutto lo spazio lasciato libero dalle rimanenti View impostate a wrap_content.
Un esempio di ciò è un LinearLayout orizzontale in cui c’è una riga con EditText e un Button impostati entrambi a wrap_content per larghezza e altezza. Dando peso pari a 1 alla EditText per allargarla al massimo dello spazio disponibile, il bottone occuperebbe il minore spazio possibile per avvolgere la stringa che contiene.
In questo caso è importante anche modificare layout_width di EditText impostandolo a 0dp: infatti wrap_content richiede un calcolo della larghezza, e il layout_weight ne richiede un altro; usando 0dp si evita il doppio calcolo.

Lanciare una seconda Activity

Supponiamo di utilizzare il bottone “Send” della figura qui sopra per aprire una nuova Activity in cui mostrare la stringa inserita in EditText.

Nel main.xml aggiungere android:onClick=”sendMessage” per indicare una funzione da chiamare quando il bottone viene cliccato; nella classe della prima Activity definire la funzione

/** Called when the user selects the Send button */
public void sendMessage(View view) {
    // Do something in response to button
}

Notare che il nome della funzione deve essere uguale al nome assegnato all’ attributo XML, la funzione deve essere public, deve ritornare void, e avere un solo parametro View.
Per passare alla seconda Activity i dati si usano gli Intent.

Per esempio:

Intent intent = new Intent(this, DisplayMessageActivity.class);

il costruttore prende 2 parametri:

  • un Context: si usa this perchè Activity è una sottoclasse di Context
  • la classe del componente a cui il sistema deve consegnare l’ intent: in questo caso sarà l’ Activity che dovrà essere aperta

poi nell’ intent istanziato si possono inserire i dati da passare:

Intent intent = new Intent(this, DisplayMessageActivity.class);
EditText editText = (EditText) findViewById(R.id.edit_message);
String message = editText.getText().toString();
intent.putExtra(EXTRA_MESSAGE, message);

la funzione putExtra() prende una “chiave” (un nome di variabile) e il suo valore come parametri: bisogna creare la chiave nella Activity (siamo ancora nella prima !)

public class MyFirstActivity extends Activity {
    public final static String EXTRA_MESSAGE = "com.example.myapp.MESSAGE";
    ...
}

è sempre bene definire la chiave usando il package name dell’ applicazione come prefisso per essere sicuri che sia univoca, nel caso di interazione con altre applicazioni.

Avviare la seconda Activity

Per avviare la nuova Activity basta completare la precedente funzione con il comando startActivity(intent):

/** Called when the user selects the Send button */
public void sendMessage(View view) {
    Intent intent = new Intent(this, DisplayMessageActivity.class);
    EditText editText = (EditText) findViewById(R.id.edit_message);
    String message = editText.getText().toString();
    intent.putExtra(EXTRA_MESSAGE, message);
    startActivity(intent);
}

Ora bisogna creare la classe DisplayMessageActivity della nuova Activity; è necessario implementare il metodo onCreate() (ogni sottoclasse di Activity deve farlo) ed è in esso che si inizializzano i componenti essenziali dell’ Activity.

E’ anche necessario dichiarare l’ Activity nel AndroidManifest.xml (con Eclipse è facile aggiungere la nuova activity).

Ora si può “ricevere” l’ intent:

Intent intent = getIntent();
String message = intent.getStringExtra(MyFirstActivity.EXTRA_MESSAGE);

notare la “chiave” definita precedentemente: ora serve a recuperare la stringa.

A questo punto abbiamo tutti i dati: rimane solo da visualizzarli e per semplicità adesso lo farò in maniera programmatica (senza usare i layout XML)

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Get the message from the intent
    Intent intent = getIntent();
    String message = intent.getStringExtra(MyFirstActivity.EXTRA_MESSAGE);

    // Create the text view
    TextView textView = new TextView(this);
    textView.setTextSize(40);
    textView.setText(message);

    setContentView(textView);
}

Preparazione dell’ ambiente di sviluppo Android

Utilizzo la IDE Eclipse: http://www.eclipse.org/ , che su Debian Wheezy (attualmente testing) installo con il pacchetto “eclipse”.
Al primo avvio indicare in quale directory Eclipse andrà a salvare i workspaces.

Al termine dell’ installazione, scarico lo SDK Starter Package (circa 25 MB) da http://developer.android.com/sdk/index.html: scompatto il tgz e lo colloco in una directory che poi dovrò indicare alla IDE (per semplicità si può tenere nella home).

Successivamente, installo lo ADT Plugin di Eclipse. Si installa direttamente da dentro la IDE: selezionare dal menu la voce Help > Install New Software….; nella finestra di dialogo che si aprirà, inserire il sito da cui prelevare il software aggiuntivo: premendo Add inserisco un nome (ad esempio Android ADT) e la URL https://dl-ssl.google.com/android/eclipse/; dopo aver premuto OK attendo che viene aggiornata la lista del software disponibile: comparirà la voce Developer Tool con altre sottovoci. Seleziono Developer Tools. Premo Next e attendo il caricamento: compare la lista del software che sarà installato; premo ancora Next e nella successiva finestra accetto le licenze, e poi clicco Finish. Inizierà il download. Può anche comparire un warning sulla non sicurezza delle fonti: continuare ugualmente. Alla fine dell’ installazione, riavviare Eclipse.

Adesso bisogna configurare ADT.
Al riavvio di Eclipse compare una finestra di dialogo che chiede dove è posizionato lo SDK Starter Package: indicare dove è stato scompattato il tgz, e scaricare il più recente SDK. Il download richiede qualche minuto.
Se non compare automaticamente la finestra, o se si vuole controllare quali file vengono installati, selezionare Window > Android SDK Manager dal menu di Eclipse.
Per usare l’ emulatore è necessario installare il pacchetto ARM EABI System Image.
Le Google APIs servono per accedere a Maps, non è necessario scaricarle, così come la documentazione e i sorgenti.

Il software scaricato da ADT viene scaricato nella directory indicata prima (è quella dello Starter Package): ci sono i file degli SDK selezionati e altri tools vari.
Se si vuole operare anche da linea di comando oltre che da Eclipse, è comodo aggiungere al path anche questa directory: editare ~/.bash_profile o ~/.bashrc e aggiungere

export PATH=${PATH}:<dir-di-sdk>/tools:<dir-di-sdk>/platform-tools