Afficher du Markdown dans une application Qt
Tags: python, markdown, qt
Published: 2021-06-17 09:16:17

Comme vous le savez déjà, [je travaille (presque) sur un frigo connecté](https://www.younup.fr/blog/ecrire-ses-keywords-robot-framework-avec-python). 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](https://riverbankcomputing.com/software/pyqt/intro), 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`](https://doc.qt.io/qt-5/qtextedit.html). On peut lui passer en entrée du texte au format Markdown. Voici un code pour afficher mon fichier `help.md` :

```python
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 :

![markdown avec qtextedit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u06a3aik29i5wcnjnoru.png)

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++)](https://doc.qt.io/qt-5/qtwebengine-webenginewidgets-markdowneditor-example.html) 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` :

```bash
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`](https://doc.qt.io/qt-5/qwebengineview.html) 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`:

```python
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_()
```

![example.com Qt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4rljqlbx4odxkipzn3a.png)

### 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](https://github.com/markedjs/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](https://github.com/sindresorhus/github-markdown-css), qui fait très bien le taf.

J'avais dit que c'était l'artillerie lourde, non ?

![lord have mercy](https://media1.tenor.com/images/30497a0628cd52b17a91a5c5d11dc555/tenor.gif?itemid=8717119)

### 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 :

```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` :

```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(/&gt;/g, '>')
    element.innerHTML = marked(markdown_text)
  </script>

</body>

</html>
```

Et _tadam_ !

![markdown avec web engine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iscdd1enh25a6w1q10i6.png)

### 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(/&gt;/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.