Introduzione
Siamo
ormai alla fine dell’anno 2000, e stiamo assistendo all’esplosione di Internet,
delle applicazioni multimediali, di giochi 3D mozzafiato, il tutto
grazie a schede grafiche che vanno a risoluzioni e profondità di
colori tali da mettere a dura prova anche il migliore dei monitor. Tuttavia,
in alcuni casi (come Desk Top Publishing o grafica avanzata), l’area di
visualizzazione offerta da un solo schermo può non bastare: in nostro
aiuto viene quindi il supporto al monitor multiplo. Il Macintosh e UNIX
(con X-Windows) lo offrono già da diverso tempo; recentemente questa
possibilità è stata anche “concessa” al mondo dei PC, con
l’introduzione di Windows 98 e, dall’inizio del corrente anno, di Windows
2000 per l’utenza professionale.
Java,
che ha sempre avuto tra i propri cavalli di battaglia il motto Write Once,
Run Anywhere (lo scrivi una volta, lo esegui dappertutto) ha sempre dovuto
fornire ai programmatori una sorta di minimo comune multiplo delle funzionalità
disponibili vari sistemi operativi, sia nell’ambito delle GUI sia in tutti
gli altri: ciò automaticamente tagliava fuori alcune caratteristiche
particolari che rendono ogni piattaforma più evoluta delle altre
in qualche aspetto. Lo stesso discorso si applica al supporto al monitor
multiplo, che non era previsto in Java fino al JDK versione 1.2.x. Siccome
ora virtualmente tutti i sistemi operativi per cui esista un’implementazione
di Java possiedono tale funzionalità, anche Java (JDK 1.3) è
stato aggiornato. Come vedremo a breve, le diverse piattaforme affrontano
il problema con approcci differenti, e l’originale implementazione adottata
dalla SUN ne tiene egregiamente conto.
Supporto al monito
multiplo: gli approcci esistenti
Le
soluzioni adottate nel gestire il monitor multiplo sono essenzialmente
di due tipi:
-
Ogni monitor
è visto come un dispositivo indipendente (a volte anche dal punto
di vista cromatico), ciascuno con un proprio sistema di coordinate. Questa
è la versione normalmente adottata in tutti i sistemi con X-Windows,
ed è pertanto caratteristica delle varie piattaforme UNIX, Linux
incluso.
-
Il sistema
di coordinate è unico (detto device virtuale), e ogni monitor fisico
ne copre una porzione che in generale è contigua e non sovrapposta
alle altre (anche se in alcuni rari casi può esserlo) come nelle
tessere di un mosaico, o come si vede spesso in certe trasmissioni televisive
dove immagini grandi sono scomposte in gruppi ordinati di teleschermi.
Questo approccio è tipico di Windows 2000 e Windows 98 (e Macintosh),
anche se alcune schede video particolari (come ad esempio le Matrox) avevano
anticipato i tempi fornendo soluzioni del tutto analoghe già con
Windows NT 4.0.
Vale
la pena ricordare che X-Windows (sotto tutte le versioni di UNIX) possiede
un protocollo di comunicazione molto sofisticato (con caratteristiche di
client-server) basato su TCP/IP per la visualizzazione di risorse grafiche
su macchine remote, cioè distinte ma collegate via rete a quella
dove le applicazioni cui tali risorse appartengono vengono eseguite; queste
funzionalità non sono (per ora) disponibili sotto Windows NT/2000,
ove la visualizzazione è comunque effettuata localmente.
Anche
se un esame dettagliato delle caratteristiche delle varie soluzioni esula
dallo scopo del presente scritto, un breve approfondimento con l’aiuto
di alcuni disegni sarà senz’altro utile. Come avviene nella realtà
pratica, si supponga che tutti i display e/o monitor possiedano la stessa
risoluzione grafica, e che i sistemi di coordinate cartesiani abbiano tutti
l’origine in alto a sinistra, con le ascisse positive verso destra e le
ordinate positive verso il basso.
Siano
dunque
e
rispettivamente l’ordinata massima e minima dell’i-esimo display, ove l’ordine
è specificato di volta in volta; per tutte vale in generale =
e =
e così via.
|
Figura
1. Esempio di sistema grafico con display associati a dispositivi fisici
indipendenti ("alla UNIX").
|
Figura
2. Esempio di sistema grafico con due monitor associati a un device virtuale
(Soluzione
Windows).
In
Figura 1 è illustrata una tipica combinazione ottenibile con un
sistema UNIX: ogni scheda video pilota uno schermo separato con proprio
sistema di coordinate cartesiane; gli angoli dei monitor sono arrotondati
per distinguerne il contorno sul sistema di assi; ognuno ha origine (0,
0). Anche se in pratica non accade, i due monitor potrebbero anche avere
risoluzione e profondità di colore diverse. Invece, in Figura 2
è mostrata una situazione tipica di Windows: in questo caso lo spazio
grafico virtuale è unico, poniamo per esempio di 2048 x 768 pixel
distribuiti su due monitor da 1024 x 768. E’ unica anche l’origine delle
coordinate, che sarà (0, 0) e posta nel monitor di sinistra. Invece,
il monitor di destra avrà l’origine delle coordinate a partire da
(1024, 0). Ovviamente, qui si può prendere una finestra e trascinarla
finché appaia metà su un monitor e metà sull’altro:
ciò non era possibile con la configurazione a display indipendenti.
Le classi per
il supporto al monitor multiplo in Java2
La
soluzione adottata dalla SUN per supportare sistemi con monitor multiplo
a partire dal JDK 1.3 è piuttosto articolata. L’approccio si basa
sul fornire una serie di classi astratte indipendenti i cui metodi possono
essere utilizzati su qualunque piattaforma. Esse, di fatto, costituiscono
l’”interfaccia” di Java per nascondere l’implementazione nativa del supporto
al monitor multiplo che poi è fornita per ciascuna piattaforma su
cui Java può funzionare. Tali classi appartengono al package java.awt,
e sono:
-
GraphicsEnvironment.
Descrive le proprietà dell’ambiente grafico su una data piattaforma,
ed in particolare i dispositivi grafici e i font di caratteri disponibili.
-
GraphicsDevice.
Rappresenta il concetto generale di dispositivo grafico, che può
essere un monitor (caso più tipico), una stampante oppure un buffer
di immagine. Gli oggetti della classe GraphicsDevice sono la destinazione
delle operazioni di rendering e disegno degli oggetti della classe Graphics2D.
-
GraphicsConfiguration.
Rappresenta il concetto di configurazione grafica (con le sue varie caratteristiche)
di un dispositivo grafico, cioè di un GraphicsDevice, che può
avere quindi associato a se stesso più di una GraphicsConfiguration,
ciascuna delle quali fornisce tutti i parametri per l’utilizzo di un GraphicsDevice
in differenti modalità. Con un oggetto di tipo GraphicsConfiguration
è possibile visualizzare nel dispositivo che esso rappresenta (principalmente
un monitor) finestre e frame (java.awt.Frame, java.awt.Window e classi
derivate, comprese quelle di Swing).
|
Figura
3. Diagramma UML della gerarchia di GraphicsEnvironment e classi associate.
La
gerarchia delle classi è illustrata nel diagramma UML di Figura
3; alcuni loro metodi che possiedono nella API non sono riportati. Si tratta
tutte e tre di classi astratte, perciò non sono direttamente istanziabili,
ma ottenibili solo tramite factory method statici opportuni. Quest’implementazione
ha senso se si pensa che al momento della creazione di un oggetto di tali
classi si dovrebbe avere a disposizione la lista completa delle risorse
del sistema, che è esso stesso a fornirla al programmatore per i
suoi applicativi.
I
metodi principali di GraphicsEnvironment sono i seguenti:
-
getLocalGraphicsEnvironment():
è un metodo statico (cioè di classe) che permette di ottenere
un’istanza di GraphicsEnvironment con le caratteristiche dell’ambiente
grafico locale.
-
getScreenDevices():
è un metodo astratto che restituisce un array di GraphicsDevice,
che rappresentano tutti quelli associati all’ambiente grafico locale. Lo
schermo grafico definito per difetto è ricavabile invocando il metodo
astratto getDefaultScreenDevice().
I metodi
principali della classe GraphicsDevice sono:
-
getConfigurations():
restituisce in un vettore tutte le GraphicsConfiguration associate con
il corrente GraphicsDevice.
-
getDefaultConfiguration():
restituisce la GraphicsConfiguration normalmente associata al corrente
GraphicsDevice.
-
getBestConfiguration(GraphicsConfigTemplate
gct): restituisce la configurazione grafica che meglio soddisfa i criteri
espressi nel parametro di classe GraphicsConfigTemplate.
-
getIDstring():
restituisce una stringa che rappresenta nella piattaforma corrente l’identificativo
con cui il dispositivo grafico su cui si chiama quel metodo. Questa stringa,
ad esempio, seguirà sotto UNIX le convenzioni con cui si nominano
i display locali: :0.0, :0.1, e così via, mentre sotto Windows vale
\Display0, \Display1, etc.
-
getType():
restituisce un intero che a seconda del valore indica il tipo di dispositivo
che il GraphicsDevice rappresenta: schermo, stampante o buffer di immagine.
Queste tipologie corrispondono ad altrettante costanti intere definite
come membri della classe (v. Figura 3).
La classe
GraphicsConfiguration possiede diversi metodi, ma al nostro scopo tre sono
particolarmente utili:
-
getBounds():
restituisce gli estremi di coordinate del dispositivo corrente. E’ utile
per gestire i virtual device, nel qual caso l’origine delle coordinate
è diverso da (0, 0).
-
getDevice():
restituisce il GraphicsDevice associato alla corrente GraphicsConfiguration.
Una differenza
sostanziale di comportamento esiste tra le macchine UNIX (display fisici)
e Windows (display virtuali); essa è illustrata in dettaglio in
Tabella 1, rifacendosi agli esempi di Figura 1 e Figura 2. Sostanzialmente,
nel primo caso ogni display indipendente è visto come un singolo
GraphicsDevice, nel secondo tutto il display virtuale costituisce un solo
GraphicsDevice, che però possiede tante GraphicsConfiguration quanti
sono i monitor; queste sono distinguibili tramite coordinate relative (bounds).
|
Tabella
1. Configurazioni per due ipotetiche macchine Unix e Windows (v. testo)
Il
Macintosh segue la stessa filosofia di Windows, e pertanto valgono le medesime
considerazioni.
Qualche esempio
pratico
In
questa sezione, senza scendere in grande dettaglio, si discutono alcuni
esempi di codice per mettere in pratica quanto esposto nei paragrafi precedenti.
I
benefici di queste nuove funzionalità saranno particolarmente bene
accette sotto UNIX. Infatti, prima dell’avvento del JDK 1.3, per dirigere
l’output di un’applicazione Java su un particolare display era necessario
impostare la variabile di ambiente DISPLAY e poi lanciare la JVM. Per cambiare
monitor, era necessario uccidere la JVM e rilanciarla su un altro monitor;
per aggiungerne un’istanza, si doveva lanciarne un’altra con evidenti problemi
di prestazioni, praticità e condivisione dei dati fra una stessa
applicazione. Ora, invece, tutti questi problemi sono solo un ricordo.
Innanzitutto,
si noti che non è possibile creare un qualunque oggetto grafico
su un display fisico, ma soltanto i contenitori che si trovano alla base
della gerarchia dei componenti (attenzione: non della gerarchia delle classi!),
e cioè Window e Frame di AWT, e JWindow e JFrame, le versioni lightweight
delle prime due. Tutte queste classi possiedono costruttori che permettono
di specificare il display di visualizzazione tramite un argomento di tipo
GraphicsConfiguration. Tutti gli altri componenti grafici, derivati da
java.awt.Component (incluse Dialog e JDialog), possiedono un campo di tipo
GraphicsConfiguration, accessibile solo in interrogazione tramite il metodo
getGraphicsConfiguration(). Il valore di tale campo non è impostabile
direttamente, ma è ricavato durante la costruzione dell’oggetto
corrente da quello che si trova al livello immediatamente superiore nell’albero
dei componenti. In tal modo, una finestra composta da pannelli, campi,
bottoni e menù variamente “annidati” fra loro darà comunque
origine ad una gerarchia i cui componenti possiedono tutti lo stesso valore
di GraphicsConfiguration.
Supponiamo
adesso di voler creare un JFrame vuoto di dimensioni 300 x 200 pixel alle
coordinate (100, 100) di ognuno degli schermi nella nostra stazione di
lavoro UNIX. Con le prime istruzioni ricaviamo i dati sull’ambiente grafico
e sul numero di dispositivi grafici della macchina:
GraphicsEnvironment
ge;
ge=GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[]
gds = ge.getScreenDevices() ;
La
dimensione dell’array gds ci permette anche di conoscere il numero effettivo
di dispositivi distinti, sulla cui GraphicsConfiguration di default potremo
visualizzare la nostra finestra. Quindi:
for (int i=0; i<gds.length; i++){
GraphicsDevice currentGD = gds[i];
GraphicsConfiguration gc = gds[i].getDefaultConfiguration();
// Crea la finestra con un titolo
JFrame aFrame = new JFrame(gc, “Finestra “ + i);
// Dà alla finestra una dimensione e posizione sensata
aFrame.setSize(300, 200); aFrame.setLocation(100, 100);
// Rende visibile il frame
aFrame.setVisible(true);
}
Se
siamo sotto Windows o Macintosh, però, questo approccio non funziona.
Il codice dell’esempio precedente dovrà essere modificato come segue:
for
(int i=0; i<gds.length; i++){
GraphicsDevice currentGD = gds[i];
// Stavolta creo un array di Conf. grafiche
// poiché mi aspetto di trovarnepiù di una
GraphicsConfiguration[] gcs = currentGD.getConfigurations();
for (int j=0; j<gcs.length; j++){
// Il sistema di coordinate virtuali ha origine
// nella GraphicsConf.
// di default. Creo i vari frame lì ma poi a
// ciascuno impongo le
// coordinate (bounds) di ogni GraphicsConfiguration
JFrame aFrame = new JFrame(gcs[j], “Finestra “ + j);
Rectangle bounds = gcs[j].getBounds();
aFrame.setLocation(bounds.x + 100, bounds.y + 100);
aFrame.setSize(300, 200);
aFrame.setVisible(true);
}
}
Il
secondo esempio costituisce in realtà una generalizzazione del primo,
nel senso che sotto UNIX la dimensione dell’array gds risulterà
due (in Windows: uno), quello dell’array gcs uno (in Windows: due), e ovviamente
bounds.x e bounds.y varranno tutt’e due zero, poiché l’origine delle
coordinate di ogni display indipendente vale sempre (0, 0).
Per
un esempio di applicazione completa, si rimanda al programma MultiFrameApplet
(e ad altri) reperibile in [2]. Questo programmino crea un JFrame per ogni
GraphicsConfiguration di ogni GraphicsDevice dell’ambiente grafico corrente.
Ogni JFrame contiene una serie di strisce rosse, verdi e blu, il numero
dello schermo, il numero della GraphicsConfiguration e gli estremi del
suo spazio di coordinate. Per queste sue caratteristiche, può essere
usato su qualunque piattaforma e come base di partenza per “l’esplorazione”
del modo in cui Java “vede” le caratteristiche fisiche della nostra macchina.
Approfondimento
sulla architettura software
In
precedenza sono state descritte le classi che normalmente vengono usate
nei programmi in Java per gestire il monitor multiplo, con i loro metodi
principali. Si ritiene interessante analizzare un po’ più da vicino
l’architettura “nascosta” delle classi, che può essere vista soltanto
grazie ad un esame approfondito e mirato delle classi della Sun. Le classi
discusse in questo paragrafo, infatti, si trovano nel file rt.jar, che
costituisce l’archivio di sistema del JDK e contiene tutti i file sotto
forma di bytecode. Tale archivio contiene non solo i bytecode delle classi
descritte nella API di Java, ma anche intere gerarchie di package “interni”
e non normalmente accessibili al programmatore. Essi ricoprono ruoli e
forniscono funzionalità tipicamente di più “basso livello”,
inteso come maggiore affinità con aspetti caratteristici della piattaforma
correntemente utilizzata.
|
Figura
4 Diagramma delle classi per l'implementazione di GraphicsEnvironment
nelle diverse piattaforme.
La
parte più interessante è costituita dalla chiamata al metodo
getLocalGraphicsEnvironment di GraphicsEnvironment. Questo metodo restituisce
un’istanza di una classe derivata da GraphicsEnvironment, le cui caratteristiche
e nome dipendono dal sistema operativo in cui si sta agendo. Le operazioni
effettuate sono sostanzialmente le seguenti:
-
Si chiede
il valore della “proprietà” di sistema “java.awt.graphicsenv” (tramite
System.getProperty(…) ), contenente il nome completo della classe derivata
da GraphicsEnvironment che fornisce l’implementazione per la piattaforma
corrente.
-
Tramite
il class loader, si carica la suddetta classe (che è essa stessa
un GraphicsEnvironment, essendone derivata) e se ne crea una nuova istanza
tramite Class.forName(name).newInstance().
La gerarchia
di classi con in evidenza la soluzione completa è descritta nel
diagramma UML di Figura 4.
La
classe “concreta” che fornisce tutte le primitive per operare in Win32
è sun.awt.Win32GraphicsEnvironment, mentre sotto UNIX è sun.awt.X11GraphicsEnvironment.
La
chiamata a System.getProperty(…) di cui al punto 1 restituirà il
nome della prima classe sotto Windows, e quello della seconda sotto UNIX.
L’approccio
tramite l’uso di una property di sistema è una soluzione molto elegante,
e sfrutta la possibilità esistente in Java (detta reflection) e
in altri linguaggi interpretati di creare un oggetto completo di una data
classe a run time senza conoscerne in dettaglio la specifica. Il vantaggio
è quello della massimizzazione dell’utilizzo del codice in tutte
le piattaforme, semplicemente cambiando il nome della classe “concreta”
(il cui nome è imposto dal fornitore della JVM) e fornendone il
codice “eseguibile” (ovvero il bytecode) tramite uno o più file
di tipo .class nella libreria; e tutto ciò, è importante
sottolineare, si ottiene solo con una manciata di righe di codice.
Le
due classi sopra menzionate hanno in comune una superclasse “madre”, sun.java2d.SunGraphicsEnvironment,
astratta e figlia a sua volta del GraphicsEnvironment che ben conosciamo.
La presenza di SunGraphicsEnvironment è giustificata dall’esigenza
di avere una classe “intermedia” tra il lato indipendente dalla piattaforma
(quello presentato allo sviluppatore, il quale utilizza GraphicsEnvironment)
e quello più vicino alla piattaforma, che utilizza classi diverse
e specializzate, le quali hanno nomi diversi e possiedono molti metodi
con implementazione in codice nativo (C / C++), che sarà contenuto
a sua volta nelle librerie native di Java, che sono per lo più dinamiche,
cioè shared objects sotto UNIX (.so) e DLL sotto Windows (.dll).
Le
classi con nome <Prefisso>GraphicsEnvironment stanno in rt.jar, l’archivio
delle librerie di sistema, che nella versione di Java per Windows conterrà
le classi con il prefisso Win32, mentre nella versione per UNIX (Solaris,
etc.) quelle aventi prefisso X11. Si noti che l’apposizione di un prefisso
per distinguere tipi orientati a implementazioni diverse di Java è
piuttosto comune nel JDK, e non si limita soltanto alle librerie grafiche.
Anche
GraphicsDevice e GraphicsConfiguration fanno parte di una gerarchia concettualmente
assimilabile a quella di GraphicsEnvironment, ma più semplice. Infatti,
abbiamo:
-
java.awt.GraphicsDevice,
astratta, dà origine a sun.awt.Win32GraphicsDevice e sun.awt.X11GraphicsDevice;
-
java.awt.GraphicsConfiguration,
astratta, dà origine a sun.awt.Win32GraphicsConfiguration e sun.awt.X11GraphicsConfiguration.
Conclusioni e
riferimenti
In
questo articolo sono stati esaminati i principali aspetti del supporto
al display multiplo in Java, anche se in realtà ci sarebbe ancora
parecchio da “sviscerare”, ad esempio la gestione dei font, la quale è
piuttosto articolata. Tuttavia, gli spunti forniti possono essere una buona
base di partenza per approfondimenti e sperimentazioni personali, che risulteranno
agevoli dato che l’uso delle API non presenta in realtà particolari
ostacoli. Complessivamente, quindi, la soluzione adottata dalla Sun (e
ovviamente implementata da tutti i fornitori che sviluppano le proprie
JVM secondo le specifiche) è di livello anche concettualmente elevato,
anche se non mancano i problemi: ne vogliamo segnalare due, che risultano
particolarmente “fastidiosi” sotto Unix.
Il
primo è un vero e proprio baco, il quale al momento della redazione
di questo articolo (metà novembre 2000) è in fase di risoluzione.
Se si crea una finestra o frame su un display diverso dal primo (cioè
“:0.0”), molti componenti e finestre (es. dialog) “figli” della prima vengono
invariabilmente visualizzati sul primo dispositivo fisico anziché
sullo stesso della finestra cui appartengono. Questo baco è stato
segnalato in Solaris e personalmente riscontrato dallo scrivente anche
in Compaq Tru64 Unix per Alpha, dove però il JDK 1.3 non è
ancora in versione definitiva.
Il
secondo problema è da considerarsi non un vero e proprio difetto,
ma piuttosto una limitazione derivata da una scelta progettuale. Essa consiste
nel fatto che, volendo impiegare in un’applicazione le funzionalità
offerte da GraphicsConfiguration e altre classi, è impossibile dirottare
la visualizzazione grafica di una finestra su un display remoto, cioè
di una macchina collegata via rete e distinta da quella ove il software
viene eseguito. Non solo, i GraphicsDevice fanno esclusivamente riferimento
ai dispositivi fisici della macchina, ignorando completamente i valori
della variabile di ambiente DISPLAY; in conseguenza di ciò, non
è permesso imporre a un GraphicsDevice, ad esempio, il valore “indirizzo:0.0”
, poiché la sua IDString è accessibile soltanto in lettura.
Quindi, la visualizzazione su multiplo display è efficace soltanto
se usata localmente, mentre in configurazioni client-server (ove, per altro,
le prestazioni e la velocità di refresh di programmi in Java si
degradano sensibilmente) è necessario procedere con il “vecchio
trucco” della JVM lanciata impostando la variabile di ambiente. La scelta
tecnica alla base è probabilmente giustificabile con la profonda
diversità tra il protocollo grafico client/server di Unix e il mondo
Windows, che non fornisce nulla di paragonabile. E’ chiaro che sviluppare
un’architettura software che da un lato potesse tener conto di questa grande
differenza e dall’altro fosse stata di facile comprensione e utilizzo (come
in effetti è quella adottata per il supporto al monitor multiplo),
sarebbe stato un’impresa quasi impossibile: e allora, nel nome della portabilità,
essa è stata sacrificata.
La
letteratura in questo campo è piuttosto avara, e pertanto i riferimenti
sono molto pochi. Valgono la pena di essere citati i seguenti:
[1].
Sun Microsystems, JDK 1.3 API documentation (package java.awt e java.2d)
[2].
Sun Microsystems, Java 2D API Guide, Enhanced Graphics and Imaging for
Java
La
seconda fonte è la più interessante, e contiene anche l’applicazione
citata nel paragrafo dedicato agli esempi.
Infine,
vale la pena di menzionare che i diagrammi UML sono stati realizzati con
Rational Rose 2000 Enterprise, in modalità analisi e sfruttando
il supporto alle classi di Java 2.
|