[Per chi suona la campana] [About] [Copertina] [Editoriale]

Articoli



Qualcosa su... programmazione in X-windows.

Il contenuto di questo articolo sara` basato unicamente su una mia esperienza personale. Non sono un guru dell'argomento, e le librerie che ho usato non sono certo l'unico modo per programmare sotto X. Ho usato le librerie xview, i cui file .h potete trovare in /usr/openwin/include/xview e gli esempi in /usr/src/xview/xview. Lo scopo e` quello di far vedere come sia relativamente facile, una volta compreso il meccanismo, dotare di una interfaccia X i propri beneamati programmi incentivandone cosi` l'aspetto user-friendly. Si intende offrire una ``chiave di lettura'' dei sorgenti, un metodo che permetta a chi non si e' mai avvicinato alla programmazione in ambiente grafico di comprendere gli esempi e di trarne vantaggio. E' utile avere una buona conoscenza del C e del C++.

Programmazione ad oggetti
Un oggetto informatico e` in genere un'entita` che non si limita alla sola conservazione dei propri dati, ma che e` in grado di effettuare operazioni su di essi (quindi su se stesso, se la relazione di uguaglianza tra due oggetti dello stesso tipo va fatta basandosi sui rispettivi dati). Gli oggetti come quelli che tratteremo sono caratterizzati da una ``intelligenza'' medio-piccola; riserviamo lo status di oggetti molto ``intelligenti'' ai tipi di dati astratti. Per fare un esempio, un oggetto tipico e` il rettangolo a video: i suoi dati (proprieta`) saranno posizione, altezza, larghezza e le sue funzioni membro (metodi) saranno Show() e Hide() ovvero esso sara` capace di mostrarsi e di nascondersi. Diamo per scontata nel lettore la conoscenza di derivazione ed ereditarieta`. Derivando un oggetto, si puo` istanziarne un'altro che ne aumenta le capacita` (proprieta` e metodi). Quindi il nuovo oggetto fa tutto quello che faceva il precedente (possiamo anche usarlo come fosse un oggetto del tipo dal quale e` derivato) e qualcosa in piu`. I sistemi grafici funzionano proprio cosi`: si costruiscono oggetti via via piu` complessi derivandone altri. Per esempio, esiste sempre un oggetto che si limita a disegnare un rettangolo sullo schermo: da questo si puo` derivare una label, ovvero un rettangolo con un testo all'interno. Sempre dal rettangolo si puo` derivare una textbox, e da quest'ultima si puo` derivare una textbox particolare, per esempio per l'input di passwords o per input formattato. Dal rettangolo ancora si puo` derivare un bottone, un bottone con testo, un bottone con immagine... e cosi` via. Poiche' ogni oggetto possiede le proprieta` e i metodi di quelli da cui e` derivato, tutti gli oggetti del nostro esempio avranno i metodi Show e Hide, che erano stati introdotti con il rettangolo. La casella di testo introdurra` una proprieta` (dato) text dove sara` possibile scrivere e leggere il testo, e la textbox particolare avra` anch'essa queste due caratteristiche. Ogni sistema grafico mette a disposizione tutta una serie di oggetti di sistema: label, textbox, pannelli, liste: occorre solo assemblarle e usarle. Occorre prestare attenzione alla regola ``E` UN'' e ``HA UN'' per evitare di derivare quando invece sarebbe stato corretto aggiungere alle proprieta`. Esempio: una finestra con tre bottoni sopra e` un oggetto derivato da una finestra che tra i suoi dati (proprieta`) contiene anche tre oggetti bottone. Chi usa gli oggetti? Il sistema grafico. Ovvero il sistema grafico converte le azioni del mouse e della tastiera in attivazione delle proprieta` degli oggetti: e` il sistema grafico che chiama la Repaint() di un oggetto quando e` ora di ridisegnarlo, o che attiva la Press() di un bottone quando e` stato premuto (per inciso in questo caso la Press sa anche come disegnare il bottone nella stato ``down'', diverso dallo stato ``up''). Il sistema grafico genera EVENTI che si tramutano nell'esecuzione di X.Y() dove X e' un generico oggetto ed Y() una sua funzione membro. Tutti gli oggetti di sistema hanno delle funzioni membro gia` definite (per esempio Show(), Hide(), Repaint()) ma nulla vieta che possano essere ridefinite. Inoltre hanno delle funzioni segnaposto (virtuali) che devono essere riempite dall'utente (Press() di un bottone).
Pensare ad oggetti - gli oggetti in C
E` necessario il C++ per un sistema grafico ? No, tanto che sono fatti tutt'ora in C, e le librerie C++ che si trovano non fanno altro che incapsulare le API in C dentro classi. Infatti, pensiamo alla nostra classe rettangolo : contiene 4 proprieta` (left, top, width, height) e 2 metodi (Show,Hide). Puo` essere inplementata in C con una
	struct rect 
		{
		int left,top,width,height;
		};
e con due funzioni Show(rect*) e Hide(rect*) che accettano una struttura rect come parametro, e vi compiono delle operazioni. Ecco il significato di ``pensare ad oggetti'' : i sistemi grafici anche se scritti in C con API in C sono di fatto ad oggetti, perche' pensati tali quando ancora non erano disponibili linguaggi object-oriented come il C++. E anche oggi i sistemi grafici appena sfornati prediligono le API in C per una questione di overhead. Da questo momento in poi penseremo in C++, e scriveremo in C. Facciamo ancora un esempio di equivalenza: in C++
	class rect
		{
		public:
		Show();
		Hide();
		protected:
		int left,top,width,height;
		};
	main()
		{
		rect R;
		R.left=R.top=100;
		R.Show()
		}
in C
	struct rect 
		{
		int left,top,width,height;
		};
	Show(rect &r);
	Hide(rect &r);
	main()
		{
		rect R;
		R.left=R.top=100;
		Show(R);
		}

Programmazione ad eventi
Un normale programma interattivo a linea di comando conduce l'utente lungo una strada ``guidata'', dove esso deve prendere delle decisioni ogni tanto, fino alla conclusione (avete presente i quiz da giornale scandalistico per scoprire se siete uomini o caporali ?). Un programma ``event driven'' presenta sempre all'utente tutti (o quasi tutti) i comandi che possono essere impartiti e se trattasi di programma con interfaccia grafica reagisce di fronte ai comandi ``impliciti''. Quali sono i comandi impliciti? Ad esempio scorrere una lista, spostarsi lungo una scrollbar, aprire un combo box (questi in realta` sono cattivi esempi: si ricordi come questi oggetti di sistema sanno reagire da soli a certi stimoli). Le sequenze possibili di eventi in ingresso (se viene considerata proprieta` della sequenza anche la sua lunghezza) sono infinite e se pensate che le strade che il programma puo` prendere sono influenzate anche dai dati datigli in input (digitazione da tastiera, input da files...) testarne la solidita` e` molto problematico. Quella che analizzeremo ora e` la tecnica che comunemente si usa in fase di progettazione di un programma event-driven. Si applica un modello a macchina sequenziale con memoria. Tutta la ``storia'' dell'esecuzione del programma e` contenuta nelle variabili di stato interne allo stesso. Queste variabili sono quelle a visibilita` globale (se vogliamo essere pignoli, anche le static all'interno di funzioni). Lo spazio di stato e` costituito da tutte le possibili combinazioni AMMISSIBILI di queste variabili. Per ammissibili si intende quelle che hanno un senso per il programma. Fanno parte delle variabili di stato anche i dati su cui lavora il programma: Anche lo spazio di stato dei dati e` quello dei valori che hanno un senso per il programma. Ogni qualvolta che l'utente effettua un'azione, applica una funzione e fa transitare (mappa) il programma da uno stato ad un'altro, dove rimane in attesa di un nuovo evento. Ogni funzione dovrebbe avere come dominio lo tutto lo spazio delle v.d.s (non fatemi scrivere variabili di stato ogni volta) il che significa che se A e` un vettore appartenente allo spazio, f(A) non manda in crash il programma qualunque f e qualunque A. Il codominio di f() deve essere contenuto nello spazio delle v.d.s (ovvero la funzione non deve mappare il programma in uno stato senza senso) qualunque f(). Il verificarsi di queste due ipotesi fa si' che possiamo affermare che il programma non andra` in crash a fronte di una qualsiasi sequenza di ingresso. Ma se e` cosi` semplice, perche' i programmi talvolta vanno in crash? Perche' le variabili di stato aumentano rapidamente, perche' non si fa o si fa male un'adeguata progettazione (si comincia a scrivere un programma spegnendo il PC e prendendo carta e penna) e perche' e` obiettivamente difficile soddisfare i due requisiti per quanto riguarda i dati del programma (si pensi ad un programma che lavori con matrici, ovvero con molte operazioni su numeri: una division by zero e` sempre in agguato). Il problema non si puo` evitare, ma si puo` semplificare un po'. Si tratta di suddividere lo spazio delle v.d.s in piu` sottospazi corrispondenti a diverse situazioni MAI concomitanti. Si pensi a un programma che lavora su dei files: certe funzioni come apri,crea, etc sono disponibili solo se non c'e` un file correntemente aperto (in verita` non e` vero, ma stabiliamo cosi`) mentre copia, incolla, inserisci immagine etc... sono disponibili solo con il file aperto. Una prima soluzione e` quella di introdurre la variabile di stato IsOpen che soddisfa alla bisogna. Tutte le f() in questione dovrebbero quindi controllare questa variabile. Suddividiamo invece lo spazio delle v.d.s in due spazi: uno per il file aperto, l'altro per il file chiuso. Copia(), incolla() avranno come dominio SOLO le v.d.s che generano il primo spazio e il loro codominio sara` quest'ultimo. Ogni funzione quindi opera su di uno spazio ``ristretto'', senza dover controllare cosa succede alle v.d.s. che non ne fanno parte. Ora entrano in gioco le funzioni di transizione di spazio, per esempio la apri() e crea() sono funzioni che hanno come dominio uno spazio, e come codominio un altro. Queste funzioni di transizione sono quelle che si occupano di rendere inacessibili durante la transizione le f() dello spazio che lasciano e accessibili quelle dello spazio di destinazione. Ci sono poi funzioni che hanno come dominio l'unione di uno o piu` spazi (per esempio la resize() che viene chiamata quando si ridimensiona la finestra) e che necessitano comunque della variabile di stato per capire come operare. Il metodo di suddivisione e` comunque valido come scomposizione di un problema in sottoproblemi.
Le API delle xview.
Le API delle xview sono in C, come avevamo anticipato. Sono implementate tuttavia in un modo particolare. Occorre quindi fare un ulteriore sforzo per comprendere l'ultima equivalenza. Ricordiamo la precedente: abbiamo visto come si puo` concettualmente trasformare una classe C++ in C, lasciando i dati dentro la struct e introducendo un parametro in piu` sulle funzioni membro. Ricordiamo che i dati sono le proprieta` di un oggetto, e le funzioni membro i metodi applicabili a quell'oggetto. In queste librerie, esiste il tipo che contiene le proprieta` dell'oggetto e che in genere porta il nome dell'oggetto stesso, come
	Frame mia_finestra;
	Panel mio_pannello:
	Textsw mia_textedit;
ma non si accede piu` alle sue proprieta` come
	mia_finestra.height=300;
	mia_textedit.top=0;
bensi` attraverso la funzione tuttofare xv_set(oggetto,...) dove oggetto sta per l'oggetto di cui vogliamo settare le proprieta` e i parametri aggiuntivi sono le proprieta` da settare con i valori. Inoltre, il tipo di dato Panel, Frame etc non e` la struct che contiene i dati ma un puntatore ad essa. Poiche' e` un puntatore,scrivere Panel pannello; non crea una struct contenente i dati per l'oggetto Panel. Occorre creare dinamicamente un oggetto siffatto e far puntare pannello ad esso. La funzione che effettua questo e` la xv_create (oggetto, tipo oggetto, ...) che tratteremo piu` avanti. La xv_create funge anche da inizializzatore dell' oggetto (quello che era il costruttore nella classe C++). Chiariamo con un esempio. Anziche' per il nostro ipotetico oggetto rect scrivere
	rect R;
	R.top=0;
	R.left=10;
scriveremo
	rect R;
	R=xv_create(...);
	xv_set(R,XV_TOP,0,XV_LEFT,10,NULL);
Notiamo innanzitutto che la xv_set e` una funzione che accetta un numero variabile di parametri: per indicare quando questa lista e' finita si usa NULL (oppure 0). Il primo parametro e` sempre l'oggetto (l'handle dell'oggetto) e i successivi sono coppie COSTANTE che indica la proprieta` da settare ed il suo VALORE. N.B. XV_TOP e XV_LEFT non esistono: in realta` sono XV_X e XV_Y. Per leggere il valore di una proprieta` si usa la xv_get(oggetto,...) allo stesso modo. Ad esempio
	printf("coordinata x : %d\n",R.left);
diventa
	printf("coordinata x : %d\n",(int)xv_get(R,XV_LEFT));
notate come non sia piu` necessario indicare la fine della lista di proprieta` poiche' se ne ottiene una alla volta come valore di ritorno. E` bene forzare il tipo di valore di ritorno con un cast per evitare antiestetici warning in compilazione. Questo e` quanto per le proprieta`. E per i metodi ? Come si fa a rilevare che il mouse passa sopra l'oggetto ? Come si fa a rilevare che e` stato selezionato un item di un oggetto lista ? E a installare una propria funzione che venga attivata all'evento Press di un bottone ? Per gli eventi che un oggetto puo` ricevere occore installare delle nostre funzioni in modo che vengano attivate al generarsi di quell'evento. Queste funzioni vengono chiamate anche callback oppure hook (uncino) perche' si ``agganciano'' ad un oggetto. Esse devono avere una lista di parametri ben definita. In genere occorre beccarne una in un esempio per imparare ad usarla. Comunque si notifica al sistema grafico che noi vogliamo usare cioe` installare una certa funzione per un certo evento sempre con la xv_set. Per esempio, supponiamo di avere un oggetto Frame (una finestra vuota) e di voler vedere sulla shell da cui abbiamo lanciato il programma un output quando la finestra subisce un resize.
	Frame f;

	void mia_funzione(Xv_Window window, Event * event)  
		{
		switch (event_action(event))  
			{
			case WIN_RESIZE: 
			printf("width %d height %d \n",(int)xv_get(f,XV_WIDTH,(int)xv_get(f,XV_HEIGHT));
			}
		}

	main()
		{
		f=xv_create(..);		
		xv_set (f, 
				WIN_EVENT_PROC,
					mia_funzione, 
					WIN_CONSUME_EVENTS,
						WIN_RESIZE,
				 		NULL, 	
				NULL);
		xv_main_loop (f);		
		}
Ci sono varie cose da commentare: la prima e` che xv-set,xv_get hanno si' un ruolo predominante ma c'e` dell'altro (event_action ? e poi cosa ci sara` ancora di nuovo ?). Ribadisco che lo scopo di questo articolo e` quello di fornire una chiave di lettura dei sorgenti xview in modo poi che si possa imparare da essi: una trattazione esauriente non e` possibile. La seconda e` la possibilita` di inserire liste di parametri nella lista di parametri di xv_set. Niente di strano: anche queste liste innestate vanno terminate da un NULL. Nel nostro caso, questa lista innestata contiene un elemento (WIN_RESIZE), e il suo inizio e` determinato da WIN_CONSUME_EVENTS che indica quali eventi dovranno essere passati alla mia_funzione. Notate che la lista di eventi da consumare fa parte degli elementi obbligati da passare quando si vuole settare WIN_EVENT_PROC. Infatti gli argomenti di WIN_EVENT_PROC sono il puntatore alla funzione in questione (mia_funzione) e la lista degli elementi da consumare. Una scrittura indentata e` consigliabile per aumentare la leggibilita` di questo codice. In genere le funzioni di notifica di eventi (esempio: per un oggetto lista, la funzione che viene chiamata quando l'utente seleziona l'item i-esimo) si installano con WIN_NOTIFY_PROC. Ho volutamente tralasciato fino ad ora un'altra funzione importante e di carattere generale : la xv-create(oggetto,...). Essa permette di aggiungere dinamicamente oggetti SU, SOPRA, IN un oggetto esistente. Tramite questa si aggiungono pulsanti, menu, liste al frame principale (si popola la finestra). Il primo parametro di xv_create e` l'oggetto sopra il quale si vuole creare un altro oggetto, oppure XV_NULL se si vuole creare un oggetto nuovo, sganciato da qualsiasi altro. Il secondo e` una costante che indica il tipo di oggetto da creare. Si noti che l'operazione di creazione di un oggetto su di uno esistente NON E` la corrispondente della derivazione in C++, ma l'aggiunta tra i dati dell'oggetto di tipo A di un oggetto di tipo B: soddisfa alla domanda ``HA UN'' , non alla ``E` UN''. Anche il puntatore creato da xv_create deve subire un cast prima di essere assegnato. Il frame (finestra) principale di un programma e` creato con
	Frame f;
	main()
		{
	 	f = (Frame) xv_create (	XV_NULL, FRAME, 
							NULL);
		xv_main_loop (f); 
		}
Notiamo la costante NULL che segnala la fine dei parametri. Perche' ? I parametri non sono automaticamente finiti dopo aver indicato il padre ed il tipo dell'oggetto ? No : la xv_create puo` essere usata anche come la xv_set. Cioe` e` possibile unire una chiamata a xv_create e una a xv_set in una sola (a xv_create). Supponiamo che dopo aver creato il frame si voglia settare la sua larghezza a 200: invece di
	Frame f;
	main()
		{
	 	f = (Frame) xv_create (	XV_NULL, FRAME, 
							NULL); 
		xv_set(f,XV_WIDTH,200,NULL);
		xv_main_loop (f);	
		}
si scrive in modo piu` compatto
	Frame f;
	main()
		{
	 	f = (Frame) xv_create (	XV_NULL, FRAME,
	 						XV_WIDTH,200, 
							NULL); 
		xv_main_loop (f);	
		}
Questo e` utile perche' in genere si crea un oggetto e se ne settano molte proprieta` subito dopo. Rimarrebbe ora da trattare la xv_destroy, per distruggere un oggetto. Tuttavia negli esempi si usa raramente la xv_destroy, segno che tutti gli oggetti grafici allocati da un processo vengono liberati alla fine del processo stesso. Questo era anche ovvio se si pensa che un sistema U**X rimane in genere acceso 24 ore su 24, e a lungo andare la non disallocazione di oggetti potrebbe creare problemi. Quindi, se nei sorgenti della SUN si fa cosi`, possiamo senza dubbio farlo anche noi.
Conclusioni
Usare queste librerie e` facile se si riesce a mettere gli occhi su qualche esempio che fa quello che ci interessa, o a dedurre per similitudine e assonanza i nomi delle proprieta` da usare curiosando tra gli header della libreria. La documentazione e` scarsissima: niente man pages, niente info, pochissimi commenti nelle librerie. Questa e` secondo me l'unica nota dolente. I vantaggi sono dati dall'aspetto ``in linea'' con l'interfaccia di sistema, la portabilita` assicurata (dove gira il textedit gireranno anche questi programmi) e la dimensione ridottissima degli eseguibili. Pensate: un programma con menu, toolbar con bitmap colorati, liste, finestre di editing e una console in meno di 80Kbyte ! Qualsiasi programma analogo con interfaccia Motif avrebbe occupato almeno 1Mbyte. E questo e` tutto. Ovviamente ci sarebbero molte altre cose da dire, ma questo e` quello che piu` o meno so sull'argomento. Forse ci sara` un seguito a questo articolo con commento di alcuni dei sorgenti piu` significativi degli esempi. Per una trattazione estesa, procuratevi Xview Programming Manual e Xview Reference Manual. NON sono pubblicati in Internet. A quanto ne so, si possono ordinare alla O'Reilly. Il mio indirizzo di posta elettronica a cui scrivere per commenti oppure esprimere il proprio sdegno e` edika@dei.unipd.it

di Pironato Massimo


[Uki e Debian] [About] [Copertina] [Editoriale]