lundi 16 avril 2012

MEF, importer le constructeur

Bien le bonjour,
Bien que j'ai déjà fait un article sur le MEF, je voudrais apporter une explication sur une fonctionnalité fort intéressante à mon gout: "importingconstructor". Le but de cette option est de partager un objet du conteneur avec l'ensemble de ses plugins. Dans mon projet actuel, il me permet de donner accès à des fonctionnalités de la Kinect à chaque composant chargé par MEF.

Dans le conteneur, lors de l'instanciation du MEF:
string[] directories = Directory.GetDirectories("Plugins");
AggregateCatalog containerMaster = new AggregateCatalog();
foreach (string directory in directories)
{
      containerMaster.Catalogs.Add(new DirectoryCatalog(directory));
}
CompositionContainer container = new CompositionContainer(containerMaster);
container.ComposeExportedValue("myKinect", _myKinect);
container.ComposeParts(this);
On remarque que l'importingconstructor ne nécessite qu'une seule ligne (l'avant dernière ligne), que celle-ci ne comporte que la variable _myKinect et le nom sous lequel on référence cette variable: "myKinect".

Une fois dans un composant, il faut récupérer cette variable. Cela se fait dans le constructeur comme suit:

[ImportingConstructor]
        public MainWindow([Import("myKinect")] KinectSensor myKinect)
        {
            _kinect = myKinect;
        }
 J'espère que cette explication vous sera utile. Je fini néanmoins par vous prévenir sur le fait que cette technique peut posséder un risque, vu que vous donner accès à des ressources sans connaître le composant qui va les exploiter.

Comme on dit chez moi: Paix sur ta couche!

mardi 13 mars 2012

Trello, l'outil de planning d'équipe

Tant que l'on est dans les outils pouvant vous aider, je me dois de vous présenter Trello. Ce site permet de garder un œil sur ce que vous, et vos collaborateurs, faites en temps réel. Le principe est assez simple, commencez tout d'abord par créer une "Board" et invitez-y vos collègues de travail. Vous vous trouvez alors devant quatre colonnes:
  • To Do             --- Ce que vous comptez faire
  • Doing              --- Ce que vous êtes entrain de faire
  • To Review      --- Ce qui est fait et doit être vérifié par un tiers
  • Done              --- Ce qui est terminé
Vous allez alors créer des cartes où vous décrivez une tâche que vous placerez dans l'une de vos colonnes. Chaque carte peut être customisée, en y ajoutant une couleur, des commentaires, un vote et le plus important: les personnes qui sont concernées. Une fois l'ensemble mis en place, on peut avoir un aperçu permanent de l'évolution du travail de chacun.

La chose est très simple et très pratique lors de projet commun entre plusieurs personnes pour ne pas se demander inutilement qui fait quoi en permanence et également pour trouver qui glande sur "l'ajout du logo" depuis 4 jours.

Stack Overflow

Je m'aperçois que je ne vous ai pas encore parlé d'un site bien utile lorsque l'on développe, je parle de www.stackoverflow.com.
Ce site communautaire permet de trouver rapidement de l'aide à vos soucis que Google n'aura pu résoudre. Vous pourrez ainsi avoir des informations émanant de personnes souvent compétente et qui vous permettront de vous faire gagner beaucoup de temps. Mais ce site fonctionne également dans l'autre sens, ainsi il vous est possible d'aider les autres membres lorsque ceux-ci sont confrontés à un problème au sujet duquel vous êtes en mesure d'apporter votre aide.

Un système de point est utilisé pour vous donner de façon progressive des droits au sein du site et ainsi donner accès à de nouveaux outils pouvant vous aider. Les points se gagnent de façon diverses, que ce soit en répondant à des questions ou en posant vous même une question de façon intéressante.
N'oubliez jamais de valider les réponses que l'on vous donne de façon à montrer clairement que votre question à trouver sa réponse.

Je ne peux que je vous encourager à découvrir ce site, qui vous sera surement utile un jour. A défaut de vous aider, il pourra toujours vous faire réviser votre maitrise de l'anglais ;)

Générer un fichier dll plutôt qu'un fichier exe

C'est le matin et voilà votre pain quotidien.
Dans certains cas, du style d'une architecture MEF, il peut être utile qu'un projet génère une dll plutôt qu'un fichier exécutable, voici donc comment procéder:
  1.  Dans les propriétés de "App.xaml", changer l’action de génération en Resource
  2.  Dans les propriétés du projet, modifier le type de sortie de Application Window en Bibliothèque de classes
  3.  Pour obtenir la DLL, on finit par aller dans le menu Générer et choisir Générer la solution
Dans le cadre d'un plugin MEF qui serait incorporé entre des balises <itemsControl>, il est nécessaire d'ajouter ces étapes:
  1. Dans le Xaml, il faut changer les balises <Window> en <UserControl> et <Window.ressources> en <UserControl.ressources>
  2.  Au niveau de l’héritage de la classe, enlever le :Window
J'espère que ça aidera l'un ou l'autre fidèle passant par ici. Peace!

mercredi 7 mars 2012

Planning Poker



Après le Poker Texas Hold'em, le Poker fermé, le strip Poker, le Poker face, voici le Planning Poker !
Loin d'être une nouvelle version que vous seriez susceptible de voir dans le projet James Bond, le Planning Poker est une méthode de planification des tâches lors d'un développement logiciel.
Imaginons 5 développeurs réunis autour d'un projet et accompagné de leur Project Owner (cf Scrum), qu'ils sont couchés dans des Fatboys en sirotant un café et munis chacun d'une main de 13 cartes existants sous plusieurs versions:
0 - 0.5 - 1 - 2 - 3 - 5 - 8 - 13 - 21 - 34 - 89 - ?
0 - 0.5 - 1 - 2 - 3 - 5 - 8 - 13 - 20 - 40 - 100 - ?

Le Project Owner pose une "User Stories" (Un module à implémenter dans le programme) et le but sera d'estimer le temps nécessaire à sa réalisation. Chaque développeur va alors prendre la carte représentant le nombre de jour qu'il pense nécessaire à la confection du module et la poser face cachée sur la table. On retourne les cartes et découvre les estimations de chaque membre. On discute alors entre développeurs pour comprendre pourquoi l'un a mit plus, et un autre moins, de façon à partager les informations et ainsi avoir un partage de connaissances. On recommence jusqu'à obtenir une même estimation pour chaque membre. En cas de conflit de plus de 5minutes, on fini par choisir la durée la plus longue et on passe à l' "User Stories" suivante.
La durée peut varier entre un "Young Programmer" et un "Senior Programmer" de façon impressionnante, il est donc impensable de baser la durée de développement sur l'un ou l'autre au dépend du second.

On a donc ainsi la possibilité d'estimer la durée des modules et donc le nombre d'entre eux à réaliser durant un sprint. Au final, l'équipe possède un plan des fonctionnalités, de leur durée estimée et de leur importance de façon à organiser leur travail pour un mieux comme on peut le voir dans cet exemple:
Le dernier point à retenir est le facteur de vélocité. C'est un facteur qui va permettre d'adapter l'estimation à la réalité car 100 unités de temps ne se feront pas en 100 unités de temps mais plutôt en 50 ou 80 suivant le niveau. Et ce pour la raison que l'on se base sur des estimations et que donc on ne tombera jamais juste.

mardi 6 mars 2012

Microsoft Innovation Center

Amis du jour, bonjour. Voilà presque un mois que je roule ma bosse à mon stage et je me suis dis qu'il serai peut-être judicieux de faire un petit article au sujet de l'entreprise qui m'accueille, j'ai nommé: Microsoft. Plus exactement le Microsoft Innovation Center de Mons.
Cette ASBL créée à partir de fonds publics en privés s'est fixé comme but, de développer l'IT sous différentes formes en région wallonne et d'encourager la création de sociétés associées à ce même domaine. Elle a également un but très louable qui est d'encadrer des étudiants en dernière année issus des écoles d'informatique de la région et de les mettre en relation avec des entreprises leur proposant un stage. C'est ainsi qu'en cette belle année 2012, on se retrouve à 27 stagiaires au sein de l'entreprise pour développer des projets divers et variés.
Mais vu que des images valent tous les mots du monde, je vous propose de visionner cette vidéo, tournée il y a peu dans nos locaux:



Comme dis précédemment, munis de différents projets nous tentons de mener à bien nos idées. Deux de ces projets relatifs à l'équipement Kinect et au robot d'Aldebaran Nao concernent 4 étudiants (dont je fais partie) et dont je vous propose de retrouver un article écrit par nos collègues de Technocité suivant ce lien.

Le Microsoft Innovation Center propose également un grand nombre de formations et de certification auxquelles vous pouvez vous inscrire ici : http://mic.fikket.be/

Retrouvez toutes nos vidéos sur YouTube : http://www.youtube.com/user/MICBELGIQUE

Le blog du MIC : http://blog.mic-belgique.be/
Le site officiel : http://www.mic-belgique.be/

lundi 5 mars 2012

Gestion des logs avec NLog

Aujourd'hui c'est lundi, c'est gratuit, dehors c'est le smog alors attaquons les logs.
Mais un log c'est quoi?
Et bien ça peut être bien des choses mais de façon simplifiée, c'est une façon de garder une trace de certains évènements qui se sont produits durant l'exécution du programme. On peut gérer ces logs de plusieurs façons mais les plus conventionnelles sont l'affichage dans un fichier texte ou dans la console.
On va dés lors, à certains moments dans notre code, placer des enregistrements du contenu de variables de façon à les analyser à posteriori.

On enregistre 3 types d'informations:
  • Le débugging: Ce qui va nous permettre en analysant les logs de voir ce qui génère les bugs et que l'on a pas su voir via le débogueur classique de Visual Studio par exemple.
  • Le tracing: On va consigner les évènements qui se sont produits dans un but de statistiques ou analyse de fonctionnement. On trouvera ainsi la trace d'une connexion à une base de donnée, le chargement d'un module, l'adresse d'une requête, ...
  • La gestion des erreurs: Une fois le logiciel fonctionnel, on va pouvoir consigner les évènements anormaux qui ont nuis au fonctionnement normal du programme. Un plantage, une requête échouée, un fichier inexistant, une écriture refusée, un serveur indisponible, ...

Bien vite indispensable, les logs doivent se faire au fur et à mesure de la conception du produit. Ainsi lors d'un plantage, une consultation des logs permet de voir la dernière action effectuée correctement et peut-être même celle qui a échoué, selon la façon que l'on a implémenté nos logs.
De façon à simplifier le travail, des outils sont mis à notre disposition, l'un d'eux est "NLog" que vous allez pouvoir télécharger via ce lien: ici
Un autre système de log se nomme "Log4Net" et est développé par Apache: ici
Des tutoriels sont disponibles sur leur site respectif et sont suffisant pour une structure simple.

Il est également intéressant de lire cet article sur la façon de reporter un bug de façon efficace en ce lieu
Et pour ceux qui pensent que le débogueur de Visual Studio ne sert qu'à mettre des points d'arrêts, voici deux liens possédant divers astuces pour vous faire gagner du temps: Ici et là.


System.Windows.MessageBox.Show("Enjoy !");

vendredi 2 mars 2012

Incorporer un texte dans une image

Aujourd'hui, petite astuce. Imaginez que vous possédez un panel d'images associées à des données mais que vu leur nombre il n'est pas toujours possible de créer une image différente pour chaque. Et bien la solution pourrai être de partir d'un corps commun et d'y ajouter le nom de la donnée de façon à pouvoir différencier les images une fois affichées dans le panel.



Tout d'abord, le traitement sur le texte à incorporer se fera via les lignes suivantes:
Font font = new Font("arial", 24, System.Drawing.FontStyle.Bold);
StringFormat format = new StringFormat {Alignment = StringAlignment.Center,
                                                    LineAlignment = StringAlignment.Center };

On extrait notre image commune et on la place dans une variable Image:
Image img = Image.FromFile("Lien vers le fichier image");
Image tmpImg = new Bitmap(img, img.Width, img.Height);
Graphics graphics = Graphics.FromImage(tmpImg);

Pour pouvoir contrôler l'endroit où le texte se situera sur l'image, on crée un rectangle (ou un point, un cercle, une ellipse, ...) dont les dimensions ne devront pas dépasser la résolution de l'image. Une petite marge permettra de ne pas voir le texte collé au bord. Ce qui donne pour une image en 300x180:
const float x = 10.0F;
const float y = 10.0F;
const float width = 280.0F;
const float height = 160.0F;
RectangleF drawRect = new RectangleF(x, y, width, height);

On place tous ces ingrédients en commun pour obtenir notre résultat:
graphics.DrawString("Mon superbe texte à incorporer", font, Brushes.White, drawRect, format);

Comme il serait dommage de devoir recréer ces images à chaque fois, il serait intéressant de stocker notre image dans un fichier temporaire duquel il sera plus simple d'en extraire la donnée:
tmpImg.Save("Lien vers le fichier à créer");


peace and prosperity!

mardi 28 février 2012

Scrum What The ... ?

Scrum est un site sur le rugby et ce qui s'y rapporte: Les dents cassées, les nuques brisées, les tonnes du boues, les contacts (qui tueraient un joueur de football en un instant), les barbus, les ... euh non, peut-être que non en fait. Car imaginer des informaticiens barbus d'accord, mais vous avez déjà vu de la boue dans un pc vous?

Donc en fait Scrum, en réalité, est un framework dédié à la gestion de projets. Cette méthodologie n'est pas une manière de développer mais plutôt de gérer le développement. Elle se base sur une série de principes simples: Une équipe de développeurs hétéroclite composée de 5 à 9 personnes environ, d'un "Project Owner" qui sera la voix du groupe et un "Scrum Master" qui lui n'est pas un développeur et dont le rôle est de faire tomber les obstacles pouvant ralentir le développement d'un produit.
Armé de ces collaborateurs, l'équipe va travailler sur des cycles de 1 mois durant lesquels un produit livrable sera créé. Durant cette période, des réunions quotidiennes, appelées "mêlée quotidienne" seront organisées et d'autres se feront en début et fin de cycle.


Mais qu'est ce que le rugby vient faire la dedans sérieux? Et bien simplement car l'ensemble des termes sont tirés du monde du rugby, probablement pour faire croire aux informaticiens qu'ils faisaient du sport chaque jour...

Pour d'avantages d'informations sur le sujet, je vous invite à lire les liens suivants: ici et là

et de passer le test: en ce lieu

jeudi 23 février 2012

MEF - Managed Extensibility Framework

Mais qu'est ce que c'est que cet MEF? Voilà à peu de chose prêt ce que je me suis demandé lorsque l'on m'en a parlé. Oui mais comment ça marche? Est la seconde question qui m'a tenu pendant des heures.

En fait, MEF est une chose très simple mais possédant peut de documentation claire, chose que je vais tenter de faire de façon à éviter à d'autres que moi de perdre un temps précieux à trouver de l'aide sur la toile.

Le but de MEF est d'importer des plugins de façon simple, qu'il se trouve dans le même projet ou sous forme d'un fichier ".dll" et c'est là que ça devient intéressant. Car la particularité de MEF comparé à une autre méthode est le fait que lors d'un appel d'un plugin, si il est là il sera traité, si ils sont plusieurs, ils le seront tous et si malheureusement la recherche ne donne rien cela ne déclanche aucunes erreurs c'est plutôt "Je vais charger des plugins. Oh mais je trouve rien là où ils devraient être. Bon ben tampis, je continue ma route". Son comportement est similaire à un bus qui se stop à un arrêt et qu'il y ai des passagers ou non, ça ne change rien pour lui.
Dans le cas présent, on va chercher des ".dll" dans un dossier "plugins" et y récupérer des donneés.
Dans votre projet et dans vos plugins, il faut rajouter la référence de Visual Studio 2010 "System.ComponentModel.Composition" car, bien que basé sur de l'opensource, le framework est désormais inclus dans le framework .Net.

Le code de notre dll est le suivant:
Plugin10.cs
using System.ComponentModel.Composition;
using GPIInterface;

namespace Plugin10
{
    [Export(typeof(IGrany))]
    public class Plugin10 : IGrany
    {
        public string PluginTitle(){
            return "Plugin numéro 10";
        }
        public string PluginDescription(){
            return "Ceci est le premier plugin";
        }
        public string PluginImageName(){
            return "10.jpg";
        }
        public string PluginName(){
            return "Plugin10";
        }
    }
}

Comment on peut le voir, le plugin implémente une interface qui défini simplement les méthodes qui doivent se trouver dans tous les plugins. Mais cela ne change rien à notre projet.
La partie la plus vitale est la ligne "[Export(typeof(IGrany))]" où l'on explique ce que l'on exporte et quelle interface est utilisée.
Le fichier qui importe devra posséder la ligne :
[ImportMany(AllowRecomposition = true)]
        public List<IGrany> Applications { get; set; }

Ensuite dans le même fichier, se trouve une méthode qui possède le code suivant:

MainWindow.cs
AggregateCatalog catalog = new AggregateCatalog(new DirectoryCatalog("Plugins"),
                new AssemblyCatalog(Assembly.GetExecutingAssembly()));
CompositionContainer container = new CompositionContainer(catalog);
container.ComposeParts(this);
           
foreach (var apps in Applications)
{
   ArrayList plugin = new ArrayList();
   plugin.Insert(0, apps.PluginDescription());
   plugin.Insert(1, apps.PluginImageName());
   plugin.Insert(2, apps.PluginName());
   plugin.Insert(3, apps.PluginTitle());
   _dataPlugins.Add(plugin);
}

Les trois premières lignes sont indispensables pour aller chercher les plugins dans le dossier et le reste permet d'en extraire les données de façon indépendante à leur nombre.

On peut dés lors ajouter des dll en quantité dans le dossier et ils seront tous gérés sans modifications du code principale.

Pour une bonne explication sur les bases de MEF, je vous recommande la vidéo suivante: http://channel9.msdn.com/Events/TechDays/TechDays-2011-Belgium/TD029
Enjoy!

FluidKit - ElementFlow

Après avoir vu les transistions, arrêtons nous un instant sur une seconde capacité du FluidKit, l'ElementFlow. Cet élément permet de faire défiler des images de façon fluide et stylée sans avoir besoin de se plonger dans un code sans fin du Xaml.
Tout comme pour l'article précédent, il est indispensable d'ajouter la dll du kit avant tout opération.
Je ne vais pas détailler toutes les options de configuration disponible, mais me contenter de vous offrir une interface contenant l'éssentiel de ce que l'on peut attendre face à une libraire d'images, c'est à dire une interface de visualisation et un "slider".

Code Xaml
MainWindow.xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:Controls="clr-namespace:FluidKit.Controls;assembly=FluidKit"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Samples="clr-namespace:Fluid_ElementFlow2" x:Class="Fluid_ElementFlow2.MainWindow"
        mc:Ignorable="d">
    <Window.Resources>
        <Samples:StringCollection x:Key="DataSource" />

        <DataTemplate x:Key="TestDataTemplate"
                      DataType="{x:Type sys:String}">
            <Border x:Name="ElementVisual"
                    Background="White"
                    Padding="5"
                    BorderThickness="5"
                    BorderBrush="LightGray"
                    Grid.Row="0">
                <Image Source="{Binding}"
                       Stretch="Fill" />
            </Border>
        </DataTemplate>

    </Window.Resources>
    <Grid Background="#FF1A1A1A" FocusManager.FocusedElement="{Binding ElementName=_elementFlow}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*" />
            <ColumnDefinition Width="0.5*" />
        </Grid.ColumnDefinitions>

        <StackPanel Orientation="Vertical"
                    Grid.Row="1"
                    Grid.Column="0"
                    Grid.ColumnSpan="2"
                    Margin="5">
            <Slider x:Name="_selectedIndexSlider"
                    Minimum="0"
                    Value="0"
                    ValueChanged="ChangeSelectedIndex" />
        </StackPanel>
        <Controls:ElementFlow x:Name="_elementFlow"
                              Grid.Row="0"
                              Grid.Column="0"
                              Grid.ColumnSpan="2"
                              ItemsSource="{DynamicResource DataSource}"
                              ItemTemplate="{DynamicResource TestDataTemplate}"
                              TiltAngle="45"
                              ItemGap="0.4"
                              FrontItemGap="1.0"
                              PopoutDistance="2.2"
                              ElementWidth="600"
                              ElementHeight="400"
                              Margin ="0,-120,0,0"
                              SelectedIndex="{Binding Value, ElementName=_selectedIndexSlider}">
            <Controls:ElementFlow.Layout>
                <Controls:CoverFlow />
            </Controls:ElementFlow.Layout>
            <Controls:ElementFlow.Background>
                <LinearGradientBrush EndPoint="0.5,1"
                                     StartPoint="0.5,0">
                    <GradientStop Color="#FF181818" />
                    <GradientStop Color="#FF7A7A7A"
                                  Offset="0.5" />
                    <GradientStop Color="#FF1A1A1A"
                                  Offset="1" />
                </LinearGradientBrush>
            </Controls:ElementFlow.Background>
            <Controls:ElementFlow.Camera>
                <PerspectiveCamera FieldOfView="50"
                                   Position="0,1,6"
                                   LookDirection="0,-1,-6" />
            </Controls:ElementFlow.Camera>
        </Controls:ElementFlow>

        <TextBlock x:Name="_elementTitle"
                           Foreground="White"
                           FontWeight="Bold"
                           VerticalAlignment="Top"
                           HorizontalAlignment="Center"
                           Margin="10,20,10,10"
                           Grid.Row="0"
                           Grid.Column="0"
                           Grid.ColumnSpan="2"
                           FontSize="30"
                           FontFamily="Arial"
                           Text=""/>
       
        <TextBlock x:Name="_osTitle"
                   Foreground="White"
                   FontWeight="Bold"
                   VerticalAlignment="Top"
                   HorizontalAlignment="Right"
                   Margin="10,10,10,10"
                   Grid.Row="0"
                   Grid.Column="1"
                   FontSize="30"
                   FontFamily="Times New Roman"
                   Text="Grany OS"/>
      </Grid>
</Window>




Code C#
MainWindow.cs

using System;
using System.IO;
using System.Windows;
using FluidKit.Controls;
using System.Collections.Generic;
using System.ComponentModel.Composition;

namespace Fluid_ElementFlow2
{
    public partial class MainWindow
    {
        private StringCollection _dataSource;

        [ImportMany(AllowRecomposition = true)]
        public List<IGrany> Applications { get; set; }
       
  public MainWindow()
  {
   InitializeComponent();
   Loaded += Window1Loaded;
  }

        /// <summary>
        /// Configuration of variables and environment
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
  private void Window1Loaded(object sender, RoutedEventArgs e)
  {
            _elementFlow.Layout = new CoverFlow();
            _dataSource = FindResource("DataSource") as StringCollection;
            AddCards(); // Loading of application's pictures
            _selectedIndexSlider.Maximum = _elementFlow.Items.Count - 1;
            _elementFlow.SelectedIndex = _elementFlow.Items.Count / 2;
  }

               
  /// <summary>
  /// Update informations during navigation between applications
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="args"></param>
        private void ChangeSelectedIndex(object sender, RoutedPropertyChangedEventArgs<double> args)
  {
            int index = (int)args.NewValue;
            string source = _dataSource[index];
            string[] titleWithExtension = source.Split(new Char[] { '\\' });
            string[] titleWithoutExtension = titleWithExtension[titleWithExtension.Length - 1].Split(new Char[] { '.' });
            _elementFlow.SelectedIndex = index;
            _elementTitle.Text = titleWithoutExtension[0];
  }
       
        /// <summary>
        /// Search all the plugins available
        /// </summary>
        private void AddCards()
        {
            string path = Directory.GetCurrentDirectory()+"\\Images";
            string[] fichiers = Directory.GetFiles(path);
            foreach (string fichier in fichiers)
            {
                _dataSource.Insert(_dataSource.Count, fichier);
            }
        }
    }
}



StringCollection.cs

using System.Collections.ObjectModel;
namespace GranyPluginsImplement
{
    public class StringCollection : ObservableCollection<string>
    {
        public StringCollection()
        {
        }
    }
}
 

Avec ces 3 fichiers, vous obtenez une interface simple et agréable que vous pourrez à votre guise personnaliser, le rendu étant le suivant:



Si vous sauhaitez d'avantages de possibilités de ElementFlow, je vous conseille de regarder l'exemple fourni avec le code source.