Navegador de recetas
Inyecta hojas de estilo personalizadas en páginas web y proporciona una herramienta de previsualización de texto enriquecido para un lenguaje de marcado personalizado.

Recipe Browser es una pequeña aplicación híbrida de navegador web. Demuestra cómo utilizar Qt WebEngine C++ classes para combinar la lógica de C++ y JavaScript de las siguientes maneras.
- Ejecutando código JavaScript arbitrario a través de
QWebEnginePage::runJavaScript()para inyectar hojas de estilo CSS personalizadas - Usando QWebEngineScript y QWebEngineScriptCollection para persistir el código JavaScript e inyectarlo en cada página
- Utilizando QWebChannel para interactuar y proporcionar una vista previa de texto enriquecido para un lenguaje de marcado personalizado.
Markdown es un lenguaje de marcado ligero con una sintaxis de formato de texto plano. Algunos servicios, como github, reconocen el formato y muestran el contenido como texto enriquecido cuando se visualiza en un navegador.
La ventana principal del Navegador de Recetas se divide en un área de navegación a la izquierda y un área de previsualización a la derecha. El área de vista previa de la derecha cambia a un editor cuando el usuario pulsa el botón Editar en la parte superior izquierda de la ventana principal. El editor es compatible con la sintaxis Markdown y se implementa utilizando QPlainTextEdit. El documento se renderiza como texto enriquecido en el área de previsualización, una vez que el usuario pulsa el botón Ver, al que se transforma el botón Editar. Este renderizado se implementa utilizando QWebEngineView. Para renderizar el texto, una librería JavaScript dentro del motor web convierte el texto Markdown a HTML. La vista previa se actualiza desde el editor a través de QWebChannel.
Ejecutar el ejemplo
Para ejecutar el ejemplo desde Qt Creatorabra el modo Welcome y seleccione el ejemplo de Examples. Para más información, consulta Qt Creator: Tutorial: Construir y ejecutar.
Exponer el texto del documento
Para renderizar el texto Markdown actual necesita ser expuesto al motor web a través de QWebChannel. Para conseguirlo tiene que formar parte del sistema de metatodos de Qt. Para ello se utiliza una clase dedicada Document que expone el texto del documento como 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 clase Document envuelve un QString m_currentText para ser establecido en el lado C++ con el método setText() y lo expone en tiempo de ejecución como una propiedad text con una señal textChanged. Definimos el método setText como sigue:
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(); }
Además, la clase Document realiza un seguimiento de la receta actual a través de m_currentPage. Llamamos aquí a las recetas páginas, porque cada receta tiene su documento HTML distinto que contiene el contenido de texto inicial. Además, m_textCollection es un QMap<QString, QString> que contiene los pares clave/valor {page, text}, de modo que los cambios realizados en el contenido de texto de una página se mantienen entre navegaciones. Sin embargo, no escribimos los contenidos de texto modificados en la unidad, sino que los persistimos entre el inicio y el cierre de la aplicación a través de QSettings.
Creación de la ventana principal
La clase MainWindow hereda de la clase 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 clase declara ranuras privadas que coinciden con los dos botones de la parte superior izquierda, sobre la vista de lista de navegación. Además, se declaran métodos de ayuda para hojas de estilo CSS personalizadas.
El diseño real de la ventana principal se especifica en un archivo .ui. Los widgets y acciones están disponibles en tiempo de ejecución en la variable miembro ui.
m_isEditMode es un booleano que alterna entre el editor y el área de vista previa. m_content es una instancia de la clase Document.
La configuración real de los diferentes objetos se realiza en el constructor 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);
El constructor primero llama a setupUi para construir los widgets y las acciones de menú de acuerdo con el archivo UI. La fuente del editor de texto se establece en una con una anchura de caracteres fija, y al widget QWebEngineView se le dice que no muestre un menú contextual. Además, el editor está oculto.
connect(ui->stylesheetsButton, &QPushButton::clicked, this, &MainWindow::showStyleSheetsDialog); connect(ui->editViewButton, &QPushButton::clicked, this, &MainWindow::toggleEditView);
Aquí las señales clicked de QPushButton están conectadas a funciones respectivas que muestran el diálogo de hojas de estilo o conmutan entre los modos de edición y vista, es decir, ocultan y muestran el editor y el área de vista previa respectivamente.
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); });
Aquí la navegación QListWidget a la izquierda está configurada con las 7 recetas. Además, la señal currentItemChanged de QListWidget está conectada a un lambda que carga la nueva página de la receta actual y actualiza la página en m_content.
m_content.setTextEdit(ui->textEdit);
A continuación, el puntero al editor ui, un QPlainTextEdit, se pasa a m_content para asegurar que las llamadas a Document::setInitialText() funcionan correctamente.
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);
Aquí la señal textChanged del editor se conecta a un lambda que actualiza el texto en m_content. Este objeto es entonces expuesto al lado JS por QWebChannel bajo el nombre 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();
Usando QSettings persistimos las hojas de estilo entre ejecuciones de la aplicación. Si no hay hojas de estilo configuradas, por ejemplo, porque el usuario las borró todas en una ejecución anterior, cargamos las predeterminadas.
ui->recipes->setCurrentItem(ui->recipes->item(0));
Finalmente, establecemos el elemento de la lista actualmente seleccionado como el primero contenido en el widget de la lista de navegación. Esto activa la señal QListWidget::currentItemChanged mencionada anteriormente y navega a la página del elemento de la lista.
Trabajar con hojas de estilo
Utilizamos JavaScript para crear y añadir elementos CSS a los documentos. Después de declarar la fuente del script, QWebEnginePage::runJavaScript() puede ejecutarlo inmediatamente y aplicar los estilos recién creados sobre el contenido actual de la vista web. Encapsular el script en un QWebEngineScript y añadirlo a la colección de scripts de QWebEnginePage hace que su efecto sea permanente.
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 eliminación de hojas de estilo puede hacerse de forma similar:
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()); }
Creación de un archivo de receta
<!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>Todas las diferentes páginas de recetas están configuradas de la misma manera.
En la parte <head> incluyen dos archivos CSS: markdown.css <div> , que da estilo al markdown, y custom.css, que da un poco más de estilo pero sobre todo oculta el con contenido id, ya que este <div> sólo contiene el texto de contenido inicial sin modificar. Además, se incluyen tres scripts JS. marked.js se encarga de analizar el markdown y transformarlo en HTML. custom.js realiza algunas tareas de configuración de marked.js, y qwebchannel.js expone la API JavaScript QWebChannel.
En el cuerpo hay dos elementos <div>. El <div> with id placeholder recibe el texto markdown inyectado que es renderizado y visible. El contenido de <div> with id está oculto por custom.css y sólo contiene el texto original y no modificado de la receta.
Por último, en la parte inferior de cada archivo HTML de receta hay un script que se encarga de la comunicación entre la parte C++ y JavaScript a través de QWebChannel. El contenido de texto original, sin modificar, dentro de <div> con contenido id se pasa a la parte C++ y se configura una llamada de retorno que se invoca cuando se emite la señal textChanged de m_content. La llamada de retorno actualiza entonces el contenido del marcador de posición <div> con el markdown analizado.
Archivos y atribuciones
El ejemplo incluye el siguiente código con licencias de terceros:
| Marcado | Licencia MIT |
| Marcado.css | Licencia 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.