Create the app class

Let's take a minute to summarize what we've done so far. We created the main app UI, which displays a list of quote authors. This main screen also includes an Add action that lets users create a new quote and add it to the list. We created the add page, which includes text fields for the new quote, first name, and last name, as well as a title bar. Finally, we created the quote page, which displays an individual quote and lets users edit or delete the quote.

In each of these components, we handled important signals that provide the fundamental behavior of our app. For example, we handled signals that GroupDataModel emits when items are added to, removed from, or updated in the data model. We also handled custom signals that our custom QML components emit, such as update() and cancel() from our EditControls component.

Now, we're ready to implement the C++ classes and functions that support our app's behavior. Specifically, we need a way to interact with the SQL database that contains our quote data. We handle this interaction in several C++ files to keep it separate from the visual aspects of the app (which we created in QML). It's usually a good idea to separate the business logic code (in our case, functions that interact with the database) from the UI-related code in your apps. This approach makes it easy to change one aspect (for example, using a different database type, or changing the appearance of list items) without affecting the other.

Modify the main.cpp file

Let's start by modifying some code in the main.cpp file in our project. The main.cpp file is created automatically when you create a new project, and it contains quite a bit of pre-populated code. This code is useful for getting a sample app running quickly, and also provides support for translation. To keep things simple, our Quotes app won't provide translation support, and we'll separate some of the existing code and place it in different source files.

Open the main.cpp file, located in your project's src folder, and remove the pre-populated code in the main() function. Our main() function needs just four lines of code that do the following:

  • Create an instance of the main Application class.
  • Create an instance of the class that represents our app, QuotesApp.
  • Call the onStart() function, which is part of QuotesApp, to perform some initialization for our app.
  • Start the event loop by calling Application::exec().
Application app(argc, argv);
QuotesApp mainApp;
mainApp.onStart();
return Application::exec();

If you need any additional information about these lines and what they do, see Create your first app.

You can also remove the pre-populated statements at the top of the file. Instead, add the following:

#include "quotesapp.h"

using ::bb::cascades::Application;

Create the QuotesApp class

The first class that we create is the QuotesApp class, which includes most of our app's functionality. In the src folder in your project, create a class called QuotesApp. Make sure that you create both an .h file and a .cpp file (if you click New > Class to create the class, both of these files are created for you). Don't worry about creating method stubs; we'll provide you with all of the code you need to put in each file. You can also remove any other .h or .cpp files that are present in the src folder (except main.cpp, of course).

Open the QuotesApp.cpp file first. We need to include a few classes at the start of this file. We also need to include the corresponding QuotesApp.h header, as well as a header called QuotesDbHelper.h (which we'll talk about a bit later). We make sure to use the bb::cascades namespace so we don't have to use fully qualified names for the Cascades controls we use.

#include "QuotesApp.h"
#include "QuotesDbHelper.h"

#include <bb/cascades/GroupDataModel>
#include <bb/cascades/ListView>
#include <bb/cascades/NavigationPane>
#include <bb/cascades/QmlDocument>

using namespace bb::cascades;

Our class constructor is empty, and our class destructor contains only one line. In the destructor, we delete an object called mQuotesDbHelper. This object is an instance of the QuotesDbHelper class, which we'll create later in the tutorial. This helper class takes care of all of the SQL database operations that our app needs, such as inserting and removing entries. When our app needs to manipulate data in the database, we simply call the corresponding functions of our QuotesDbHelper object. When we're done with this object, we need to free the memory that we allocated for it.

QuotesApp::QuotesApp()
{
}

QuotesApp::~QuotesApp()
{
    delete mQuotesDbHelper;
}

Implement the setup functions

Next, we implement the onStart() function. Remember that we called this function in main.cpp to perform some initial setup operations for our app. The onStart() function creates the instance of the QuotesDbHelper class, mQuotesDbHelper, that we discussed above. It also calls loadQMLScene() to load our main.qml file and set up our data model.

void QuotesApp::onStart()
{
    // Instantiate the database helper object.
    mQuotesDbHelper = new QuotesDbHelper();

    if (!loadQMLScene()) {
        qWarning("Failed to load QML scene.");
    }
}

Let's implement the loadQMLScene() function, which contains most of the initialization operations. We create a QML document from our main.qml file, then test to see whether the creation was successful. If so, we set the context property for the document to _quoteApp. By setting the context property, we can call functions from this class in QML (specifically, functions that we mark as Q_INVOKABLE). If you recall, we called some of these C++ functions from our QML code, such as addNewRecord() in AddPage.qml.

bool QuotesApp::loadQMLScene()
{
    QmlDocument *qmlDocument = QmlDocument::create("asset:///main.qml");

    if (!qmlDocument->hasErrors()) {
        qmlDocument->setContextProperty("_quoteApp", this);

We create a NavigationPane object from the root NavigationPane control in our QML document, and we test to see whether it was created successfully. If so, we call the loadDataBase() function of our helper object mQuotesDbHelper. This function takes two arguments: the name of the database file (which is located in the assets/sql folder in our project) and the name of the SQL table inside the database file.

NavigationPane* navigationPane = qmlDocument->
                                            createRootObject<NavigationPane>();

if (navigationPane) {
    QVariantList sqlData = mQuotesDbHelper->loadDataBase("quotes.db",
                                                          "quotes");

If the database was loaded successfully, we locate the GroupDataModel in our QML document and insert the data into the GroupDataModel. We also locate the ListView that represents our list of quote authors; we'll need to refer to this ListView later when we implement the deleteRecord() function.

if (!sqlData.isEmpty()) {
    mDataModel = navigationPane->findChild<GroupDataModel*>("quotesModel");
    mDataModel->insertList(sqlData);

    mListView = navigationPane->findChild<ListView*>("quotesList");
}

To finish the loadQMLScene() function, we set the main scene of the application and return true to indicate that loading was successful. If any of our tests above failed, we return false.

            Application::instance()->setScene(navigationPane);
            return true;
        }
    }

    return false;
}

Implement the add and update functions

In our QML code, we called a function called addNewRecord() when we wanted to add a new quote to the database, so let's implement that function now. We add all of the function parameters (first name, last name, and quote) to a QVariantMap, which makes it easy to insert the data into our database and data model. We call the insert() function of our helper object mQuotesDbHelper, and if the insertion is successful, we receive a unique primary key for the entry as a return value. We'll use this key later when we want to edit or delete the entry, so we store the key in our QVariantMap and insert the map into our data model.

void QuotesApp::addNewRecord(const QString &firstName,
                                       const QString &lastName,
                                       const QString &quote)
{
    QVariantMap map;
    map["firstname"] = QString(firstName);
    map["lastname"] = QString(lastName);
    map["quote"] = QString(quote);

    QVariant insertId = mQuotesDbHelper->insert(map);

    if (!insertId.isNull()) {
        map["id"] = insertId;
        mDataModel->insert(map);
    }
}

We need a function that updates a particular quote with new information, so we create the updateSelectedRecord() function for this purpose. To determine which quote item we need to update, we call the selected() function of our ListView, which returns the index path of the selected item. If the index path is valid, we retrieve the data that's located at that index path in the data model and convert the data to a QVariantMap. We use the function parameters to update this map, and then we call update() and updateItem() to save the changes to the database and data model, respectively.

void QuotesApp::updateSelectedRecord(const QString &firstName,
                                               const QString &lastName,
                                               const QString &quote)
{
    QVariantList indexPath = mListView->selected();

    if (!indexPath.isEmpty()) {
        QVariantMap itemMapAtIndex = mDataModel->data(indexPath).toMap();

        itemMapAtIndex["firstname"] = QString(firstName);
        itemMapAtIndex["lastname"] = QString(lastName);
        itemMapAtIndex["quote"] = QString(quote);

        mQuotesDbHelper->update(itemMapAtIndex);
        mDataModel->updateItem(indexPath, itemMapAtIndex);
    }
}

Implement the delete function

Finally, we implement the deleteRecord() function, which removes a quote from the database and data model. Similar to updateSelectedRecord(), we start by retrieving the index path of the selected quote item. Then, we retrieve the data at that location as a QVariantMap and call deleteById() to delete the quote from the database.

void QuotesApp::deleteRecord()
{
    QVariantList indexPath = mListView->selected();

    if (!indexPath.isEmpty()) {
        QVariantMap map = mDataModel->data(indexPath).toMap();

        if (mQuotesDbHelper->deleteById(map["id"])) {

After a quote is deleted, we want another quote to be displayed automatically. When we created our QML code, it was mentioned that determining the new quote item to select was a tricky task. There are several steps to determine the correct item to select.

First, we store the number of items that are located in the category from which the item was removed. For our purposes, a category refers to a set of quote authors whose last names start with the same letter. In our ListView, these authors all appear under a heading with that letter. For example, in the initial data model for our app, the "L" category contains three entries: "Steven Levy", "Staffan Lincoln", and "Ada Lovelace".

Diagram showing a category.

After we store this number, we remove the selected item from the data model.

QVariantList categoryIndexPath;
categoryIndexPath.append(indexPath.first());
int childrenInCategory = mDataModel->childCount(categoryIndexPath);

mDataModel->remove(map);

Next, we select another item relative to the one that was removed. After we remove the item, if the item's category still contains items (that is, the number of items that we stored above is greater than 1), we select an item within that same category. Either we select the next item in the category (relative to the removed item) or, if the last item in the category was the item that was removed, we select the previous item.

if (childrenInCategory > 1) {
    int itemInCategory = indexPath.last().toInt();

    if (itemInCategory < childrenInCategory - 1) {
        mListView->select(indexPath);
    } else {
        indexPath.replace(1, QVariant(itemInCategory - 1));
        mListView->select(indexPath);
    }

If the item's category doesn't contain any more items (that is, the removed item was the only item in its category), we move to the next category and select an item from there. If there are no more categories below the one with the removed item, we move to the previous category instead. If there are no items left at all (the removed item was the last item in the entire list), we navigate back to the (empty) list of quote authors.

            } else {
                QVariantList lastIndexPath = mDataModel->last();

                if (!lastIndexPath.isEmpty()) {
                    if (indexPath.first().toInt() <= lastIndexPath.first()
                        .toInt()) {
                        mListView->select(indexPath);
                    } else {
                        mListView->select(mDataModel->last());
                    }
                }
            }
        } //end of inner if statement
    } // end of outer if statement
} // end of deleteRecord()

Complete the QuotesApp header file

We've completed the implementation of our QuotesApp class. We just need to complete the associated header file. Open the QuotesApp.h file in the src folder of your project. The contents of this file are straightforward. We include several supporting classes, use forward declarations for other classes that we need, and then declare each function that we defined in our QuotesApp.cpp file above. Note that we use the Q_INVOKABLE macro for the functions that we call from QML.

#ifndef QUOTESAPP_H
#define QUOTESAPP_H

#include <bb/cascades/Application>
#include <bb/cascades/DataModel>
#include <bb/data/SqlDataAccess>
#include <QObject>

using namespace bb::cascades;
using namespace bb::data;

namespace bb
{
    namespace cascades
    {
        class GroupDataModel;
        class ListView;
        class NavigationPane;
    }
}

class QuotesDbHelper;

class QuotesApp: public QObject
{
Q_OBJECT

public:
    QuotesApp();
    ~QuotesApp();

    void onStart();

    Q_INVOKABLE
    void addNewRecord(const QString &firstName,
                      const QString &lastName,
                      const QString &quote);

    Q_INVOKABLE
    void updateSelectedRecord(const QString &firstName,
                              const QString &lastName,
                              const QString &quote);

    Q_INVOKABLE
    void deleteRecord();

private:
    bool loadQMLScene();

    QuotesDbHelper *mQuotesDbHelper;
    GroupDataModel *mDataModel;
    ListView *mListView;
};

#endif

Our Quotes app is almost complete. In the final section of this tutorial, we'll create the helper class QuotesDbHelper that we use to interact with the SQL database.

Last modified: 2014-06-24



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus