Navigateur de recettes
Injecter des feuilles de style personnalisées dans les pages web et fournir un outil de prévisualisation de texte riche pour un langage de balisage personnalisé.

Recipe Browser est un petit navigateur web hybride. Elle montre comment utiliser le langage C++ pour combiner la logique C++ et JavaScript. Qt WebEngine C++ classes pour combiner les logiques C++ et JavaScript de la manière suivante.
- Exécution d'un code JavaScript arbitraire via
QWebEnginePage::runJavaScript()pour injecter des feuilles de style CSS personnalisées. - Utilisation de QWebEngineScript et QWebEngineScriptCollection pour conserver le code JavaScript et l'injecter dans chaque page.
- Utilisation de QWebChannel pour interagir avec un langage de balisage personnalisé et en fournir un aperçu en texte enrichi
Markdown est un langage de balisage léger avec une syntaxe de formatage de texte brut. Certains services, comme github, reconnaissent le format et rendent le contenu sous forme de texte enrichi lorsqu'il est affiché dans un navigateur.
La fenêtre principale du navigateur de recettes est divisée en une zone de navigation sur la gauche et une zone de prévisualisation sur la droite. La zone de prévisualisation à droite se transforme en éditeur lorsque l'utilisateur clique sur le bouton Editer en haut à gauche de la fenêtre principale. L'éditeur prend en charge la syntaxe Markdown et est implémenté à l'aide de QPlainTextEdit. Le document est rendu sous forme de texte enrichi dans la zone de prévisualisation, une fois que l'utilisateur a cliqué sur le bouton Afficher, auquel le bouton Éditer se transforme. Ce rendu est réalisé à l'aide de QWebEngineView. Pour rendre le texte, une bibliothèque JavaScript dans le moteur web convertit le texte Markdown en HTML. L'aperçu est mis à jour à partir de l'éditeur via QWebChannel.
Exécution de l'exemple
Pour exécuter l'exemple à partir de Qt Creatorouvrez le mode Welcome et sélectionnez l'exemple à partir de Examples. Pour plus d'informations, voir Qt Creator: Tutoriel : Construire et exécuter.
Exposition du texte du document
Pour rendre le texte Markdown actuel, il doit être exposé au moteur web par l'intermédiaire de QWebChannel. Pour ce faire, il doit faire partie du système de métatype de Qt. Pour ce faire, on utilise une classe Document dédiée qui expose le texte du document sous la forme d'un Q_PROPERTY:
class Document : public QObject { Q_OBJECT Q_PROPERTY(QString text MEMBER m_currentText NOTIFY textChanged FINAL) public: explicit Document(QObject *parent = nullptr); void setTextEdit(QPlainTextEdit *textEdit); void setCurrentPage(const QString &page); public slots: void setInitialText(const QString &text); void setText(const QString &text); signals: void textChanged(const QString &text); private: QPlainTextEdit *m_textEdit; QString m_currentText; QString m_currentPage; QMap<QString, QString> m_textCollection; };
La classe Document contient un QString m_currentText à définir du côté C++ avec la méthode setText() et l'expose à l'exécution en tant que propriété text avec un signal textChanged. Nous définissons la méthode setText comme suit :
void Document::setText(const QString &text) { if (text == m_currentText) return; m_currentText = text; emit textChanged(m_currentText); QSettings settings; settings.beginGroup("textCollection"); settings.setValue(m_currentPage, text); m_textCollection.insert(m_currentPage, text); settings.endGroup(); }
En outre, la classe Document garde la trace de la recette en cours via m_currentPage. Nous appelons ici les recettes des pages, car chaque recette possède son propre document HTML qui contient le texte initial. En outre, m_textCollection est un QMap<QString, QString> qui contient les paires clé/valeur {page, texte}, de sorte que les modifications apportées au contenu textuel d'une page sont conservées entre les navigations. Néanmoins, nous n'écrivons pas le contenu textuel modifié sur le lecteur, mais nous le conservons entre le démarrage et l'arrêt de l'application via QSettings.
Création de la fenêtre principale
La classe MainWindow hérite de la classe QMainWindow:
class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); void insertStyleSheet(const QString &name, const QString &source, bool immediately); void removeStyleSheet(const QString &name, bool immediately); bool hasStyleSheet(const QString &name); void loadDefaultStyleSheets(); private slots: void showStyleSheetsDialog(); void toggleEditView(); private: Ui::MainWindow *ui; bool m_isEditMode; Document m_content; };
La classe déclare des emplacements privés qui correspondent aux deux boutons en haut à gauche, au-dessus de la vue de la liste de navigation. En outre, des méthodes d'aide pour les feuilles de style CSS personnalisées sont déclarées.
La disposition réelle de la fenêtre principale est spécifiée dans un fichier .ui. Les widgets et les actions sont disponibles à l'exécution dans la variable membre ui.
m_isEditMode est un booléen qui permet de basculer entre l'éditeur et la zone de prévisualisation. m_content est une instance de la classe Document.
La configuration réelle des différents objets est effectuée dans le constructeur MainWindow:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), m_isEditMode(false) { ui->setupUi(this); ui->textEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); ui->textEdit->hide(); ui->webEngineView->setContextMenuPolicy(Qt::NoContextMenu);
Le constructeur appelle d'abord setupUi pour construire les widgets et les actions de menu conformément au fichier UI. La police de l'éditeur de texte est définie sur une police dont la largeur de caractère est fixe, et le widget QWebEngineView est invité à ne pas afficher de menu contextuel. De plus, l'éditeur est caché.
connect(ui->stylesheetsButton, &QPushButton::clicked, this, &MainWindow::showStyleSheetsDialog); connect(ui->editViewButton, &QPushButton::clicked, this, &MainWindow::toggleEditView);
Ici, les signaux clicked de QPushButton sont connectés aux fonctions respectives qui affichent la boîte de dialogue des feuilles de style ou basculent entre le mode édition et le mode affichage, c'est-à-dire qu'ils masquent et affichent respectivement l'éditeur et la zone de prévisualisation.
ui->recipes->insertItem(0, "Burger"); ui->recipes->insertItem(1, "Cupcakes"); ui->recipes->insertItem(2, "Pasta"); ui->recipes->insertItem(3, "Pizza"); ui->recipes->insertItem(4, "Skewers"); ui->recipes->insertItem(5, "Soup"); ui->recipes->insertItem(6, "Steak"); connect(ui->recipes, &QListWidget::currentItemChanged, this, [this](QListWidgetItem *current, QListWidgetItem * /* previous */) { const QString page = current->text().toLower(); const QString url = QStringLiteral("qrc:/pages/") + page + QStringLiteral(".html"); ui->webEngineView->setUrl(QUrl(url)); m_content.setCurrentPage(page); });
Ici, la navigation QListWidget à gauche est configurée avec les 7 recettes. En outre, le signal currentItemChanged de QListWidget est connecté à un lambda qui charge la nouvelle page de recette actuelle et met à jour la page dans m_content.
m_content.setTextEdit(ui->textEdit);
Ensuite, le pointeur vers l'éditeur d'interface utilisateur, QPlainTextEdit, est transmis à m_content pour s'assurer que les appels à Document::setInitialText() fonctionnent correctement.
connect(ui->textEdit, &QPlainTextEdit::textChanged, this, [this]() { m_content.setText(ui->textEdit->toPlainText()); }); QWebChannel *channel = new QWebChannel(this); channel->registerObject(QStringLiteral("content"), &m_content); ui->webEngineView->page()->setWebChannel(channel);
Ici, le signal textChanged de l'éditeur est connecté à un lambda qui met à jour le texte dans m_content. Cet objet est ensuite exposé au côté JS par QWebChannel sous le nom content.
QSettings settings; settings.beginGroup("styleSheets"); QStringList styleSheets = settings.allKeys(); if (styleSheets.empty()) { // Add back default style sheets if the user cleared them out loadDefaultStyleSheets(); } else { for (const auto &name : std::as_const(styleSheets)) { StyleSheet styleSheet = settings.value(name).value<StyleSheet>(); if (styleSheet.second) insertStyleSheet(name, styleSheet.first, false); } } settings.endGroup();
En utilisant QSettings, nous conservons les feuilles de style entre les exécutions de l'application. Si aucune feuille de style n'est configurée, par exemple parce que l'utilisateur les a toutes supprimées lors d'une exécution précédente, nous chargeons les feuilles de style par défaut.
ui->recipes->setCurrentItem(ui->recipes->item(0));
Enfin, nous définissons l'élément de liste actuellement sélectionné comme étant le premier contenu dans le widget de la liste de navigation. Cela déclenche le signal QListWidget::currentItemChanged mentionné précédemment et permet de naviguer vers la page de l'élément de la liste.
Travailler avec des feuilles de style
Nous utilisons JavaScript pour créer et ajouter des éléments CSS aux documents. Après avoir déclaré la source du script, QWebEnginePage::runJavaScript() peut l'exécuter immédiatement et appliquer les styles nouvellement créés au contenu actuel de la vue Web. L'encapsulation du script dans un fichier QWebEngineScript et son ajout à la collection de scripts de QWebEnginePage rendent son effet permanent.
void MainWindow::insertStyleSheet(const QString &name, const QString &source, bool immediately) { QWebEngineScript script; QString s = QString::fromLatin1("(function() {" " css = document.createElement('style');" " css.type = 'text/css';" " css.id = '%1';" " document.head.appendChild(css);" " css.innerText = '%2';" "})()") .arg(name, source.simplified()); if (immediately) ui->webEngineView->page()->runJavaScript(s, QWebEngineScript::ApplicationWorld); script.setName(name); script.setSourceCode(s); script.setInjectionPoint(QWebEngineScript::DocumentReady); script.setRunsOnSubFrames(true); script.setWorldId(QWebEngineScript::ApplicationWorld); ui->webEngineView->page()->scripts().insert(script); }
La suppression des feuilles de style peut être effectuée de la même manière :
void MainWindow::removeStyleSheet(const QString &name, bool immediately) { QString s = QString::fromLatin1("(function() {" " var element = document.getElementById('%1');" " element.outerHTML = '';" " delete element;" "})()") .arg(name); if (immediately) ui->webEngineView->page()->runJavaScript(s, QWebEngineScript::ApplicationWorld); const QList<QWebEngineScript> scripts = ui->webEngineView->page()->scripts().find(name); if (!scripts.isEmpty()) ui->webEngineView->page()->scripts().remove(scripts.first()); }
Création d'un fichier de recette
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Insanity Burger</title>
<link rel="stylesheet" type="text/css" href="../3rdparty/markdown.css">
<link rel="stylesheet" type="text/css" href="../custom.css">
<script src="../3rdparty/marked.js"></script>
<script src="../custom.js"></script>
<script src="qrc:/qtwebchannel/qwebchannel.js"></script>
</head>
<body>
<div id="placeholder"></div>
<div id="content">
<img src="images/burger.jpg" alt="Insanity Burger" title="Insanity Burger" />
Insanity burger
===============
### Ingredients
* 800 g minced chuck steak
* olive oil
* 1 large red onion
* 1 splash of white wine vinegar
* 2 large gherkins
* 4 sesame-topped brioche burger buns
* 4-8 rashers of smoked streaky bacon
* 4 teaspoons American mustard
* Tabasco Chipotle sauce
* 4 thin slices of Red Leicester cheese
* 4 teaspoons tomato ketchup
#### For the burger sauce
* ¼ of an iceberg lettuce
* 2 heaped tablespoons mayonnaise
* 1 heaped tablespoon tomato ketchup
* 1 teaspoon Tabasco Chipotle sauce
* 1 teaspoon Worcestershire sauce
* 1 teaspoon brandy, or bourbon (optional)
### Instructions
For the best burger, go to your butcher’s and ask them to mince 800g of chuck steak for you.
This cut has a really good balance of fat and flavoursome meat. Divide it into 4 and, with wet
hands, roll each piece into a ball, then press into flat patties roughly 12cm wide and about 2cm
wider than your buns. Place on an oiled plate and chill in the fridge. Next, finely slice the red
onion, then dress in a bowl with the vinegar and a pinch of sea salt. Slice the gherkins and halve
the buns. Finely chop the lettuce and mix with the rest of the burger sauce ingredients in a bowl,
then season to taste.
I like to only cook 2 burgers at a time to achieve perfection, so get two pans on the go – a large
non-stick pan on a high heat for your burgers and another on a medium heat for the bacon. Pat your
burgers with oil and season them with salt and pepper. Put 2 burgers into the first pan, pressing
down on them with a fish slice, then put half the bacon into the other pan. After 1 minute, flip
the burgers and brush each cooked side with ½ a teaspoon of mustard and a dash of Tabasco. After
another minute, flip onto the mustard side and brush again with another ½ teaspoon of mustard and
a second dash of Tabasco on the other side. Cook for one more minute, by which point you can place
some crispy bacon on top of each burger with a slice of cheese. Add a tiny splash of water to the
pan and place a heatproof bowl over the burgers to melt the cheese – 30 seconds should do it. At the
same time, toast 2 split buns in the bacon fat in the other pan until lightly golden. Repeat with
the remaining two burgers.
To build each burger, add a quarter of the burger sauce to the bun base, then top with a cheesy
bacon burger, a quarter of the onions and gherkins. Rub the bun top with a teaspoon of ketchup,
then gently press together. As the burger rests, juices will soak into the bun, so serve right
away, which is great, or for an extra filthy experience, wrap each one in greaseproof paper, then
give it a minute to go gorgeous and sloppy.
**Enjoy!**
</div><!--End of content-->
<script>
'use strict';
var jsContent = document.getElementById('content');
var placeholder = document.getElementById('placeholder');
var updateText = function(text) {
placeholder.innerHTML = marked.parse(text);
}
new QWebChannel(qt.webChannelTransport,
function(channel) {
var content = channel.objects.content;
content.setInitialText(jsContent.innerHTML);
content.textChanged.connect(updateText);
}
);
</script>
</body>
</html>Toutes les pages de recettes sont configurées de la même manière.
Dans la partie <head>, deux fichiers CSS sont inclus : markdown.css Le premier, qui stylise le markdown, et le second, custom.css, qui apporte un peu plus de style, mais surtout qui cache le contenu de <div> with id, car cette page <div> ne contient que le texte initial, non modifié, du contenu. Trois scripts JS sont également inclus. marked.js est responsable de l'analyse du markdown et de sa transformation en HTML. custom.js effectue une partie de la configuration de marked.js, et qwebchannel.js expose l'API JavaScript de QWebChannel.
Le corps de la page contient deux éléments <div>. L'élément <div> with id placeholder reçoit le texte markdown injecté qui est rendu et visible. Le contenu de <div> with id est caché par custom.css et ne contient que le texte original, non modifié, de la recette.
Enfin, au bas de chaque fichier HTML de recette se trouve un script qui est responsable de la communication entre le côté C++ et JavaScript via QWebChannel. Le contenu textuel original non modifié à l'intérieur de <div> avec le contenu id est transmis au côté C++ et un rappel est configuré qui est invoqué lorsque le signal textChanged de m_content est émis. Le callback met alors à jour le contenu de l'espace réservé <div> avec le texte analysé.
Fichiers et attributions
L'exemple contient le code suivant avec des licences tierces :
| Marqué | Licence MIT |
| Markdown.css | Licence Apache 2.0 |
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.