QXmlStream 书签示例

演示如何读取和写入 XBEL 文件。

QXmlStream 书签示例提供了一个用于 XML 书签交换语言(XBEL)文件的查看器。它可以使用 Qt 的QXmlStreamReader读取书签,并使用QXmlStreamWriter将其写回。鉴于本例旨在展示如何使用这些读者和写入器类型,它不提供打开书签、添加新书签或合并两个书签文件的方法,对于编辑书签的功能也非常有限。尽管如此,如果需要,它仍然可以扩展这些功能。

XbelWriter 类定义

XbelWriter类接受一个树小部件,描述包含书签的文件夹层次结构。它的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)、起始元素、版本,并将写入顶级项的操作委托给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 接收一个 树控件 以填充包含描述书签层次的条目。它支持从 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() 函数读取一个起始元素的名称并调用适当的函数来读取它,具体取决于其标签名是 "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" 属性作为项的第二列文本,并在扫描元素余下的部分以查找标题元素来覆盖它之前,暂时将该第一列文本设置为 "未知标题",跳过任何未识别的子元素。

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的子类,具有文件菜单和帮助菜单。

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对象添加到它们中,各种绑定到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);
}

这创建了下面截图中的菜单

当触发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

© 2024 Qt公司有限公司。本文档中包含的贡献物均为各自所有者的版权。所提供的文档受自由软件基金会发布的GNU自由文档 License 第1.3版条款许可。