QXmlStream 북마크 예제

XBEL 파일을 읽고 쓰는 방법을 보여줍니다.

QXmlStream 북마크 예제는 XML 북마크 교환 언어(XBEL) 파일에 대한 뷰어를 제공합니다. 이 예제는 Qt의 QXmlStreamReader 를 사용하여 북마크를 읽고 QXmlStreamWriter 를 사용하여 다시 쓸 수 있습니다. 이 예제는 이러한 리더와 라이터 유형을 사용하는 방법을 보여주기 위한 것이므로 북마크를 열거나 새 북마크를 추가하거나 두 개의 북마크 파일을 병합하는 수단은 제공하지 않으며 북마크 편집을 위한 최소한의 범위만 제공합니다. 그럼에도 불구하고 원하는 경우 이러한 기능으로 확장할 수 있습니다.

XbelWriter 클래스 정의

XbelWriter 클래스는 북마크가 포함된 폴더의 계층 구조를 설명하는 tree widget 을 사용합니다. writeFile() 은 이 계층구조를 지정된 출력 장치에 XBEL 형식으로 출력하는 수단을 제공합니다.

내부적으로는 주어진 트리 위젯을 기록하고 QXmlStreamWriter 의 비공개 인스턴스를 패키징하여 XML을 스트리밍할 수 있는 수단을 제공합니다. 내부에는 트리의 각 항목을 쓰기 위한 writeItem() 이 있습니다.

class XbelWriter
{
public:
    explicit XbelWriter(const QTreeWidget *treeWidget);
    bool writeFile(QIODevice *device);

private:
    void writeItem(const QTreeWidgetItem *item);
    QXmlStreamWriter xml;
    const QTreeWidget *treeWidget;
};

XbelWriter 클래스 구현

XbelWriter 생성자는 설명할 treeWidget 을 받아들입니다. 이를 저장하고 QXmlStreamWriter 의 자동 서식 지정 속성을 활성화합니다. 마지막으로 데이터를 여러 줄로 분할하고 들여쓰기를 통해 트리의 구조를 표시하여 XML 출력을 더 쉽게 읽을 수 있도록 합니다.

XbelWriter::XbelWriter(const QTreeWidget *treeWidget) : treeWidget(treeWidget)
{
    xml.setAutoFormatting(true);
}

writeFile() 함수는 QIODevice 객체를 받아들이고 QXmlStreamWriter 멤버에게 setDevice() 을 사용하여 이 장치에 쓰도록 지시합니다. 그런 다음 이 함수는 문서 유형 정의(DTD), 시작 요소, 버전을 작성하고 treeWidget 의 각 최상위 항목에 대한 쓰기를 writeItem() 에 위임합니다. 마지막으로 문서를 닫고 반환합니다.

bool XbelWriter::writeFile(QIODevice *device)
{
    xml.setDevice(device);

    xml.writeStartDocument();
    xml.writeDTD("<!DOCTYPE xbel>"_L1);
    xml.writeStartElement("xbel"_L1);
    xml.writeAttribute("version"_L1, "1.0"_L1);
    for (int i = 0; i < treeWidget->topLevelItemCount(); ++i)
        writeItem(treeWidget->topLevelItem(i));

    xml.writeEndDocument();
    return true;
}

writeItem() 함수는 QTreeWidgetItem 객체를 받아들이고 해당 객체의 표현을 UserRole, "folder", "bookmark" 또는 "separator" 중 하나에 따라 달라지는 XML 스트림에 씁니다. 각 폴더 내에서 각 하위 항목에서 자신을 재귀적으로 호출하여 폴더의 XML 요소 내에 각 하위 항목의 표현을 재귀적으로 포함합니다.

void XbelWriter::writeItem(const QTreeWidgetItem *item)
{
    QString tagName = item->data(0, Qt::UserRole).toString();
    if (tagName == "folder"_L1) {
        bool folded = !item->isExpanded();
        xml.writeStartElement(tagName);
        xml.writeAttribute("folded"_L1, folded ? "yes"_L1 : "no"_L1);
        xml.writeTextElement("title"_L1, item->text(0));
        for (int i = 0; i < item->childCount(); ++i)
            writeItem(item->child(i));
        xml.writeEndElement();
    } else if (tagName == "bookmark"_L1) {
        xml.writeStartElement(tagName);
        if (!item->text(1).isEmpty())
            xml.writeAttribute("href"_L1, item->text(1));
        xml.writeTextElement("title"_L1, item->text(0));
        xml.writeEndElement();
    } else if (tagName == "separator"_L1) {
        xml.writeEmptyElement(tagName);
    }
}

XbelReader 클래스 정의

XbelReadertree widget 을 받아 북마크 계층 구조를 설명하는 항목으로 채웁니다. 이 클래스는 이러한 항목의 소스로서 QIODevice 에서 XBEL 데이터를 읽는 것을 지원합니다. XBEL 데이터 구문 분석에 실패하면 무엇이 잘못되었는지 보고할 수 있습니다.

내부적으로는 채울 QTreeWidget 을 기록하고 컴패니언 클래스인 QXmlStreamReader 의 인스턴스를 QXmlStreamWriter 로 패키징하여 XBEL 데이터를 읽는 데 사용합니다.

class XbelReader
{
public:
    XbelReader(QTreeWidget *treeWidget);

    bool read(QIODevice *device);
    QString errorString() const;

private:
    void readXBEL();
    void readTitle(QTreeWidgetItem *item);
    void readSeparator(QTreeWidgetItem *item);
    void readFolder(QTreeWidgetItem *item);
    void readBookmark(QTreeWidgetItem *item);

    QTreeWidgetItem *createChildItem(QTreeWidgetItem *item);

    QXmlStreamReader xml;
    QTreeWidget *treeWidget;

    QIcon folderIcon;
    QIcon bookmarkIcon;
};

XbelReader 클래스 구현

XBEL 리더는 XML 요소를 읽는 데만 관심이 있으므로 readNextStartElement() 편의 함수를 광범위하게 사용합니다.

XbelReader 생성자에는 QTreeWidget 을 채워야 합니다. 이 함수는 트리 위젯의 스타일을 적절한 아이콘으로 채웁니다: 각 폴더가 열려 있는지 또는 닫혀 있는지를 나타내는 형태로 변경되는 폴더 아이콘과 해당 폴더 내의 개별 북마크에 대한 표준 파일 아이콘.

XbelReader::XbelReader(QTreeWidget *treeWidget) : treeWidget(treeWidget)
{
    QStyle *style = treeWidget->style();

    folderIcon.addPixmap(style->standardPixmap(QStyle::SP_DirClosedIcon), QIcon::Normal,
                         QIcon::Off);
    folderIcon.addPixmap(style->standardPixmap(QStyle::SP_DirOpenIcon), QIcon::Normal, QIcon::On);
    bookmarkIcon.addPixmap(style->standardPixmap(QStyle::SP_FileIcon));
}

read() 함수는 QIODevice. QXmlStreamReader 멤버에게 해당 디바이스에서 콘텐츠를 읽도록 지시합니다. QXmlStreamReader 에서 수락하려면 XML 입력이 제대로 형식화되어 있어야 합니다. 먼저 외부 구조를 읽고 콘텐츠가 XBEL 1.0 파일인지 확인합니다. 만약 그렇다면 read() 은 콘텐츠의 실제 읽기를 내부 readXBEL() 에 위임합니다.

그렇지 않으면 raiseError() 함수가 오류 메시지를 기록하는 데 사용됩니다. 입력에 오류가 발생하면 리더 자체도 동일한 작업을 수행할 수 있습니다. read() 이 완료되면 오류가 없으면 참을 반환합니다.

bool XbelReader::read(QIODevice *device)
{
    xml.setDevice(device);

    if (xml.readNextStartElement()) {
        if (xml.name() == "xbel"_L1 && xml.attributes().value("version"_L1) == "1.0"_L1)
            readXBEL();
        else
            xml.raiseError(QObject::tr("The file is not an XBEL version 1.0 file."));
    }

    return !xml.error();
}

read() 함수가 false를 반환하면 호출자는 errorString() 함수를 호출하여 스트림 내의 줄 및 열 번호와 함께 오류에 대한 설명을 얻을 수 있습니다.

QString XbelReader::errorString() const
{
    return QObject::tr("%1\nLine %2, column %3")
            .arg(xml.errorString())
            .arg(xml.lineNumber())
            .arg(xml.columnNumber());
}

readXBEL() 함수는 시작 요소의 이름을 읽고 태그 이름이 "folder", "bookmark" 또는 "separator" 인지 여부에 따라 적절한 함수를 호출하여 해당 요소를 읽습니다. 다른 요소가 발견되면 건너뜁니다. 이 함수는 전제 조건으로 시작하여 XML 리더가 방금 "xbel" 요소를 열었는지 확인합니다.

void XbelReader::readXBEL()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "xbel"_L1);

    while (xml.readNextStartElement()) {
        if (xml.name() == "folder"_L1)
            readFolder(nullptr);
        else if (xml.name() == "bookmark"_L1)
            readBookmark(nullptr);
        else if (xml.name() == "separator"_L1)
            readSeparator(nullptr);
        else
            xml.skipCurrentElement();
    }
}

readBookmark() 함수는 단일 북마크를 나타내는 편집 가능한 항목을 새로 만듭니다. 이 함수는 현재 요소의 XML "href" 속성을 항목의 두 번째 열 텍스트로 기록하고 첫 번째 열 텍스트를 "Unknown title" 로 임시 설정한 후 나머지 요소에서 제목 요소를 검색하여 이를 덮어쓰고 인식되지 않은 하위 요소를 건너뜁니다.

void XbelReader::readTitle(QTreeWidgetItem *item)
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "title"_L1);
    item->setText(0, xml.readElementText());
}

readTitle() 함수는 북마크의 제목을 읽고 이를 호출된 항목의 제목(첫 번째 열 텍스트)으로 기록합니다.

void XbelReader::readSeparator(QTreeWidgetItem *item)
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "separator"_L1);
    constexpr char16_t midDot = u'\xB7';
    static const QString dots(30, midDot);

    QTreeWidgetItem *separator = createChildItem(item);
    separator->setFlags(item ? item->flags() & ~Qt::ItemIsSelectable : Qt::ItemFlags{});
    separator->setText(0, dots);
    xml.skipCurrentElement();
}

readSeparator() 함수는 구분 기호를 생성하고 해당 플래그를 설정합니다. 구분 기호 항목의 텍스트는 중앙에 30개의 점으로 설정됩니다. 그런 다음 나머지 요소는 skipCurrentElement()를 사용하여 건너뜁니다.

void XbelReader::readSeparator(QTreeWidgetItem *item)
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "separator"_L1);
    constexpr char16_t midDot = u'\xB7';
    static const QString dots(30, midDot);

    QTreeWidgetItem *separator = createChildItem(item);
    separator->setFlags(item ? item->flags() & ~Qt::ItemIsSelectable : Qt::ItemFlags{});
    separator->setText(0, dots);
    xml.skipCurrentElement();
}

readFolder() 함수는 항목을 생성하고 폴더 요소의 콘텐츠를 반복하여 이 항목에 자식을 추가하여 폴더 요소의 콘텐츠를 나타냅니다. 폴더 콘텐츠에 대한 반복은 readXBEL() 의 형식과 비슷하지만 제목 요소를 사용하여 폴더의 제목을 설정할 수 있다는 점을 제외하면 비슷합니다.

void XbelReader::readFolder(QTreeWidgetItem *item)
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "folder"_L1);

    QTreeWidgetItem *folder = createChildItem(item);
    bool folded = xml.attributes().value("folded"_L1) != "no"_L1;
    folder->setExpanded(!folded);

    while (xml.readNextStartElement()) {
        if (xml.name() == "title"_L1)
            readTitle(folder);
        else if (xml.name() == "folder"_L1)
            readFolder(folder);
        else if (xml.name() == "bookmark"_L1)
            readBookmark(folder);
        else if (xml.name() == "separator"_L1)
            readSeparator(folder);
        else
            xml.skipCurrentElement();
    }
}

createChildItem() 헬퍼 함수는 지정된 항목의 하위 항목이거나 부모 항목이 지정되지 않은 경우 트리 위젯의 직접 하위 항목인 새 트리 위젯 항목을 만듭니다. 이 함수는 새 항목의 UserRole 을 현재 XML 요소의 태그 이름으로 설정하며, XbelWriter::writeFile()이 UserRole 을 사용하는 방식과 일치합니다.

QTreeWidgetItem *XbelReader::createChildItem(QTreeWidgetItem *item)
{
    QTreeWidgetItem *childItem = item ? new QTreeWidgetItem(item) : new QTreeWidgetItem(treeWidget);
    childItem->setData(0, Qt::UserRole, xml.name().toString());
    return childItem;
}

메인윈도우 클래스 정의

MainWindow 클래스는 QMainWindow 의 하위 클래스이며 File 메뉴와 Help 메뉴가 있습니다.

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

public slots:
    void open();
    void saveAs();
    void about();
#if QT_CONFIG(clipboard) && QT_CONFIG(contextmenu)
    void onCustomContextMenuRequested(const QPoint &pos);
#endif
private:
    void createMenus();

    QTreeWidget *const treeWidget;
};

MainWindow 클래스 구현

MainWindow 생성자는 QTreeWidget 객체, treeWidget 를 자체 중앙 위젯으로 설정하고 각 책 표시의 제목과 위치에 대한 열 제목을 지정합니다. 사용자가 트리 위젯 내에서 개별 북마크에 대한 작업을 수행할 수 있는 사용자 지정 메뉴를 구성합니다.

createMenus() 을 호출하여 자체 메뉴와 해당 작업을 설정합니다. 제목을 설정하고, 준비되었음을 알리고, 사용 가능한 화면 공간의 적절한 비율에 맞게 크기를 설정합니다.

MainWindow::MainWindow() : treeWidget(new QTreeWidget)
{
    treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch);
    treeWidget->setHeaderLabels(QStringList{tr("Title"), tr("Location")});
#if QT_CONFIG(clipboard) && QT_CONFIG(contextmenu)
    treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(treeWidget, &QWidget::customContextMenuRequested,
            this, &MainWindow::onCustomContextMenuRequested);
#endif
    setCentralWidget(treeWidget);

    createMenus();

    statusBar()->showMessage(tr("Ready"));

    setWindowTitle(tr("QXmlStream Bookmarks"));
    const QSize availableSize = screen()->availableGeometry().size();
    resize(availableSize.width() / 2, availableSize.height() / 3);
}

사용자가 북마크를 마우스 오른쪽 버튼으로 클릭하면 트리거되는 사용자 정의 메뉴는 북마크를 링크로 복사하거나 북마크가 참조하는 URL을 열도록 데스크톱 브라우저에 지시하는 기능을 제공합니다. 이 메뉴는 onCustomContextMenuRequested() 에 의해 구현됩니다(관련 기능이 활성화된 경우).

#if QT_CONFIG(clipboard) && QT_CONFIG(contextmenu)
void MainWindow::onCustomContextMenuRequested(const QPoint &pos)
{
    const QTreeWidgetItem *item = treeWidget->itemAt(pos);
    if (!item)
        return;
    const QString url = item->text(1);
    QMenu contextMenu;
    QAction *copyAction = contextMenu.addAction(tr("Copy Link to Clipboard"));
    QAction *openAction = contextMenu.addAction(tr("Open"));
    QAction *action = contextMenu.exec(treeWidget->viewport()->mapToGlobal(pos));
    if (action == copyAction)
        QGuiApplication::clipboard()->setText(url);
    else if (action == openAction)
        QDesktopServices::openUrl(QUrl(url));
}
#endif // QT_CONFIG(clipboard) && QT_CONFIG(contextmenu)

createMenus() 함수는 fileMenuhelpMenu 를 생성하고 QAction 객체를 추가하여 QWidget::close() 및 QApplication::aboutQt()과 함께 open(), saveAs()about() 함수에 다양하게 바인딩합니다. 연결은 아래와 같습니다:

void MainWindow::createMenus()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
    QAction *openAct = fileMenu->addAction(tr("&Open..."), this, &MainWindow::open);
    openAct->setShortcuts(QKeySequence::Open);

    QAction *saveAsAct = fileMenu->addAction(tr("&Save As..."), this, &MainWindow::saveAs);
    saveAsAct->setShortcuts(QKeySequence::SaveAs);

    QAction *exitAct = fileMenu->addAction(tr("E&xit"), this, &QWidget::close);
    exitAct->setShortcuts(QKeySequence::Quit);

    menuBar()->addSeparator();

    QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
    helpMenu->addAction(tr("&About"), this, &MainWindow::about);
    helpMenu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
}

이렇게 하면 아래 스크린샷에 표시된 메뉴가 생성됩니다:

open() 함수는 트리거되면 사용자에게 북마크 파일을 선택하는 데 사용할 수 있는 파일 대화 상자를 제공합니다. 파일을 선택하면 XBelReader 을 사용하여 파싱하여 treeWidget 에 북마크를 채웁니다. 파일을 열거나 구문 분석하는 데 문제가 발생하면 파일 이름과 오류 메시지 등 적절한 경고 메시지가 사용자에게 표시됩니다. 그렇지 않으면 파일에서 읽은 북마크가 표시되고 창의 상태 표시줄에 파일이 로드되었음을 간략하게 알려줍니다.

void MainWindow::open()
{
    QFileDialog fileDialog(this, tr("Open Bookmark File"), QDir::currentPath());
    fileDialog.setMimeTypeFilters({"application/x-xbel"_L1});
    if (fileDialog.exec() != QDialog::Accepted)
        return;

    treeWidget->clear();

    const QString fileName = fileDialog.selectedFiles().constFirst();
    QFile file(fileName);
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("QXmlStream Bookmarks"),
                             tr("Cannot read file %1:\n%2.")
                                     .arg(QDir::toNativeSeparators(fileName), file.errorString()));
        return;
    }

    XbelReader reader(treeWidget);
    if (!reader.read(&file)) {
        QMessageBox::warning(
                this, tr("QXmlStream Bookmarks"),
                tr("Parse error in file %1:\n\n%2")
                        .arg(QDir::toNativeSeparators(fileName), reader.errorString()));
    } else {
        statusBar()->showMessage(tr("File loaded"), 2000);
    }
}

saveAs() 함수는 사용자에게 북마크 데이터의 사본을 저장할 fileName 을 입력하라는 QFileDialog 을 표시합니다. open() 함수와 마찬가지로 이 함수 역시 파일을 쓸 수 없는 경우 경고 메시지를 표시합니다.

void MainWindow::saveAs()
{
    QFileDialog fileDialog(this, tr("Save Bookmark File"), QDir::currentPath());
    fileDialog.setAcceptMode(QFileDialog::AcceptSave);
    fileDialog.setDefaultSuffix("xbel"_L1);
    fileDialog.setMimeTypeFilters({"application/x-xbel"_L1});
    if (fileDialog.exec() != QDialog::Accepted)
        return;

    const QString fileName = fileDialog.selectedFiles().constFirst();
    QFile file(fileName);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, tr("QXmlStream Bookmarks"),
                             tr("Cannot write file %1:\n%2.")
                                     .arg(QDir::toNativeSeparators(fileName), file.errorString()));
        return;
    }

    XbelWriter writer(treeWidget);
    if (writer.writeFile(&file))
        statusBar()->showMessage(tr("File saved"), 2000);
}

about() 함수는 예제에 대한 간략한 설명이나 Qt에 대한 일반 정보 및 사용 중인 버전과 함께 QMessageBox 을 표시합니다.

void MainWindow::about()
{
    QMessageBox::about(this, tr("About QXmlStream Bookmarks"),
                       tr("The <b>QXmlStream Bookmarks</b> example demonstrates how to use Qt's "
                          "QXmlStream classes to read and write XML documents."));
}

main() 함수

main() 함수는 MainWindow 을 인스턴스화하고 show() 함수를 호출하여 표시한 다음, 사용자가 가장 먼저 하고 싶어할 가능성이 높은 open() 을 호출합니다.

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow mainWin;
    mainWin.show();
    mainWin.open();
    return app.exec();
}

XBEL 파일에 대한 자세한 내용은 XML 북마크 교환 언어 리소스 페이지를 참조하세요.

예제 프로젝트 @ 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.