MokaByte 47 - Dicembre 2000

di
Emmanuele Sordini
Applicativi su Monitor
multipli con Java2
Vai alla prima pagina di questo mese
L’uscita delle prime versioni di Windows nei primi anni ‘90 ha consentito anche alla piattaforma Intel (e compatibili) di “vestirsi” di un’interfaccia grafica (GUI, o Graphical User Interface) che costituisse il contesto normale delle operazioni dell’utente, liberandolo in tal modo dal giogo della vecchia e ormai poco pratica riga di comando del DOS. Sebbene molti altri sistemi (ad esempio Amiga, Unix, Macintosh) possedessero già da almeno quattro - cinque anni una GUI anche piuttosto evoluta, l’introduzione di questa nel mondo dei PC permise, con l’enorme base d'installato a livello mondiale, di rendere l’uso della grafica popolare in tutti i segmenti di mercato e presso tutti gli utilizzatori.

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:

  1. 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. 
  2. 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:

  1. 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.
  2. 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.
 

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java® è un marchio registrato da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
mokainfo@mokabyte.it