Comme vous le savez déjà, je travaille (presque) sur un frigo connecté. Il existe une application PC pour faire la maintenance de ces frigos dans les centres agréés.
Récemment, j'ai voulu fournir un document d'aide pour l'utilisation de ce logiciel. J'aime beaucoup écrire mes documents en Markdown mais il faut avouer que ce format n'est pas adapté aux gens normaux, qui n'ont sans doute pas de logiciels adaptés pour ouvrir ce genre de fichiers sur leurs ordinateurs.
Mon application est écrite en Qt et je me suis dit que Qt devait bien être capable d'afficher un fichier Markdown dans un super widget prévu pour. Il s'est avéré que ce n'est pas aussi plug and play que ça... Je vais vous montrer comment j'ai fait (et si vous avez une autre technique magique, dites-moi tout en commentaire !).
J'utilise PyQt, le binding Python de Qt. Le code présenté ici sera donc en Python mais vous pourrez facilement l'adapter en C++.
Objectif
Mon but est d'avoir un fichier <span class="css-span">help.md</span> à côté de mes fichiers <span class="css-span">*.py</span> et de l'afficher dans un widget. Voici un exemple de fichier avec de nombreuses fonctionnalités de Markdown :
<pre><code># YouFridge
## Présentation
Blabla pour présenter le logiciel pour la maintenance du frigo connecté by [Younup](https://www.younup.fr/blog).
> Pensez à bien remettre [les bières Younup](https://www.linkedin.com/feed/update/urn:li:activity:6745640505482219525/) au frais après la maintenance.
## Versions
| Version | Changements |
| ------- | ---------------- |
| 1.0.0 | Blabla |
| 1.0.1 | Oh no! |
| 1.1.0 | Blablabla blabla |
## Code des erreurs
⚠️ Avez-vous bien branché la valise de diagnostic ? Vous avez peut-être un problème avec votre câble.
```bash
> fridge connect
Connecting to the fridge...
Connexion established!
Error code = 42
```
| Code | Détails |
| ---- | ------- |
| 0 | OK |
| 42 | Pas OK |
![](https://www.younup.fr/theme/younup/assets/images/logo_younup.svg?beec11acb0)</code></pre>
En Python, je souhaite ouvrir le fichier, charger le texte qu'il contient, et l'afficher avec un rendu correct et si possible joli.
Une solution simple mais imparfaite : <span class="css-span">QTextEdit</span>
La première solution est le classique widget QTextEdit. On peut lui passer en entrée du texte au format Markdown. Voici un code pour afficher mon fichier <span class="css-span">help.md</span> :
<pre><code>from PyQt5.QtWidgets import QApplication, QTextEdit
app = QApplication([])
text_edit = QTextEdit()
text_edit.setReadOnly(True)
with open('help.md', encoding='utf8') as f:
markdown = f.read()
text_edit.setMarkdown(markdown)
text_edit.show()
app.exec_()</code></pre>
Il est important de choisir l'encodage à l'ouverture du fichier pour que les accents et pictogrammes soient correctement lus.
On obtient :
Le rendu est (presque) correct, bien que loin d'être joli. Quelques défauts :
- Le style du texte ne peut pas être changé.
- La seule astuce que j'ai trouvée pour augmenter la taille de la police est de faire <span class="css-span">text_edit.zoomIn(2)</span>.
- Les images sans texte alternatif ne sont pas affichées, comme si leurs chemins étaient invalides.
- Il semble y avoir un bug d'affichage des tableaux s'ils sont précédés d'un bloc de code.
- Les pictogrammes (qui sont des caractères spéciaux Unicode) ne sont pas bien jolis (mais c'est peut-être juste une question de police de caractères).
- Le rendu des citations est mauvais.
- Les liens ne sont pas cliquables (même si j'avoue ne pas avoir vraiment cherché si on pouvait changer ça).
Bref, c'est pas mal mais dans mon cas, ce n'était pas super satisfaisant.
L'artillerie lourde
En quête d'un rendu plus joli, je me suis intéressé à un exemple officiel de Qt (en C++) utilisant le web engine de Qt. Ça n'a pas vraiment été facile à adapter à mon projet mais j'ai finalement obtenu de très bons résultats.
Dépendances
En plus de <span class="css-span">PyQt5</span>, il faut installer le paquet <span class="css-span">PyQtWebEngine</span> :
<pre><code>pip install PyQtWebEngine</code></pre>
Afficher une page web avec <span class="css-span">QWebEngineView</span>
Après quelques expérimentations, je me suis dit que la solution la plus simple était d'utiliser la classe QWebEngineView qui hérite de <span class="css-span">QWidget</span>. Ca me permettait de l'intégrer facilement dans mon application à la place de mon <span class="css-span">QTextEdit</span>. Voici un script pour afficher <span class="css-span">www.example.com</span>:
<pre><code>from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
app = QApplication([])
view = QWebEngineView()
view.setUrl(QUrl('http://www.example.com'))
view.show()
app.exec_()</code></pre>
Afficher du Markdown avec <span class="css-span">QWebEngineView</span>
Et maintenant la question à 10$ : comment remplacer <span class="css-span">www.example.com</span> par mon fichier Markdown ?
Ben on peut pas ! Il faut transformer le Markdown en HTML. Pour cela, j'ai plus ou moins suivi le tutoriel officiel de Qt :
- j'ai un fichier avec une page HTML template
- je remplace le contenu d'un élément HTML par le texte en Markdown
- j'enregistre le résultat dans un nouveau fichier HTML
- j'affiche ce nouveau fichier HTML
- une bibliothèque Javascript se charge de convertir le Markdown en HTML
- une bibliothèque CSS se charge d'ajouter du style à tout ce petit monde
J'ai réutilisé la bibliothèque Javascript proposée par le tutoriel, Marked mais j'ai dû trouver une autre bibliothèque CSS car celle proposée n'est plus accessible. J'ai choisi github-markdown-css, qui fait très bien le taf.
J'avais dit que c'était l'artillerie lourde, non ?
Le code
Si le code est assez simple au final, j'avoue m'être pas mal battu pour faire fonctionner tout ça, mais je suis très satisfait du résultat !
Le code Python :
<pre><code>from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
class Page(QWebEnginePage):
def acceptNavigationRequest(self, new_url, navigation_type, is_main_frame):
if navigation_type == QWebEnginePage.NavigationTypeLinkClicked:
QDesktopServices.openUrl(new_url)
return False
else:
return super().acceptNavigationRequest(new_url, navigation_type, is_main_frame)
app = QApplication([])
with open('help.md', encoding='utf8') as f:
markdown = f.read()
with open('template.html', encoding='utf8') as f:
html = f.read()
with open('generated.html', 'w', encoding='utf8') as f:
generated = html.replace('markdown_content_placeholder', markdown)
f.write(generated)
view = QWebEngineView()
page = Page(view)
view.setPage(page)
view.load(QUrl('file:///generated.html'))
view.show()
app.exec_()</code></pre>
La page <span class="css-span">template.html</span> :
<pre><code><!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href=https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<div id="content" class="markdown-body">markdown_content_placeholder</div>
<script>
const element = document.getElementById('content')
markdown_text = element.innerHTML.replace(/>/g, '>')
element.innerHTML = marked(markdown_text)
</script>
</body>
</html></code></pre>
Et tadam !
Quelques détails
Je définis une version spécialisée de <span class="css-span">QWebEnginePage</span> pour pouvoir ouvrir les liens dans le navigateur par défaut du système. Si je ne fais pas ça, les liens s'ouvrent dans le widget, sauf qu'il n'est pas possible de naviguer en arrière.
Vous avez peut-être remarqué l'astuce <span class="css-span">replace(/>/g, '>')</span> pour que les citations ne partent pas en vrille. Il y a sûrement d'autres améliorations de robustesse à faire.
Vous noterez enfin qu'il faut être connecté à Internet pour récupérer les bibliothèques JS et CSS. Si vous passez sous un tunnel, c'est le drame ! Pensez à rapatrier en local les fichiers si votre application doit fonctionner en mode non connectée. Quelques tests m'ont toutefois montré que les fichiers semblaient être récupérés d'un quelconque cache si je ne suis pas connecté à Internet.
Conclusion
Je suis très satisfait de la solution obtenue avec QWebEngineView ! Manipuler un QWidget rend très simple l'intégration et le placement de l'aide dans l'IHM. Je conserve la souplesse d'écrire du Markdown pour écrire le fichier d'aide et le rendu dans mon application est joli.
Que la vie de Pierre, expert embarqué Younup, serait terne sans les variadic templates et les fold expressions de C++17. Heureusement pour lui, Python a tué l'éternel débat sur l’emplacement de l’accolade : "alors, à la fin de la ligne courante ou au début de la ligne suivante ?"
Homme de terrain, il est aussi à l’aise au guidon de son VTT à sillonner les chemins de forêt, dans une salle de concert de black metal ou les mains dans les soudures de sa carte électronique quand il doit déboguer du code (bon ça, il aime moins quand même !)
Son vœu pieux ? Il hésite encore... Faire disparaitre le C embarqué au profit du C++ embarqué ? Ou stopper la génération sans fin d'entropie de son bureau.