View Single Post
Old 22-03-2006, 17:31   #10
PGI-Bis
Senior Member
 
L'Avatar di PGI-Bis
 
Iscritto dal: Nov 2004
Città: Tra Verona e Mantova
Messaggi: 4553
Il messaggio che segue è oscenamente lungo

Come anticipato, si tratta di materia molto semplice anche se un po' prolissa. Ti mostro come disegnare e interagire con una tabella rappresentata dagli ipotetici elementi XML:

XMLTable
XMLTableRow
XMLTableCell
PCData

La base è un Tipo XMLElement che, contrariamente ai dettami del Model-View-Controller, incapsula vista e modello. Si tratta di un irrigidimento che, a mio modo di vedere, semplifica la comprensione del sistema, probabilmente perchè riduce il livello di astrazione. Più chiaro ma meno flessibile, per farla breve.

XMLElement

Codice:
package it.tukano.xmlbrowser;

public interface XMLElement {
//MODELLO
	XMLElement getParent();
	
	void setParent(XMLElement e);
	
	void addChildren(XMLElement...elements);
	
	java.util.Collection<? extends XMLElement> getChildren();
	
//VISTA
	int getHeight(int width, java.awt.Graphics context);
	
	java.awt.Rectangle paint(PaintInfo p);
	
	java.awt.Rectangle getBounds();
	
	XMLElement getChildAt(java.awt.Point p);
}
La prima parte è quella comune al nodo di un albero. XMLElement ha un genitore e dei figli che rappresentano il suo contenuto. La seconda parte è relativa al meccanismo di proiezione. Il metodo getHeight restituisce l'altezza del contenuto proiettato dall'elemento. Nel caso di un elemento che contenga del testo (come PCData), l'altezza dipende dal contesto grafico. Il parametro "width" è necessario nel caso in cui sia previsto che un certo contenuto (tipicamente il testo) possa dover essere spezzato su più linee. Il numero di linee e quindi l'altezza totale dell'elemento dipenderà dallo spazio orizzontale disponibile. Nell'esempio che ti faccio non separo il testo su più linee dunque non uso questo intero width, per brevità di esposizione. La separazione richiede la scrittura di un proiettore di testi che rende il tutto non più complicato ma più lungo. Sarebbe inoltre necessario un metodo getWidth(Graphics) al fine di poter calcolare l'altezza di una linea su cui siano presenti più elementi, di altezza diversa.

Il metodo paint proietta il contenuto di questo XMLElement usando un PaintInfo per alcuni parametri di disegno. Il disegno di un documento strutturato richiede che ciascun elemento abbia delle informazioni sulla posizione in cui proiettare il suo contenuto i cui valori dipendono dagli elementi precedenti. La riga di una tabella deve essere disegnata sotto la riga precedente nella stessa tabella, una cella dopo l'altra e via discorrendo.

Il sistema di proiezione del documento funziona così: ogni genitore disegna i propri figli informandoli, uno ad uno, del punto a parire dal quale inziare la proiezione, dello spazio a disposizione e del contesto grafico da usare per il disegno. I figli rispondono al genitore dicendo "con i dati che mi hai passato, mi sono proiettato in una regione pari a...". Il genitore usa le dimensioni del rettangolo di proiezione di un figlio per stabilire i dati di proiezione del figlio successivo.

Il metodo getBounds() restituisce la regione di spazio occupata da un elemento DOPO la sua proiezione. Questa regione può essere usate per stabilire quale XMLElement si trovi in un certo punto dello spazio.

Il metodo getChildAt restituisce l'elemento figlio di questo XMLElement che si trovi in posizione p o null.

PaintInfo

PaintInfo rappresenta i dati necessari alla proiezione di un XMLElement. E' poco più di un insieme di puntatori, con le conseguenze del caso.

Codice:
package it.tukano.xmlbrowser;

import java.awt.*;

public class PaintInfo {
	public int x, y, width;
	public Graphics graphics;
	
	public PaintInfo(int x0, int y0, int w, Graphics g) {
		x = x0;
		y = y0;
		width = w;
		graphics = g;
	}
}
XMLBrowser

Il proiettore di un documento XML è un JComponent a cui è affidato un certo XMLElement, supposta radice del documento.

Codice:
package it.tukano.xmlbrowser;

import java.awt.*;
import javax.swing.*;

public class XMLBrowser extends JComponent {
	private XMLElement root;
	
	public XMLBrowser(XMLElement root) {
		this.root = root;
	}
	
	public XMLElement getRoot() {
		return root;
	}
	
	public XMLElement getElementAt(Point p) {
		return root.getChildAt(p);
	}

	protected void paintComponent(Graphics g) {
		g.setColor(Color.WHITE);
		g.fillRect(0, 0, getWidth(), getHeight());
		Insets margin = getInsets();
		PaintInfo info = new PaintInfo(
			margin.top,
			margin.left,
			getWidth() - margin.left - margin.right,
			g);
		Rectangle rootArea = root.paint(info);
	}
}
Il componente stabilisce la regione iniziale in cui la radice dovrà proiettare il suo contenuto. Passa la palla alla radice e se ne lava le mani .

AbstractXMLElement

Questo oggetto semplifica la vita fornendo una definizione comune al modello di un XMLElement. E' fatto com'è fatto perchè così mi è sembrato più facile scrivere un albero di elementi XML (grande profondità teorica ).

Codice:
package it.tukano.xmlbrowser;

import java.util.*;

public abstract class AbstractXMLElement implements XMLElement {
	private ArrayList<XMLElement> children = new ArrayList<XMLElement>(1);
	private XMLElement parent;
	
	public AbstractXMLElement(XMLElement parent, XMLElement...childElements) {
		this.parent = parent;
		if(parent != null) {
			parent.addChildren(this);
		}
		if(childElements != null) {
			for(XMLElement e : childElements) {
				children.add(e);
				e.setParent(this);
			}
		}
	}
	
	public void setParent(XMLElement parent) {
		this.parent = parent;
	}
	
	public XMLElement getParent() {
		return parent;
	}
	
	public void addChildren(XMLElement...e) {
		children.ensureCapacity(children.size() + e.length);
		for(XMLElement x : e) {
			children.add(x);
			x.setParent(this);
		}
	}
	
	public Collection<? extends XMLElement> getChildren() {
		return children;
	}
}
Ribadisco il principio che ho usato per il disegno degli elementi prima di vederli. In fondo l'obiettivo è il meccanismo e non la sua realizzazione. Un elemento riceve delle informazioni di proiezione relative alla regione di spazio in cui dovrà proiettare il suo contenuto. Un elemento restituisce le dimensioni della regione che abbia occupato per il disegno. Un genitore userà le regioni occupate da un figlio per stabilire l'area di proiezione del figlio successivo.

XMLTable

La tabella è definita come un insieme di elementi disposti su più righe. La proiezione della tabella è proeizione dei suoi figli, uno sotto l'altro. L'area occupata dalla tabella è la massima larghezza di una riga e la somma delle altezza delle righe.

Codice:
package it.tukano.xmlbrowser;

import java.awt.*;

public class XMLTable extends AbstractXMLElement {
	private Rectangle bounds = new Rectangle();

	public XMLTable(XMLElement parent) {
		super(parent);
	}
	
	public Rectangle getBounds() {
		return bounds;
	}
	
	public XMLElement getChildAt(Point p) {
		if(bounds.contains(p)) {
			for(XMLElement row : getChildren()) {
				XMLElement target = row.getChildAt(p);
				if(target != null) return target;
			}
			return this;
		} else {
			return null;
		}
	}
	
	public int getHeight(int lineWidth, Graphics context) {
		int height = 0;
		for(XMLElement row : getChildren()) {
			height += row.getHeight(lineWidth, context);
		}
		return height;
	}
	
	public Rectangle paint(PaintInfo info) {
		int x0 = info.x;
		int y0 = info.y;
		int maxWidth = 0;
		int maxHeight = 0;
		for(XMLElement row : getChildren()) {
			Rectangle rowArea = row.paint(info);
			info.y += rowArea.height;
			info.x = x0;
			if(rowArea.width > maxWidth) maxWidth = rowArea.width;
			maxHeight += rowArea.height;
		}
		bounds.setRect(x0, y0, maxWidth, maxHeight);
		return new Rectangle(0, 0, maxWidth, maxHeight);
	}
	
	public String toString() {
		String s = "Table\n";
		for(XMLElement e : getChildren()) {
			s += "\t" + e + "\n";
		}
		return s;
	}
}

La proiezione per righe è realizzata spostando info.y di una quantità pari all'altezza dell'ultima riga disegnata. Ogni riga usa (info.x, info.y) come punto di partenza per il disegno da cui l'effetto risultante.

XMLTableRow

XMLTableRow è come XMLTable, solo che disegna i figli uno dopo l'altro lungo una sola linea orizzontale. Qui è info.x a spostarti mentre prima era info.y.

Codice:
package it.tukano.xmlbrowser;

import java.awt.*;

public class XMLTableRow extends AbstractXMLElement {
	private Rectangle bounds = new Rectangle();

	public XMLTableRow(XMLElement parent, XMLElement...children) {
		super(parent, children);
	}
	
	public Rectangle getBounds() {
		return bounds;
	}
	
	public XMLElement getChildAt(Point p) {
		if(bounds.contains(p)) {
			for(XMLElement child : getChildren()) {
				XMLElement target = child.getChildAt(p);
				if(target != null) return target;
			}
			return this;
		} else {
			return null;
		}
	}
	
	public int getHeight(int width, Graphics context) {
		int height = 0;
		for(XMLElement cell : getChildren()) {
			int cellHeight = cell.getHeight(width, context);
			if(cellHeight > height) height = cellHeight;
		}
		return height;
	}
	
	public Rectangle paint(PaintInfo info) {
		bounds.setLocation(info.x, info.y);
		int maxHeight = 0;
		int maxWidth = 0;
		for(XMLElement cell : getChildren()) {
			Rectangle cellArea = cell.paint(info);
			info.x += cellArea.width;
			if(cellArea.height > maxHeight) maxHeight = cellArea.height;
			maxWidth += cellArea.width;
		}
		bounds.setSize(maxWidth, maxHeight);
		return new Rectangle(0, 0, maxWidth, maxHeight);
	}
	
	public String toString() {
		String s = "ROW";
		for(XMLElement e : getChildren()) {
			s += " | " + e;
		}
		return s;
	}
}

XMLTableCell

Una cella è un elemento che contiene un altro elemento. E' in grado di realizzare qualsiasi malefatta grafica, questa fa poco o niente salvo dire al suo contenuto "disegnati".

Codice:
package it.tukano.xmlbrowser;

import java.awt.*;

public class XMLTableCell extends AbstractXMLElement {
	private XMLElement content;

	public XMLTableCell(XMLElement parent, XMLElement content) {
		super(parent, content);
		this.content = content;
	}
	
	public int getHeight(int lineWidth, Graphics context) {
		return content.getHeight(lineWidth, context);
	}
	
	public XMLElement getChildAt(Point p) {
		return content.getChildAt(p);
	}
	
	public Rectangle getBounds() {
		return content.getBounds();
	}
	
	public Rectangle paint(PaintInfo info) {
		int x = info.x;
		int y = info.y;
		Rectangle area = content.paint(info);
		info.graphics.setColor(Color.BLUE);
		info.graphics.drawRect(x, y, area.width, area.height);
		return area;
	}
	
	public String toString() {
		return "(" + content + ")";
	}
}
PCData

Questo è il contenuto che uso nell'esempio. Testo semplice. Molto semplice .

Codice:
ackage it.tukano.xmlbrowser;

import java.awt.*;

public class PCData extends AbstractXMLElement {
	private String text;
	private Rectangle bounds = new Rectangle();

	public PCData(XMLElement parent, String content) {
		super(parent);
		text = content;
	}
	
	public int getHeight(int width, Graphics context) {
		return context.getFontMetrics().getHeight();
	}
	
	public Rectangle getBounds() {
		return bounds;
	}
	
	public XMLElement getChildAt(Point p) {
		return bounds.contains(p) ? this : null;
	}
	
	public Rectangle paint(PaintInfo info) {
		FontMetrics m = info.graphics.getFontMetrics();
		int w = m.stringWidth(text);
		int h = m.getHeight();
		info.graphics.setColor(Color.BLACK);
		info.graphics.drawString(text, info.x, info.y + h - m.getDescent());
		bounds.setRect(info.x, info.y, w, h);
		return new Rectangle(0, 0, w, h);
	}
	
	public String toString() {
		return text;
	}
}
Main

Qui tiriamo le fila del discorso. Affidiamo un XMLBrowser ad un JFrame. Connettiamo ad XMLBrowser un MouseListener. Al click del mouse, richiediamo ad XMLBrowser quale sia l'elemento che si trovi in posizione e.getPoint(). XMLBrowser risponderà interrogando la sua radice, che interroga i figli, e i figli i figli e via dicendo. L'unica cosa da notare è che se al click corrisponda un PCData il MouseListener stamperà a video il suo genitore perchè, idealmente, PCData non contiene attributi che non sia già visibili all'utente quando osservi il browser.

Codice:
package it.tukano.xmlbrowser;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Main {
	private XMLBrowser browser;
	private JFrame frame = new JFrame("XMLBrowser sample");

	private Runnable starter = new Runnable() {
		public void run() {
			frame.setVisible(true);
		}
	};
	
	private MouseListener clickHandler = new MouseAdapter() {
		public void mouseClicked(MouseEvent e) {
			XMLElement target = browser.getElementAt(e.getPoint());
			if(target == null) {
				target = browser.getRoot();
			} else if(target instanceof PCData) {
				target = target.getParent();
			}
			System.out.println("And the winner is: " + target);
		}
	};
	
	private void startApp() {
		XMLElement root = new XMLTable(null);
		XMLTableRow row = new XMLTableRow(root);
		row.addChildren(
			new XMLTableCell(null, new PCData(null, "Cella 0,0")),
			new XMLTableCell(null, new PCData(null, "Cella 0,1")),
			new XMLTableCell(null, new PCData(null, "Cella 0,2")),
			new XMLTableCell(null, new PCData(null, "Cella 0,3")));
		row = new XMLTableRow(root);
		row.addChildren(
			new XMLTableCell(null, new PCData(null, "Cella 1,0")),
			new XMLTableCell(null, new PCData(null, "Cella 1,1")),
			new XMLTableCell(null, new PCData(null, "Cella 1,2")),
			new XMLTableCell(null, new PCData(null, "Cella 1,3")));
		browser = new XMLBrowser(root);
		browser.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
		browser.addMouseListener(clickHandler);
		frame.add(browser);
		frame.setSize(400, 400);
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		SwingUtilities.invokeLater(starter);
	}
	
	public static void main(String...args) { new Main().startApp(); }
}
Qui si nota la bieca funzione di AbstractXMLElement .

Ci sono due cose che mancano nell'esempio. La proiezione degli elementi è molto semplificata. Il presupposto, non realistico, è che tutti gli elementi su una stessa riga abbiano la stessa altezza. Il procedimento per supportare la proiezione di elementi in linea con altezze diverse può essere questo:

1. calcola la larghezza di ogni elemento;
2. data la larghezza della linea, ricava l'altezza di ogni elemento che si trovi su quella linea, fino ad esaurimento dello spazio orizzontale disponibile (da cui il getWidth(Graphics));
3. nel caso in cui l'ultimo elemento del passaggio precedente ecceda la larghezza della linea, lo conserva;
4. stabilisce l'altezza massima tra quelle determinate nel punto 2 che diventa altezza della linea corrente;
5. stabilisce l'altezza di base per disegnare il testo in base all'altezza della linea;
6. disegna il contenuto usando l'altezza della linea corrente, l'altezza di base, il punto iniziale e la larghezza della linea;
7. riparte da 1, spostandosi verso il basso di un valore pari all'altezza della linea corrente: se l'ultimo elemento fosse eccedente, nel sendo specificato in 3, usa questo elemento come prima della linea, per la parte in eccesso. Altrimenti parte dall'elemento successivo.

Il tutto vale non solo per la proiezione di testo stilizzato ma, in generale, per qualsiasi successione di elementi in linea (tipo image, span, a, b, i eccetera).

Manca inoltre l'attribuzione di una dimensione ad XMLBrowser. La dimensione può essere calcolata usando la regione di spazio risultante dalla proiezione della radice.

Come si suol dire, è un lavoro "certosino". Sono tutti passaggi non complicati solo che occorre farne un bel po'.

Mi spiace per la lunghezza del messaggio ma l'alternativa era parlare a vanvera per tre o quattro righe e lasciarti con un pungo di mosche in mano.

Ultima modifica di PGI-Bis : 22-03-2006 alle 23:54. Motivo: la grammatica non è un'opinione
PGI-Bis è offline   Rispondi citando il messaggio o parte di esso