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 help.md
à côté de mes fichiers *.py
et de l'afficher dans un widget. Voici un exemple de fichier avec de nombreuses fonctionnalités de Markdown :
# 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)
(Note : le rendu montre 3 blocs de code mais il s'agit bien d'un seul et même texte en Markdown).
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 : QTextEdit
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 help.md
:
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_()
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
text_edit.zoomIn(2)
. - 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 PyQt5
, il faut installer le paquet PyQtWebEngine
:
pip install PyQtWebEngine
Afficher une page web avec QWebEngineView
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 QWidget
. Ca me permettait de l'intégrer facilement dans mon application à la place de mon QTextEdit
. Voici un script pour afficher www.example.com
:
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_()
Afficher du Markdown avec QWebEngineView
Et maintenant la question à 10$ : comment remplacer www.example.com
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 :
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_()
La page template.html
:
<!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>
Et tadam !
Quelques détails
Je définis une version spécialisée de QWebEnginePage
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 replace(/>/g, '>')
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.