レシピブラウザ
カスタムスタイルシートをウェブページに注入し、カスタムマークアップ言語用のリッチテキストプレビューツールを提供します。
Recipe Browserは小さなハイブリッドWebブラウザーアプリケーションです。Qt WebEngine C++ classes 、C++とJavaScriptのロジックを以下のように組み合わせる方法を示しています。
QWebEnginePage::runJavaScript()
を介して任意の JavaScript コードを実行し、カスタム CSS スタイルシートを注入する。- QWebEngineScript とQWebEngineScriptCollection を使用して JavaScript コードを永続化し、すべてのページに注入する。
- QWebChannel を使用して、カスタム・マークアップ言語のリッチ・テキスト・プレビューを提供する。
Markdownは、プレーンテキストのフォーマット構文を持つ軽量のマークアップ言語です。githubのようないくつかのサービスは、このフォーマットを認め、ブラウザで表示されたときにコンテンツをリッチテキストとしてレンダリングします。
レシピブラウザのメインウィンドウは、左側のナビゲーションと右側のプレビューエリアに分かれています。ユーザーがメインウィンドウの左上にある編集ボタンをクリックすると、右側のプレビューエリアがエディタに切り替わります。エディタはMarkdown構文をサポートし、QPlainTextEdit を使用して実装されています。ユーザが編集ボタンが切り替える表示ボタンをクリックすると、ドキュメントはプレビュー領域でリッチテキストとしてレンダリングされます。このレンダリングはQWebEngineView を使って実装されています。テキストをレンダリングするために、ウェブエンジン内のJavaScriptライブラリがMarkdownテキストをHTMLに変換します。プレビューはエディタからQWebChannel 。
例の実行
Qt Creator からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細については、Building and Running an Exampleを参照してください。
ドキュメント・テキストの表示
現在のMarkdownテキストをレンダリングするには、QWebChannel 。そのためには、Qtメタタイプ・システムの一部である必要があります。これは、ドキュメント・テキストをQ_PROPERTY
として公開する専用のDocument
クラスを使用することで行われます:
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; };
Document
クラスは、setText()
メソッドで C++ 側で設定されるQString m_currentText
をラップし、textChanged
シグナルでtext
プロパティとして実行時に公開します。setText
メソッドを次のように定義します:
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(); }
さらに、Document
クラスはm_currentPage
を通して現在のレシピを追跡します。各レシピは最初のテキストコンテンツを含む個別のHTMLドキュメントを持つため、ここではレシピをページと呼びます。さらに、m_textCollection
は、QMap<QString,QString>で、キーと値のペア{page, text}を含んでおり、ページのテキストコンテンツに加えられた変更がナビゲーション間で永続化されるようになっています。とはいえ、変更されたテキスト・コンテンツをドライブに書き込むのではなく、QSettings を介してアプリケーションの起動と終了の間に永続化します。
メイン・ウィンドウの作成
MainWindow
クラスは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; };
このクラスは、ナビゲーション・リスト・ビューの上にある左上の2つのボタンにマッチするプライベート・スロットを宣言します。さらに、カスタムCSSスタイルシート用のヘルパー・メソッドが宣言されています。
メイン・ウィンドウの実際のレイアウトは.ui
。ウィジェットとアクションは、ui
メンバ変数で実行時に利用可能です。
m_isEditMode
m_content
は クラスのインスタンスです。Document
異なるオブジェクトの実際のセットアップは、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);
コンストラクタは最初にsetupUi
を呼び出し、UIファイルに従ってウィジェットとメニュー・アクションを構築します。テキストエディタのフォントは固定文字幅のものに設定され、QWebEngineView ウィジェットはコンテキストメニューを表示しないように指示されています。さらに、エディタは隠されている。
connect(ui->stylesheetsButton, &QPushButton::clicked, this, &MainWindow::showStyleSheetsDialog); connect(ui->editViewButton, &QPushButton::clicked, this, &MainWindow::toggleEditView);
ここでは、QPushButton のclicked
シグナルが、スタイルシート・ダイアログを表示したり、編集モードと表示モードを切り替えたりする関数、つまりエディタとプレビュー・エリアをそれぞれ隠したり表示したりする関数に接続されている。
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); });
ここでは、左側のナビゲーションQListWidget 、7つのレシピがセットアップされている。また、QListWidget の currentItemChanged シグナルは、新しい現在のレシピページをロードし、m_content
のページを更新するラムダに接続されています。
m_content.setTextEdit(ui->textEdit);
次に、Document::setInitialText()
への呼び出しが正しく動作するように、uiエディタへのポインタであるQPlainTextEdit がm_content
に渡されます。
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);
エディターのtextChanged
シグナルはm_content
のテキストを更新するラムダに接続されます。このオブジェクトは、QWebChannel
によって、content
という名前でJS側に公開されます。
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();
QSettings を使用することで、アプリケーションの実行間でスタイルシートを永続化します。ユーザーが以前の実行でスタイルシートをすべて削除したなどの理由で、スタイルシートが設定されていない場合は、デフォルトのものをロードします。
ui->recipes->setCurrentItem(ui->recipes->item(0));
最後に、現在選択されているリスト項目を、ナビゲーション・リスト・ウィジェットに含まれる最初の項目に設定します。これにより、前述のQListWidget::currentItemChanged シグナルがトリガーされ、リスト項目のページにナビゲートされます。
スタイルシートを使う
JavaScriptを使ってCSS要素を作成し、ドキュメントに追加します。スクリプト・ソースを宣言した後、QWebEnginePage::runJavaScript() はそれを即座に実行し、新しく作成されたスタイルをウェブ・ビューの現在のコンテンツに適用することができます。スクリプトをQWebEngineScript にカプセル化し、QWebEnginePage のスクリプト・コレクションに追加すると、その効果は永続的になります。
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); }
スタイルシートの削除も同様に行えます:
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()); }
レシピファイルの作成
<!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>
すべての異なるレシピページは同じようにセットアップされます。
<head>
、2つのCSSファイルが含まれている:markdown.css
マークダウンをスタイルするcustom.cssと、さらにスタイルするcustom.cssであるが、最も重要なのは、idコンテンツを含む<div>
を非表示にすることである。この<div>
は、修正されていない最初のコンテンツ・テキストのみを含むからである。また、3つのJSスクリプトが含まれている。marked.js
はマークダウンの解析とHTMLへの変換を担当する。custom.js
はmarked.js
のいくつかの設定を行い、qwebchannel.js
はQWebChannel JavaScript APIを公開する。
本文には2つの<div>
要素がある。<div>
with idplaceholderは、レンダリングされ表示されるマークダウン・テキストを注入します。<div>
with idコンテンツは custom.css
によって隠され、レシピのオリジナルの変更されていないテキストコンテンツのみを含みます。
最後に、各レシピのHTMLファイルの下部には、QWebChannel を介してC++とJavaScriptの間の通信を行うスクリプトがあります。idコンテンツを持つ<div>
内のオリジナルの変更されていないテキストコンテンツはC++側に渡され、m_content
のtextChanged
シグナルが発せられたときに呼び出されるコールバックがセットアップされます。コールバックは、<div>
プレースホルダーの内容を解析されたマークダウンで更新します。
ファイルと属性
この例では、以下のコードをサードパーティ・ライセンスでバンドルしています:
マーク | MITライセンス |
マークダウン.css | Apacheライセンス2.0 |
©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。