DEV Community

Pierre Gradot for Younup

Posted on • Edited on • Originally published at younup.fr

Afficher du Markdown dans une application Qt

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.
Enter fullscreen mode Exit fullscreen mode
```bash
> fridge connect
Connecting to the fridge...
Connexion established!
Error code = 42
```
Enter fullscreen mode Exit fullscreen mode
| Code | Détails |
| ---- | ------- |
| 0    | OK      |
| 42   | Pas OK  |

![](https://www.younup.fr/theme/younup/assets/images/logo_younup.svg?beec11acb0)
Enter fullscreen mode Exit fullscreen mode

(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_()
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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_()
Enter fullscreen mode Exit fullscreen mode

example.com Qt

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 ?

lord have mercy

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 :

import pathlib

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)
file = str(pathlib.Path('generated.html').resolve())
url = QUrl.fromLocalFile(file)
view.load(url)
view.show()
app.exec_()

Enter fullscreen mode Exit fullscreen mode

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@14.1.3/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.parse(markdown_text)
  </script>

</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Et tadam !

markdown avec web engine

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.

Top comments (4)

Collapse
 
ladiff666 profile image
ladiff666

and what about performance, it stills good ,and can i do the same thinks with charts.js (i wanna use a js chart library instead of Qcharts ),is it a good solution to use QWebEngineView in my dashboard?

Collapse
 
pgradot profile image
Pierre Gradot • Edited

I didn't really look at performances. It seems obvious that the solution with QWebEngineView is slower than the solution with QTextEdit, but I don't have any figures to prove it.

QWebEngineView works pretty well. You should try and see if you can get what you need.

Collapse
 
vnon_thevenon_ac0aaae3da profile image
Vénon Thevenon

No matter what i do (use defer, add marked in local, etc) i get the error js: Uncaught ReferenceError: marked is not defined. (I'm in pyqt6)

Collapse
 
pgradot profile image
Pierre Gradot

Hello

The code was even broken with PyQt5. I have updated to pin the version of marked (and avoid future API breaking changes) and to improve the URL to the generated file (on Linux, it seems mandatory to have an absolute path).

I have tweaked the code on my machine to run with PyQt6 and I indeed get the same error. I believe that the neither marked.min.js nor github-markdown.min.css are actually downloaded. If you add <h1>Title</h1> in the HTML template, you will see that the style isn't applied. Furthermore, firefox displays correctly generated.html.

I can't tell you more, sorry. You will have to dig to understand why.