Génération de rapports BIRT à l'aide de la 'BIRT Design Engine API'

Ce tutoriel, pour débutant, a pour objectif de vous présenter une méthode pour générer dynamiquement des rapports BIRT.

16 commentaires Donner une note à l'article (4.5)

Article lu   fois.

Les deux auteurs

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

BIRT (The Business Intelligence and Reporting Tools) est un projet qui fournit deux outils majeurs :
- le moteur de visualisation de rapports. Il repose sur la REAPI : Report Engine API.
- le moteur de conception de rapports. Il repose sur la DEAPI : Design Engine API.
L'outil de conception graphique des rapports BIRT (présent dans Eclipse) ne suffit pas toujours lorsque le développeur doit implémenter BIRT dans son application web : en effet il arrive qu'il soit nécessaire de générer des rapports dynamiquement. La DEAPI de BIRT répond à ce besoin.

II. Objectifs du présent tutoriel

Ce tutoriel a pour objectif de présenter une façon de réaliser des rapports BIRT en utilisant la Design Engine API (API de conception des rapports BIRT).
Pour cela on se place dans un contexte assez simple :
- on dispose d'une base de données contenant un certain nombre d'éléments à visualiser : une liste de produits.
- on souhaite visualiser ces données dans un rapport en fonction du type de produit (alimentaire ou mobilier).

On va donc créer un rapport BIRT qui permet d'effectuer cette visualisation. L'utilisateur qui aura suivi ce tutoriel aura ensuite les bases nécessaires pour générer des rapports dynamiquement.

III. Près-requis

III-A. Java

Il est nécessaire d'avoir au minimum la version java 1.5.x.

Téléchargeable depuis le site de Sun : http://java.sun.com/javase/downloads/index_jdk5.jsphttp://java.sun.com/javase/downloads/index_jdk5.jsp

III-B. Eclipse

La version d'Eclipse utilisée dans ce tutoriel est Eclipse Ganimède 3.4.2.

Téléchargeable depuis le site d'Eclipse-BIRT : http://download.eclipse.org/birt/downloads/http://download.eclipse.org/birt/downloads/

III-C. PostgreSql

La version de PostgreSql utilisée dans ce tutoriel est PostgreSql 8.2.

Téléchargeable depuis le site de PostgreSql : http://www.postgresql.org/download/http://www.postgresql.org/download/

III-D. BIRT Runtime

La version du BIRT Runtime utilisé dans ce tutoriel est BIRT Runtime 2.3.2.2.

Téléchargeable depuis le site d'Eclipse-BIRT : http://download.eclipse.org/birt/downloads/http://download.eclipse.org/birt/downloads/

IV. Préparation de l'environnement

IV-A. Préparation de l'espace de travail Eclipse

Dans l'espace de travail courant, créer un nouveau projet java.
Ensuite :
- Créer un dossier nommé 'runtime'. Dans ce dossier copier le contenu du répertoire 'birt-runtime-2_3_2' issu de l'archive 'birt-runtime-2_3_2.zip'.
- Créer un dossier nommé 'reports'. Ce répertoire accueillera les futurs fichiers .rptdesign.
- Ajouter ensuite au Build Path l'ensemble des librairies contenues dans le répertoire 'runtime/ReportEngine/lib'.
L'espace de travail est maintenant près pour la génération de rapports BIRT à partir de la DEAPI. Ci-dessous une copie d'écran de ce à quoi doit ressembler l'espace de travail.

Espace de travail
Espace de travail


A propos du contenu du répertoire 'runtime'
Ce répertoire est constitué par :
- le répertoire 'about_files' : contient le fichier de licence Apache
- le répertoire WebViewerExample : arborescence de l'application web permettant de visualiser des rapports BIRT
- les fichier about.html et notice.html : fichiers décrivant le cadre juridique pour l'utilisation de BIRT
- le fichier readme.txt : fichier donnant une description générale sur BIRT et la façon de lancer BIRT en mode console.
- le fichier epl-v10.html : licence Eclipse
- le fichier birt.war : archive war du WebViewerExample
- le répertoire ReportEngine : son contenu est explicité plus bas

Le répertoire ReportEngine est constitué par :
- le répertoire 'configuration' : ce répertoire contient le fichier de configuration pour la gestion des moteurs BIRT.
- le répertoire 'lib' : ce répertoire contient les librairies utiles pour la manipulation des objets BIRT.
- le répertoire 'plugins' : ce répertoire contient les librairies nécessaires pour la gestion des moteurs BIRT.
- le répertoire 'samples' : ce répertoire contient des exemples de rapports au format rptdesign.
- le fichier 'genReport.bat' : permet de créer des rapports au format pdf et html en mode console (à partir de fichiers rptdesign) dans un environnement windows.
- le fichier 'genReport.sh' : permet de créer des rapports au format pdf et html en mode console (à partir de fichiers rptdesign) dans un environnement unix.

IV-B. Préparation de la base de données

Le rapport BIRT se base sur une source de données constituée d'une table 'table_des_produits'.
Il s'agit d'une table qui contient une liste de produits caractérisés par :
- un identifiant
- un type
- un nom
- un prix

Voici le script de création de la structure de données :

 
Sélectionnez

CREATE TABLE table_des_produits
(
id serial NOT NULL,
type_produit character varying,
nom character varying,
prix character varying,
CONSTRAINT table_des_produits_pkey PRIMARY KEY (id)
) 
WITHOUT OIDS;
ALTER TABLE table_des_produits OWNER TO postgres;

Voici le script d'insertion d'un jeu de données :

 
Sélectionnez

INSERT INTO table_des_produits (id, type_produit, nom, prix) VALUES (1, 'alimentaire', 'Lot de 3 tablettes de chocolat', '5');
INSERT INTO table_des_produits (id, type_produit, nom, prix) VALUES (2, 'alimentaire', 'saucisson', '8');
INSERT INTO table_des_produits (id, type_produit, nom, prix) VALUES (3, 'alimentaire', 'reblochon', '11');
INSERT INTO table_des_produits (id, type_produit, nom, prix) VALUES (4, 'mobilier', 'bureau', '120');
INSERT INTO table_des_produits (id, type_produit, nom, prix) VALUES (5, 'mobilier', 'chaise', '70');
INSERT INTO table_des_produits (id, type_produit, nom, prix) VALUES (6, 'mobilier', 'lampe', '30');

Lancer successivement ces deux scripts SQL en utilisant par exemple pgadmin.

V. Mise en place du script de génération de rapports

V-A. Création de la classe 'GenerateurRapportBIRT'

Créer un paquetage métier sous le paquetage source.

Dans ce tutoriel nous ne créerons qu'une seule classe et qu'une seule méthode : la classe GenerateurRapportBIRT qui n'aura qu'une méthode main (le but étant seulement d'apprendre à générer des rapports depuis la DEAPI).

V-B. Configuration du moteur de conception de rapports

DesignConfig est une classe fille de la classe PlatformConfig. Comme cette dernière, elle implémente l'interface IPlatformConfig. Elle permet de spécifier la localisation des services à charger pour le lancement du moteur de conception de rapports.

 
Sélectionnez
//Récupération du chemin du répertoire courant
String repertoireCourant = System.getProperty("user.dir");
		
//Affectation du chemin vers la plateforme BIRT
String BIRT_HOME = repertoireCourant + "/runtime/ReportEngine";
		
//Configuration OSGi
DesignConfig config = new DesignConfig();
config.setBIRTHome(BIRT_HOME);

V-C. Démarrage du moteur de conception de rapports

La classe Platform utilise le framework OSGI pour mettre à disposition les services de conception de rapports BIRT.

 
Sélectionnez
//Démarrage de la plateforme
Platform.startup(config);

V-D. Création d'un concepteur de rapports

IDesignEngine représente 'le concepteur de rapports BIRT'. Pour en récupérer une instance, il faut passer par une fabrique, 'IDesignEngineFactory'. On récupère d'abord cette fabrique via la plateforme. Cette fabrique nous permet ensuite de récupérer une instance de IDesignEngine.

 
Sélectionnez
//Récupération de la fabrique de 'concepteurs de rapports'
IDesignEngineFactory iDesignEngineFactory = (IDesignEngineFactory) Platform.createFactoryObject(IDesignEngineFactory.EXTENSION_DESIGN_ENGINE_FACTORY);
			
//Récupération d'une instance de 'concepteur de rapport'
IDesignEngine engine = iDesignEngineFactory.createDesignEngine(config);

V-E. Création d'une session de travail

Cette classe représente l'état de conception c'est à dire 'une vue du travail de conception en cours'. Par exemple, dans un environnement Eclipse, elle représente la liste de tous les rapports ouverts. Il est ainsi nécessaire de récuperer une instance de la classe Session pour pouvoir concevoir des rapports. On la créé à partir de notre concepteur de rapports.

 
Sélectionnez
//Création d'une session de travail
SessionHandle session = engine.newSessionHandle(ULocale.FRENCH);

V-F. Création d'une instance de rapport

Une instance de ReportDesignHandle est un objet java correspondant au fichier .rptdesign représentatif d'un rapport. On l'obtient à partir d'un objet session.

 
Sélectionnez
//Création d'un élement rapport
ReportDesignHandle design = session.createDesign();

V-G. Création des différents éléments constitutifs du rapport

V-G-1. Principe

On passe par une fabrique d'éléments qui nous fournit des modules qu'on ajoute au fur et à mesure à notre instance de rapport.

 
Sélectionnez
//Création d'une fabrique de modules à insérer dans le rapport
ElementFactory factory = design.getElementFactory();

V-G-2. Création de la 'Master Page'

Une 'master page' représente le type de page qui va accueillir notre rapport : une page orientée texte avec tableaux, images ou bien une page orientée graphisme. On a choisi ici de ne faire que des tableaux. Aussi va-t-on utiliser la classe SimpleMasterPage. On en récupère une instance via la fabrique d'éléments.

 
Sélectionnez

/*
* Création d'un élément master page
* */
//Création d'un élément Master Page
DesignElementHandle element = factory.newSimpleMasterPage("Page Master");
//Ajout de l'élément au rapport
design.getMasterPages().add(element);

V-G-3. Création de la source de données

Il suffit de récupérer via la fabrique d'éléments une instance de la classe OdaDataSourceHandle en spécifiant bien qu'il s'agit d'une datasource de type jdbc.

 
Sélectionnez

/*
* Création d'un élément source de données
* */
			
// Spécification des paramètres de connection
String nomDriver = "org.postgresql.Driver";
String urlBase = "jdbc:postgresql://localhost:5432/tutoriel_birt";
String loginBase = "postgres";
String passBase = "postgres";
String nomSourceDeDonnees = "table_des_produits";
			
// création d'un élément source de données de type jdbc
OdaDataSourceHandle dsHandle = factory.newOdaDataSource(nomSourceDeDonnees, "org.eclipse.birt.report.data.oda.jdbc");
			
//customisation de l'élément
dsHandle.setProperty("odaDriverClass", nomDriver);
dsHandle.setProperty("odaURL", urlBase);
dsHandle.setProperty("odaUser", loginBase);
dsHandle.setProperty("odaPassword", passBase);
			
//ajout de l'élément au rapport
design.getDataSources().add(dsHandle);

V-G-4. Création d'un magasin de données support pour le paramètre de sélection

Le paramètre de sélection est associé à une liste de choix (dans notre cas). Ici on choisit d'associer cette liste à un magasin de données (on aurait pu spécifier des paramètres statiques).

 
Sélectionnez

/*
* Création d'un élément magasin de données (dataset)
* sur lequel se base le paramètre de sélection
* */
String nomDuDataSetPourParametreDeSelection="MagasinPourParametreDeSelection";
String requete="SELECT type_produit FROM table_des_produits group by type_produit;";
OdaDataSetHandle odaDataSetHandle = factory.newOdaDataSet(nomDuDataSetPourParametreDeSelection,"org.eclipse.birt.report.data.oda.jdbc.JdbcSelectDataSet");
odaDataSetHandle.setDataSource(nomSourceDeDonnees);
odaDataSetHandle.setQueryText(requete);
			
//ajout de l'élément au rapport
design.getDataSets().add(odaDataSetHandle);

V-G-5. Création d'un paramètre de sélection

On instancie le paramètre de sélection en lui associant un nom.

 
Sélectionnez

/*
* Création d'un élément paramètre de sélection
* */
String nomDuParametreFiltre = "Type Produit";
					
ScalarParameterHandle scalarParameterHandle = factory.newScalarParameter(nomDuParametreFiltre);

Un paramètre de sélection peut présenter quatre types de choix :
- Text Box : l'utilisateur doit rentrer une valeur qu'il saisit au clavier
- Combo Box : il s'agit d'un choix entre une 'List Box' ou une 'Text Box' c'est à dire que l'utilisateur peut choisir entre une liste de valeurs ou bien peut rentrer la valeur à la main.
- List Box : il s'agit d'une liste de choix dans laquelle l'utilisateur doit choisir une ou plusieurs valeurs.
- Radio Button : l'utilisateur doit choisir une valeur dans une liste de type 'radio'.
Ici on va créer une 'List Box'.

 
Sélectionnez
//choix du type de choix d'affichage (ici liste-box)
scalarParameterHandle.setControlType(DesignChoiceConstants.PARAM_CONTROL_LIST_BOX);



Un paramètre de sélection ( de type 'List Box' ou 'Combo Box') repose sur deux types de sources :
- statique : les valeurs possibles que l'utilisateur peut choisir reposent sur un jeu de valeurs figé (ces valeurs sont 'statiques')
- dynamique : les valeurs possibles que l'utilisateur peut choisir reposent sur un jeu de valeurs non figé (ces valeurs reposent sur un magasin de données sur lequel est fait un appel dynamique)
Ici on va créer un paramètre de sélection de type dynamique.

 
Sélectionnez
//choix du type de valeurs (dynamique ici)
scalarParameterHandle.setValueType(DesignChoiceConstants.PARAM_VALUE_TYPE_DYNAMIC);



Un paramètre de sélection autorise deux volumes de sélection :
- un volume unitaire : on ne peut choisir qu'une seule valeur
- un volume multiple : on peut choisir plusieurs valeurs
Ici on va choisir un paramètre de sélection avec volume unitaire.

 
Sélectionnez

//choix du volume de sélection possible (ici volume unitaire)
scalarParameterHandle.setParamType(DesignChoiceConstants.SCALAR_PARAM_TYPE_SIMPLE);



Les types de données que peut contenir un paramètre de sélection sont de huit types différents :
- de type 'Boolean'
- de type 'Date'
- de type 'DateTime'
- de type 'Decimal'
- de type 'Float'
- de type 'Integer'
- de type 'String'
- de type 'Integer'



Ici on va choisir un paramètre de sélection avec des données de type 'String'.

 
Sélectionnez
//choix du type des données contenues dans le paramètre 
scalarParameterHandle.setDataType(DesignChoiceConstants.PARAM_TYPE_STRING);



Le reste du paramétrage reste assez intuitif (voir ci-dessous).

 
Sélectionnez
//texte à afficher dans la fenêtre
String textePourAffichage = "Choisir le type";
scalarParameterHandle.setPromptText(textePourAffichage);
			
//Spécification que la valeur choisie doit être contenue dans la List-Box
scalarParameterHandle.setMustMatch(true);
			
//Spécification que l'affichage doit être fait de la même manière que dans la source de données du paramètre de sélection
scalarParameterHandle.setFixedOrder(true);
			
//Spécification de la non présentation de doublon dans la liste de choix
scalarParameterHandle.setDistinct(true);
			
//affectation du dataset pour obtenir la liste des paramètres d'affichage
scalarParameterHandle.setDataSetName(nomDuDataSetPourParametreDeSelection);
			
//Spécification de la valeur d'affichage de la source associée à la séléction
scalarParameterHandle.setValueExpr("dataSetRow[\"type_produit\"]");
			
//Spécification du libellé d'affichage de la source associée à la séléction
scalarParameterHandle.setLabelExpr("dataSetRow[\"type_produit\"]");
			
//Pas de formatage de la valeur
scalarParameterHandle.setCategory("Unformatted");
			
//Ajout du paramètre au rapport
design.getParameters().add(scalarParameterHandle);

V-G-6. Construction du magasin de données principal

Le magasin de données principal est rendu dynamique : on ne ramène que les données spécifiées par le paramètre de sélection (on aurait pu ramener toutes les données et ensuite appliquer un filtre de tri par l'intermédiaire du paramètre).

 
Sélectionnez

/*
* Construction du magasin de données principal
* */
			
String nomDuDataSetPrincipal="DataSetPrincipal";
String requetePrincipal="SELECT id, type_produit, nom, prix FROM table_des_produits where type_produit=?";
OdaDataSetHandle odaDataSetHandlePrincipal = factory.newOdaDataSet(nomDuDataSetPrincipal,"org.eclipse.birt.report.data.oda.jdbc.JdbcSelectDataSet");
			
odaDataSetHandlePrincipal.setDataSource(nomSourceDeDonnees);
odaDataSetHandlePrincipal.setQueryText(requetePrincipal);

//affectation du report parameter
PropertyHandle propertyHandle = odaDataSetHandlePrincipal.getPropertyHandle(OdaDataSetHandle.PARAMETERS_PROP);
			
OdaDataSetParameter odaDataSetParameter = StructureFactory.createOdaDataSetParameter();
			
odaDataSetParameter.setName("param1");
odaDataSetParameter.setDataType(DesignChoiceConstants.PARAM_TYPE_STRING);
odaDataSetParameter.setPosition(1);
odaDataSetParameter.setIsInput(true);
odaDataSetParameter.setIsOutput(false);
odaDataSetParameter.setParamName(nomDuParametreFiltre);			
propertyHandle.addItem(odaDataSetParameter);
			
//ajout de l'élément au rapport
design.getDataSets().add(odaDataSetHandlePrincipal);

V-G-7. Construction d'une mise en forme (Layout)

La construction du Layout est assez intuitive également.

Quatre grandes étapes sont à distinguer :
- création du tableau qui va accueillir les données
- association des colonnes aux valeurs
- formatage de la première ligne
- formatage de la ligne courante

V-G-7-1. Création du tableau de données

On commence par créer le mapping entre le nom réel des colonnes et le nom qui figurera dans le rapport. On crée ensuite le tableau qui va être inséré dans le rapport et qui va contenir les données à afficher.

 
Sélectionnez

/*
* Construction de la mise en forme (Layout)
* */
			
			
CellHandle cell;
HashMap <String, String > mappingNomColonneLibelle = new HashMap< String, String>();
mappingNomColonneLibelle.put("id", "Identifiant");
mappingNomColonneLibelle.put("type_produit", "Type de Produit");
mappingNomColonneLibelle.put("nom", "Nom du produit");
mappingNomColonneLibelle.put("prix", "Prix du produit");
			
			
TableHandle tableHandle = factory.newTableItem("Tableau Test",mappingNomColonneLibelle.size());
tableHandle.setWidth("100%");
tableHandle.setDataSet(design.findDataSet(nomDuDataSetPrincipal));

V-G-7-2. Création du 'column-binding'

Le 'column-binding' permet d'associer un 'nom' de colonne (niveau rapport) à une colonne du magasin de données.

 
Sélectionnez

// Préparation du mapping données-valeurs.

PropertyHandle computedSet = tableHandle.getColumnBindings();
ComputedColumn cs1 = null;

Set<String> listeColonne = mappingNomColonneLibelle.keySet();
Iterator<String> iterateurListeColonne = listeColonne.iterator();
String valeur;
String libelle;

while (iterateurListeColonne.hasNext()) {
	valeur = iterateurListeColonne.next();
	libelle = mappingNomColonneLibelle.get(valeur);
	cs1 = StructureFactory.createComputedColumn();
	cs1.setName(valeur);
	cs1.setExpression("dataSetRow[\"" + valeur + "\"]");
	computedSet.addItem(cs1);
}

V-G-7-3. Formatage de la ligne d'entête

Les éléments 'Label' sont des éléments destinés à recevoir des contenus statiques. On va en placer un dans chaque colonne de la ligne d'entête afin de mettre un titre pour le contenu de la colonne. La classe StyleHandle est chargée du formatage css.

 
Sélectionnez


StyleHandle styleHandle;

// table header (libellés)
RowHandle tableHeader = (RowHandle) tableHandle.getHeader().get(0);
int i = -1;
iterateurListeColonne = listeColonne.iterator();
while (iterateurListeColonne.hasNext()) {
	i = i + 1;
	valeur = iterateurListeColonne.next();
	libelle = mappingNomColonneLibelle.get(valeur);
	LabelHandle label1 = factory.newLabel(valeur);
	label1.setText(libelle);
	cell = (CellHandle) tableHeader.getCells().get(i);
	cell.getContent().add(label1);

// formatage du css
	styleHandle = cell.getPrivateStyle();
	ColorHandle colorHandle = styleHandle.getBackgroundColor();
	colorHandle.setValue("gray");
	styleHandle.setTextAlign(DesignChoiceConstants.TEXT_ALIGN_CENTER);
	styleHandle.setVerticalAlign(DesignChoiceConstants.VERTICAL_ALIGN_MIDDLE);
	styleHandle.setBorderBottomStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
	styleHandle.setBorderTopStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
	styleHandle.setBorderRightStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
	styleHandle.setBorderLeftStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
}

V-G-7-4. Formatage de la ligne de données courante

Les éléments 'Data' sont des éléments destinés à recevoir des contenus dynamiques. On va en placer un dans chaque colonne de la 'ligne courante' afin d'afficher les données du rapport. Ici on utilise le 'column-binding' : on associe à la colonne à la position i la colonne qui a pour nom la valeur suivante de la liste 'listeColonne'.

 
Sélectionnez

// table detail (valeurs)
RowHandle tableDetail = (RowHandle) tableHandle.getDetail().get(0);

i = -1;
iterateurListeColonne = listeColonne.iterator();
while (iterateurListeColonne.hasNext()) {
	i = i + 1;
	valeur = iterateurListeColonne.next();
	cell = (CellHandle) tableDetail.getCells().get(i);
	DataItemHandle data = factory.newDataItem("data_"+valeur);
	data.setResultSetColumn(valeur);
	cell.getContent().add(data);
	
	// formatage du css
	styleHandle = cell.getPrivateStyle();
	styleHandle.setTextAlign(DesignChoiceConstants.TEXT_ALIGN_CENTER);
	styleHandle.setBorderBottomStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
	styleHandle.setBorderTopStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
	styleHandle.setBorderRightStyle(DesignChoiceConstants.LINE_STYLE_SOLID);
	styleHandle.setBorderLeftStyle(DesignChoiceConstants.LINE_STYLE_SOLID);

}

// On ajoute finalement l'élément mise en forme
design.getBody().add(tableHandle);

Attention : Lors de la création des LabelHandle et des DataItemHandle, il faut veiller à attribuer aux objets associés (instances de ILabel et IDataItem) des noms différents. En effet les éléments 'Label' et 'Data' sont repérés par BIRT par leur nom.

V-H. Sauvegarde finale du rapport

 
Sélectionnez

//On sauvegarde finalement le rapport
String cheminRapport=repertoireCourant+"/reports/testBIRT.rptdesign";
design.saveAs(cheminRapport);
design.close();

V-I. Arrêt du moteur

 
Sélectionnez

//Arrêt de la plateforme
Platform.shutdown();

VI. Test du script et du fichier généré

Lancer le script. Vérifier l'existence du fichier 'testBIRT.rptdesign'. Ouvrir ce fichier dans la perspective 'report design' d'Eclipse. Lancer un preview afin de vérifier que le rapport a le fonctionnement attendu (voir copie d'écran ci-dessous).

Fenêtre du paramètre de sélection
Fenêtre du paramètre de sélection



Rapport
Rapport

VII. Conclusion

Ici a été présenté une ébauche de ce que peut faire la DEAPI. Cette API offre néanmoins encore de nombreuses possibilités notamment en utilisant d'autres types de datasource : les scripted data source, les fichiers plats, les sources de données xml ou encore les web services. Par ailleurs, la DEAPI peut également être utilisée pour modifier des rapports existants ou pour assembler différents rapports et ainsi simuler les sous-rapports. La maîtrise de la REAPI est ensuite l'étape indispensable pour implémenter BIRT dans son application web.

Télécharger les sources du tutoTélécharger les sources du tuto

VIII. Remerciements

Enfin, merci à Bérénice MAUREL, Bruno2r et Fleur pour l'aide fourni pour la mise en place de ce tutoriel :). Merci également à autofill pour sa relecture. Merci à Ykk_Jeff pour ses remarques qui ont permis d'améliorer le tutoriel.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+