Programmer le Palm

Cet article a été publié dans le numéro 5 (janvier 2000) du magazine Team Palmtops.
Il est reproduit avec l'aimable autorisation de Posse Press.

 

Après avoir défini, le mois dernier, l'interface utilisateur de notre première application réellement interactive, soulevons aujourd'hui le capot et intéressons-nous au code, dont les sources sont disponibles sur le CD.

Notre objectif consiste à doter notre application du code qui lui permet d'être notifiée des actions de l'utilisateur (ou plus généralement de l'environnement), et bien sûr d'y réagir. Dans la mesure où ils ne relèvent pas spécifiquement de la programmation sous PalmOS, nous ne nous focaliserons pas sur les traitements proprement dits, par exemple le calcul de statistiques sur les réponses au sondage.

Notre application comporte quatre formulaires : les deux pages du questionnaire idFormPage1 et idFormPage2, la fiche récapitulative idFormFiche et la boîte informative idFormAPropos. Elle contient par ailleurs un menu, identifié par idMenuGeneral.

En termes de cinématique (voir figure 1), l'utilisateur remplit successivement les deux pages du questionnaire. S'il valide la réponse, l'application affiche une synthèse de sa réponse avant de revenir à la première page. Si par contre l'utilisateur annule la réponse, l'application retourne directement à la première page.

Le menu associé aux pages du questionnaire contient deux sous-menus "Edition" et "Options", qui comportent un certain nombre d'items (copier, coller, etc.), que nous n'implémenterons pas réellement : nous les acquitterons par l'intermédiaire de la boîte d'alerte idAlertTrace, dont la fonction se résume à afficher le choix effectué. Seul le choix "Options / A propos..." sera traité, qui déclenche l'affichage de la boîte informative idFormAPropos.

Figure 1 - Cinématique de notre application.

Page 1 du questionnaire

Bonne nouvelle : nous n'avons pratiquement rien à faire ! Dans le numéro précédent, nous avons vu que le gestionnaire d'événements d'un formulaire renvoie un booléen qui indique si l'événement a été traité ou non. Lorsque ce booléen est à faux, ce qui signifie que l'événement n'a pas été traité, PalmOS applique un traitement par défaut, qui correspond au comportement standard.

En l'occurence, notre liste, nos boutons radio et nos cases à cocher sont utilisés de manière standard, moyennant quoi nous n'avons pas de traitement particulier à implémenter pour les gérer. Seuls deux éléments ne sont pas utilisés de façon standard : le bouton "Suivant" et le menu. En effet, ils sont inopérants par défaut, ce qui n'est évidemment pas notre but. Sur pression du bouton "Suivant", nous souhaitons mémoriser les informations saisies par l'utilisateur, c'est-à-dire la sélection dans la liste, ainsi que l'état des boutons et des cases à cocher, dans une structure FicheSondage :

typedef struct {
	Int plateforme;
	Boolean palmtop;
	Boolean litMag[NB_MAGS];
	Int sexe;
	Char coords[MAX_COORDS+1];
	Int trancheAge;
	DateType achat;
} FicheSondage;

L'événement à intercepter est ctlSelectEvent (événement de sélection d'un contrôle), qui véhicule l'identifiant de l'élément d'interface concerné. Dans le cas qui nous intéresse, il s'agit du bouton "Suivant", identifié par idPage1BtnSuivant. Le gestionnaire d'événements de la page 1 se présentera comme indiqué dans le listing 1, dans lequel n'ont été conservées que les lignes significatives.

static Boolean Page1EventHandler(EventPtr e)
{
	...
	// quel est le type d'événement ?
	switch (e->eType) {
		...
		// sélection d'un contrôle
		case ctlSelectEvent :

			// quel est le contrôle sélectionné ?
			id = e->data.ctlSelect.controlID;
			switch (id) {
				...
				// bouton "Suivant"
				case idPageBtnSuivant :
					// traitement correspondant
					...
					break;
				...
			}
		}
		break;
		...
	}
	...
}

Listing 1 - Gestion du bouton "suivant".

Pour récupérer l'état des différents éléments d'interface constituant la page 1, nous nous appuyons sur les API fournies par PalmOS (voir listing 2) : LstGetSelection() permet d'obtenir le numéro d'ordre de la valeur sélectionnée dans une liste, tandis que CtlGetValue() retourne la valeur d'un contrôle. Enfin, nous utilisons FrmGotoForm() pour passer à la page 2.

// on enregistre les données dans la fiche
fiche.plateforme = LstGetSelection(GetObjectPtr(idPage1LstPlateforme));
fiche.palmtop = CtlGetValue(GetObjectPtr(idPage1PbtPalmtopOui));

for (i = 0; i < NB_MAGS; i++) {
	fiche.litMag[i] = CtlGetValue(GetObjectPtr(idPage1ChkTeamPT + i));
}

fiche.sexe = CtlGetValue(GetObjectPtr(idPage1ChkMasculin));

// et on passe à la page 2
FrmGotoForm(idFormPage2);

Listing 2 - Traitement du bouton "suivant".

La gestion du menu est très similaire. L'événement à intercepter est menuEvent, qui transporte l'identifiant de l'item sélectionné par l'utilisateur. Pour l'intercepter, nous ajoutons un "case menuEvent" dans le "switch (e->eType)". Le reste du traitement est délégué à une fonction MenuAction(), également invoquée dans les mêmes circonstances par le gestionnaire d'événéments de la page 2.

Dans cette fonction, présentée dans le listing 3, nous ventilons les événements en fonction de l'item du menu qui a été sélectionné. Si l'utilisateur a choisi "Options / A Propos...", nous affichons la boîte d'information idFormAPropos en s'appuyant sur l'API FrmDoDialog(), très commode car elle prend tout en charge et rend la main lorsque l'utilisateur a actionné le bouton "OK". Si l'utilisateur a effectué un autre choix, nous tirons parti de la macro Trace(), décrite dans l'encadré 1, pour signaler que l'action a bien été détectée.

// on enregistre les données dans la fiche

static Int MenuAction(Word id)
{
	Int handled = true;

	switch (id) {
		
		case idGenMenEditCopier :
			Trace("Edition/Copier");
			break;
		case idGenMenEditCouper :
			Trace("Edition/Couper");
			break;
		...		
		case idGenMenOptionsAPropos :
			// Affichage du formulaire "à propos"
			FrmDoDialog(FrmInitForm(idFormAPropos));
			break;
		
		default :
			handled = false;
	}
	return handled;
}

Listing 3 - La fonction MenuAction().

 

static void AppTrace(Char *a, Char *b, Long n)
{
	Char buf[12];
	
	if (b == 0) b = "";
	FrmCustomAlert(idAlertTrace, a, b, StrIToA(buf, n));
}

#define Trace(msg) AppTrace(msg, __FILE__, __LINE__)
#define TraceNum(name, val) AppTrace(name, 0, val)
#define TraceStr(name, val) AppTrace(name, val, 0)

La fonction AppTrace() admet trois arguments. Les deux premiers sont des pointeurs sur des chaînes de caractères, le troisième est un entier long. Cette fonction affiche la boîte d'alerte idAlertTrace, dont le message est "A=^1\nB=^2\nN=^3". Derrière cette formule cabalistique se cache une fonctionnalité bien utile offerte par l'API FrmCustomAlert() : en effet, les marqueurs ^1, ^2 et ^3 sont automatiquement remplacés par les chaînes de caractères transmises, un peu à la manière d'un printf(). Il devient ainsi très facile d'afficher des messages d'alerte dont le contenu est variable.

Encadré 1 - Les macros Trace(), TraceNum() et TraceStr().

Pour terminer, il nous reste à préciser les traitements effectués à l'ouverture de la page 1 (événement frmOpenEvent) : nous affichons le formulaire grâce à l'API FrmDrawForm(), puis nous initialisons de la structure FicheSondage en utilisant un MemSet() (attention aux majuscules, il ne s'agit pas du memset() classique du C !) et nous présélectionnons le bouton radio "Oui" à l'aide de l'API CtlSetValue(), car il n'existe pas de clause PilRC qui nous aurait permis de fixer cette valeur par défaut, lors de la définition de l'interface utilisateur.

Page 2 du questionnaire

La seconde page de notre questionnaire n'est guère plus complexe à gérer que la première. En particulier, l'initialisation ne requiert pas de traitement particulier et nous nous bornons à afficher le formulaire à l'aide de FrmDrawForm(). En revanche, nous devons gérer spécifiquement trois contrôles, pour déclencher le sélecteur de date, valider le questionnaire ou, au contraire, l'annuler. En conséquence, le "switch (id)" du gestionnaire d'événements de la page 2 comporte trois clauses "case", pour les contrôles idPage2StrDate, idPage2BtnOK et idPage2BtnCancel, respectivement.

Le traitement associé au bouton d'annulation idPage2BtnCancel n'appelle aucun commentaire particulier : nous utilisons FrmGotoForm() pour passer à la page 1. Le traitement de validation, déclenché par le bouton idPage2BtnOK, ressemble beaucoup à son homologue pour la page 1. Simplement, pour récupérer le contenu du champ de saisie, nous invoquons l'API FldGetTextPtr(), qui renvoie un pointeur sur le texte saisi, s'il existe (attention au pointeur nul en retour !). Pour le reste, rien de spécial.

La partie du code qui prend en charge la gestion du sélecteur de date (listing 4) mérite un examen plus attentif. Le sélecteur est géré par l'API SelectDay(), qui requiert cinq arguments : un titre, le jour, le mois et l'année (ces trois arguments sont à la fois en entrée et en sortie) et le type de sélection (par semaine ou par jour). Pour préparer ces arguments, nous commençons par récupérer la date courante dans une structure DateType (voir note 1), en transmettant la valeur de l'horloge temps réel renvoyée par TimGetSeconds() à l'API DateSecondsToDate(). Nous transférons ensuite la valeur du jour, du mois et de l'année dans trois variables. Il nous reste à récupérer le titre du sélecteur, défini par la ressource idStrSelectDayTitre (voir note 2). Pour ce faire, nous faisons appel à DmGetResource(), MemHandleLock() et plus loin à MemPtrUnlock(), que nous ne détaillerons pas dans ce volet. SelectDay() renvoie une valeur non-nulle si l'utilisateur a sélectionné une date. Dans ce cas, nous mémorisons cette date dans la structure FicheSondage, puis nous mettons à jour le déclencheur de sélection pour qu'il affiche la date choisie : CtlGetLabel() nous fournit un pointeur sur le libellé, DateToDOWDMFormat() nous permet d'y stocker la date au format souhaité (sachant que le libellé obtenu sera plus court que le libellé initial) et CtlSetLabel() effectue la mise-à-jour proprement dite.

// on initialise avec la date du jour
DateSecondsToDate(TimGetSeconds(), &date);
jj = date.day;
mm = date.month;
aa = date.year + firstYear;  // attention à l'offset !

// le titre du sélecteur est dans les ressources
// pour y accéder il faut verrouiller la ressource
titre = MemHandleLock(DmGetResource(strRsc, idStrSelectDayTitre));

// on peut maintenant appeler le sélecteur
if (SelectDay(selectDayByDay, &mm, &jj, &aa, titre)) {

	// l'utilisateur a choisi une date, on la mémorise
	fiche.achat.day = jj;
	fiche.achat.month = mm;
	fiche.achat.year = aa - firstYear;  // toujours l'offset !

	// il faut aussi mettre à jour le sélecteur
	dateText = CtlGetLabel(GetObjectPtr(idPage2StrDate));
	DateToDOWDMFormat(mm, jj, aa, dfDMYWithSlashes, dateText);
	CtlSetLabel(GetObjectPtr(idPage2StrDate), dateText);
}

// ne pas oublier de libérer la mémoire
MemPtrUnlock(titre);

Listing 4 - Gestion du sélecteur de date.

Note 1 : Pour des raisons de compacité, le champ year d'une structure DateType fait l'objet d'un décalage de 1904 ans, représenté par la constante firstYear : si l'année est 1999, date.year vaut 1999 - firstYear = 95. Faute de tenir compte de cette particularité, on a tôt fait d'obtenir des résultats aberrants !

Note 2 : Au lieu de définir titres, libellés et autres données textuelles directement dans le code de l'application, il est nettement préférable de les définir en tant que ressources. Cette précaution facilite grandement les évolutions ultérieures, et notamment la "localisation", c'est-à-dire la déclinaison de l'application dans une autre langue.

Fiche de synthèse

Après validation du questionnaire, l'application affiche une synthèse des informations saisies, par l'intermédiaire de la fiche idFormFiche, qui ne contient qu'un bouton "OK", dont le traitement se résume à passer la main à la première page du questionnaire. Tout le reste est fait à l'ouverture de la fiche, sur réception de l'événement frmOpenEvent. Après avoir dessiné la fiche avec un FrmDrawForm(), nous invoquons la fonction AfficherFiche(), dont la mission est ... d'afficher le contenu de la fiche. Pour faciliter cette tâche, nous avons défini deux fonctions PrintNum() et PrintText(). La première permet d'afficher un nombre à l'abscisse et l'ordonnée souhaitées. La seconde permet d'afficher une chaîne de caractères sur la ligne d'ordonnée souhaitée et gère par ailleurs un pliage de la ligne si le texte est trop long. Le source d'AfficherFiche() est donné dans le listing 5.

static void AfficherFiche(void)
{
	Char tmp[80];
	Int i;
	
	PrintNum(10, 10, fiche.plateforme);
	PrintNum(10, 20, fiche.palmtop);
	for (i = 0; i < NB_MAGS; i++)
		PrintNum(10 + i * 20, 30, fiche.litMag[i]);
	PrintNum(10, 40, fiche.sexe);
	PrintText(50, fiche.coords);
	PrintNum(10, 110, fiche.trancheAge);
	PrintNum(10, 120, fiche.achat.day);
	PrintNum(30, 120, fiche.achat.month);
	PrintNum(50, 120, fiche.achat.year + firstYear);
}

Listing 5 - Affichage de la fiche de synthèse.

Conclusion

Nous savons désormais définir l'interface utilisateur d'une application, ainsi que le code associé. Le mois prochain, nous reviendrons sur un plan plus théorique en abordant la gestion de la mémoire, des ressources et des bases de données. Vos questions et commentaires sont bienvenus : n'hésitez pas à les transmettre par courrier électronique à l'adresse palmprog@ablivio.com.

Denis Faivre - palmprog@ablivio.com.

Copyright © 1999-2000 - Denis Faivre et Posse Press.
Tous droits réservés.