Files:
The SAX Bookmarks example provides a reader for XML Bookmark Exchange Language (XBEL) files that uses Qt's SAX-based API to read and parse the files. The DOM Bookmarks example provides an alternative way to read this type of file.
In this example we'll learn how to use the QXmlSimpleReader, QXmlDefaultHandler and QTextStream classes to parse, modify and generate an XML document. Additionally we'll see how to generate UI objects (that are described in a QML file) on-the-fly and how to access and modify their properties.
The UI of this sample application consists of three buttons and a container. The first two buttons allow the user to load two different XBEL documents and the third button allows the user to save the currently loaded XBEL document back to file system (including possible changes done to the bookmarks).
The container is the target location for the tree of controls, that visualize the structure of the XBEL document. The XBEL standard defines the elements
The folder is represented by a Container with a folder icon and a title label (FolderItem.qml). It provides a custom property 'tagName' and a property 'title' that is aliased to the 'text' property of the titleField label. So whenever the 'title' property is changed, the 'text' property will be updated.
// Container for the visual representation of a XBEL folder element Container { property string tagName: "folder" property alias title: titleField.text topMargin: 20 leftPadding: 20 Container { layout: StackLayout { orientation: LayoutOrientation.LeftToRight } // A standard ImageView ImageView { imageSource: "asset:///images/folder.png" preferredWidth: 32 preferredHeight: 32 } // A standard Label for the folder title element Label { id: titleField leftMargin: 10 // Defines custom text style textStyle { base: SystemDefaults.TextStyles.SmallText fontWeight: FontWeight.Bold color: Color.White } } } }
The bookmarks are visualized by a Container with a star icon, a title label and a TextField that contains the bookmark URL (BookmarkItem.qml). The TextField is invisible by default and will be faded in when the user clicks on the title label.
// Container for the visual representation of a XBEL bookmark element Container { property string tagName: "bookmark" property alias title: titleField.text property alias url: urlField.text topMargin: 10 leftMargin: 20 leftPadding: 20 Container { layout: StackLayout { orientation: LayoutOrientation.LeftToRight } // A standard ImageView ImageView { verticalAlignment: VerticalAlignment.Center minWidth: 82 minHeight: 62 imageSource: "asset:///images/bookmark.png" scalingMethod: ScalingMethod.AspectFit } // A standard Label for the bookmark title element Label { id: titleField verticalAlignment: VerticalAlignment.Center leftMargin: 10 textStyle { base: SystemDefaults.TextStyles.SmallText color: Color.White } onTouch: { if (event.isDown()) { urlField.visible = !urlField.visible } } } } // A standard TextField for the bookmark url attribute value TextField { id: urlField verticalAlignment: VerticalAlignment.Center leftMargin: 10 visible: false textStyle { base: SystemDefaults.TextStyles.SmallText } } }
A separator is an image of a horizontal line (SeparatorItem.qml).
// Container used to create a separator black visual line Container { property string tagName: "separator" topMargin: 20 leftPadding: 40 rightPadding: 20 ImageView { horizontalAlignment: HorizontalAlignment.Fill imageSource: "asset:///images/separator.png" } }
The business logic of the application is encapsulated in the class App, which is exported to the UI as '_app'.
// A standard Button Button { id: frank layoutProperties: StackLayoutProperties { spaceQuota: 1 } text: qsTr ("Frank") // Load the selected xbel file on click onClicked: { jennifer.opacity = 0.5; save.opacity = 0.5; frank.opacity = 1.0; _app.load ("frank.xbel"); } }
Whenever the user clicks one of the load buttons, the load() method of the App object is invoked.
// A standard Button Button { id: save layoutProperties: StackLayoutProperties { spaceQuota: 1 } text: qsTr ("Save") // Save the changes to a temporary xbel file in the // application tmp/ directory onClicked: { frank.opacity = 0.5; jennifer.opacity = 0.5; save.opacity = 1.0; _app.save (); } }
If the user clicks the 'Save' button, the save() method of the App object is invoked.
// Container for displaying the loaded XBEL output ScrollView { topMargin: 10 scrollViewProperties { scrollMode: ScrollMode.Vertical } layoutProperties: StackLayoutProperties { spaceQuota: 1 } Container { objectName: "treeContainer" leftPadding: 10 rightPadding: 10 bottomPadding: 10 } }
The container has the 'objectName' property set, so that it can be looked up from within C++.
Inside the constructor of App we load the main.qml file and retrieve the C++ object that represents the root node of the QML document. With the findChild() method we look up the Container object where we have assigned 'treeContainer' to the 'objectName' property.
App::App() { // Load the main QML file and make the App object available as context property QmlDocument *qml = QmlDocument::create("asset:///main.qml"); if (!qml->hasErrors()) { qml->setContextProperty("_app", this); Page *appPage = qml->createRootObject<Page>(); if (appPage) { Application::instance()->setScene(appPage); // Retrieve the tree container control from the QML file m_treeContainer = appPage->findChild<Container*>("treeContainer"); } } }
Whenever the user clicks one of the load buttons, the load() method of the App object is invoked with the file name passed as parameter.
void App::load(const QString &fileName) { // Do sanity check if (!m_treeContainer) return; // Update the status property m_status = tr("Loading..."); emit statusChanged(); // Clean all previous generated bookmark controls from the tree container m_treeContainer->removeAll(); // Create the XBEL handler and pass the tree container it will work on XbelHandler handler(m_treeContainer); // Create the SAX reader and set the XBEL handler as default handler QXmlSimpleReader reader; reader.setContentHandler(&handler); reader.setErrorHandler(&handler); // Open the XBEL file which the user has selected QFile file("app/native/assets/" + fileName); file.open(QIODevice::ReadOnly); // Wrap the XBEL file into a XML input source QXmlInputSource xmlInputSource(&file); // Parse XBEL file and generate the bookmark controls const bool ok = reader.parse(xmlInputSource); // Update the status property again if (ok) m_status = tr("Loaded successfully"); else m_status = handler.errorString(); emit statusChanged(); }
Inside load() we first remove all previously created controls from the treeContainer and then use the XbelHandler class in combination with the QXmlSimpleReader class to parse the XML document and generate new controls inside the treeContainer.
If the user clicks the 'Save' button, the save() method of the App object is invoked.
void App::save() { // Do sanity check if (!m_treeContainer) return; // Update the status property m_status = tr("Saving..."); emit statusChanged(); const QString fileName("tmp/saxbookmarks.xbel"); // Open the target file where the modified XBEL document will be written to QFile file(fileName); file.open(QIODevice::WriteOnly); // Create the XBEL generator on the tree container and let it generate the XBEL document from the bookmark controls XbelGenerator generator(m_treeContainer); const bool ok = generator.write(&file); // Update the status property again if (ok) m_status = tr("Saved successfully"); else m_status = tr("Error while saving"); emit statusChanged(); }
Inside save() we try to open the file 'saxbookmarks.xbel' inside the applications temp directory and then we use the XbelGenerator class to iterate over the treeContainer, generate the XML document and store it to the file.
The XbelGenerator class contains a reference to the Container instance where the bookmark hierarchy is stored and a QTextStream instance that is used to write out the XML data. The actual generation of the XML document is done inside the write() method.
/** * The XbelGenerator is responsible for generating a XBEL document from the * controls in the tree container. * * To generate the XBEL document the XbelParser uses the QTextStream class from Qt. */ class XbelGenerator { public: XbelGenerator(bb::cascades::Container *treeContainer); // Starts the generation of the XBEL document bool write(QIODevice *device); private: // A helper method that returns an indention block static QString indent(int indentLevel); // A helper method that escapes special characters in a string static QString escapedText(const QString &str); // A helper method that escapes special characters in XML attribute static QString escapedAttribute(const QString &str); // A helper method that generates an element in the XBEL document for a control void generateItem(bb::cascades::Control *item, int depth); // The container object the controls are located in QPointer<bb::cascades::Container> m_treeContainer; // The text stream that is used to generate the XBEL document QTextStream m_stream; };
The XbelGenerator constructor accepts a treeContainer to initialize within its definition.
XbelGenerator::XbelGenerator(Container *treeContainer) : m_treeContainer(treeContainer) { }
The write() method creates configures the QTextStream instance and writes out the processing instructions and top-level element for a valid XBEL document.
Afterwards it iterates recursively over the treeContainer to calls generateItem() for each child control of the container.
bool XbelGenerator::write(QIODevice *device) { // Set the output device on the stream m_stream.setDevice(device); m_stream.setCodec("UTF-8"); // Add the root element with the necessary 'xbel' element and version attribute m_stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" << "<!DOCTYPE xbel>\n" << "<xbel version=\"1.0\">\n"; // Iterate over the controls of the tree container and generate an XBEL element for each of them for (int i = 0; i < m_treeContainer->count(); ++i) generateItem(m_treeContainer->at(i), 1); // Finish the XBEL document m_stream << "</xbel>\n"; return true; }
The generateItem() method accepts a Control object and the indention depth of the current XML element. It first extracts the type and the title from the control and then writes out the XML data for the current object depending on the tagName, which can either be a 'folder', 'bookmark', or 'separator'. If the control is a Container it will call itself recursively with an increased indention depth.
void XbelGenerator::generateItem(Control *control, int depth) { /** * Retrieve the tag name and title from the control. * These two properties have been set on the controls by the XbelHandler. */ const QString tagName = control->property("tagName").toString(); const QString title = control->property("title").toString(); // Depending on the tag name write a new element to the XBEL document if (tagName == "folder") { m_stream << indent(depth) << "<folder folded=\"no\">\n" << indent(depth + 1) << "<title>" << escapedText(title) << "</title>\n"; // Iterate over the child controls of the container const Container *container = qobject_cast<Container*>(control); for (int i = 0; i < container->count(); ++i) generateItem(container->at(i), depth + 1); m_stream << indent(depth) << "</folder>\n"; } else if (tagName == "bookmark") { const QString url = control->property("url").toString(); m_stream << indent(depth) << "<bookmark"; if (!url.isEmpty()) m_stream << " href=" << escapedAttribute(url); m_stream << ">\n" << indent(depth + 1) << "<title>" << escapedText(title) << "</title>\n" << indent(depth) << "</bookmark>\n"; } else if (tagName == "separator") { m_stream << indent(depth) << "<separator/>\n"; } }
XbelHandler inherits from QXmlDefaultHandler, which is an interface with virtual methods that act as callbacks for the SAX parser.
The XbelHandler contains a reference to the Container that is used to group the bookmarks according to their hierarchy. Since a SAX parser only reports when a new XML element starts and ends, we have to keep track of the hierarchy of the XML document ourself. Therefor the XBelHandler also keeps a reference to the Control it is currently working on.
/** * The XbelHandler is responsible for generating controls, that represent * the bookmark entries, on the tree container. * Its reimplemented methods from QXmlDefaultHandler are invoked by the SAX parser. */ class XbelHandler : public QXmlDefaultHandler { public: XbelHandler(bb::cascades::Container *treeContainer); // This method is called whenever the SAX parser processes a start element bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes); // This method is called whenever the SAX parser processes an end element bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName); // This method is called whenever the SAX parser processes characters inbetween an element bool characters(const QString &str); // This method is called whenever the SAX parser encounters an error bool fatalError(const QXmlParseException &exception); // Returns a textual representation of the error if one occurred QString errorString() const; private: // A helper method that generates a control for a specific XBEL element bb::cascades::Container *createChildItem(const QString &tagName); // The container object the controls are created in QPointer<bb::cascades::Container> m_treeContainer; // The control that is current processed bb::cascades::Container *m_item; // The characters that have been collected during the invokation of characters() QString m_currentText; // The textual representation of an error QString m_errorStr; // A flag for whether the xbel tag has been found already bool m_metXbelTag; };
The XbelHandler constructor accepts a Container to initialize the treeContainer within its definition.
XbelHandler::XbelHandler(Container *treeContainer) : m_treeContainer(treeContainer) { m_item = 0; m_metXbelTag = false; }
The startElement() method is called whenever the SAX parser discovers an opening XML element. In this case we do the sanity check whether it's a valid XBEL document and then, depending on the element's tag name, we create a new Control by calling createChildItem() and setup its properties.
bool XbelHandler::startElement(const QString & /* namespaceURI */, const QString & /* localName */, const QString &qName, const QXmlAttributes &attributes) { // The first element must be the 'xbel' element if (!m_metXbelTag && qName != "xbel") { m_errorStr = QObject::tr("The file is not an XBEL file."); return false; } // Read the properties from the element and store them in the control if (qName == "xbel") { const QString version = attributes.value("version"); if (!version.isEmpty() && version != "1.0") { m_errorStr = QObject::tr("The file is not an XBEL version 1.0 file."); return false; } m_metXbelTag = true; } else if (qName == "folder") { m_item = createChildItem(qName); m_item->setProperty("title", QObject::tr("Folder")); } else if (qName == "bookmark") { m_item = createChildItem(qName); m_item->setProperty("title", QObject::tr("Unknown title")); m_item->setProperty("url", attributes.value("href")); } else if (qName == "separator") { m_item = createChildItem(qName); } m_currentText.clear(); return true; }
The errorString() function is used if an error occurred, in order to obtain a description of the error complete with line and column number information.
QString XbelHandler::errorString() const { return m_errorStr; }
The endElement() method is called whenever the SAX parser discovers a closing XML element. In this case we change the current control back to its parent control, to get the mapping of the XML hierarchy to the control object hierarchy right.
bool XbelHandler::endElement(const QString & /* namespaceURI */, const QString & /* localName */, const QString &qName) { if (qName == "title") { if (m_item) m_item->setProperty("title", m_currentText); } else if (qName == "folder" || qName == "bookmark" || qName == "separator") { m_item = qobject_cast<Container*>(m_item->parent()); } return true; }
The createChildItem() method loads a QML file and returns the C++ object that represents the root node of the QML file. Depending on the current tag name, different QML files are loaded.
Container *XbelHandler::createChildItem(const QString &tagName) { // Use a different QML file depending on the requested XBEL element type const QString qmlFile = (tagName == "folder" ? "asset:///FolderItem.qml" : tagName == "bookmark" ? "asset:///BookmarkItem.qml" : tagName == "separator" ? "asset:///SeparatorItem.qml" : QString()); Container *container = 0; // Load the QML file ... QmlDocument *qml = QmlDocument::create(qmlFile); if (!qml->hasErrors()) { // ... create the top-level control ... container = qml->createRootObject<Container>(); // ... and add it to the parent container if (m_item) { m_item->add(container); } else { m_treeContainer->add(container); } } return container; }
See the XML Bookmark Exchange Language Resource Page for more information about XBEL files.