本页内容

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 对象,并将其表示形式写入其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 ,并封装一个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 成员从该设备读取内容。请注意,XML输入必须是结构正确的,才能被QXmlStreamReader 接受。首先,它会读取外部结构并验证内容是否为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" ,随后扫描元素的其余部分以查找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() 函数会显示一个QMessageBox ,其中包含示例的简要说明,或关于Qt及其当前使用版本的一般信息。

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

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