QXmlStream のブックマーク例
XBEL ファイルの読み書き方法を示します。
QXmlStream ブックマークの例は、XML Bookmark Exchange Language (XBEL) ファイル用のビューアを提供します。QtのQXmlStreamReader を使用してブックマークを読み込み、QXmlStreamWriter を使用してそれらを書き戻すことができます。 このサンプルは、これらの読み取りおよび書き込みクラスの使用方法を示すことを目的としているため、ブックマークを開いたり、新しいブックマークを追加したり、2つのブックマークファイルをマージしたりする機能は備えておらず、ブックマークの編集機能も最小限に留まっています。とはいえ、必要に応じて、こうした機能を追加して拡張することは可能です。

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 メンバーに指示して、そのデバイスからコンテンツを読み取らせます。なお、QXmlStreamReader によって受け入れられるためには、XML入力がウェルフォームである必要があります。まず、外側の構造を読み取り、コンテンツがXBEL 1.0ファイルであることを検証します。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" を項目の2列目のテキストとして記録し、1列目のテキストを暫定的に"Unknown title" に設定した後、要素の残りの部分をスキャンして、それを上書きするtitle要素を探します。その際、認識されない子要素はすべてスキップされます。
void XbelReader::readTitle(QTreeWidgetItem *item)
{
Q_ASSERT(xml.isStartElement() && xml.name() == "title"_L1);
item->setText(0, xml.readElementText());
}readTitle() 関数は、ブックマークのタイトルを読み取り、それを呼び出された項目のタイトル(1列目のテキスト)として記録します。
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() ヘルパー関数は、新しいツリーウィジェット項目を作成します。この項目は、指定された項目の子となるか、親項目が指定されていない場合はツリーウィジェットの直接の子となります。また、この関数は、新しい項目の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);
}これにより、以下のスクリーンショットに示すようなメニューが作成されます:
![]() | ![]() |
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 Bookmark Exchange Language リソースページを参照してください。
© 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.

