QXmlStream 书签示例
演示如何读写 XBEL 文件。
QXmlStream Bookmarks 示例为 XML 书签交换语言(XBEL)文件提供了一个查看器。它可以使用 Qt XML 的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 。它将用合适的图标填充树形 widget 的样式:一个文件夹图标,该图标会改变形式,以显示每个文件夹是打开还是关闭;以及一个标准文件图标,用于显示这些文件夹中的单个书签。
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()
完成后,如果没有错误,则返回 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()
中的循环类似,只是现在接受了一个 title 元素来设置文件夹的标题。
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()
辅助函数创建了一个新的树状 widget 项,该项可以是给定项的子项,如果没有给定父项,则可以是树状 widget 的直接子项。它将新项的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
构造函数将其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); }
这样就创建了下图所示的菜单:
![]() | ![]() |
触发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 Bookmark Exchange Language Resource Page。
© 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.