Sur cette page

Exemple de signets QXmlStream

Montre comment lire et écrire des fichiers XBEL.

L'exemple « Signets QXmlStream » fournit une visionneuse pour les fichiers XML Bookmark Exchange Language (XBEL). Il permet de lire des signets à l'aide de la classe ` QXmlStreamReader ` de Qt et de les réécrire à l'aide de la classe ` QXmlStreamWriter`. Cet exemple ayant pour but de montrer comment utiliser ces types de lecture et d’écriture, il ne permet pas d’ouvrir un signet, d’en ajouter un nouveau ou de fusionner deux fichiers de signets, et n’offre qu’un champ d’action minimal pour la modification des signets. Il pourrait néanmoins être facilement étendu avec de telles fonctionnalités, si nécessaire.

Arborescence des signets avec les colonnes « Titre » et « Emplacement »

Définition de la classe XbelWriter

La classe ` XbelWriter ` prend en paramètre un ` tree widget ` décrivant une hiérarchie de dossiers contenant des signets. Sa méthode ` writeFile() ` permet d’écrire cette hiérarchie, au format XBEL, sur un périphérique de sortie donné.

En interne, elle enregistre le widget arborescent qui lui a été fourni et encapsule une instance privée de ` QXmlStreamWriter`, qui lui permet de diffuser du XML. Elle dispose d’un ` writeItem() ` interne pour écrire chaque élément de son arborescence.

class XbelWriter
{
public:
    explicit XbelWriter(const QTreeWidget *treeWidget);
    bool writeFile(QIODevice *device);

private:
    void writeItem(const QTreeWidgetItem *item);
    QXmlStreamWriter xml;
    const QTreeWidget *treeWidget;
};

Implémentation de la classe XbelWriter

Le constructeur de ` XbelWriter ` accepte l’ treeWidget qu’il va décrire. Il le stocke et active la propriété de mise en forme automatique de ` QXmlStreamWriter`. Cette dernière divise les données en plusieurs lignes, avec une indentation indiquant la structure de l’arborescence, ce qui rend la sortie XML plus facile à lire.

XbelWriter::XbelWriter(const QTreeWidget *treeWidget) : treeWidget(treeWidget)
{
    xml.setAutoFormatting(true);
}

La fonction ` writeFile() ` accepte un objet ` QIODevice ` et ordonne à son membre ` QXmlStreamWriter ` d’écrire sur ce périphérique, à l’aide de ` setDevice()`. Cette fonction écrit ensuite la définition de type de document (DTD), l’élément de début, la version, puis délègue l’écriture de chacun des éléments de niveau supérieur de ` treeWidget` à ` writeItem()`. Enfin, elle ferme le document et retourne la valeur.

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

La fonction writeItem() accepte un objet QTreeWidgetItem et écrit dans son flux XML une représentation de l’objet, qui dépend de son UserRole, pouvant être soit un "folder", soit un "bookmark", soit un "separator". Au sein de chaque dossier, elle s’appelle elle-même de manière récursive sur chaque élément enfant, afin d’inclure de manière récursive une représentation de chaque enfant dans l’élément XML du dossier.

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

Définition de la classe XbelReader

La classe ` XbelReader ` prend en paramètre un ` tree widget ` qu’elle remplit avec des éléments décrivant une hiérarchie de signets. Elle prend en charge la lecture de données XBEL à partir d’un ` QIODevice ` comme source de ces éléments. Si l’analyse des données XBEL échoue, elle peut signaler la nature du problème.

En interne, elle enregistre l’ QTreeWidget qu’elle va remplir et encapsule une instance de ` QXmlStreamReader`, la classe associée à ` QXmlStreamWriter`, qu’elle utilisera pour lire les données 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;
};

Implémentation de la classe XbelReader

Comme le lecteur XBEL se charge uniquement de lire les éléments XML, il utilise abondamment la fonction de commodité ` readNextStartElement()`.

Le constructeur XbelReader nécessite un objet QTreeWidget qu’il va initialiser. Il configure le style du widget arborescent avec des icônes adaptées : une icône de dossier dont la forme change pour indiquer si chaque dossier est ouvert ou fermé ; et une icône de fichier standard pour les signets individuels contenus dans ces dossiers.

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

La fonction ` read() ` accepte un ` QIODevice`. Elle ordonne à son membre ` QXmlStreamReader ` de lire le contenu à partir de ce périphérique. Notez que l’entrée XML doit être bien formée pour être acceptée par ` QXmlStreamReader`. Elle lit d’abord la structure externe et vérifie que le contenu est un fichier XBEL 1.0 ; si c’est le cas, ` read() ` délègue la lecture effective du contenu à la fonction interne ` readXBEL()`.

Sinon, la fonction raiseError() est utilisée pour enregistrer un message d’erreur. Le lecteur lui-même peut également procéder de la sorte s’il rencontre des erreurs dans le flux d’entrée. Lorsque read() a terminé, il renvoie « true » s’il n’y a pas eu d’erreurs.

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

Si read() renvoie « false », son appelant peut obtenir une description de l’erreur, accompagnée du numéro de ligne et de colonne dans le flux, en appelant la fonction errorString().

QString XbelReader::errorString() const
{
    return QObject::tr("%1\nLine %2, column %3")
            .arg(xml.errorString())
            .arg(xml.lineNumber())
            .arg(xml.columnNumber());
}

La fonction ` readXBEL() ` lit le nom d’un élément `startElement` et appelle la fonction appropriée pour le lire, selon que le nom de la balise est ` "folder"`, ` "bookmark" ` ou ` "separator"`. Tous les autres éléments rencontrés sont ignorés. La fonction commence par une condition préalable, vérifiant que le lecteur XML vient d’ouvrir un élément ` "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();
    }
}

La fonction ` readBookmark() ` crée un nouvel élément modifiable représentant un seul signet. Elle enregistre l’attribut XML ` "href" ` de l’élément courant comme texte de la deuxième colonne de l’élément et définit provisoirement le texte de la première colonne sur ` "Unknown title" ` avant de parcourir le reste de l’élément à la recherche d’un élément `title` permettant de le remplacer, en ignorant tous les éléments enfants non reconnus.

void XbelReader::readTitle(QTreeWidgetItem *item)
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "title"_L1);
    item->setText(0, xml.readElementText());
}

La fonction ` readTitle() ` lit le titre d’un signet et l’enregistre comme titre (texte de la première colonne) de l’élément pour lequel elle a été appelée.

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

La fonction ` readSeparator() ` crée un séparateur et définit ses indicateurs. Le texte de l’élément séparateur est défini sur 30 points centrés. Le reste de l’élément est ensuite ignoré à l’aide de ` 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();
}

La fonction ` readFolder() ` crée un élément et parcourt le contenu de l’élément de dossier, en ajoutant des enfants à cet élément pour représenter le contenu de l’élément de dossier. La boucle sur le contenu du dossier est similaire à celle de ` readXBEL()`, à ceci près qu’elle accepte désormais un élément `title` pour définir le titre du dossier.

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

La fonction d'aide ` createChildItem() ` crée un nouvel élément du widget arborescent qui est soit un enfant de l'élément donné, soit, si aucun élément parent n'est fourni, un enfant direct du widget arborescent. Elle définit la propriété ` UserRole ` du nouvel élément sur le nom de balise de l'élément XML courant, conformément à la manière dont `XbelWriter::writeFile()` utilise cette propriété ` 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;
}

Définition de la classe MainWindow

La classe ` MainWindow ` est une sous-classe de ` QMainWindow`, dotée d’un menu « File » et d’un menu « 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;
};

Implémentation de la classe MainWindow

Le constructeur de MainWindow configure son objet QTreeWidget, treeWidget, comme widget central, avec des en-têtes de colonnes pour le titre et l’emplacement de chaque signet. Il configure un menu personnalisé qui permet à l’utilisateur d’effectuer des actions sur des signets individuels au sein du widget arborescent.

Il appelle ` createMenus() ` pour configurer ses propres menus et les actions correspondantes. Il définit son titre, signale qu’il est prêt et ajuste sa taille à une proportion raisonnable de l’espace disponible à l’écran.

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

Un menu personnalisé, qui s'affiche lorsque l'utilisateur clique avec le bouton droit de la souris sur un signet, permet de copier le signet sous forme de lien ou de demander à un navigateur de bureau d'ouvrir l'URL à laquelle il renvoie. Ce menu est mis en œuvre (lorsque les fonctionnalités correspondantes sont activées) par 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)

La fonction createMenus() crée les objets fileMenu et helpMenu, puis y ajoute des objets QAction, liés respectivement aux fonctions open(), saveAs() et about(), ainsi qu’aux fonctions QWidget::close() et QApplication::aboutQt(). Les liaisons sont illustrées ci-dessous :

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

Cela crée le menu illustré dans les captures d’écran ci-dessous :

Menu « Fichier » avec les options « Ouvrir », « Enregistrer sous » et « Quitter »Menu « Aide » avec les rubriques « À propos » et « À propos de Qt »

La fonction ` open() `, lorsqu’elle est déclenchée, propose à l’utilisateur une boîte de dialogue permettant de sélectionner un fichier de signets. Si un fichier est sélectionné, il est analysé à l’aide d’un ` XBelReader ` afin de remplir l’objet ` treeWidget ` avec les signets. En cas de problème lors de l’ouverture ou de l’analyse du fichier, un message d’avertissement approprié s’affiche à l’intention de l’utilisateur, indiquant le nom du fichier et le message d’erreur. Dans le cas contraire, les signets lus à partir du fichier s’affichent et la barre d’état de la fenêtre indique brièvement que le fichier a été chargé.

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

La fonction ` saveAs() ` affiche une boîte de dialogue « QFileDialog » invitant l’utilisateur à indiquer un emplacement de stockage ( fileName) dans lequel enregistrer une copie des données de signets. À l’instar de la fonction ` open() `, cette fonction affiche également un message d’avertissement si l’écriture dans le fichier est impossible.

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

La fonction ` about() ` affiche une fenêtre de dialogue ` QMessageBox ` contenant une brève description de l'exemple ou des informations générales sur Qt et la version utilisée.

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() Fonction

La fonction ` main() ` instancie une ` MainWindow ` et appelle la fonction ` show() ` pour l’afficher, puis sa fonction ` open()`, car c’est très probablement ce que l’utilisateur souhaitera faire en premier lieu.

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow mainWin;
    mainWin.show();
    mainWin.open();
    return app.exec();
}

Consultez la page de ressources sur le langage d’échange de signets XML (XBEL) pour plus d’informations sur les fichiers XBEL.

Exemple de projet sur 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.