레시피 브라우저

웹 페이지에 사용자 지정 스타일시트를 삽입하고 사용자 지정 마크업 언어에 대한 서식 있는 텍스트 미리보기 도구를 제공합니다.

레시피 브라우저는 작은 하이브리드 웹 브라우저 애플리케이션입니다. 이 애플리케이션은 Qt WebEngine C++ classes 를 사용하여 C++와 JavaScript 로직을 결합하는 방법을 다음과 같은 방식으로 보여줍니다.

  • QWebEnginePage::runJavaScript() 을 통해 임의의 JavaScript 코드를 실행하여 사용자 정의 CSS 스타일시트 삽입하기
  • QWebEngineScriptQWebEngineScriptCollection 을 사용하여 자바스크립트 코드를 유지하고 모든 페이지에 삽입하기
  • QWebChannel 을 사용하여 사용자 정의 마크업 언어와 상호 작용하고 서식 있는 텍스트 미리 보기 제공

마크다운은 일반 텍스트 서식 구문을 사용하는 경량 마크업 언어입니다. github와 같은 일부 서비스는 이 형식을 인식하여 브라우저에서 볼 때 콘텐츠를 서식 있는 텍스트로 렌더링합니다.

레시피 브라우저 메인 창은 왼쪽의 탐색 영역과 오른쪽의 미리보기 영역으로 나뉩니다. 오른쪽의 미리보기 영역은 사용자가 메인 창의 왼쪽 상단에 있는 편집 버튼을 클릭하면 편집기로 전환됩니다. 편집기는 마크다운 구문을 지원하며 QPlainTextEdit 을 사용하여 구현됩니다. 사용자가 보기 버튼을 클릭하면 편집 버튼이 변환되는 미리보기 영역에서 문서가 서식 있는 텍스트로 렌더링됩니다. 이 렌더링은 QWebEngineView 을 사용하여 구현됩니다. 텍스트를 렌더링하기 위해 웹 엔진 내부의 JavaScript 라이브러리가 마크다운 텍스트를 HTML로 변환합니다. 미리보기는 편집기에서 QWebChannel 을 통해 업데이트됩니다.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 예제를 실행하려면 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 Qt Creator: 튜토리얼을 참조하세요 : 빌드 및 실행을 참조하세요.

문서 텍스트 노출하기

현재 마크다운 텍스트를 렌더링하려면 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 클래스는 QString m_currentTextsetText() 메서드로 C++ 측에서 설정하고 런타임에 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> 로서 페이지의 텍스트 콘텐츠에 대한 변경 사항이 탐색 간에 유지되도록 합니다. 하지만 수정된 텍스트 내용을 드라이브에 기록하지 않고 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;
};

이 클래스는 왼쪽 상단의 탐색 목록 보기 위에 있는 두 개의 버튼과 일치하는 비공개 슬롯을 선언합니다. 또한 사용자 정의 CSS 스타일시트에 대한 헬퍼 메서드가 선언되어 있습니다.

기본 창의 실제 레이아웃은 .ui 파일에 지정됩니다. 위젯과 액션은 런타임에 ui 멤버 변수에서 사용할 수 있습니다.

m_isEditMode 는 편집기와 미리보기 영역 사이를 전환하는 부울입니다. m_contentDocument 클래스의 인스턴스입니다.

다른 객체의 실제 설정은 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);

여기서 QPushButtonclicked 신호는 스타일시트 대화 상자를 표시하거나 편집 모드와 보기 모드 사이를 전환하는, 즉 편집기와 미리보기 영역을 각각 숨기고 표시하는 각각의 기능에 연결됩니다.

    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 편집기에 대한 포인터인 QPlainTextEditm_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 의 텍스트를 업데이트하는 람다에 연결됩니다. 그런 다음 이 객체는 content 이라는 이름으로 QWebChannel 에 의해 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> 부분에는 두 개의 CSS 파일이 포함되어 있습니다: markdown.css 마크다운의 스타일을 지정하는 custom.css와 몇 가지 추가 스타일을 지정하지만 가장 중요한 것은 <div> 에는 수정되지 않은 초기 콘텐츠 텍스트만 포함되어 있기 때문에 아이디 콘텐츠가 있는 <div> 을 숨기는 것입니다. marked.js 은 마크다운을 파싱하고 HTML로 변환하는 역할을 하고, custom.jsmarked.js 의 일부 구성을 수행하며, qwebchannel.jsQWebChannel JavaScript API를 노출합니다.

본문에는 두 개의 <div> 요소가 있습니다. id가 있는 <div> 플레이스홀더에는 마크다운 텍스트가 삽입되어 렌더링되고 표시됩니다. <div> with id 콘텐츠는 custom.css 에 의해 숨겨지며 레시피의 수정되지 않은 원본 텍스트 콘텐츠만 포함합니다.

마지막으로 각 레시피 HTML 파일의 맨 아래에는 QWebChannel 을 통해 C++ 과 JavaScript 측 간의 통신을 담당하는 스크립트가 있습니다. <div> 내부의 수정되지 않은 원본 텍스트 콘텐츠와 ID 콘텐츠가 C++ 측으로 전달되고 m_contenttextChanged 신호가 전송될 때 호출되는 콜백이 설정됩니다. 그러면 콜백은 파싱된 마크다운으로 <div> 플레이스홀더의 콘텐츠를 업데이트합니다.

파일 및 어트리뷰션

이 예에서는 다음 코드를 타사 라이선스와 함께 번들로 제공합니다:

MarkedMIT 라이선스
Markdown.cssApache 라이선스 2.0

예제 프로젝트 @ code.qt.io

© 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.