Rezeptur-Browser
Einfügen von benutzerdefinierten Stylesheets in Webseiten und Bereitstellung eines Rich-Text-Vorschau-Tools für eine benutzerdefinierte Auszeichnungssprache.
Recipe Browser ist eine kleine hybride Webbrowser-Anwendung. Sie demonstriert, wie man die Qt WebEngine C++ classes um C++- und JavaScript-Logik auf die folgenden Arten zu kombinieren.
- Ausführen von beliebigem JavaScript-Code über
QWebEnginePage::runJavaScript()
, um benutzerdefinierte CSS-Stylesheets einzufügen - Verwendung von QWebEngineScript und QWebEngineScriptCollection, um den JavaScript-Code aufrechtzuerhalten und ihn in jede Seite einzubauen
- Verwendung von QWebChannel, um mit einer benutzerdefinierten Auszeichnungssprache zu interagieren und eine Rich-Text-Vorschau für diese bereitzustellen
Markdown ist eine leichtgewichtige Auszeichnungssprache mit einer einfachen Textformatierungssyntax. Einige Dienste, wie z. B. Github, erkennen das Format an und stellen den Inhalt als Rich-Text dar, wenn er in einem Browser angezeigt wird.
Das Hauptfenster von Recipe Browser ist in eine Navigation auf der linken Seite und einen Vorschaubereich auf der rechten Seite unterteilt. Der Vorschaubereich auf der rechten Seite wechselt zu einem Editor, wenn der Benutzer auf die Schaltfläche Bearbeiten oben links im Hauptfenster klickt. Der Editor unterstützt die Markdown-Syntax und ist unter QPlainTextEdit implementiert. Das Dokument wird im Vorschaubereich als Rich-Text gerendert, sobald der Benutzer auf die Schaltfläche Ansicht klickt, in die sich die Schaltfläche Bearbeiten verwandelt. Dieses Rendering wird mit Hilfe von QWebEngineView realisiert. Um den Text zu rendern, wandelt eine JavaScript-Bibliothek innerhalb der Web-Engine den Markdown-Text in HTML um. Die Vorschau wird vom Editor aus über QWebChannel aktualisiert.
Ausführen des Beispiels
Zum Ausführen des Beispiels von Qt Creatorauszuführen, öffnen Sie den Modus Welcome und wählen Sie das Beispiel unter Examples aus. Weitere Informationen finden Sie unter Erstellen und Ausführen eines Beispiels.
Freigeben von Dokumenttext
Um den aktuellen Markdown-Text zu rendern, muss er der Web-Engine über QWebChannel zugänglich gemacht werden. Um dies zu erreichen, muss er Teil des Qt-Metatypensystems sein. Dies geschieht durch die Verwendung einer speziellen Document
Klasse, die den Dokumententext als Q_PROPERTY
darstellt:
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; };
Die Klasse Document
umhüllt eine QString m_currentText
, die auf der C++-Seite mit der Methode setText()
gesetzt wird, und stellt sie zur Laufzeit als text
Eigenschaft mit einem textChanged
Signal dar. Wir definieren die Methode setText
wie folgt:
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(); }
Zusätzlich behält die Klasse Document
das aktuelle Rezept über m_currentPage
im Auge. Wir nennen die Rezepte hier Seiten, weil jedes Rezept ein eigenes HTML-Dokument hat, das den ursprünglichen Textinhalt enthält. Außerdem ist m_textCollection
ein QMap<QString, QString>, das die Schlüssel/Wertpaare {Seite, Text} enthält, so dass Änderungen am Textinhalt einer Seite zwischen den Navigationen erhalten bleiben. Allerdings schreiben wir die geänderten Textinhalte nicht auf das Laufwerk, sondern wir persistieren sie zwischen dem Start und dem Beenden der Anwendung über QSettings.
Erstellen des Hauptfensters
Die Klasse MainWindow
erbt von der Klasse 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; };
Die Klasse deklariert private Slots, die zu den beiden Schaltflächen oben links über der Navigationslistenansicht passen. Zusätzlich werden Hilfsmethoden für eigene CSS-Stylesheets deklariert.
Das eigentliche Layout des Hauptfensters wird in einer .ui
Datei festgelegt. Die Widgets und Aktionen sind zur Laufzeit in der Membervariablen ui
verfügbar.
m_isEditMode
ist ein Boolescher Wert, der zwischen dem Editor und dem Vorschaubereich umschaltet. m_content
ist eine Instanz der Klasse Document
.
Die eigentliche Einrichtung der verschiedenen Objekte erfolgt im MainWindow
Konstruktor:
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);
Der Konstruktor ruft zunächst setupUi
auf, um die Widgets und Menüaktionen entsprechend der UI-Datei zu konstruieren. Die Schriftart des Texteditors wird auf eine Schriftart mit fester Zeichenbreite eingestellt, und das Widget QWebEngineView wird angewiesen, kein Kontextmenü anzuzeigen. Außerdem wird der Editor ausgeblendet.
connect(ui->stylesheetsButton, &QPushButton::clicked, this, &MainWindow::showStyleSheetsDialog); connect(ui->editViewButton, &QPushButton::clicked, this, &MainWindow::toggleEditView);
Hier sind die clicked
-Signale von QPushButton mit entsprechenden Funktionen verbunden, die den Stylesheet-Dialog anzeigen oder zwischen Bearbeitungs- und Ansichtsmodus umschalten, d. h. den Editor und den Vorschaubereich aus- bzw. einblenden.
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); });
Hier ist die Navigation QListWidget auf der linken Seite mit den 7 Rezepten eingerichtet. Außerdem ist das currentItemChanged-Signal von QListWidget mit einem Lambda verbunden, das die neue, aktuelle Rezeptseite lädt und die Seite in m_content
aktualisiert.
m_content.setTextEdit(ui->textEdit);
Als Nächstes wird der Zeiger auf den UI-Editor, ein QPlainTextEdit, an m_content
übergeben, um sicherzustellen, dass die Aufrufe an Document::setInitialText()
richtig funktionieren.
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);
Hier wird das Signal textChanged
des Editors mit einem Lambda verbunden, das den Text in m_content
aktualisiert. Dieses Objekt wird dann auf der JS-Seite von QWebChannel
unter dem Namen content
veröffentlicht.
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();
Durch die Verwendung von QSettings werden Stylesheets zwischen Anwendungsläufen persistiert. Sollten keine Stylesheets konfiguriert sein, z. B. weil der Benutzer sie in einem früheren Lauf gelöscht hat, laden wir Standard-Stylesheets.
ui->recipes->setCurrentItem(ui->recipes->item(0));
Schließlich setzen wir das aktuell ausgewählte Listenelement auf das erste, das im Navigationslisten-Widget enthalten ist. Dies löst das bereits erwähnte Signal QListWidget::currentItemChanged aus und navigiert zur Seite des Listenelements.
Arbeiten mit Stylesheets
Wir verwenden JavaScript, um CSS-Elemente zu erstellen und an die Dokumente anzuhängen. Nachdem die Skriptquelle deklariert wurde, kann QWebEnginePage::runJavaScript() sie sofort ausführen und neu erstellte Stile auf den aktuellen Inhalt der Webansicht anwenden. Wird das Skript in ein QWebEngineScript gekapselt und zur Skriptsammlung von QWebEnginePage hinzugefügt, ist seine Wirkung dauerhaft.
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); }
Das Entfernen von Stylesheets kann auf ähnliche Weise erfolgen:
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()); }
Erstellen einer Rezeptdatei
<!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>
Alle Rezeptseiten sind auf die gleiche Weise aufgebaut.
Im Teil <head>
sind zwei CSS-Dateien enthalten: markdown.css
custom.css, die einige weitere Formatierungen vornimmt, aber vor allem die <div>
mit id-Inhalt ausblendet, da diese <div>
nur den unveränderten, ursprünglichen Inhaltstext enthält. Außerdem sind drei JS-Skripte enthalten. marked.js
ist für das Parsen des Markdowns und die Umwandlung in HTML zuständig. custom.js
übernimmt die Konfiguration von marked.js
, und qwebchannel.js
stellt die JavaScript-API von QWebChannel bereit.
Im Body gibt es zwei <div>
Elemente. In den <div>
mit id-Platzhalter wird der Markdown-Text eingefügt, der gerendert wird und sichtbar ist. Der Inhalt von <div>
mit id wird von custom.css
ausgeblendet und enthält nur den ursprünglichen, unveränderten Textinhalt des Rezepts.
Schließlich befindet sich am Ende jeder Rezept-HTML-Datei ein Skript, das für die Kommunikation zwischen der C++- und der JavaScript-Seite über QWebChannel zuständig ist. Der ursprüngliche, unveränderte Textinhalt innerhalb des <div>
mit id-Inhalt wird an die C++-Seite weitergegeben und ein Callback eingerichtet, der aufgerufen wird, wenn das Signal textChanged
von m_content
ausgegeben wird. Der Callback aktualisiert dann den Inhalt des Platzhalters <div>
mit dem geparsten Markdown.
Dateien und Attribute
Das Beispiel bündelt den folgenden Code mit Lizenzen Dritter:
Markiert | MIT-Lizenz |
Markdown.css | Apache-Lizenz 2.0 |
© 2025 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.