이 페이지에서

QXmlStream 북마크 예제

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

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

제목 및 위치 열이 포함된 북마크 트리

XbelWriter 클래스 정의

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

내부적으로는 전달받은 트리 위젯을 기록하고, XML 스트리밍 기능을 제공하는 QXmlStreamWriter 의 비공개 인스턴스를 포함합니다. 또한 트리 내의 각 항목을 기록하기 위한 내부 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 객체를 받아, setDevice() 를 사용하여 QXmlStreamWriter 멤버가 이 장치에 쓰도록 지시합니다. 그런 다음 이 함수는 문서 유형 정의(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 객체를 인수로 받아, 해당 객체의 표현을 XML 스트림에 기록합니다. 이 표현은 객체의 UserRole 에 따라 달라지며, "folder", "bookmark" 또는 "separator" 중 하나일 수 있습니다. 각 폴더 내에서 이 함수는 각 자식 항목에 대해 재귀적으로 호출되어, 폴더의 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 클래스 정의

XbelReader 은 북마크 계층 구조를 설명하는 항목들로 채울 ` tree widget `을 인수로 받습니다. 이 클래스는 이러한 항목의 소스로서 ` QIODevice `에서 XBEL 데이터를 읽는 기능을 지원합니다. XBEL 데이터 구문 분석에 실패할 경우, 어떤 문제가 발생했는지 보고할 수 있습니다.

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

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 멤버가 해당 장치에서 내용을 읽도록 지시합니다. XML 입력은 QXmlStreamReader 에서 받아들여지려면 잘 구성된(well-formed) 형태여야 한다는 점에 유의하십시오. 먼저 외부 구조를 읽고 내용이 XBEL 1.0 파일인지 확인합니다. 만약 그렇다면, read() 는 실제 내용 읽기 작업을 내부 readXBEL() 에 위임합니다.

그렇지 않은 경우, raiseError() 함수를 사용하여 오류 메시지를 기록합니다. 리더 자체도 입력에서 오류를 발견하면 동일한 조치를 취할 수 있습니다. read() 가 처리를 완료하면 오류가 없었을 경우 true를 반환합니다.

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() 함수는 startElement의 이름을 읽은 후, 태그 이름이 "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 클래스 정의

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() 함수는 fileMenu 및 helpMenu 을 생성하고, 여기에 QAction 객체를 추가합니다. 이 객체들은 open(), saveAs(), about() 함수는 물론 QWidget::close() 및 QApplication::aboutQt() 함수와도 다양하게 바인딩됩니다. 연결 관계는 다음과 같습니다:

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);
}

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

‘열기’, ‘다른 이름으로 저장’, ‘종료’가 포함된 파일 메뉴‘정보’ 및 ‘Qt 정보’가 포함된 도움말 메뉴

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() 함수는 QFileDialog 을 표시하고, 북마크 데이터의 사본을 저장할 fileName 을 사용자에게 입력받습니다. 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 북마크 교환 언어(XML Bookmark Exchange Language, XBEL) 리소스 페이지를 참조하십시오.

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

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