En esta página

Ejemplo de marcadores QXmlStream

Demuestra cómo leer y escribir archivos XBEL.

El ejemplo QXmlStream Bookmarks proporciona un visor para archivos XML Bookmark Exchange Language (XBEL). Puede leer marcadores usando QXmlStreamReader de Qt y escribirlos de nuevo usando QXmlStreamWriter. Como este ejemplo pretende mostrar cómo utilizar estos tipos de lector y escritor, no proporciona ningún medio para abrir un marcador, añadir uno nuevo, o combinar dos archivos de marcadores, y sólo tiene un alcance mínimo para la edición de marcadores. No obstante, si se desea, podría ampliarse con estas funciones.

Definición de la clase XbelWriter

La clase XbelWriter toma un tree widget que describe una jerarquía de carpetas que contienen marcadores. Su writeFile() proporciona los medios para escribir esta jerarquía, en formato XBEL, a un dispositivo de salida dado.

Internamente, registra el widget de árbol que se le ha dado y empaqueta una instancia privada de QXmlStreamWriter, que le proporciona los medios para transmitir XML. Dispone de un writeItem() interno para escribir cada elemento de su árbol.

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

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

Implementación de la clase XbelWriter

El constructor XbelWriter acepta el treeWidget que va a describir. Lo almacena y activa la propiedad de autoformateo de QXmlStreamWriter. Esta última divide los datos en varias líneas, con sangría para indicar la estructura del árbol, lo que facilita la lectura de la salida XML.

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

La función writeFile() acepta un objeto QIODevice y ordena a su miembro QXmlStreamWriter que escriba en este dispositivo, utilizando setDevice(). A continuación, esta función escribe la definición del tipo de documento (DTD), el elemento de inicio, la versión y delega la escritura de cada uno de los elementos de nivel superior de treeWidget en writeItem(). Por último, cierra el documento y lo devuelve.

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 función writeItem() acepta un objeto QTreeWidgetItem y escribe en su flujo XML una representación del objeto, que depende de su UserRole, que puede ser uno de los siguientes: "folder", "bookmark", o "separator". Dentro de cada carpeta, se llama a sí misma recursivamente en cada elemento hijo, para incluir recursivamente una representación de cada hijo dentro del elemento XML de la carpeta.

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

Definición de la clase XbelReader

XbelReader toma un tree widget para rellenarlo con elementos que describen una jerarquía de marcadores. Admite la lectura de datos XBEL de QIODevice como fuente de estos elementos. Si el análisis sintáctico de los datos XBEL falla, puede informar de lo que ha ido mal.

Internamente, registra el QTreeWidget que rellenará y empaqueta una instancia de QXmlStreamReader, la clase compañera de QXmlStreamWriter, que utilizará para leer los datos 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;
};

Implementación de la clase XbelReader

Dado que el lector XBEL sólo se ocupa de leer elementos XML, utiliza ampliamente la función readNextStartElement().

El constructor XbelReader requiere un QTreeWidget que rellenará. Rellena el estilo del widget de árbol con los iconos adecuados: un icono de carpeta que cambia de forma para indicar si cada carpeta está abierta o cerrada; y un icono de archivo estándar para los marcadores individuales dentro de esas carpetas.

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 función read() acepta un QIODevice. Indica a su miembro QXmlStreamReader que lea el contenido de ese dispositivo. Tenga en cuenta que la entrada XML debe estar bien formada para ser aceptada por QXmlStreamReader. Primero lee la estructura externa y verifica que el contenido sea un archivo XBEL 1.0; si lo es, read() delega la lectura real del contenido a la función interna readXBEL().

En caso contrario, utiliza la función raiseError() para registrar un mensaje de error. El propio lector también puede hacer lo mismo si encuentra errores en la entrada. Cuando read() ha terminado, devuelve true si no ha habido errores.

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() devuelve false, la persona que llama puede obtener una descripción del error, completa con el número de línea y columna dentro del flujo, llamando a la función errorString().

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

La función readXBEL() lee el nombre de un startElement y llama a la función apropiada para leerlo, dependiendo de si su nombre de etiqueta es "folder", "bookmark" o "separator". Cualquier otro elemento encontrado se salta. La función comienza con una precondición, verificando que el lector XML acaba de abrir un elemento "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 función readBookmark() crea un nuevo elemento editable que representa un único marcador. Registra el atributo XML "href" del elemento actual como texto de la segunda columna del elemento y establece provisionalmente el texto de su primera columna en "Unknown title" antes de explorar el resto del elemento en busca de un elemento title que lo sustituya, omitiendo cualquier elemento hijo no reconocido.

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

La función readTitle() lee el título de un marcador y lo registra como el título (texto de la primera columna) del elemento para el que fue llamada.

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 función readSeparator() crea un separador y establece sus banderas. El texto del elemento separador se establece en 30 puntos centrados. El resto del elemento se salta con 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 función readFolder() crea un elemento e itera sobre el contenido del elemento carpeta, añadiendo hijos a este elemento para representar el contenido del elemento carpeta. El bucle sobre el contenido de la carpeta es similar en forma al de readXBEL(), salvo que ahora acepta un elemento title para establecer el título de la carpeta.

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 función de ayuda createChildItem() crea un nuevo elemento del widget de árbol que es hijo del elemento dado o, si no se da ningún elemento padre, hijo directo del widget de árbol. Establece el nuevo elemento UserRole al nombre de la etiqueta del elemento XML actual, coincidiendo con la forma en que XbelWriter::writeFile() utiliza ese 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;
}

Definición de la clase MainWindow

La clase MainWindow es una subclase de QMainWindow, con un menú File y un menú 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;
};

Implementación de la Clase MainWindow

El constructor MainWindow configura su objeto QTreeWidget, treeWidget, como su propio widget central, con encabezados de columna para el título y la ubicación de cada marca-libro. Configura un menú personalizado que permite al usuario realizar acciones en marcadores individuales dentro del widget de árbol.

Invoca createMenus() para configurar sus propios menús y sus acciones correspondientes. Establece su título, se anuncia a sí mismo como listo y establece su tamaño en una proporción razonable del espacio disponible en pantalla.

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 menú personalizado, que se activa cuando el usuario pulsa el botón derecho del ratón sobre un marcador, permite copiar el marcador como enlace o dirigir un navegador de escritorio para abrir la URL a la que hace referencia. Este menú se implementa (cuando las funciones relevantes están habilitadas) mediante 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 función createMenus() crea los objetos fileMenu y helpMenu y les añade QAction, ligados de diversas formas a las funciones open(), saveAs() y about(), junto con QWidget::close() y QApplication::aboutQt(). Las conexiones son las que se muestran a continuación:

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

Esto crea el menú que se muestra en las capturas de pantalla siguientes:

La función open(), cuando se activa, ofrece al usuario un diálogo de archivo para seleccionar un archivo de marcadores. Si se selecciona un archivo, se analiza utilizando XBelReader para rellenar treeWidget con marcadores. Si surgen problemas al abrir o analizar el archivo, se muestra al usuario un mensaje de advertencia adecuado, que incluye el nombre del archivo y el mensaje de error. En caso contrario, se muestran los marcadores leídos del archivo y la barra de estado de la ventana informa brevemente de que el archivo se ha cargado.

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 función saveAs() muestra un mensaje QFileDialog, solicitando al usuario un archivo fileName, en el que guardar una copia de los datos de los marcadores. Al igual que la función open(), esta función también muestra un mensaje de advertencia si no se puede escribir en el archivo.

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 función about() muestra un QMessageBox con una breve descripción del ejemplo, o información general sobre Qt y la versión del mismo en uso.

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() Función

La función main() instanciará MainWindow e invocará a la función show() para mostrarla, y después a su open(), ya que es muy probable que esto sea lo que el usuario quiera hacer en primer lugar.

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

Consulte la página de recursos del lenguaje de intercambio de marcadores XML para obtener más información sobre los archivos XBEL.

Proyecto de ejemplo @ 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.