Author Archives: renaud

Mettre Ă  jour Postgresql sur debian Stretch

Stretch

Nous allons voir comment mettre à jour votre base de données postgresql après l’update de Jessie à Stretch.
La version de jessie était 9.4. La version pour Stretch est 9.6.

Les commandes suivantes sont exécutées en root. Vous pouvez bien sûr utiliser sudo.

Lister les clusters postgresql installés sur votre machine:

# pg_lsclusters

Si votre installation de postgresql date de Jessie vous aurez un truc comme cela.

[pastacode lang=”bash” manual=”Ver%20Cluster%20Port%20Status%20Owner%20Data%20directory%20Log%20file%0A9.4%20main%20%20%20%205432%20online%20postgres%20%2Fvar%2Flib%2Fpostgresql%2F9.4%2Fmain%20%2Fvar%2Flog%2Fpostgresql%2Fpostgresql-9.4-main.log%0A9.6%20main%20%20%20%205433%20online%20postgres%20%2Fvar%2Flib%2Fpostgresql%2F9.6%2Fmain%20%2Fvar%2Flog%2Fpostgresql%2Fpostgresql-9.6-main.log” message=”” highlight=”” provider=”manual”/]

 

Si vous n’avez pas fait la mise à jour de votre cluster après la mise à jour de wheezy vers jessie. Vous devriez voir un truc dans ce style la.

[pastacode lang=”bash” manual=”Ver%20Cluster%20Port%20Status%20Owner%20Data%20directory%20Log%20file%0A9.1%20main%20%20%20%205432%20online%20postgres%20%2Fvar%2Flib%2Fpostgresql%2F9.1%2Fmain%20%2Fvar%2Flog%2Fpostgresql%2Fpostgresql-9.1-main.log%0A9.4%20main%20%20%20%205433%20online%20postgres%20%2Fvar%2Flib%2Fpostgresql%2F9.4%2Fmain%20%2Fvar%2Flog%2Fpostgresql%2Fpostgresql-9.4-main.log%0A9.6%20main%20%20%20%205434%20online%20postgres%20%2Fvar%2Flib%2Fpostgresql%2F9.6%2Fmain%20%2Fvar%2Flog%2Fpostgresql%2Fpostgresql-9.6-main.log” message=”” highlight=”” provider=”manual”/]

Le port par défaut de postgresql est le 5432.  Le cluster ayant 5432 comme port est votre cluster courant.

Normalement Debian installe la nouvelle version de postgresql à la mise à jour de version et il crée un nouveau cluster dans la foulée.
Si ce n’est pas le cas, vous pouvez l’installer par cette ligne:

# apt-get install postgresql-9.6

Supprimer le cluster postgresql 9.6 créé

Installer une nouvelle version de postgresql sur debian crée un cluster principal (main) qui empêche la commande pg_upgradecluster de fonctionner.

Il faut donc supprimer cette instance dans le but d’obtenir une base de données propre.
Faite très attention, ne vous tromper pas dans la commande.
Ne supprimer surtout pas votre vieille base de données. C’est elle qui contient toutes vos données. 🙂

Nous détruisons la nouvelle; la vide.

# pg_dropcluster --stop 9.6 main

Mettre Ă  jour la vieille base vers le nouveau format

La syntaxe est la suivante:

# pg_upgradecluster -v NouvelleVersion AncienneVersion main

La fonction va créer un nouvelle base main dans le format NouvelleVersion.
Pour ensuite, copier les données de AncienneVersion vers NouvelleVersion.
Cela prendra un peu de temps.

[pastacode lang=”bash” manual=”%23%20pg_upgradecluster%20-v%209.6%209.4%20main%0AStopping%20old%20cluster…%0ANotice%3A%20extra%20pg_ctl%2Fpostgres%20options%20given%2C%20bypassing%20systemctl%20for%20stop%20operation%0ADisabling%20connections%20to%20the%20old%20cluster%20during%20upgrade…%0ARestarting%20old%20cluster%20with%20restricted%20connections…%0ARedirecting%20start%20request%20to%20systemctl%0ACreating%20new%20cluster%209.6%2Fmain%20…%0Aconfig%20%2Fetc%2Fpostgresql%2F9.6%2Fmain%0Adata%20%2Fvar%2Flib%2Fpostgresql%2F9.6%2Fmain%0Alocale%20en_US.UTF-8%0Asocket%20%2Fvar%2Frun%2Fpostgresql%0Aport%205434%0ADisabling%20connections%20to%20the%20new%20cluster%20during%20upgrade…%0ARedirecting%20start%20request%20to%20systemctl%0ARoles%2C%20databases%2C%20schemas%2C%20ACLs…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20pbf…%0AAnalyzing%20database%20pbf…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20template1…%0AAnalyzing%20database%20template1…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20livres…%0AAnalyzing%20database%20livres…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20cmdline…%0AAnalyzing%20database%20cmdline…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20systemtest…%0AAnalyzing%20database%20systemtest…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20creatures…%0AAnalyzing%20database%20creatures…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20articles…%0AAnalyzing%20database%20articles…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20postgres…%0AAnalyzing%20database%20postgres…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20campagnes…%0AAnalyzing%20database%20campagnes…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20animations…%0AAnalyzing%20database%20animations…%0AFixing%20hardcoded%20library%20paths%20for%20stored%20procedures…%0AUpgrading%20database%20asso_member…%0AAnalyzing%20database%20asso_member…%0ARe-enabling%20connections%20to%20the%20old%20cluster…%0ARe-enabling%20connections%20to%20the%20new%20cluster…%0ACopying%20old%20configuration%20files…%0ACopying%20old%20start.conf…%0ACopying%20old%20pg_ctl.conf…%0ACopying%20old%20server.crt…%0ACopying%20old%20server.key…%0AStopping%20target%20cluster…%0ARedirecting%20stop%20request%20to%20systemctl%0AStopping%20old%20cluster…%0ARedirecting%20stop%20request%20to%20systemctl%0ADisabling%20automatic%20startup%20of%20old%20cluster…%0AConfiguring%20old%20cluster%20to%20use%20a%20different%20port%20(5434)…%0AStarting%20target%20cluster%20on%20the%20original%20port…%0ARedirecting%20start%20request%20to%20systemctl%0ASuccess.%20Please%20check%20that%20the%20upgraded%20cluster%20works.%20If%20it%20does%2C%0Ayou%20can%20remove%20the%20old%20cluster%20with%0A%0Apg_dropcluster%209.4%20main” message=”” highlight=”” provider=”manual”/]

Supprimer l’ancienne version

Si la copie s’est bien passée. Vous pouvez supprimer l’ancienne version. Elle est maintenant entièrement copié dans le cluster main 9.6.

# pg_dropcluster 9.4 main

Mettre en ligne ses parties !

Je suis venu vous conter une histoire. Cette histoire est issue d’un dĂ©sir coupable de frimer un peu.
Non, je n’ai pas rĂ©ussi Ă  faire quelque chose d’exceptionnel ou d’impossible comme inventer une ampoule qui dure 10 fois plus longtemps que toutes les autres.
Je souhaite juste montrer comment GNU/Linux, le système D et la philosophie OpenSource/libre/DIY peuvent permettre d’automatiser un processus de production vidĂ©o.

Contexte:

Je suis dĂ©veloppeur de Rolisteam. J’avais besoin de faire un peu la promotion du logiciel. De plus, en tant que maĂ®tre de jeu, j’avais envie de garder une trace de ma campagne, histoire
d’en faire profiter d’autres gens.
L’enregistrement des parties en vidĂ©o et audio nous a semblĂ© la meilleure solution pour atteindre ces objectifs.

Le choix des outils:

Après une phase de recherche, mon choix s’est porté sur:

SimpleScreenRecorder (SSR)

Il est pratique, simple et la qualitĂ© vidĂ©o est plutĂ´t bonne. Le seul problème c’est l’enregistrement audio. Impossible d’enregistrer en mĂŞme temps mon micro et
les voix de mes camarades Ă  moins de configurer pulseaudio ou l’usage de Jack. Je n’avais pas envie de changer toute la configuration son de ma bĂ©cane. C’est peut-ĂŞtre facile Ă  faire mais j’avais pas envie de me lancer la dedans. De plus, la qualitĂ© audio de SSR est moins bonne que Teamspeak (je trouve en tout cas).

Teamspeak:

Ok, ce n’est pas libre mais pour des raisons de qualitĂ© et d’habitude des joueurs, on est restĂ© sur cette solution pour l’audio.
Les fruits de l’enregistrement

Grâce Ă  ces outils, j’ai rĂ©alisĂ© les enregistrements.
J’avais donc un fichier son et une vidĂ©o par partie. Dans un premier temps, j’ai fusionnĂ© les deux fichiers avec ffmpeg pour obtenir la vidĂ©o de la partie (avec le son).

ffmpeg -i video.mp4 -i audio.wav -c:v copy -c:a aac -strict experimental output.mp4

Améliorer l’expérience visuelle

Souhaitant offrir une meilleure expĂ©rience pour les Ă©ventuels spectateurs, j’ai codĂ© un plugin Ă  teamspeak qui envoie sur dbus le statut de la voix de chaque joueur.

J’ai crĂ©e une application qui Ă©coute ces messages dbus, quand le joueur parle, le portrait de son personnage s’affiche en couleur. Quand il est silencieux le portrait du personnage s’affiche en niveau de gris. Il m’a fallu plusieurs tests et Ă©tapes pour arriver Ă  ce fonctionnel final.
Les joueurs ne voulant pas montrer leur visage par webcam. Cela semblait la meilleur solution de suivre facilement les conversations.

Vous trouverez le code ici: https://github.com/obiwankennedy/GameVisualisationHelper/tree/cops/display

Cette application a apportĂ© une contrainte supplĂ©mentaire sur les vidĂ©os. Le son et l’image doivent ĂŞtre synchronisĂ©s avec prĂ©cision. Ce n’était pas le cas avant car les Ă©lĂ©ments de l’écran pouvait ĂŞtre en avance ou en retard par rapport Ă  la voix, il n’y avait aucun repère visuel pour le remarquer.

Caler le son et l’image

J’ai d’abord pensĂ© merger les deux fichiers dans un Ă©diteur vidĂ©o.
J’ai essayĂ© PitiVi, OpenShot et Kdenlive. Les deux premiers agonisent dans d’atroces souffrances après le chargement de fichiers supĂ©rieurs Ă  3h. Kdenlive s’en sort mieux. Il n’agonise qu’une fois sur deux. J’ai installĂ© les versions de ma distribution et j’ai fait de report de bug mais je pouvais pas attendre la rĂ©solution des problèmes.

Dans ce contexte, trouver le bon timing pour synchroniser l’audio sur l’image, c’est compliquĂ©. Le drag and drop de fichier de 3h fait assez mal aux logiciels d’Ă©dition vidĂ©o. Sans parler de la prĂ©cision pour les dĂ©placements. Bref, pas pratique.

Je me suis dit “c’est idiot, il faudrait synchroniser le dĂ©but de l’enregistrement de l’audio et la vidĂ©o”.
Je n’ai pas le code source de Teamspeak mais il est possible de crĂ©er un plugin (ou de modifier celui que j’ai créé) et j’ai le code source de SimpleScreenRecorder (SSR).

J’ai donc dĂ©cidĂ© d’exposer l’API de SSR sur DBUS et mon plugin teamspeak envoie des commandes dbus. Vive Dbus !
En gros, j’ai Ă©tudiĂ© le code de SSR pour identifier la fonction qui dĂ©marre l’enregistrement. J’ai créé la petite tambouille pour l’exposer (ainsi que la fonction pour mettre l’enregistrement en pause) sur dbus.

Si vous voulez voir comment faire cela: http://renaudguezennec.eu/index.php/2011/03/10/introduction-a-dbus-avec-qt4/

Pour le coup, après quelques essais et des modifications sur l’ensemble des participants Ă  l’affaire. J’ai une solution qui tourne bien. Je peux utiliser ffmpeg pour fusionner mes fichiers vidĂ©os avec le son et cela correspond parfaitement.

ffmpeg -i video.mp4 -i audio.wav -c:v copy -c:a aac -strict experimental output.mp4

Une étape d’accomplie

J’ai fait un pull request Ă  l’auteur de SSR. Mon but Ă©tait de montrer comment faire car l’auteur de SSR n’est pas formĂ© Ă  Dbus. Il est très intĂ©ressĂ© mais clairement je n’ai pas le temps de gĂ©nĂ©raliser l’usage de Dbus dans SSR, ni lui d’ailleurs.

Ma version est accessible ici : https://github.com/obiwankennedy/ssr
Ma pull request : https://github.com/MaartenBaert/ssr/pull/399

Montage des génériques

Après ces Ă©tapes, j’ai des vidĂ©os de mes parties assez brutes. IdĂ©alement, il me reste Ă  ajouter un gĂ©nĂ©rique de dĂ©but et de fin.

Pour le faire, j’ai fait un programme en QML avec deux animations qui se courent après. C’est pas jolie mais cela fait le job. Le gĂ©nĂ©rique dure ~10 secondes, je l’ai enregistrĂ© avec SSR aussi.
J’ai utilisĂ© Kdenlive pour caler une musique libre dessus.

A la fin de cette Ă©tape, j’ai ma petite vidĂ©o de gĂ©nĂ©rique de dĂ©but, idem pour le gĂ©nĂ©rique de fin et mes Ă©pisodes (plus de 75).
Vous l’avez compris, l’Ă©tape ici est de crĂ©er des vidĂ©os contenant les gĂ©nĂ©riques.
J’ai cherchĂ© un peu dans ffmpeg pour arriver Ă  cela. C’est une simple fonction de concatĂ©nation des vidĂ©os.
J’ai Ă©crit ma petite commande, ça marche.

[pastacode lang=”bash” manual=”cd%20%2Fracine%2Fdes%2Fvideos%2F%0AOPENING%3D%2Fchemin%2Fvers%2Fgenerique%2Fdebut.mp4%0AENDING%3D%2Fchemin%2Fvers%2Fgenerique%2Ffin.mp4%0Avideo%3D%2Fchemin%2Fvers%2Fvideos.mp4%0ALIST_FILE%3D%2Ftmp%2Fmylist.txt%0AvideoExtLess%3D%5C%60echo%20%24video%20%7C%20awk%20-F%20′.’%20’%7Bprint%20%241%7D’%5C%60%0Aecho%20%22file%20’%24OPENING’%22%20%3E%20%24LIST_FILE%0Aecho%20%22file%20’%24video’%22%20%3E%3E%20%24LIST_FILE%0Aecho%20%22file%20’%24ENDING’%22%20%3E%3E%20%24LIST_FILE%0Affmpeg%20-safe%200%20-f%20concat%20-i%20%2Ftmp%2Fmylist.txt%20%24%7BvideoExtLess%7D_avec_generiques.mp4″ message=”Concat avec ffmpeg” highlight=”” provider=”manual”/]

Du moins, je croyais que cela marchait. En vérité, cela cassait la synchronisation son/image. Ce fut très embêtant.
Je retourne Ă  la case dĂ©part “logiciel de montage vidĂ©o” (Kdenlive). Il y a un peu moins de manipulation prĂ©cise Ă  faire. Je colle les trois fichiers: gĂ©nĂ©rique de dĂ©but, l’épisode, gĂ©nĂ©rique de fin et c’est parti.
Ça a fonctionné un temps.

Extraction du son et traitement

Une fois la vidĂ©o complète avec les gĂ©nĂ©riques. J’en extrait le son pour la diffusion en podcast, vraiment facile avec ffmpeg.

ffmpeg -i videos.mp4 -codec:a libmp3lame -qscale:a 2 output.mp3

Avant de sortir l’Ă©pisode en podcast, je le re-travaille un peu avec audacity pour supprimer les silences et pour normaliser le son (avec le filtre compresseur du logiciel).
Cela raccourcit l’épisode d’environ 30 mins et le son est bien meilleur.

Traitement sur la vidéo

Je souhaite effectuer les mĂŞmes traitements sur les vidĂ©os. J’ai bien luttĂ© pour trouver une solution.

Normalisation

Pour la normalisation, j’ai trouvĂ© un petit script python: ffmpeg-normalize.

ffmpeg-normalize -vu -p normalized-episode40.mp4 Episode_40.mp4

Supprimer les moments inutiles

Couper la vidĂ©o quand il y a du silence fut bien plus complexe. Aucun logiciel de montage vidĂ©o n’offre de filtre pour cela (je n’ai pas trouvĂ© en tout cas) et j’allais pas le faire Ă  la main. Cela m’aurait pris trop de temps.

J’ai donc cherchĂ© Ă  droite Ă  gauche et c’est Ryzz (Merci Ă  lui) sur Linuxfr.org qui m’a envoyĂ© vers une bonne piste: MoviePy

Un module python pour faire de l’Ă©dition vidĂ©o. Il y a mĂŞme un exemple utilisant MoviePy pour crĂ©er le rĂ©sumĂ© d’un match de foot grâce au son des supporters.

J’ai donc créé un script python pour couper les silences. J’en ai profitĂ© Ă©galement ajouter les gĂ©nĂ©riques avec MoviePy.
Quand mon script fut prĂŞt j’avais dĂ©jĂ  sorti 20 Ă©pisodes. J’en avais donc 40 en stock Ă  refaire. J’ai donc lancer le script sur les 40 restant. Après 3 jours d’exĂ©cution. J’avais
tous mes épisodes prêts.

Si vous souhaitez voir le code du script et des explications techniques:
http://renaudguezennec.eu/index.php/2017/03/03/montage-video-en-python/

La réduction des silences est moins efficace qu’audacity mais il y a un gain indéniable.

VoilĂ , le degrĂ©s maximum que j’ai atteins dans l’automatisation.

Aller encore plus loin

Dans tout ce processus, il me manque encore des choses pour vraiment automatiser toute la chaîne.

Le premier point, c’est audacity. Il n’est pas possible d’utiliser audacity en ligne de commande. Je me suis un peu renseignĂ©, il y a eu des tentatives mais c’est très complexe Ă  mettre en place.
J’ai regardĂ© un peu le code, espĂ©rant trouver un moyen d’appliquer les deux filtres dont j’ai besoin. Le code est peu lisible avec des define partout pour diffĂ©rencier les OS. Bref, un cauchemar Ă  maintenir. Du coup, j’hĂ©site Ă  mis mettre vraiment ou rester en Ă©dition manuelle.

Ensuite, il me reste à automatiser la partie “mise en ligne” sur youtube et sur le wordpress.
Je suis certains que c’est possible mais pour l’instant, je n’ai pas pris le temps de le faire.
Si vous voulez voir le résultat:

Le wordpress pour écouter les épisodes: http://blog.rolisteam.org/
La playlist youtube des épisodes : https://www.youtube.com/playlist?list=PLBSt0cCTFfS5fi3v1LtB9sfeA8opY-Ge1
Les premiers n’ont pas bénéficié de tous les outils. Il y a clairement une marque de progression dans les épisodes jusqu’à l’épisode 20 environ.

Montage Vidéo en python

Mon besoin

Dans le cadre de la promotion de Rolisteam, je diffuse en ligne des enregistrements de parties.
Ces enregistrements nécessitent différents traitement afin d’être rendu plus audibles et intéressants.
Les taches à réaliser sont les suivantes:

1 – Associer la piste audio et la piste vidĂ©o
2 – Ajouter le gĂ©nĂ©rique de dĂ©but et de fin.
3 – Couper la vidĂ©o en fonction des silences
4 – AmĂ©liorer le son (le compresser/normaliser)

Pour réaliser la tache 1, une simple commande ffmpeg suffit. J’ai facilement créé un script bash pour l’automatiser.
La tâche 2 peut également être réalisée par ffmpeg avec l’option concat mais cela ne s’est pas passé comme prévu.
La solution de replie fut kdenlive, un logiciel de montage vidéo sur linux (Un des rares qui ne plante pas tout le temps quand on lui donne à monter une vidéo de trois heures).

L’étape 3 fut bien plus complexe à réaliser. Il n’y a aucun outil clé en main pour faire cela dans ffmpeg ou kdenlive. C’est très probablement faisable avec ces outils mais je n’ai pas trouvé comment. Je ne me voyais pas couper les moments de silence à la main.
Je commençais à désespérer quand Ryzz sur Linuxfr.org a évoqué le package MoviePy. Un module de manipulation vidéo pour python avec un exemple d’emploi proche de mon objectif.

Créer un résumé automatique d’un match de foot: http://zulko.github.io/blog/2014/07/04/automatic-soccer-highlights-compilations-with-python/
La doc de l’API: http://zulko.github.io/moviepy/index.html

Le code python: MoviePy

Voila un outil pour manipuler le son et la vidéo en codant avec python. Outil parfait pour réaliser les actions 2 et 3 de façon automatique.
Mes fichiers d’entrées sont biens rangés dans des dossiers, cela rend l’automatisation plus facile.

Pour la dernier Ă©tape, j’ai trouvĂ© un petit script python “ffmpeg-normalize” qui fait cela. Ce n’est clairement pas aussi puissant que les filtres d’audacity mais cela suffit.

Première étape, Parcourir les dossiers, trouver fichier associant la vidéo et le son pour y ajouter les génériques.

[pastacode lang=”python” manual=”for%20subfolder%20in%20sorted(os.listdir(rootFolder))%3A%0Aif(%22_done%22%20not%20in%20subfolder)%3A%0Afor%20subfile%20in%20os.listdir(os.path.join(rootFolder%2Csubfolder))%3A%0Aif((%22mp4%22%20in%20subfile)and(%22Partie%22%20in%20subfile))%3A%0Alink%20%3D%20os.path.join(rootFolder%2Csubfolder)%0Avideo%20%3D%20os.path.join(link%2Csubfile)%0Adest%20%3D%20subfile.replace(%22.mp4%22%2C%22_ending.mp4%22%2C1)%0Adestination%20%3D%20os.path.join(link%2Cdest)%0Avideoclip%20%3D%20VideoFileClip(video)%0A%23concatenation%20of%20opening%2C%20video%20and%20ending%0Afinalclip%20%3D%20concatenate_videoclips(%5Bopening%2Cvideoclip%2Cending%5D)%0Afinalclip.write_videofile(destination%2Cfps%3D25)” message=”Parcours des dossiers” highlight=”” provider=”manual”/]

Je ne fais que reconstruire le chemin pour arriver jusqu’à l’épisode.

Ensuite, l’autre partie intéressante (grandement inspiré de l’exemple sur le foot), est de découper le film en échantillon d’une seconde. Le volume de chaque échantillon est calculé.
Il faut stocké le volume de chaque échantillon.

[pastacode lang=”python” manual=”%23split%20resulting%20video%20in%20audio%20subclip%0Aclip%20%3D%20VideoFileClip(destination)%0Acut%20%3D%20lambda%20i%3A%20clip.audio.subclip(i%2Ci%2B1).to_soundarray(fps%3D22000)%0Avolume%20%3D%20lambda%20array%3A%20np.sqrt(((1.0*array)**2).mean())%0Avolumes%20%3D%20%5Bvolume(cut(i))%20for%20i%20in%20range(0%2Cint(clip.audio.duration-2))%5D%0Afinal_times%3D%20%5B%5D” message=”Les volumes” highlight=”” provider=”manual”/]

Le dernière étape consiste à regrouper les périodes de temps qui doivent être sauvegardé. Dans un tableau à deux éléments, je conserve le début et la fin de chaque période à conserver.

[pastacode lang=”python” manual=”i%20%3D%201%0Aduo%20%3D%20%5B%5D%0Astart%20%3D%20-1%0Aend%20%3D%200%0AsumVideo%20%3D%200%0A%23identify%20all%20part%20with%20sounds.%20What%20we%20keep.%0Afor%20vol%20in%20volumes%3A%0Aif((%20vol%20%3D%3D%200.0%20)and%20(start!%3D-1))%3A%0Aend%20%3D%20i-1%0Aduo%20%3D%20%5Bstart%2Cend%5D%0Afinal_times.append(duo)%0AsumVideo%20%2B%3D%20(end-start)%0Astart%20%3D%20-1%0Aif((start%20%3D%3D%20-1)and%20(vol%3E0.0))%3A%0Astart%3Di%0Ai%3Di%2B1″ message=”SĂ©paration des duos” highlight=”” provider=”manual”/]

Quand l’ensemble des duos sont identifiés, il suffit de les concatener dans le fichier de sortie.

[pastacode lang=”python” manual=”finalpath%20%3D%20destination.replace(%22_ending%22%2C%22_cutted%22)%0Aprint%20finalpath%0Aprint%20final_times%0A%23concatenate%20all%20kept%20parts.%0Afinal%20%3D%20concatenate_videoclips(%5Bclip.subclip(t%5B0%5D%2Ct%5B1%5D)%0Afor%20t%20in%20final_times%5D)%0A%23write%20the%20file%0Afinal.write_videofile(finalpath%2Cfps%3D25)” message=”Sauvegarde des Ă©chantillons” highlight=”” provider=”manual”/]

Code complet: http://www.renaudguezennec.eu/file/cutVideos.py

Créer un WebService en C++ avec Qt

Un petit article pour évoquer la possibilité de faire un webservice, en C++/Qt.

Mon besoin est simple, je veux fournir une interface web pour réaliser des lancer de dés avec le système de lancement de dés DiceParser, issu de Rolisteam.

DiceParser est écrit en C++ avec Qt5. A partir de ce constat, je pouvais soit faire un serveur web en C++, soit faire un site web en python/php/whatever pour lancer une commande système et utiliser dice, le client en ligne de commande de DiceParser.

La deuxième solution est  la plus facile mais clairement un peu «sale» et niveau sécurité ce n’est pas idéal. Le «challenge» se trouve dans la première méthode.
Je n’avais pas envie de rĂ©inventer la roue donc j’ai cherchĂ© une solution technique pour satisfaire mon besoin. Un composant C++/Qt qui permet de crĂ©er un serveur http avec possibilitĂ© d’ĂŞtre notifier Ă  chaque requĂŞte. J’ai trouvĂ© un composant qui rĂ©alise cela.

Le dépôt git du composant : https://github.com/azadkuh/qhttp.git

Implémentation du serveur

[pastacode lang=”cpp” manual=”%20%20m_server%20%3D%20new%20qhttp%3A%3Aserver%3A%3AQHttpServer(this)%3B%0A%20%20%20%20m_server-%3Elisten(%20%2F%2F%20listening%20on%200.0.0.0%3A8080%0A%20%20%20%20%20%20%20%20%20%20%20%20QHostAddress%3A%3AAny%2C%20port%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B%3D%5D(qhttp%3A%3Aserver%3A%3AQHttpRequest*%20req%2C%20qhttp%3A%3Aserver%3A%3AQHttpResponse*%20res)%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20req-%3EcollectData(1024)%3B%0A%0A%09%09%2F%2F%20Ici%20mettre%20le%20code%20d’analyse%20de%20la%20requ%C3%AAte%0A%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20if%20(%20!m_server-%3EisListening()%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20qDebug()%20%3C%3C%20%22failed%20to%20listen%22%3B%0A%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20else%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20qDebug()%3C%3C%20%22Server%20is%20On!!%22%3B%0A%20%20%20%20%7D” message=”Instancier le serveur web” highlight=”” provider=”manual”/]

Nous crĂ©ons une instance du serveur, nous dĂ©marrons l’Ă©coute sur le port (80 par dĂ©faut pour le protocole http).
L’Ă©coute est rĂ©alisĂ© par une fonction lambda, cela n’a rien d’obligatoire, mais c’est plus minimaliste.

Notre fonction est notifiĂ©e Ă  la rĂ©ception d’une requĂŞte http.

RĂ©cupĂ©rer les informations d’une requĂŞte

[pastacode lang=”cpp” manual=”%2F%2F%20R%C3%A9cup%C3%A8re%20les%20donn%C3%A9es%0Areq-%3EcollectData(1024)%3B%0A%0A%2F%2F%20R%C3%A9cup%C3%A8re%20l’url%20(les%20donn%C3%A9es%20du%20GET)%0AQString%20getArg%20%3D%20req-%3Eurl().toString()%3B%0A%0A%2F%2F%20D%C3%A9coupage%20des%20arguments%0AgetArg%3DgetArg.replace(%22%2F%3F%22%2C%22%22)%3B%0AQStringList%20args%20%3D%20getArg.split(‘%26’)%3B%0AQHash%3CQString%2CQString%3E%20m_hashArgs%3B%0Afor(%20auto%20argument%20%3A%20args)%0A%7B%0A%20%20%20%20QStringList%20keyValue%20%3D%20argument.split(‘%3D’)%3B%0A%20%20%20%20if(keyValue.size()%3D%3D2)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20m_hashArgs.insert(keyValue%5B0%5D%2CkeyValue%5B1%5D)%3B%0A%20%20%20%20%7D%0A%7D%0A%0A%2F%2F%20recherche%20dans%20la%20table%20des%20arguments%20la%20pr%C3%A9sence%20de%20fonction.%0Aif(m_hashArgs.contains(%22cmd%22))%0A%7B%0A%09%2F%2F%20r%C3%A9pondre%20%C3%A0%20la%20requ%C3%AAte%20reconnue.%0A%7D” message=”DĂ©coupage” highlight=”” provider=”manual”/]

Ici, nous analysons la requête pour identifier les actions à réaliser. Nous cherchons le paramètre cmd, pour trouver la commander à identifier. Le travail préalable est des créer une tableau associatif (dans une QHash) pour conserver les paramètres et leur valeur.

Une fois la commande de dĂ©s identifiĂ©s, il faut l’exĂ©cuter.

Exécuter la commande et répondre

[pastacode lang=”cpp” manual=”%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20QString%20result%20%3D%20startDiceParsing(QUrl%3A%3AfromPercentEncoding(m_hashArgs%5B%22cmd%22%5D.toLocal8Bit()))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20res-%3EsetStatusCode(qhttp%3A%3AESTATUS_OK)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20res-%3EaddHeader(%22Access-Control-Allow-Origin%22%2C%20%22*%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20res-%3EaddHeader(%22Access-Control-Allow-Methods%22%2C%20%22POST%2C%20GET%2C%20OPTIONS%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20res-%3EaddHeader(%22Access-Control-Allow-Headers%22%2C%20%22x-requested-with%22)%3B%0A%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20QString%20html(%22%3C!doctype%20html%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Chtml%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Chead%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%20%20%3Cmeta%20charset%3D%5C%22utf-8%5C%22%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%20%20%3Ctitle%3ERolisteam%20Dice%20System%20Webservice%3C%2Ftitle%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%20%20%3Cstyle%3E.dice%20%7Bcolor%3A%23FF0000%3Bfont-weight%3A%20bold%3B%7D%3C%2Fstyle%3E%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3C%2Fhead%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Cbody%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%251%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3C%2Fbody%3E%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3C%2Fhtml%3E%5Cn%22)%3B%0A%0Ares-%3Eend(html.arg(result).toLocal8Bit())%3B” message=”” highlight=”” provider=”manual”/]

La première action est de convertir la commande pour la transformer en données lisibles. Elle est actuellement encodé en mode pourcent. Qt fournit une méthode statique pour faire la conversion. Le resultat de cette conversion est ensuite envoyé à une fonction qui exécute la commande et donne le résultat.

Il faut ensuite générer la réponse. Le premier truc à définir est le code réponse du protocole http. Le module qhttp propose des raccourcis pour cela.

Ensuite, la rĂ©ponse doit contenir 3 paramètres dans les headers afin d’ĂŞtre accessible par des frameworks Ajax/javascript/Web 2.0 (vous ressentez mon dĂ©dain envers ces techno ?).
Après la définition des headers, il faut créer le code html (ou autre) de la réponse.

Créer une page web cliente du service

[pastacode lang=”markup” manual=”%3C!DOCTYPE%20html%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%3Chtml%3E%20%20%20%20%20%20%0A%3Chead%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%3Cmeta%20charset%3D%22utf-8%22%3E%20%0A%3Cmeta%20name%3D%22Author%22%20content%3D%22Renaud%20GUEZENNEC%22%2F%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%3Ctitle%3ERolisteam%20Dice%20System%20Webservice%3C%2Ftitle%3E%20%20%0A%20%20%3Cstyle%3E.dice%20%7Bcolor%3A%23FF0000%3Bfont-weight%3A%20bold%3B%7D%3C%2Fstyle%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%3Cscript%20type%3D%22text%2Fjavascript%22%20src%3D%22js%2Fjquery-3.1.1.min.js%22%20%3E%3C%2Fscript%3E%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%3Cscript%20type%3D%22text%2Fjavascript%22%3E%20%20%0A%20%20%20%20function%20displayResult()%20%7B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20var%20request%20%3D%20%24.ajax(%7B%20%20%20%0A%20%20%20%20%20%20%20%20method%3A%20%22get%22%2C%20%20%20%20%0A%20%20%20%20%20%20%20%20url%3A%20%22http%3A%2F%2F127.0.0.1%3A8085%2F%22%2C%20%20%0A%20%20%20%20%20%20%20%20dataType%3A%20%22html%22%2C%20%20%0A%20%20%20%20%20%20%20%20async%3A%20false%2C%0A%20%20%20%20%20%20%20%20data%3A%20%7Bcmd%3A%20%24(‘%23cmd’).val()%7D%20%0A%20%20%20%20%20%20%7D)%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20request.done(function(data)%7B%20%20%0A%20%20%20%20%20%20%20%20var%20str%20%3D%22%3Cp%3E%22%20%0A%20%20%20%20%20%20%20%20str%20%3D%20str.concat(data%2C%22%3C%2Fp%3E%22)%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%24(‘.diceresult’).prepend(str)%3B%20%20%20%20%0A%20%20%20%20%20%20%7D)%3B%20%0A%20%20%20%20%7D%20%20%20%0A%20%20%3C%2Fscript%3E%20%20%0A%3C%2Fhead%3E%20%20%20%20%20%20%20%0A%3Cbody%3E%20%20%20%20%0A%3Cform%20method%3D%22POST%22%20action%3D%22%22%2F%3E%20%0A%3Cinput%20id%3D%22cmd%22%20name%3D%22cmd%22%2F%3E%20%20%0A%3Cinput%20id%3D%22roll%22%20name%3D%22roll%22%20type%3D%22button%22%20onclick%3D%22javascript%3AdisplayResult()%3B%20return%200%3B%22%20value%3D%22Roll%22%2F%3E%0A%3C%2Fform%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%3Ca%20href%3D%22index.html%22%3EClear%3C%2Fa%3E%20%0A%3Cdiv%20class%3D%22diceresult%22%3E%0A%3C%2Fdiv%3E%20%20%20%20%20%20%0A%3C%2Fbody%3E%20%0A%3C%2Fhtml%3E” message=”Page cliente” highlight=”” provider=”manual”/]

Le travail ici est assez simple. Il faut crĂ©er un formulaire pour permettre Ă  l’utilisateur de saisir la commande de dĂ©s. J’ai utilisĂ© JQuery comme framework javascript pour rĂ©cupĂ©rer la commande et envoyer la requĂŞte de mon webservice. Quand la requĂŞte s’est bien passĂ©e, le rĂ©sultat s’affiche dans la page sans recharger la page.

Le lien vers le code source: https://github.com/Rolisteam/DiceParser/tree/master/webserver
Le webservice fait partie de projet DiceParser: https://github.com/Rolisteam/DiceParser

rand() moi un entier !

Cet article est une synthèse de plusieurs conférences, de la documentation C++ et de mon expérience sur la génération de nombre aléatoire en C++.

Mon besoin

Je suis le dĂ©veloppeur de Rolisteam. Un logiciel pour faire du jeu de rĂ´le en ligne. Il intègre une solution badass (oui, je m’envoie des fleurs) de lancer de dĂ©s. J’ai passĂ© beaucoup de temps entre juillet 2014 et fĂ©vrier 2015 pour produire un langage de script interprĂ©tĂ© de lancement de commandes de dĂ©s. Vous pouvez y jeter un Ĺ“il sur ce lien: DiceParser. Bref, j’ai crĂ©e tout un système pour lancer les dĂ©s et faire des opĂ©rations sur les rĂ©sultats: tri, relance, filtre, explosion, arithmĂ©tique, gestion des prioritĂ©s mathĂ©matiques et il est mĂŞme capable de gĂ©nĂ©rer l’arbre d’exĂ©cution avec dot.

L’aléatoire

Pour la partie alĂ©atoire dans tout le système, j’ai rĂ©utilisĂ© le code historique de rolistik (l’ancĂŞtre de rolisteam).
Certains utilisateurs sont venus me voir pour me demander de vérifier le système d’aléatoire car ils avaient fait des tests et avez déterminé que Rolisteam favorisait les résultats haut; ou bas selon les personnes.
J’avais droit Ă  un tableau de statistique rĂ©alisĂ© sur quelques dizaines de lancer de dĂ©s.
Argument qu’il est facile de dĂ©truire avec une Ă©tude statistique plus riche en lancer. DiceParser fonctionne en ligne de commande, donc très facile Ă  «scripter». Bref, des gens avaient des doutes sans rĂ©elle preuve.
Du coup, je me suis un peu renseignĂ© s’il n’y avait pas des mĂ©thodes plus «C++» avec le C++11, C++14 et/ou C++17.

L’existant

Quand il s’agit de gĂ©nĂ©rer des nombres alĂ©atoires, la première implĂ©mentation qu’on apprend est souvent celle ci :

[pastacode lang=”cpp” manual=”int%20resultat%20%3D%20std::rand()%25MAX%2Bdebut%3B” message=”usage classique de rand();” highlight=”” provider=”manual”/]

Je pense que tous les Ă©tudiants ont implĂ©mentĂ© cette mĂ©thode, dans leurs premières annĂ©es d’Ă©tudes.  Elle est facile Ă  comprendre. Elle est disponible dans beaucoup de langages. Elle vient du C. Elle met en avant le modulo, ce qui lui donne peut-ĂŞtre une valeur pĂ©dagogique. Elle suffit dans de très nombreux cas.

Nous allons voir un peu ce qui cloche avec cette méthode.

Les défauts

1/ Pauvre entropie
DĂ©jĂ , rand() ou std::rand() sont dĂ©finis dans le “man” comme une solution pauvre pour obtenir de l’alĂ©atoire. Il est prĂ©cisĂ© que les vieilles implĂ©mentations de rand() ou les implĂ©mentations sur d’autres systèmes fournissent un alĂ©atoire pauvre sur les bits de poids faibles. Cela explique peut-ĂŞtre pourquoi certains utilisateurs sous Windows se plaignent.
Donc l’entropie de rand n’est pas bonne. Il existe une autre mĂ©thode: random() qui fonctionne normalement mieux, avec un pĂ©riode beaucoup plus grande.

2/ Erreur de répartition
Ces deux mĂ©thodes gĂ©nèrent un nombre entre 0 et RAND_MAX. Maintenant, nous souhaitons obtenir des rĂ©sultats sur un dĂ©s Ă  100 faces. (C’est un format de dĂ©s courant en Jeux de rĂ´le).
Imaginons que RAND_MAX vaut 32767.

Si je découpe la valeur maximum en tranche de 100 cela donne ceci:

[0 : 99] => 100 valeurs
[100 : 199] => 100 valeurs
[200 : 299] => 100 valeurs
:
:
[32700 : 32767 ] => 67 valeurs.

Nous avons 327 tranches complètes et une dernière de 67.  L’usage du modulo vient faire la correspondance entre la valeur tirĂ©e dans l’espace de valeurs dĂ©sirĂ©es. Cela favorise les rĂ©sultats entre 0 et 67. Il y a une chance supplĂ©mentaire de tomber en dessous de 67. La diffĂ©rence est probablement nĂ©gligeables mais tout de mĂŞme, cela me pose problème d’avoir cela dans mon logiciel.

3/ Problème de portabilité

La valeur RAND_MAX vaut au minimum 32767 dans la norme. Elle diffèrent en fonction des implĂ©mentations, nous avons vu Ă©galement que la portabilitĂ© de la mĂ©thode rand() n’est pas garantie.
Cette somme de problèmes vient conforter les utilisateurs de Rolisteam dans leur idée de problème relatif au système de dés. Deux problèmes de portabilité se cumulent.

Le constat de ces erreurs m’a poussĂ© Ă  chercher une solution moderne pour gĂ©nĂ©rer ces nombres.

Dans les API C++ est arrivĂ© un nouveau “module” avec le C++11. Il s’agit de random.

#include <random>

Ce module présente toute une série de classes pour générer des nombres aléatoires et les répartir.
Il propose un nombre intĂ©ressant d’algorithmes ou de moteurs diffĂ©rents pour la gĂ©nĂ©ration. Je ne suis pas un expert en gĂ©nĂ©ration pseudo-alĂ©atoire de nombre. Du coup, le nom des algorithmes ne me parle pas.
Cependant, il semble que deux éléments sortent du lot: le std::random_device et le std::mt19937.

Le std::random_device

Il est dĂ©fini dans la documentation comme: Un gĂ©nĂ©rateur de nombre alĂ©atoire non dĂ©terministe. En gros, cela signifie qu’on ne peut pas prĂ©voir son comportement.
D’habitude, vous initialisez votre mĂ©thode alĂ©atoire par un seed. Il peut ĂŞtre intĂ©ressant pour des tests automatiques ou une rĂ©solution de bug de pouvoir rejouer une sĂ©quence entière de nombre alĂ©atoire.
Pour atteindre cet objectif, il suffit de mettre la mĂŞme seed. Donc vous avez lĂ  une mĂ©thode de nombre alĂ©atoire qui est dĂ©terministe. random_device n’a pas de seed. Il n’a pas besoin d’ĂŞtre initialiser. Il est donc impossible de lui faire “jouer” deux fois la mĂŞme chose. Dans mon cas, cela me satisfait complètement.

C’est facile Ă  mettre en place:

[pastacode lang=”cpp” manual=”std%3A%3Arandom_device%20rd%3B%0Astd%3A%3Auniform_int_distribution%3Cint%3E%20dist(1%2C%2010)%3B%0Aint%20randomNumber%20%3D%20dist(rd)%3B%0Astd%3A%3Acout%20%3C%3C%20%20randomNumber%20%3C%3C%20std%3A%3Aendl%3B” message=”” highlight=”” provider=”manual”/]

On gĂ©nère une nombre et on le distribue entre 1 et 10. Cela suffit. Il y a rien besoin de plus. C’est propre, Ă©lĂ©gant etc.
J’ai implĂ©mentĂ© cette mĂ©thode dans rolisteam 1.8, malgrĂ© les avertissements qu’on trouve Ă  droit Ă  gauche. Cela donne de bons rĂ©sultats sous Linux. J’ai fait une version pour Windows et lĂ , c’est le drame.
L’entropie du module sous windows est de zĂ©ro. Cela signifie qu’il renvoie toujours le mĂŞme rĂ©sultat. Bref, les dĂ©s Ă©taient toujours de la mĂŞme valeur.
La solution du random_device ne marche pas. Elle n’est pas portable.

Le std::mt19937

Il reste donc la mĂ©thode du std::mt19937. Elle fait toujours partie de l’API de <random>. Voici l’implĂ©mentation finale dans rolisteam.

[pastacode lang=”cpp” manual=”%2F%2F%20dans%20le%20constructeur%20de%20ma%20classe%20d%C3%A9%0Aauto%20seed%20%3D%20std%3A%3Achrono%3A%3Ahigh_resolution_clock%3A%3Anow().time_since_epoch().count()%3B%0Am_rng%20%3D%20std%3A%3Amt19937(quintptr(this)%2Bseed)%3B%0A%0A%2F%2Fdans%20la%20fonction%20lancer%20(roll())%0Astd%3A%3Auniform_int_distribution%3Cqint64%3E%20dist(m_base%2Cm_faces)%3B%0Aqint64%20value%20%3D%20dist(m_rng)%3B” message=”L’implementation dans rolisteam.” highlight=”” provider=”manual”/]

On peut voir qu’elle est plus proche de l’ancienne mĂ©thode car Il est nĂ©cessaire de l’initialiser avec un seed. Pour cela, il est Ă©galement possible d’utiliser une API C++11: <chrono>.

[pastacode lang=”cpp” manual=”%20%23include%20%3Cchrono%3E%0A%E2%80%A6%0Aauto%20seed%20%3D%20std%3A%3Achrono%3A%3Ahigh_resolution_clock%3A%3Anow().time_since_epoch().count()%3B%0Am_rng%20%3D%20std%3A%3Amt19937(quintptr(this)%2Bseed)%3B” message=”Politique Agricole Commune” highlight=”” provider=”manual”/]

 

Autres méthodes:

Conclusion:

J’espère avoir fait le tour des nouvelles API pour la gĂ©nĂ©ration alĂ©atoire de nombres entiers en C++. Il est important de garder Ă  l’esprit que l’API <random> fournit bien sur bien plus de chose que ce que j’ai montrĂ©. Je n’ai pas fait d’étude statistique pour dĂ©montrer que c’est mieux maintenant. Dans tous les cas, j’ai implĂ©mentĂ© le device_random dans mon lecteur audio maison, et je dĂ©couvre plein de nouvelle musique de ma collection.

Les sources:

CppCon 2016: Cheinan Marks “I Just Wanted a Random Integer!”:
https://www.youtube.com/watch?v=4_QO1nm7uJs
Stephan T. Lavavej – rand() Considered Harmful:
https://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful

Distribuer une application Qt

Bonjour,

Si vous suivez ce blog, vous savez que je suis développeur C++/Qt et que je contribue beaucoup à Rolisteam (www.rolisteam.org). C’est un logiciel libre pour faire du jeu de rôle. Il vise un public assez large. Il est donc multi-plateforme. Il fonctionne sous GNU/Linux, windows et Mac Os. Je suis un linuxien convaincu mais je développe sur les autres systèmes pour Rolisteam.

L’aspect développement n’est pas vraiment un problème, grâce à Qt. Cela marche grosso modo bien. La chose devient plus périlleuse quand il s’agit de distribuer le logiciel.

Dans mon activité professionnelle, j’ai constaté,  qu’il est assez rare qu’un développeur connaisse les solutions pour distribuer facilement un logiciel. Je vais essayer ici d’expliquer comment faire. Du moins, comment je fais dans rolisteam et si vous avez d’autres astuces. N’hésitez pas à en faire part dans les commentaires.

Prédicat de base:

Vous avez un logiciel écrit en C++/Qt, il est géré par un fichier .pro. Certains systèmes de build propose des systèmes identiques, voir plus puissant mais plus complexe (e.g: cmake). Je me contente d’évoquer les outils Qt.

Pour GNU/Linux :

Pour ma part, j’ai opté pour une distribution source et la création de package Ubuntu. Grâce à Launchpad, il est possible de créer un dépôt pour son projet et de soumettre des versions. Il faut l’admettre, c’est très pratique. Il existe probablement des services pour faire cela pour d’autres distributions.

Des contributeurs ont créé des package pour d’autres distributions (fedora…). Pour arch-linux, j’ai crĂ©e un script de compilation (Ă  tester).

L’ensemble des outils utilisĂ©es ne sont pas des outils Qt. Cela n’est donc pas le propos de cet article.  Je me suis mis de cĂ´tĂ© la tâche de jeter un coup d’œil Ă  appimage. Cela pourrait ĂŞtre une solution intĂ©ressante.

Pour Windows:

Trouver tous les éléments

Une application a des dĂ©pendances. Sous windows, il est courant d’installer l’exĂ©cutable et ses dĂ©pendances dans un mĂŞme dossier. Il est parfois difficile d’identifier les dĂ©pendances. Rassurez-vous un outil magique a Ă©tĂ© créée. Il s’agit de “windeployqt.exe“. C’est un outil distribuĂ© avec Qt. Son but est de copier toutes les dĂ©pendances Qt d’un binaire dans un dossier.

[pastacode lang=”bash” manual=”windeployqt%20%20%2Fchemin%2Fvers%2Fmon%2Fexecutable.exe” message=”Commande Windeployqt” highlight=”” provider=”manual”/]

Cette commande va lire le binaire et va copier l’ensemble des dépendances identifiés à côté du binaire.

Par soucis de clarté, je préfère utiliser un répertoire propre et distinct du répertoire de compilation.

[pastacode lang=”bash” manual=”windeployqt%20–dir%20%2Fchemin%2Fvers%2Fun%2Fdossier%2Fpropre%20%2Fchemin%2Fvers%2Fmon%2Fapplication.exe” message=”Dossier sĂ©parĂ©” highlight=”” provider=”manual”/]

L’ajout du l’argument –dir permet de dĂ©finir ce «dossier de destination».

Une autre difficulté entre en jeu quand votre application utilise du QML. En effet, il convient de rajouter un argument à la commande afin de permettre à l’outil d’aller lire votre code QML pour trouver ses dépendances.

[pastacode lang=”bash” manual=”windeployqt%20–qmldir%20%2Fchemin%2Fvers%2Fle%2Fdossier%2Fdes%2Fqml%20–dir%20%2Fdossier%2Fpropre%20application.exe” message=”Distributer du QML” highlight=”” provider=”manual”/]

Et après ?

Une fois cette étape terminée, vous aurez dans votre dossier de destination l’ensemble des dépendances Qt (et QML) de votre projets.

Je vous mets un exemple pour un logiciel utilisant Qt5 et un peu de QML le tout fut compilé avec Visual Studio.

├── accessible/
├── audio/
├── bearer/
├── designer/
├── Enginio.dll
├── iconengines/
├── icudt52.dll
├── icuin52.dll
├── icuuc52.dll
├── imageformats
├── mediaservice
├── msvcp120.dll
├── msvcr120.dll
├── platforms/
│ └── qwindows.dll
├── playlistformats/
├── position/
├── printsupport/
├── qml1tooling/
├── qmltooling/
├── Qt5Core.dll
├── Qt5Declarative.dll
├── Qt5Gui.dll
├── Qt5Network.dll
├── Qt5OpenGL.dll
├── Qt5Qml.dll
├── Qt5Quick.dll
├── Qt5WebKit.dll
├── Qt5Widgets.dll
├── Qt5Xml.dll
├── Qt5XmlPatterns.dll
├── sensorgestures/
├── sensors/
├── sqldrivers/
├── unins000.dat
└── unins000.exe

Si votre projet utilise d’autres bibliothèques (zlib …), il convient de rajouter les .dll correspondantes dans le dossier. La dernière étape est d’inclure le .exe de votre application.
En double-clickant sur le .exe, il devrait se lancer. Si ce n’est pas le cas, une dll est probablement manquante.
Cette procédure est à faire après un changement de version de Qt. Il faut vous assurer que les dll embarquées soient de la bonne version.

A ce stade, il ne reste qu’à mettre en place l’outil de publication de votre application.

 

Publication

Pour cette étape, il y a principalement deux méthodes:

La méthode de l’archive Zip, il suffit d’archiver le dossier de destination dans une archive .zip. Ce fichier peut être extrait sur n’importe quel windows, et cela fonctionnera. Il faudra créer à la main les raccourcis et il n’y pas d’autre moyen pour dés-installer le logiciel que de supprimer le dossier extrait à la main.

L’autre méthode est d’utiliser un outil qui va créer un installeur. Les plus connus sont Inno setup et NSIS.
J’utilise pour ma part: InstallForge (http://installforge.net), il est graphique et simple.
Qu’importe l’outil choisi, il faudra lui dire d’installer tous les fichiers de votre dossier de destination (en respectant la hiérarchie des fichiers) chez vos utilisateurs. Ces outils permettent de créer automatiquement des scripts de désinstallation, ils créent également les raccourcis sur le bureau et la barre de démarrage. Ils permettent aussi d’afficher du contenu à l’utilisateur (licence, logo du logiciel) et cela lui permet de prendre des décisions, au cours de l’installation (si besoin).

Pour finir, après cette étape, vous aurez un installeur soit en mode .zip, soit un .exe. Il ne reste plus qu’à mettre cet installeur accessible à vos utilisateurs et c’est bon.

Pensez Ă  le tester quand mĂŞme avant de faire des annonces publiques. On sait jamais !

 

Pour Mac OS:

Pour MacOs, il existe un outil similaire qui s’appelle: macqtdeploy.
Il utilise les mêmes paramètres que la version windows, la seule différence réside dans l’utilisation d’un .app (en lieu et place du .exe).

[pastacode lang=”bash” manual=”macdeploy%20application.app%20-qmldir%3D%2FUser%2Fdossier%2Fvers%2Fcode%2Fqml%20″ message=”sur MacOs X” highlight=”” provider=”manual”/]

Bien sûr, cela peut être intégrer dans QtCreator comme étape de déployement.
Sur MacOs, il est possible de demander à Qt de compiler une application dans un bundle (c’est d’ailleurs la solution par défaut). Un bundle est un dossier se terminant par «.app». Dans l’environnement graphique de MacOs, un double click lance le logiciel qu’il contient. Il est également possible, d’explorer le contenu du bundle en ligne de commande (cela se comporte comme un dossier) ou par un click droit: «afficher le contenu du packet» en graphique.

Dans tous les cas, l’utilisation de la commande va venir placer l’ensemble des dépendances Qt dans le bundle. Pour être précis, les bibliothèques sont placées dans le dossier Contents/Frameworks du bundle. Les modules QML sont dans Resources/qml.

A l’instar de windows, il faut mettre également d’éventuelles autres bibliothèques dans le bundle. C’est une étape un peu technique. Pour rolisteam, j’utilise zlib, voilà le programme bash qui me permet de faire l’ajout et de bien définir les chemins.

[pastacode lang=”bash” manual=”%23!%2Fbin%2Fsh%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20Rolisteam%20%0A%23%20Script%20to%20deploy%20libz%20into%20rolisteam.%0A%23%0A%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%0Aecho%20’Display%20dependancies’%0Aotool%20-L%20rolisteam.app%2FContents%2FMacOS%2Frolisteam%0A%0A%0A%0Aecho%20’Create%20directory’%0Amkdir%20rolisteam.app%2FContents%2FFrameworks%2Flibz.framework%2F%0A%0Aecho%20’Copy%20lib%20binary’%0Acp%20%2Fusr%2Flib%2Flibz.1.dylib%20rolisteam.app%2FContents%2FFrameworks%2Flibz.framework%2F%0A%0Aecho%20’Rewrite%20path%20to%20the%20lib’%0Ainstall_name_tool%20-change%20%2Fusr%2Flib%2Flibz.1.dylib%20%40executable_path%2F..%2FFrameworks%2Flibz.framework%2Flibz.1.dylib%20rolisteam.app%2FContents%2FMacOS%2Frolisteam” message=”Ajouter une dĂ©pendances Ă  un bundle” highlight=”” provider=”manual”/]

En gros, il y a la copie du fichier en lui même puis, la modification de l’exécutable pour lui dire d’aller chercher zlib dans le bundle et non sur mon poste. Cette étape est très importante car sinon, votre bundle ne sera pas portable sur une autre machine.

Et après ?

Le fichier .app complet suffit pour ĂŞtre installĂ© sur une autre machine. Cependant, il est de coutume de distribuer un logiciel par un dmg. Cela permet de le compresser pour faciliter sa distribution et surtout MacOs permet de rendre graphique l’installation par un dmg.

Pour réaliser cela, j’utilise un petit utilitaire: node-appdmg. Il suffit d’un petit fichier de configuration en json pour définir les paramètres.

[pastacode lang=”javascript” manual=”%7B%0A%20%20%22title%22%3A%20%22Rolisteam%20%20-%20Mac%20Os%20X%22%2C%0A%20%20%22icon%22%3A%20%22rolisteam.icns%22%2C%0A%20%20%22background%22%3A%20%221500-rolisteam.png%22%2C%0A%20%20%22icon-size%22%3A%2080%2C%0A%20%20%22contents%22%3A%20%5B%0A%20%20%20%20%7B%20%22x%22%3A%20448%2C%20%22y%22%3A%20344%2C%20%22type%22%3A%20%22link%22%2C%20%22path%22%3A%20%22%2FApplications%22%20%7D%2C%0A%20%20%20%20%7B%20%22x%22%3A%20192%2C%20%22y%22%3A%20344%2C%20%22type%22%3A%20%22file%22%2C%20%22path%22%3A%20%22rolisteam.app%22%20%7D%0A%20%20%5D%0A%7D” message=”Configuration node-appdmg pour rolisteam” highlight=”” provider=”manual”/]

Vous lancez l’utilitaire avec le fichier de configuration et le nom de votre dmg pour obtenir votre dmg tout beau, tout propre.

node-appdmg dmg.json monapplication.dmg

Il ne reste qu’à distribuer votre dmg.

Conclusion

Nous venons de voir les solutions et les outils mis en place pour rolisteam.
Si vous avez des questions sur les outils,  ou si vous voulez en proposer d’autres, les commentaires sont là pour ça.

L5R présentation

Retours d’expérience d’une présentation QML

Pour préparer ma conférence à Pas Sage En Seine [FR], j’ai décidé de faire une présentation QML.
Cela offre un bien meilleur contrôle, bien plus de possibilité graphique (animations, intégration de code, 3D…) et de même de l’interactivité.
Bien sĂ»r, une prĂ©sentation QML demande plus de temps Ă  faire. Le but de l’article est ici d’expliquer les astuces que j’ai mises en place. EspĂ©rant gagner du temps la prochaine fois.

Le contenu de la présentation QML:

Comme vous pouvez le voir, la prĂ©sentation est une application C++ qui affiche du QML. Cette mĂ©thode permet de faciliter le contrĂ´le de la partie QML. Je m’en sers pour enregistrer des captures d’Ă©cran de la prĂ©sentation afin d’en faire un PDF. C’est le meilleur pour distribuer la prĂ©sentation ou dans le cas ou pour Ă©viter l’effet dĂ©mo.
Tout en haut, il y a les classes C++, les fichiers de management du projet et le fichier main.qml. Dans le rĂ©pertoire «pages», vous y trouverez les diffĂ©rentes slides de la prĂ©sentation. Dans rsrc, il y a l’ensemble des images nĂ©cessaire Ă  la prĂ©sentation.

├── cpphighlighter.cpp
├── cpphighlighter.h 
├── deployment.pri 
├── LICENSE 
├── main.cpp 
├── main.qml 
├── pages 
│ ├── 01_intro.qml 
│ ├── 02_presentation.qml 
│ ├── 03_jdr_et_rolisteam.qml 
│ ├── 043_Exemple_code_1.qml 
│ ├── 04_jdr_avantages_pb.qml 
│ ├── 05_avantage_jdr_virtuel.qml 
│ ├── 06_fonctionnalites_rolisteam.qml 
│ ├── 07_rolisteam_debut.qml 
│ ├── 08_Rolistik_a_Rolisteam.qml 
│ ├── 10_frise_chronologique.qml 
│ ├── 11_son_usage.qml 
│ ├── 12_son_fonctionnement.qml 
│ ├── 13_dice_parser.qml 
│ ├── 14_themes_audio_3_pistes.qml 
│ ├── 15_nouveaute_1_8.qml 
│ ├── 16_projet_avenir.qml 
│ ├── 17_reussites.qml 
│ ├── 18_les_lecons.qml 
│ ├── 19_objectif_rolisteam_libre.qml 
│ ├── 20_FAQ.qml 
├── pasSageEnSeine.pro 
├── pasSageEnSeine.pro.user 
├── qmlcontroler.cpp 
├── qmlcontroler.h 
├── qmlcontroler.ui 
├── qml.qrc 
├── README.md 
├── rsrc 
│ ├── all.png 
│ ├── cc.png 
│ └── chat.png

L’application C++

Il peut ĂŞtre utile de connaĂ®tre l’Ă©tat de la prĂ©sentation QML, et de pouvoir lire des notes sur chaque volet. Pour faire cela, j’ai Ă©cris une petite application C++.

le fichier main.cpp

Le premier objectif de l’application est d’afficher la prĂ©sentation QML. Il faut donc charger le QML et Ă©tablir des moyens de communication entre le monde QML et le C++. Comme vous pouvez le voir, je passe en paramètre la taille de la prĂ©sentation en paramètre du QML. Ce dernier Ă©lĂ©ment n’est pas obligatoire car par dĂ©faut j’utilise la taille de l’Ă©cran comme rĂ©fĂ©rence.

[pastacode lang=”cpp” manual=”%23include%20%3CQApplication%3E%0A%23include%20%3CQQmlApplicationEngine%3E%0A%23include%20%22qmlcontroler.h%22%0A%23include%20%3CQQmlContext%3E%0A%23include%20%3CQQuickTextDocument%3E%0A%0A%23include%20%22cpphighlighter.h%22%0A%0Aint%20main(int%20argc%2C%20char%20*argv%5B%5D)%0A%7B%0A%20%20%20%20QApplication%20app(argc%2C%20argv)%3B%0A%0A%20%20%20%20QQmlApplicationEngine%20engine%3B%0A%0A%20%20%20%20engine.rootContext()-%3EsetContextProperty(%22ScreenW%22%2C1280)%3B%0A%20%20%20%20engine.rootContext()-%3EsetContextProperty(%22ScreenH%22%2C720)%3B%0A%0A%20%20%20%20engine.load(QUrl(QStringLiteral(%22qrc%3A%2Fmain.qml%22)))%3B%0A%0A%20%20%20%20QmlControler%20ctr%3B%0A%20%20%20%20ctr.setEngine(%26engine)%3B%0A%0A%20%20%20%20return%20app.exec()%3B%0A%7D%0A” message=”main.cpp” highlight=”” provider=”manual”/]

Dans ce projet, la classe QmlControler est l’objet (fenĂŞtre) C++ qui affiche l’aperçu de la prĂ©sentation et les commentaires/notes.

Regardons en détails cet élément:

La fenĂŞtre d’aperçu

C’est une simple fenĂŞtre. Elle est dĂ©coupĂ©e en deux parties.  Celle de gauche affiche dans un QLabel l’aperçu de la prĂ©sentation QML. La partie de droite affiche les notes et commentaires sur la page courante dans un QTextArea.

[pastacode lang=”cpp” manual=”void%20QmlControler%3A%3AcurrentPageHasChanged(int%20i)%0A%7B%0A%20%20%20%20m_currentScreen%20%3D%20i%3B%0A%20%20%20%20QImage%20img%20%3D%20m_window-%3EgrabWindow()%3B%0A%0A%20%20%20%20if(img.isNull())%0A%20%20%20%20%20%20%20%20return%3B%0A%0A%20%20%20%20static%20int%20count%20%3D%200%3B%0A%0A%0A%20%20%20%20img.save(tr(%22screens%2F%251_screen.png%22).arg(%2B%2Bcount%2C3%2C10%2CQChar(‘0’))%2C%22png%22)%3B%0A%20%20%20%20qDebug()%20%3C%3C%20%22screen%20shot%20save%22%20%3C%3C%20count%3B%0A%0A%20%20%20%20m_ratioImage%20%3D%20(double)img.size().width()%2Fimg.size().height()%3B%0A%20%20%20%20m_ratioImageBis%20%3D%20(double)img.size().height()%2Fimg.size().width()%3B%0A%0A%20%20%20%20m_label-%3EsetPixmap(QPixmap%3A%3AfromImage(img))%3B%0A%0A%20%20%20%20if((i%2B1%3E%3D0)%26%26(i%2B1%3Cm_commentData.size()))%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20ui-%3EtextEdit-%3EsetHtml(m_commentData.at(i%2B1))%3B%0A%20%20%20%20%7D%0A%20%20%20%20resizeLabel()%3B%0A%7D” message=”Current slide has changed” highlight=”” provider=”manual”/]

Quand la page courante change, la fenĂŞtre C++ est informĂ© par l’appel du slot: currentPageHasChanged alors l’application rĂ©cupère une capture de la vue QML, l’affiche Ă  l’Ă©cran (dans la partie gauche), affiche les notes Ă  propos de la page courante et peut sauvegarder la capture dans un fichier.

Sauvegarder les captures dans des fichiers vous permet de crĂ©er un fichier PDF de votre prĂ©sentation. Il s’agit lĂ  d’une solution de secours en cas de problème avec le matĂ©riel de la prĂ©sentation ou une facilitĂ© pour distribuer votre prĂ©sentation.

La commande qui va bien (sous linux) :

$ convert *.png mypresentation.pdf

 

L’application QML

Le système de chargement (Loader)

Par souci de clartĂ©, je prĂ©fère avoir un fichier par page. Cela Ă©vite l’Ă©cueil d’un fichier QML long et complexe. L’application charge les pages en accord avec le modele de donnĂ©es principal dans le mĂŞme ordre.  Pour faire cela, j’ai utilisĂ© un ListModel. Une page est dĂ©finie par un titre, un nom de fichier, un temps (non utilisĂ©), et le nom de la page suivante.
Le fichier main.qml  affiche toutes les pages comme un «delegate» d’un pathview.  Toutes les pages sont chargĂ©s depuis le fichier de ressources de Qt (inclue dans le binaire grâce au système de ressources de Qt).

[pastacode lang=”css” manual=”ListModel%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20id%3A%20panelModel%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Intro%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20path%3A%20%2201_intro.qml%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20time%3A%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20next%3A%20%22Pr%C3%A9sentation%20de%20Rolisteam%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D” message=”First item of the model.” highlight=”” provider=”manual”/]

La donnée vraiment utile pour le chargement est bien sûr le nom du fichier.
Les autres sont là pour aider la rédaction ou la présentation.

Le loader fait son travail, voici les lignes importantes:

[pastacode lang=”css” manual=”%20%20%20%20PathView%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20view%0A%20%20%20%20%20%20%20%20anchors.fill%3A%20parent%0A%20%20%20%20%20%20%20%20model%3A%20panelModel%0A%20%20%20%20%20%20%20%20highlightRangeMode%3APathView.StrictlyEnforceRange%0A%20%20%20%20%20%20%20%20snapMode%3A%20PathView.SnapOneItem%0A%20%20%20%20%20%20%20%20delegate%3A%20%20Loader%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20source%3A%20%22pages%2F%22%2Bpath%0A%20%20%20%20%20%20%20%20%7D” message=”Path View” highlight=”source: “pages/”+path” provider=”manual”/]

Le temps Ă©tait destinĂ© Ă  afficher un compte Ă  rebours pour chaque slide mais je n’ai pas eu le temps de l’implĂ©menter.

Il est important de mettre dans le main.qml tous les Ă©lĂ©ments que vous souhaitez avoir sur toutes les pages: le sommaire, l’indication de la page courante sur le total de pages, etc.

Ajouter un sommaire

Pour rĂ©aliser un sommaire, j’ai ajoutĂ© un ListView avec le modele suivant:

[pastacode lang=”css” manual=”%20%20%20%20ListView%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20listView1%0A%20%20%20%20%20%20%20%20x%3A%20ScreenW*0.02%0A%20%20%20%20%20%20%20%20y%3A%20ScreenH*0.3%0A%20%20%20%20%20%20%20%20width%3A%20ScreenW%2F2%0A%20%20%20%20%20%20%20%20height%3A%20ScreenH*0.2%0A%20%20%20%20%20%20%20%20delegate%3A%20Item%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20ScreenW%2F2%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3A%20listView1.height%2FlistView1.count%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Text%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20view.currentIndex%3E%3Dindex%20%3F%20%22black%22%20%3A%20%22gray%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text%3A%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20font.pointSize%3A%20ScreenH%2F48%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20anchors.verticalCenter%3A%20parent.verticalCenter%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20font.bold%3A%20true%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20visible%3A%20view.currentIndex%3E0%20%3F%20true%20%3A%20false%0A%0A%20%20%20%20%20%20%20%20model%3A%20ListModel%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Concepts%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A1%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Chroniques%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Logiciel%22%2F%2Fsyst%C3%A8me%20de%20build%2C%20code%20sp%C3%A9cifique%20par%20OS.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A9%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Bilan%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A15%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D” message=”Table of contents in QML ” highlight=”” provider=”manual”/]

Le champs index du modèle permet de connaître la page qui démarre cette section du sommaire.

Page suivante

Quand il y a beaucoup de pages il peut ĂŞtre intĂ©ressant de connaĂ®tre la prochaine page afin de faire une petite transition vers elle. Dans mon cas, j’affiche dans le coin haut-droit le titre de la prochaine page. Ce fut assez utile pour les rĂ©pĂ©titions, je n’en ai presque pas eu besoin pendant la confĂ©rence.

[pastacode lang=”css” manual=”%20%20%20%20Text%20%7B%0A%20%20%20%20%20%20%20%20anchors.top%3A%20parent.top%0A%20%20%20%20%20%20%20%20anchors.right%3A%20parent.right%0A%20%20%20%20%20%20%20%20text%3A%20panelModel.get(view.currentIndex).next%2B%22%3E%22%0A%20%20%20%20%7D” message=”Next slide” highlight=”” provider=”manual”/]

L’autre possibilitĂ© est d’utiliser les notes dans la fenĂŞtre C++.

Créer une Page de présentation QML

Chaque page est indĂ©pendante, elles peuvent ĂŞtre totalement diffĂ©rentes cependant pour des raisons de simplicitĂ©, elle utilise une conception identique. Dans mon cas, elles sont constituĂ©es d’une ListView avec un modèle. Chaque Ă©lĂ©ment du modèle est un titre de section Ă  Ă©voquer pendant la prĂ©sentation.

Les Ă©lĂ©ments ont un index. Cet index est contrĂ´lĂ© par le clavier (flèche bas pour augmenter l’index, haut pour rĂ©duire l’index).  Grâce Ă  cet index, il est possible de cacher la liste et d’afficher une image, un schĂ©ma ou une vidĂ©o.

Par exemple, dans ma prĂ©sentation la fonctionnalitĂ© «Alias de dĂ©s» a pour index 10. Quand la index de la page devient 10, l’Ă©lĂ©ment «Alias de dĂ©s» apparaĂ®t avec une petite animation. Puis l’index passe Ă  11, j’affiche une image illustrant la fonctionnalitĂ© des alias de dĂ©s. A 12, l’image disparaĂ®t et le texte revient.

Position et taille

Pour s’assurer de la lisibilitĂ© des Ă©lĂ©ments, je rĂ©alise l’ensemble des calculs en rapport Ă  la taille de l’Ă©cran. Le positionnement est assurĂ© par des ancres (anchors en qml).

[pastacode lang=”css” manual=”%20%20%20%20Image%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20image1%0A%20%20%20%20%20%20%20%20anchors.left%3A%20parent.left%0A%20%20%20%20%20%20%20%20anchors.top%3A%20parent.top%0A%20%20%20%20%20%20%20%20anchors.leftMargin%3A%20ScreenW*0.04%0A%20%20%20%20%20%20%20%20fillMode%3A%20Image.PreserveAspectFit%0A%20%20%20%20%20%20%20%20source%3A%20%22qrc%3A%2Frsrc%2FRolisteam.svg%22%0A%20%20%20%20%20%20%20%20width%3A%20ScreenW*0.2%0A%20%20%20%20%7D” message=”Display the logo at the right position and size.” highlight=”” provider=”manual”/]

Autre piste

Il existe un module qui fournit des Ă©lĂ©ments QML pour faciliter la rĂ©alisation d’une prĂ©sentation. Je ne l’ai pas utilisĂ© mais il y a des choses intĂ©ressantes.

Module QML

Obtenir le code de mes présentations

Je vous invite Ă  cloner ce code : https://github.com/obiwankennedy/pses

Autre exemple avec une image: https://github.com/obiwankennedy/l5rSummary

L5R présentation QML

Exemple de présentation QML

 

 

Rolisteam Evolution

The function evolution…

I would like to show the evolution of one function in rolisteam code.
This function is called when the user (Game Master) closes a map or image.

The function must close the map or image and send a network order to each player to close the image or plan.

At the beginning, the function looked like that.
Some code line and comments were written in French. No coding rules were respected.
Low-level code for networking. The function was very long.

[pastacode lang=”cpp” message=”” highlight=”” provider=”manual” manual=”%20%2F%2F%20On%20recupere%20la%20fenetre%20active%20(qui%20est%20forcement%20de%20type%20CarteFenetre%20ou%20Image%2C%20sans%20quoi%20l’action%0A%20%20%20%20%20%20%20%20%2F%2F%20ne%20serait%20pas%20dispo%20dans%20le%20menu%20Fichier)%0A%20%20%20%20%20%20%20%20QWidget%20*active%20%3D%20workspace-%3EactiveWindow()%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Ne%20devrait%20jamais%20arriver%0A%20%20%20%20%20%20%20%20if%20(!active)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20qWarning(%22Close%20map%20action%20called%20when%20no%20widget%20is%20active%20in%20the%20workspace%20(fermerPlanOuImage%20-%20MainWindow.h)%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20On%20verifie%20pour%20le%20principe%20qu’il%20s’agit%20bien%20d’une%20CarteFenetre%20ou%20d’une%20Image%0A%20%20%20%20%20%20%20%20if%20(active-%3EobjectName()%20!%3D%20%22CarteFenetre%22%20%26%26%20active-%3EobjectName()%20!%3D%20%22Image%22)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20qWarning(%22not%20expected%20type%20of%20windows%20(fermerPlanOuImage%20-%20MainWindow.h)%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Creation%20de%20la%20boite%20d’alerte%0A%20%20%20%20%20%20%20%20QMessageBox%20msgBox(this)%3B%0A%20%20%20%20%20%20%20%20msgBox.addButton(QMessageBox%3A%3AYes)%3B%0A%20%20%20%20%20%20%20%20msgBox.addButton(QMessageBox%3A%3ACancel)%3B%0A%20%20%20%20%20%20%20%20msgBox.setIcon(QMessageBox%3A%3AInformation)%3B%0A%20%20%20%20%20%20%20%20msgBox.move(QPoint(width()%2F2%2C%20height()%2F2)%20%2B%20QPoint(-100%2C%20-50))%3B%0A%20%20%20%20%20%20%20%20%2F%2F%20On%20supprime%20l’icone%20de%20la%20barre%20de%20titre%0A%20%20%20%20%20%20%20%20Qt%3A%3AWindowFlags%20flags%20%3D%20msgBox.windowFlags()%3B%0A%20%20%20%20%20%20%20%20msgBox.setWindowFlags(flags%20%5E%20Qt%3A%3AWindowSystemMenuHint)%3B%0A%20%20%20%20%20%20%20%20%2F%2F%20M.a.j%20du%20titre%20et%20du%20message%0A%20%20%20%20%20%20%20%20if%20(active-%3EobjectName()%20%3D%3D%20%22CarteFenetre%22)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20msgBox.setWindowTitle(tr(%22Close%20Map%22))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20msgBox.setText(tr(%22Do%20you%20want%20to%20close%20this%20map%3F%5CnIt%20will%20be%20closed%20for%20everybody%22))%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20else%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20msgBox.setWindowTitle(tr(%22Close%20Picture%22))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20msgBox.setText(tr(%22Do%20you%20want%20to%20close%20this%20picture%3F%5CnIt%20will%20be%20closed%20for%20everybody%22))%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20msgBox.exec()%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Si%20l’utilisateur%20n’a%20pas%20clique%20sur%20%22Fermer%22%2C%20on%20quitte%0A%20%20%20%20%20%20%20%20if%20(msgBox.result()%20!%3D%20QMessageBox%3A%3AYesRole)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Emission%20de%20la%20demande%20de%20fermeture%20de%20la%20carte%0A%20%20%20%20%20%20%20%20if%20(active-%3EobjectName()%20%3D%3D%20%22CarteFenetre%22)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Recuperation%20de%20l’identifiant%20de%20la%20carte%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20QString%20idCarte%20%3D%20((CarteFenetre%20*)active)-%3Ecarte()-%3EidentifiantCarte()%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Taille%20des%20donnees%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20quint32%20tailleCorps%20%3D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Taille%20de%20l’identifiant%20de%20la%20carte%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sizeof(quint8)%20%2B%20idCarte.size()*sizeof(QChar)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Buffer%20d’emission%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20char%20*donnees%20%3D%20new%20char%5BtailleCorps%20%2B%20sizeof(enteteMessage)%5D%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Creation%20de%20l’entete%20du%20message%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20enteteMessage%20*uneEntete%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete%20%3D%20(enteteMessage%20*)%20donnees%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete-%3Ecategorie%20%3D%20plan%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete-%3Eaction%20%3D%20fermerPlan%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete-%3EtailleDonnees%20%3D%20tailleCorps%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Creation%20du%20corps%20du%20message%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int%20p%20%3D%20sizeof(enteteMessage)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Ajout%20de%20l’identifiant%20de%20la%20carte%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20quint8%20tailleIdCarte%20%3D%20idCarte.size()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20memcpy(%26(donnees%5Bp%5D)%2C%20%26tailleIdCarte%2C%20sizeof(quint8))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%2B%3Dsizeof(quint8)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20memcpy(%26(donnees%5Bp%5D)%2C%20idCarte.data()%2C%20tailleIdCarte*sizeof(QChar))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%2B%3DtailleIdCarte*sizeof(QChar)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Emission%20de%20la%20demande%20de%20fermeture%20de%20la%20carte%20au%20serveur%20ou%20a%20l’ensemble%20des%20clients%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20emettre(donnees%2C%20tailleCorps%20%2B%20sizeof(enteteMessage))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Liberation%20du%20buffer%20d’emission%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20delete%5B%5D%20donnees%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Suppression%20de%20la%20CarteFenetre%20et%20de%20l’action%20associee%20sur%20l’ordinateur%20local%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20((CarteFenetre%20*)active)-%3E~CarteFenetre()%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Emission%20de%20la%20demande%20de%20fermeture%20de%20l’image%0A%20%20%20%20%20%20%20%20else%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Recuperation%20de%20l’identifiant%20de%20la%20carte%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20QString%20idImage%20%3D%20((Image%20*)active)-%3EidentifiantImage()%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Taille%20des%20donnees%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20quint32%20tailleCorps%20%3D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Taille%20de%20l’identifiant%20de%20la%20carte%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sizeof(quint8)%20%2B%20idImage.size()*sizeof(QChar)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Buffer%20d’emission%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20char%20*donnees%20%3D%20new%20char%5BtailleCorps%20%2B%20sizeof(enteteMessage)%5D%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Creation%20de%20l’entete%20du%20message%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20enteteMessage%20*uneEntete%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete%20%3D%20(enteteMessage%20*)%20donnees%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete-%3Ecategorie%20%3D%20image%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete-%3Eaction%20%3D%20fermerImage%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20uneEntete-%3EtailleDonnees%20%3D%20tailleCorps%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Creation%20du%20corps%20du%20message%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int%20p%20%3D%20sizeof(enteteMessage)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Ajout%20de%20l’identifiant%20de%20la%20carte%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20quint8%20tailleIdImage%20%3D%20idImage.size()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20memcpy(%26(donnees%5Bp%5D)%2C%20%26tailleIdImage%2C%20sizeof(quint8))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%2B%3Dsizeof(quint8)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20memcpy(%26(donnees%5Bp%5D)%2C%20idImage.data()%2C%20tailleIdImage*sizeof(QChar))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%2B%3DtailleIdImage*sizeof(QChar)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Emission%20de%20la%20demande%20de%20fermeture%20de%20l’image%20au%20serveur%20ou%20a%20l’ensemble%20des%20clients%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20emettre(donnees%2C%20tailleCorps%20%2B%20sizeof(enteteMessage))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Liberation%20du%20buffer%20d’emission%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20delete%5B%5D%20donnees%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Suppression%20de%20l’Image%20et%20de%20l’action%20associee%20sur%20l’ordinateur%20local%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20((Image%20*)active)-%3E~Image()%3B%0A%20%20%20%20%20%20%20%20%7D”/]

A big step forward, the networking code has been reworked.
So the readability is improved. It is also possible to see the beginning of polymorphism. MediaContener is used but not everywhere.

[pastacode lang=”cpp” message=”Version IntermĂ©diaire” highlight=”” provider=”manual” manual=”QMdiSubWindow*%20subactive%20%3D%20m_mdiArea-%3EcurrentSubWindow()%3B%0AQWidget*%20active%20%3D%20subactive%3B%0AMapFrame*%20bipMapWindow%20%3D%20NULL%3B%0A%0Aif%20(NULL!%3Dactive)%0A%7B%0A%0A%20%20%20%20QAction*%20action%3DNULL%3B%0A%0A%20%20%20%20Image*%20%20imageFenetre%20%3D%20dynamic_cast(active)%3B%0A%0A%20%20%20%20QString%20mapImageId%3B%0A%20%20%20%20QString%20mapImageTitle%3B%0A%20%20%20%20mapImageTitle%20%3D%20active-%3EwindowTitle()%3B%0A%20%20%20%20bool%20image%3Dfalse%3B%0A%20%20%20%20%2F%2Fit%20is%20image%0A%20%20%20%20if(NULL!%3DimageFenetre)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20m_pictureList.removeOne(imageFenetre)%3B%0A%0A%20%20%20%20%20%20%20%20mapImageId%20%3D%20imageFenetre-%3EgetMediaId()%3B%0A%20%20%20%20%20%20%20%20image%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20action%20%3D%20imageFenetre-%3EgetAction()%3B%0A%20%20%20%20%7D%0A%20%20%20%20else%2F%2Fit%20is%20a%20map%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20bipMapWindow%3D%20dynamic_cast(active)%3B%0A%20%20%20%20%20%20%20%20if(NULL!%3DbipMapWindow)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20mapImageId%20%3D%20bipMapWindow-%3EgetMediaId()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20action%20%3D%20bipMapWindow-%3EgetAction()%3B%0A%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20else%2F%2F%20it%20is%20undefined%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20QMessageBox%20msgBox(this)%3B%0A%20%20%20%20msgBox.setStandardButtons(QMessageBox%3A%3AYes%20%7C%20QMessageBox%3A%3ACancel%20)%3B%0A%20%20%20%20msgBox.setDefaultButton(QMessageBox%3A%3ACancel)%3B%0A%20%20%20%20msgBox.setIcon(QMessageBox%3A%3AInformation)%3B%0A%20%20%20%20msgBox.move(QPoint(width()%2F2%2C%20height()%2F2)%20%2B%20QPoint(-100%2C%20-50))%3B%0A%20%20%20%20Qt%3A%3AWindowFlags%20flags%20%3D%20msgBox.windowFlags()%3B%0A%20%20%20%20msgBox.setWindowFlags(flags%20%5E%20Qt%3A%3AWindowSystemMenuHint)%3B%0A%0A%20%20%20%20if%20(!image)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20msgBox.setWindowTitle(tr(%22Close%20Map%22))%3B%0A%20%20%20%20%7D%0A%20%20%20%20else%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20msgBox.setWindowTitle(tr(%22Close%20Picture%22))%3B%0A%20%20%20%20%7D%0A%20%20%20%20msgBox.setText(tr(%22Do%20you%20want%20to%20close%20%251%20%252%3F%5CnIt%20will%20be%20closed%20for%20everybody%22).arg(mapImageTitle).arg(image%3Ftr(%22%22)%3Atr(%22(Map)%22)))%3B%0A%0A%20%20%20%20msgBox.exec()%3B%0A%20%20%20%20if%20(msgBox.result()%20!%3D%20QMessageBox%3A%3AYes)%0A%20%20%20%20%20%20%20%20return%3B%0A%0A%20%20%20%20if%20(!image)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20NetworkMessageWriter%20msg(NetMsg%3A%3AMapCategory%2CNetMsg%3A%3ACloseMap)%3B%0A%20%20%20%20%20%20%20%20msg.string8(mapImageId)%3B%0A%20%20%20%20%20%20%20%20msg.sendAll()%3B%0A%0A%20%20%20%20%20%20%20%20m_mapWindowMap.remove(mapImageId)%3B%0A%20%20%20%20%20%20%20%20m_playersListWidget-%3Emodel()-%3EchangeMap(NULL)%3B%0A%20%20%20%20%20%20%20%20m_toolBar-%3EchangeMap(NULL)%3B%0A%20%20%20%20%7D%0A%20%20%20%20else%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20NetworkMessageWriter%20msg(NetMsg%3A%3APictureCategory%2CNetMsg%3A%3ADelPictureAction)%3B%0A%20%20%20%20%20%20%20%20msg.string8(mapImageId)%3B%0A%20%20%20%20%20%20%20%20msg.sendAll()%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20MediaContainer*%20%20mediaContener%20%3D%20dynamic_cast(subactive)%3B%0A%20%20%20%20if(NULL!%3DmediaContener)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20CleverURI*%20cluri%20%3D%20mediaContener-%3EgetCleverUri()%3B%0A%20%20%20%20%20%20%20%20cluri-%3EsetDisplayed(false)%3B%0A%20%20%20%20%20%20%20%20if(NULL!%3Dm_sessionManager)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20m_sessionManager-%3EupdateCleverUri(cluri)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20delete%20action%3B%0A%20%20%20%20delete%20subactive%3B%0A%7D%0A”/]

Then, we implements a solution to describe map, vmap (v1.8) or image as mediacontener.
They share a large part of their code, so it becomes really easy to do it.

[pastacode lang=”cpp” message=”Actuel” highlight=”” provider=”manual” manual=”QMdiSubWindow*%20subactive%20%3D%20m_mdiArea-%3EcurrentSubWindow()%3B%0A%20%20%20%20MediaContainer*%20container%20%3D%20dynamic_cast(subactive)%3B%0A%20%20%20%20if(NULL%20!%3D%20container)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20CleverURI%3A%3AContentType%20type%20%3D%20container-%3EgetContentType()%3B%0A%20%20%20%20%20%20%20%20if(CleverURI%3A%3AVMAP%20%3D%3D%20type)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20removeVMapFromId(container-%3EgetMediaId())%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20else%20if(CleverURI%3A%3AMAP%20%3D%3D%20type)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20removeMapFromId(container-%3EgetMediaId())%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20else%20if(CleverURI%3A%3APICTURE%20%3D%3D%20type%20)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20removePictureFromId(container-%3EgetMediaId())%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A”/]

Make your own slide show presentation in QML

To prepare my conference at Pas Sage En Seine [FR], a French hacking festival, I chose to write my slide presentation in QML.
It allows me to have better control and be free to do whatever I want (such as a timeline or any kind of animation).
Of course, That comes with a price. It is longer to do it that way but now I find some solutions. So, next time will be faster.

File hierarchy:

I preferred use QML through C++ Application. It provides more helpful feature, such as: the ability to make screenshots of your presentation at any time (useful as backup plan).
At the top level, you will found all C++ classes, project files and the main qml. Then, you will have a directory with all your pages and if it is required a directory with all your images.

├── cpphighlighter.cpp
├── cpphighlighter.h
├── deployment.pri
├── LICENSE
├── main.cpp
├── main.qml
├── pages
│   ├── 01_intro.qml
│   ├── 02_presentation.qml
│   ├── 03_jdr_et_rolisteam.qml
│   ├── 043_Exemple_code_1.qml
│   ├── 04_jdr_avantages_pb.qml
│   ├── 05_avantage_jdr_virtuel.qml
│   ├── 06_fonctionnalites_rolisteam.qml
│   ├── 07_rolisteam_debut.qml
│   ├── 08_Rolistik_a_Rolisteam.qml
│   ├── 10_frise_chronologique.qml
│   ├── 11_son_usage.qml
│   ├── 12_son_fonctionnement.qml
│   ├── 13_dice_parser.qml
│   ├── 14_themes_audio_3_pistes.qml
│   ├── 15_nouveaute_1_8.qml
│   ├── 16_projet_avenir.qml
│   ├── 17_reussites.qml
│   ├── 18_les_lecons.qml
│   ├── 19_objectif_rolisteam_libre.qml
│   ├── 20_FAQ.qml
├── pasSageEnSeine.pro
├── pasSageEnSeine.pro.user
├── qmlcontroler.cpp
├── qmlcontroler.h
├── qmlcontroler.ui
├── qml.qrc
├── README.md
├── rsrc
│   ├── all.png
│   ├── cc.png
│   └── chat.png

The C++ application

The main

It can be useful to see the state of the presentation and to read some extra notes about the current slide. To manage that, I wrote a small C++ application.
The first goal is to show the QML view, then add some features and communication between the QML view and the C++ window.

[pastacode lang=”cpp” manual=”%23include%20%3CQApplication%3E%0A%23include%20%3CQQmlApplicationEngine%3E%0A%23include%20%22qmlcontroler.h%22%0A%23include%20%3CQQmlContext%3E%0A%23include%20%3CQQuickTextDocument%3E%0A%0A%23include%20%22cpphighlighter.h%22%0A%0Aint%20main(int%20argc%2C%20char%20*argv%5B%5D)%0A%7B%0A%20%20%20%20QApplication%20app(argc%2C%20argv)%3B%0A%0A%20%20%20%20QQmlApplicationEngine%20engine%3B%0A%0A%20%20%20%20engine.rootContext()-%3EsetContextProperty(%22ScreenW%22%2C1280)%3B%0A%20%20%20%20engine.rootContext()-%3EsetContextProperty(%22ScreenH%22%2C720)%3B%0A%0A%20%20%20%20engine.load(QUrl(QStringLiteral(%22qrc%3A%2Fmain.qml%22)))%3B%0A%0A%20%20%20%20QmlControler%20ctr%3B%0A%20%20%20%20ctr.setEngine(%26engine)%3B%0A%0A%20%20%20%20return%20app.exec()%3B%0A%7D%0A” message=”main.cpp” highlight=”” provider=”manual”/]

Really easy, it loads the main.qml from the resources management system provided by Qt. It defines the targeted resolution by setting two constant into QML word: ScreenW and ScreenH.

In this project, the QmlControler class is the C++ window which provides slide feedback and additional information.

Let’s take a look to it:

Feedback window

This window is really simple. It has two parts: On the left, there is a label which displays screenshot of the qml view, and the right part is a QTextArea which display any additional note about the current slide.

[pastacode lang=”cpp” manual=”void%20QmlControler%3A%3AcurrentPageHasChanged(int%20i)%0A%7B%0A%20%20%20%20m_currentScreen%20%3D%20i%3B%0A%20%20%20%20QImage%20img%20%3D%20m_window-%3EgrabWindow()%3B%0A%0A%20%20%20%20if(img.isNull())%0A%20%20%20%20%20%20%20%20return%3B%0A%0A%20%20%20%20static%20int%20count%20%3D%200%3B%0A%0A%0A%20%20%20%20img.save(tr(%22screens%2F%251_screen.png%22).arg(%2B%2Bcount%2C3%2C10%2CQChar(‘0’))%2C%22png%22)%3B%0A%20%20%20%20qDebug()%20%3C%3C%20%22screen%20shot%20save%22%20%3C%3C%20count%3B%0A%0A%20%20%20%20m_ratioImage%20%3D%20(double)img.size().width()%2Fimg.size().height()%3B%0A%20%20%20%20m_ratioImageBis%20%3D%20(double)img.size().height()%2Fimg.size().width()%3B%0A%0A%20%20%20%20m_label-%3EsetPixmap(QPixmap%3A%3AfromImage(img))%3B%0A%0A%20%20%20%20if((i%2B1%3E%3D0)%26%26(i%2B1%3Cm_commentData.size()))%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20ui-%3EtextEdit-%3EsetHtml(m_commentData.at(i%2B1))%3B%0A%20%20%20%20%7D%0A%20%20%20%20resizeLabel()%3B%0A%7D” message=”Current slide has changed” highlight=”” provider=”manual”/]

When the current slide has changed, the c++ window is notified thought the slot currentPageHasChanged, The application gets screenshot of the qml view, save it as a file, display it thought the label, then it looks for data about the current slide into the model. If any, there are displayed in the textedit.

Saving screenshots into file allows you to create a pdf file as backup plan for your presentation.

$ convert *.png mypresentation.pdf

 

QML Application

Loader system.

For readability reason, it is easier to have each page into one qml file. The application has to load those pages in the right order. To reach this goal, we have to define the order. I did it thank to a data model inside the main.qml file.
The main.qml displays all pages as item of a pathview. All items are loaded from the qt resource management system.

[pastacode lang=”css” manual=”ListModel%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20id%3A%20panelModel%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Intro%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20path%3A%20%2201_intro.qml%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20time%3A%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20next%3A%20%22Pr%C3%A9sentation%20de%20Rolisteam%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D” message=”First item of the model.” highlight=”” provider=”manual”/]

A page is mainly defined by two data: name and path. The path is the name of the qml file.
All other data are here as help, the time has not been used.

Then, the loader does its job, the key lines are the following:

[pastacode lang=”css” manual=”%20%20%20%20PathView%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20view%0A%20%20%20%20%20%20%20%20anchors.fill%3A%20parent%0A%20%20%20%20%20%20%20%20model%3A%20panelModel%0A%20%20%20%20%20%20%20%20highlightRangeMode%3APathView.StrictlyEnforceRange%0A%20%20%20%20%20%20%20%20snapMode%3A%20PathView.SnapOneItem%0A%20%20%20%20%20%20%20%20delegate%3A%20%20Loader%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20source%3A%20%22pages%2F%22%2Bpath%0A%20%20%20%20%20%20%20%20%7D” message=”Path View” highlight=”source: “pages/”+path” provider=”manual”/]

 

Table of Contents

To manage the table of contents, I added a listview with a model:

[pastacode lang=”css” manual=”%20%20%20%20ListView%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20listView1%0A%20%20%20%20%20%20%20%20x%3A%20ScreenW*0.02%0A%20%20%20%20%20%20%20%20y%3A%20ScreenH*0.3%0A%20%20%20%20%20%20%20%20width%3A%20ScreenW%2F2%0A%20%20%20%20%20%20%20%20height%3A%20ScreenH*0.2%0A%20%20%20%20%20%20%20%20delegate%3A%20Item%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20ScreenW%2F2%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3A%20listView1.height%2FlistView1.count%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Text%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3A%20view.currentIndex%3E%3Dindex%20%3F%20%22black%22%20%3A%20%22gray%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text%3A%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20font.pointSize%3A%20ScreenH%2F48%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20anchors.verticalCenter%3A%20parent.verticalCenter%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20font.bold%3A%20true%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20visible%3A%20view.currentIndex%3E0%20%3F%20true%20%3A%20false%0A%0A%20%20%20%20%20%20%20%20model%3A%20ListModel%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Concepts%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A1%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Chroniques%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Logiciel%22%2F%2Fsyst%C3%A8me%20de%20build%2C%20code%20sp%C3%A9cifique%20par%20OS.%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A9%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ListElement%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3A%20%22Bilan%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index%3A15%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D” message=”Table of contents in QML ” highlight=”” provider=”manual”/]

Next slide

When you have many slides it can be helpful to have indication about the next one. I chose to display the title in the top-right corner. It was the easier way.

[pastacode lang=”css” manual=”%20%20%20%20Text%20%7B%0A%20%20%20%20%20%20%20%20anchors.top%3A%20parent.top%0A%20%20%20%20%20%20%20%20anchors.right%3A%20parent.right%0A%20%20%20%20%20%20%20%20text%3A%20panelModel.get(view.currentIndex).next%2B%22%3E%22%0A%20%20%20%20%7D” message=”Next slide” highlight=”” provider=”manual”/]

 

Design a page

Each page are independent but they are all based on the same pattern. In my case, they have a listview with model. Each item of the model is an point I should talk about it.

Each item has a index. The index is controlled with keyboard (down to increase, up to decrease). The page manages what is shown or hidden given the value of the index.

For example, the feature of dice alias has 10 as index. When the index page value becomes 10, the «Dice Alias»  item is displayed with an animation. Then, at 11, I can show a screen shot about the dice alias. At 12, the screenshot disappears and another text is displayed.

Position and Size

To ensure that all items will be display at the proper position and size.  I have based all computation on anchor or the screen size.

[pastacode lang=”css” manual=”%20%20%20%20Image%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20image1%0A%20%20%20%20%20%20%20%20anchors.left%3A%20parent.left%0A%20%20%20%20%20%20%20%20anchors.top%3A%20parent.top%0A%20%20%20%20%20%20%20%20anchors.leftMargin%3A%20ScreenW*0.04%0A%20%20%20%20%20%20%20%20fillMode%3A%20Image.PreserveAspectFit%0A%20%20%20%20%20%20%20%20source%3A%20%22qrc%3A%2Frsrc%2FRolisteam.svg%22%0A%20%20%20%20%20%20%20%20width%3A%20ScreenW*0.2%0A%20%20%20%20%7D” message=”Display the logo at the right position and size.” highlight=”” provider=”manual”/]

Other way

There is a module that provides Items to create QML presentation. I don’t use it for this one but it may provide interesting things.

https://github.com/qt-labs/qml-presentation-system

Get the code

You are invited to clone the code at : https://github.com/obiwankennedy/pses

Convertir toutes les images (png) d’un dossier en niveaux de gris.

Bonjour,

Une petite commande pour créer des copies en niveaux de gris des images png du dossier courant.

for i in *.png; do name=`echo $i | awk -F '.' '{print $1}'`; convert $i -colorspace Gray $name-gray.png; done

Les copies portent le mĂŞme nom que l’original avec le suffixe -gray.
Cela peut facilement ĂŞtre adaptĂ© pour des jpg ou autres formats d’images.