1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\example webenginewidgets/recipebrowser
\title Recipe Browser
\meta tag {widgets, webengine, webchannel, webenginescript}
\ingroup webengine-widgetexamples
\brief Injecting custom stylsheets into web pages and providing a rich text preview
tool for a custom markup language.
\examplecategory {Web Technologies}
\image recipebrowser.webp
\e {Recipe Browser} is a small hybrid web browser application. It demonstrates how to
use the \l{Qt WebEngine Widgets C++ Classes} {Qt WebEngine C++ classes} to combine
C++ and JavaScript logic in the following ways.
\list
\li Running arbitrary JavaScript code via \c QWebEnginePage::runJavaScript() to
inject custom CSS stylesheets
\li Using QWebEngineScript and QWebEngineScriptCollection to persist the JavaScript
code and inject it to every page
\li Using QWebChannel to interact with and provide a rich text preview for a custom
markup language
\endlist
\l{http://daringfireball.net/projects/markdown/}{Markdown} is a lightweight
markup language with a plain text formatting syntax.
Some services, such as \l{http://github.com}{github}, acknowledge the
format, and render the content as rich text when viewed in a browser.
The Recipe Browser main window is split into a navigation on the left and
a preview area on the right. The preview area on the right switches to an
editor when the user clicks the Edit button on the top left of the main window.
The editor supports the Markdown syntax and is implemented by using
QPlainTextEdit. The document is rendered as rich text in the preview area,
once the user clicks the View button,to which the Edit button transforms to.
This rendering is implemented by using QWebEngineView. To render the text,
a JavaScript library inside the web engine converts the Markdown text to HTML.
The preview is updated from the editor through QWebChannel.
\include examples-run.qdocinc
\section1 Exposing Document Text
To render the current Markdown text it needs to be exposed to the web engine through
QWebChannel. To achieve this it has to be part of Qt metatype system. This is done
by using a dedicated \c Document class that exposes the document text as a
\c {Q_PROPERTY}:
\quotefromfile webenginewidgets/recipebrowser/document.h
\skipto class Document
\printto #endif
The \c Document class wraps a QString \c m_currentText to be set on the C++
side with the \c setText() method and exposes it at runtime as a \c text
property with a \c textChanged signal. We define the \c setText method as
follows:
\quotefromfile webenginewidgets/recipebrowser/document.cpp
\skipto Document::setText(const QString &text)
\printuntil /^\}/
Additionally, the \c Document class keeps track of the current recipe via
\c m_currentPage. We call the recipes pages here, because each recipe has
its distinct HTML document that contains the initial text content.
Furthermore, \c m_textCollection is a QMap<QString, QString> that contains
the key/value pairs \{page, text\}, so that changes made to the text content
of a page is persisted between navigation. Nevertheless, we do not write the
modified text contents to the drive, but instead we persist them between
application start and shutdown via QSettings.
\section1 Creating the Main Window
The \c MainWindow class inherits the QMainWindow class:
\quotefromfile webenginewidgets/recipebrowser/mainwindow.h
\skipto class MainWindow :
\printto endif
The class declares private slots that match the two buttons on the
top left, over the navigation list view. Additionally, helper
methods for custom CSS stylesheets are declared.
The actual layout of the main window is specified in a \c .ui file.
The widgets and actions are available at runtime in the \c ui member
variable.
\c m_isEditMode is a boolean that toggles between the editor and the
preview area.
\c m_content is an instance of the \c Document class.
The actual setup of the different objects is done in the \c MainWindow
constructor:
\quotefromfile webenginewidgets/recipebrowser/mainwindow.cpp
\skipto MainWindow::MainWindow
\printto connect
The constructor first calls \c setupUi to construct the widgets and menu
actions according to the UI file. The text editor font is set to one
with a fixed character width, and the QWebEngineView widget is told not
to show a context menu. Furthermore, the editor is hidden away.
\printto ui->recipes
Here the \c clicked signals of QPushButton are connected to respective functions
that show the stylesheets dialog or toggle between edit and view mode, that is,
hide and show the editor and preview area respectively.
\printto m_content.setTextEdit
Here the navigation QListWidget on the left is setup with the 7 recipes.
Also, the currentItemChanged signal of QListWidget is connected to a lambda
that loads the new, current recipe page and updates the page in \c m_content.
\printto connect
Next, the pointer to the ui editor, a QPlainTextEdit, is passed to \c m_content to ensure that
calls to \c Document::setInitialText() work properly.
\printto QSettings
Here the \c textChanged signal of the editor is connected to a lambda that
updates the text in \c m_content. This object is then exposed to the JS side
by \c QWebChannel under the name \c{content}.
\printto ui->recipes
By using QSettings we persist stylesheets between application runs. If there
should be no stylesheets configured, for example, because the user deleted all of them
in a previous run, we load default ones.
\printto }
Finally, we set the currently selected list item to the first contained in the
navigation list widget. This triggers the previously mentioned
QListWidget::currentItemChanged signal and navigates to the page of the list item.
\section1 Working With Stylesheets
We use JavaScript to create and append CSS elements to the documents.
After declaring the script source, QWebEnginePage::runJavaScript() can run it
immediately and apply newly created styles on the current content of the web view.
Encapsulating the script into a QWebEngineScript and adding it to the script collection
of QWebEnginePage makes its effect permanent.
\quotefromfile webenginewidgets/recipebrowser/mainwindow.cpp
\skipto MainWindow::insertStyleSheet
\printuntil /^\}/
Removing stylesheets can be done similarly:
\quotefromfile webenginewidgets/recipebrowser/mainwindow.cpp
\skipto MainWindow::removeStyleSheet
\printuntil /^\}/
\section1 Creating a recipe file
\quotefile webenginewidgets/recipebrowser/assets/pages/burger.html
All the different recipe pages are set up the same way.
In the \c <head> part they
include two CSS files: \c markdown.css, that styles the markdown, and
custom.css, that does some further styling but most importantly hides the
\c <div> with id \e content, as this \c <div> only contains the unmodified,
initial content text. Also, three JS scripts are included. \c marked.js
is responsible for parsing the markdown and transforming it into HTML.
\c custom.js does some configuration of \c marked.js, and \c qwebchannel.js
exposes the QWebChannel JavaScript API.
In the body there are two \c <div> elements. The \c <div> with id \e placeholder
gets the markdown text injected that is rendered and visible. The \c <div> with id
\e content is hidden by \c custom.css and only contains the original, unmodified
text content of the recipe.
Finally, on the bottom of each recipe HTML file is a script that is responsible for
the communication between the C++ and JavaScript side via QWebChannel. The original,
unmodified text content inside the \c <div> with id \e content is passed to the C++
side and a callback is setup that is invoked when the \c textChanged signal of
\c m_content is emitted. The callback then updates the contents of the \c <div>
\e placeholder with the parsed markdown.
\section1 Files and Attributions
The example bundles the following code with third-party licenses:
\table
\row
\li \l{recipebrowser-marked}{Marked}
\li MIT License
\row
\li \l{recipebrowser-markdowncss}{Markdown.css}
\li Apache License 2.0
\endtable
*/
|