Tutorial: Lists

This tutorial shows you how to create the Quotes app that's included on the Sample Apps page. The Quotes app lets you view quotes by famous people. The main screen of the app contains a list of authors, and you can tap a name in the list to view that author's quote. Data for the quotes is stored in an SQL database, and you can edit existing quotes or add your own.

You will learn to:

  • Use a ListView to display a list of data
  • Associate a DataModel with the ListView to provide the data to display
  • Use SQL to read data from a database and update the database with new entries
  • Create custom QML components to represent different parts of the app UI
Animation showing a list item being selected in the Quotes sample app.

Before you begin

This tutorial includes concepts such as data storage and list views. It assumes that you're familiar with the fundamentals of Cascades and the Momentics IDE, and that you've built and run several Cascades apps already. If you're just starting out and need to know some of the basics, you might want to check out a few of these resources before you start this tutorial:

Downloading the full source code

This tutorial uses a step-by-step approach to build the Quotes app from scratch. If you want to take a look at the complete source code for the finished app, you can download the entire project and import it into the Momentics IDE.

Download the full source code

Set up your project

Before we start creating our application, create an empty project in the Momentics IDE, making sure to select the Standard empty project template. There are several graphical assets that our app uses, such as background images and custom buttons. We need to import the following images:

The background for the Quotes sample app.

background.png  - A textured background to use when we display individual quotes

The quote border for the Quotes sample app.

border_bubble.png  - The border of each quote

The custom add button for the Quotes sample app.

Add.png  - A custom image for the Add action

The custom edit button for the Quotes sample app.

Edit.png  - A custom image for the Edit action

We also need to import a special image file called border_bubble.amd, which is an asset metadata file. The quotes in our app can have different lengths, so we need the quote bubble image to scale to fit the dimensions of the quote. We use a nine-sliced image, as an .amd file, to make sure that only the area inside the quote bubble is scaled (not the quotation marks). To learn more about nine-slice scaling, see Images.

Finally, we need to import the SQL database file that contains our quote data. This file is called quotes.db and is placed in a folder called sql in our project's assets folder.

To import the images and database file into your project:

  1. Download the assets.zip file.
  2. Extract the images folder and sql folder into the assets folder of your project.

Create the main UI

Now that we've imported our images, we can start creating the UI of the app. There are several parts of the UI that we need to create, such as the main list of quote authors, the page that displays an individual quote, and so on. We'll use custom QML components for some of these parts, each of which will be defined in its own .qml file. Later on, we'll add supporting functionality, such as SQL database access, using C++.

Create the root control and properties

Start by opening the main.qml file that's located in the assets folder of your project. This file contains some pre-populated code; go ahead and remove this code, but make sure to keep the import statement. Our app uses a NavigationPane as the root control, which makes it easy to display new pages when we need them.

The main UI of the Quotes app.
import bb.cascades 1.0
// PLACEHOLDER: The AddPage import statement

NavigationPane {
    id: nav

In the code sample above, notice the PLACEHOLDER comment. You'll see these types of comments throughout the tutorial. They indicate a missing section of code that's related to a concept we discuss later, but that we're not quite ready to talk about yet. Remember the locations of these placeholders; you'll be asked to revisit them and add the missing code later.

When you tap an author's name in the list, the associated quote is displayed on a separate Page that's pushed on to the NavigationPane stack. Instead of creating a new Page with new content for each quote, we can use a custom property to store the current quote that we want to display. This property, called _contentView, is declared with a type of variant, which indicates that it can store a wide variety of data types. In our case, the _contentView property stores a single quote (and associated information such as the author's name) that's retrieved from our SQL database of quotes. We'll populate this property a bit later in the tutorial.

property variant _contentView

// PLACEHOLDER: The addShown property

Create the initial page and the list view

Next, we add the initial Page of our root NavigationPane. This page contains our list of quote authors and a single action called Add.

Page {
    id: quoteListPage

Inside this Page, we add a Container, which itself contains the ListView that represents our list of authors. We make sure to give the ListView an objectName so we can access the list using C++. The ListView uses a StackListLayout, meaning that the list items are arranged in a stack, one after another, from top-to-bottom.

We also use the headerMode property to customize the behavior of the header items in the list. By using ListHeaderMode.Sticky, when the list scrolls and a header reaches the top of the visible area, the header stays there (or "sticks") as the list continues to scroll, until a new header reaches the top.

Diagram showing the list view and actions.
Container {
            
    ListView {
        id: quotesList
        objectName: "quotesList"
                
        layout: StackListLayout {
            headerMode: ListHeaderMode.Sticky
        }

Next, we set the data model for the ListView. Our data model, quotesModel, is defined in the attachedObjects list property, which we'll get to a bit later.

dataModel: quotesModel
Diagram showing the Header and StandardListItem items.

We need to define how the list items and header items in our list should look. To do this, we use the listItemComponents list property and populate it with two ListItemComponent objects. One of these objects defines how list items look (using a StandardListItem) and the other defines how headers look (using a Header). Each entry in our data model includes firstname and lastname fields, so we can use these fields to display the names of the quote authors in the list. We also make sure that names are displayed correctly even if only the lastname field is populated.

listItemComponents: [
    ListItemComponent {
        type: "item"
        StandardListItem {
            imageSpaceReserved: false
            title: {
                if (ListItemData.firstname == undefined) {
                    ListItemData.lastname
                } else {
                    ListItemData.firstname + " " + ListItemData.lastname;
                }
            }
        }
    },
    ListItemComponent {
        type: "header"
        Header {
            title: ListItemData
        }
    }
]

Define signal handlers for the list view

To implement the behavior of our list, there are a couple of signals that we should respond to. A ListView emits the triggered() signal when a list item is tapped, and we use the onTriggered signal handler to respond to this signal.

In this handler, we select the item that was tapped. Remember that tapping an item in a list doesn't select it, so we need to call select() to select the item manually.

Diagram showing unselected and selected list items.

Selecting the item changes the appearance of the item (the item's background changes to reflect the selection), and the ListView also emits the selectionChanged() signal, which we'll use to update the quote content that we want to display.

To finish the onTriggered signal handler, we create the Page that contains the quote and push the Page on to the NavigationPane stack to display it. Notice that we create the Page dynamically by using a ComponentDefinition object called quotePageDefinition. This approach is an efficient way to create new Page objects, especially when you're creating them over and over in your app. The quotePageDefinition object is defined later, in the attachedObjects list property of our NavigationPane. To learn more about creating QML components dynamically, see Dynamic QML components.

onTriggered: {

    select(indexPath);

    var page = quotePageDefinition.createObject();
    nav.push(page);
}

The second signal we want to respond to is selectionChanged(), using the onSelectionChanged signal handler. When a list item is selected, we retrieve the corresponding item from the data model, make sure that there isn't an undefined firstname field (which would result in "undefined" being displayed in the UI), and set the item as the value of the _contentView property. Later in this tutorial, you'll see how we use this property to display the quote.

onSelectionChanged: {
    if (selected) {
        var chosenItem = dataModel.data(indexPath);

        if (chosenItem.firstname == undefined) {
            chosenItem.firstname = "";
        }

        _contentView = chosenItem;
    }
}

Create the data model

Now, let's add our attached objects to our ListView. The ListView includes only one attached object: a GroupDataModel that's used as the data model for the list. We set up the GroupDataModel here using QML and then populate the model with data using C++ later on. This GroupDataModel sorts items based on the lastname and firstname fields, and groups items by the first character.

attachedObjects: [
    GroupDataModel {
        id: quotesModel
        objectName: "quotesModel"
        grouping: ItemGrouping.ByFirstChar
        sortingKeys: ["lastname", "firstname"]

Define signal handlers for the data model

Similar to our ListView above, there are several signals that GroupDataModel emits that we're interested in. The first signal is itemAdded(), which is emitted when an item is added to the data model. In our app, this signal is emitted after a user taps the Add action on the main screen, types an author name and quote, and taps Save.


Screens showing how the itemAdded signal is emitted.

When the user adds a new quote, we want the list to select the new item and scroll to it. We also want to push a new Page on to the NavigationPane stack to display the new quote immediately. Remember that when we select an item in the list by calling select(), the selectionChanged() signal is emitted and we handle it by updating the _contentView property with the data of the selected item. The new item is added to the data model by using C++, as you'll see later.

onItemAdded: {
    if (addShown) {
        if (nav.top == quoteListPage) {
            quotesList.clearSelection();
            quotesList.select(indexPath);
            quotesList.scrollToItem(indexPath, ScrollAnimation.Default);

            var page = quotePageDefinition.createObject();
            nav.push(page);
        }
    }
}

Notice that we refer to an addShown variable in the code above, but we haven't defined it yet. Here's when we need to jump back to one of our PLACEHOLDER comment lines and add a bit of missing code. The itemAdded() signal is emitted each time a new item is added to the data model, including when the initial items are added to the model when the app starts. We don't want to respond to the itemAdded() signal when the initial items are added, but instead want to process it only when the user adds a new quote to the list.

We use a custom Boolean property called addShown to indicate whether it's safe to process the itemAdded() signal. This property has an initial value of false, and we'll change it to true when the Add action is tapped for the first time.

To define the addShown property, go back in your code and find the comment line PLACEHOLDER: The addShown property. Replace that line with the following:

property bool addShown: false

The next signal in GroupDataModel that we're interested in is itemRemoved(), which is emitted when an item is removed from the data model. This signal is emitted when a user selects the Delete action while viewing a quote.


Screens showing how the itemRemoved() signal is emitted.

When this signal is emitted, we want to remove the quote from the database and change the displayed quote to the next quote in the list. The logic that's required to select the correct item is fairly complicated, so it's easier to implement it in C++ a bit later. The only thing that we do in QML is navigate away from the quote content page if there are no more items in the list (that is, if the user deleted the only remaining quote in the list).

onItemRemoved: {
    var lastIndexPath = last();
    if (lastIndexPath[0] == undefined) {
        if (nav.top != quoteListPage) {
            nav.popAndDelete();
        }
    }
}

The last signal in GroupDataModel that we want to handle is itemUpdated(), which is emitted when the data for an item is updated in the data model. This signal is emitted after a user taps the Edit action while viewing a quote, makes a change to the quote or the author, and taps Update.


Screens showing how the itemUpdated() signal is emitted.

When this signal is emitted, we want to update the _contentView property with the data of the updated item.

                onItemUpdated: {
                    var chosenItem = data(indexPath);
                    _contentView = chosenItem;
                }
            } // end of GroupDataModel
        ] // end of attachedObjects
    } // end of ListView
} // end of Container

Add attached objects

Now that the QML portions of our ListView and GroupDataModel are complete, we can populate the attachedObjects list for our main Page. The first attached object is a Sheet that lets users add a new quote. We use a custom QML component called AddPage for the content of this Sheet. This component includes a custom signal, addPageClose(), that's emitted when the AddPage is closed. This component also includes a custom JavaScript function, newQuote(), that sets up a blank quote record for us to populate. We'll use both the custom signal and JavaScript function when we create the AddPage component in the next section of this tutorial.

attachedObjects: [
    Sheet {
        id: addSheet
        AddPage {
            id: add
            onAddPageClose: {
                addSheet.close();
            }
        }
        onClosed: {
            add.newQuote();
        }
    },

To use the AddPage component in our app, we need to import it. Go back in your code and find the comment line PLACEHOLDER: The addPage import statement. Replace that line with the following:

import "AddPage"

The second attached object on the Page is our ComponentDefinition object, quotePageDefinition. Remember that we used this object to dynamically create a Page to display the content of a quote. The Page is defined as a custom QML component in QuotePage.qml.

    ComponentDefinition {
        id: quotePageDefinition
        source: "QuotePage/QuotePage.qml"
    }
] // end of attachedObjects

Add actions

Next, we add actions to the main Page. Our app has only one action on this Page: the Add action that lets users add a new quote. We use the ActionBar.placement property to make sure that the action appears on the action bar and not in the action menu. When a user taps the action, the Sheet we defined above is opened. Also, the addShown property is set to true, which indicates that we can safely process itemAdded() signals when items are added to the data model.

    actions: [
        ActionItem {
            title: "Add"
            imageSource: "asset:///images/Add.png"
            ActionBar.placement: ActionBarPlacement.OnBar
            onTriggered: {
                addSheet.open();
                nav.addShown = true;
            }
        } 
    ] // end of attachedObjects
} // end of Page

Define signal handlers for the navigation pane

We're almost finished with the main UI of our app. We just need to handle two more signals that our NavigationPane emits. The first signal, topChanged(), is emitted when the top Page of the NavigationPane changes. In our app, this occurs when a quote is displayed, and it also occurs when a user taps the back button (called "Names" in our app) to close the quote and return to the main list of quotes.


Screens showing how the topChanged() signal is emitted.

When a quote is closed, we don't want the previously selected quote author to still be selected in the list, so we clear the selection.

onTopChanged: {
    if (page == quoteListPage) {
        quotesList.clearSelection();
    }
}

The second signal, popTransitionEnded(), is emitted when a quote is closed and the user returns to the main list of quotes. Each time we display a quote, we create a new Page using createObject() and populate it with the data for the quote. When the quote is closed, we need to delete the Page to avoid a memory leak, so we call destroy() on the Page when we no longer need it.

    onPopTransitionEnded: {
        page.destroy();
    }
} // end of NavigationPane

At this point, we've finished creating the main UI of our app. In the next section, we'll create all of the custom QML components that our app uses.

Create the add page

So far, we've created the main UI for our app, which consists of a ListView, an associated GroupDataModel, and an Add action. We've also set up our app to handle some important signals, such as triggered() of ListView and itemAdded() of GroupDataModel. Our app relies on several custom QML components, so the next thing to do is create these components in their own .qml files.

Create the AddPage.qml file and root control

The first component that we'll create is the AddPage component. This component represents a screen that lets users type information for a new quote, such as first name, last name, and quote text. This component also includes a title bar, with Cancel and Save buttons, that appears at the top of the screen.

Screen showing the main UI for the Quotes app.

Start by creating a new folder called AddPage in the assets folder of your project. Inside the AddPage folder, create a .qml file called AddPage.qml.

The root control of this component is a Page. We also declare a custom signal called addPageClose(), and our AddPage component emits this signal when a user taps Cancel or Save.

import bb.cascades 1.0

Page {
    id: addPage
    signal addPageClose()

Add a title bar

We use the titleBar property to specify a title bar for the Page. We make sure that the title bar is visible by using the visibility property, and we add the Cancel and Save buttons by using the dismissAction and acceptAction properties, respectively.

The dismissAction always appears on the left side of the title bar, and the acceptAction always appears on the right side.

Diagram showing the dismiss and accept actions.

When a user taps Cancel, our app emits the custom addPageClose() signal. This signal indicates that we want to close the Sheet that contains the AddPage component, and we handled this signal earlier in the tutorial. When the user taps Save, our app also emits the addPageClose() signal, but first we call a C++ function called addNewRecord(). This function interacts with the data model and underlying SQL database to add the new quote data. We'll implement this function later when we get to the C++ portion of the tutorial.

titleBar: TitleBar {
    id: addBar
    title: "Add"
    visibility: ChromeVisibility.Visible
        
    dismissAction: ActionItem {
        title: "Cancel"
        onTriggered: {
            addPage.addPageClose();
        }
    }
        
    acceptAction: ActionItem {
        title: "Save"
        onTriggered: {
            _quoteApp.addNewRecord(firstNameField.text, lastNameField.text,
                                   quoteField.text);
            addPage.addPageClose();
        }
    }
}

Add the text fields

Next, we add a Container to represent the entire screen below the title bar. This container uses a DockLayout and includes margins to separate the text fields from the edges of the screen.

Container {        
    id: editPane
    property real margins: 40
    background: Color.create("#f8f8f8")
    topPadding: editPane.margins
    leftPadding: editPane.margins
    rightPadding: editPane.margins
        
    layout: DockLayout {
    }
Diagram showing the TextArea and TextField controls that are used on the add page.

Inside this top-level container, we add our text fields. We use three fields: a TextArea for the quote text, a TextField for the author's first name, and another TextField for the author's last name. Let's add the TextArea first. To get the look we want, we set properties such as margins, preferred height, maximum height, and horizontal alignment. Notice that the enabled property is set to false. We don't allow users to start typing the quote or first name until the last name field is populated. This approach ensures that a last name is always provided for a new quote, which makes it easier to manage the data in the data model (we can assume that every entry in the model includes, at least, a value for the lastname field).

Container {
    layout: StackLayout {
    }

    TextArea {
        id: quoteField
        hintText: "Quote"
        topMargin: editPane.margins
        bottomMargin: topMargin
        enabled: false
        preferredHeight: 450
        maxHeight: 450
        horizontalAlignment: HorizontalAlignment.Fill                
    }

Now, let's add a Container to hold the two TextField controls. We use a left-to-right stack layout for this container to position the fields side-by-side. In the TextField that represents the author's last name, we include the onTextChanging signal handler. When a user types in this field, the textChanging() signal is emitted, and we test to determine whether there is any text in the field (that is, whether the text length is greater than zero). If so, we enable the acceptAction, quote field, and first name field to allow the user to type in them.

        Container {
            layout: StackLayout {
                orientation: LayoutOrientation.LeftToRight
            }

            TextField {
                id: firstNameField
                rightMargin: editPane.margins
                hintText: "First name"
                enabled: false
            }

            TextField {
                id: lastNameField
                hintText: "Last name"                    
                    
                onTextChanging: {
                    if (text.length > 0) {
                        addPage.titleBar.acceptAction.enabled = true;
                        quoteField.enabled = true;
                        firstNameField.enabled = true;
                    } else {
                        addPage.titleBar.acceptAction.enabled = false;
                        quoteField.enabled = false;
                        firstNameField.enabled = false;
                    }
                }
            } // end of last name field
        } // end of name text Container
    } // end of text Container (name text + quote text)
} // end of top-level Container

Create a custom JavaScript function

To finish off the AddPage component, we create a custom JavaScript function called newQuote(). This function prepares the AddPage for a blank entry by clearing all of the text fields and disabling the acceptAction, quote field, and first name field. We call this function from main.qml when the Sheet that contains the AddPage is closed.

    function newQuote() {
        firstNameField.text = "";
        lastNameField.text = "";
        quoteField.text = "";
        addPage.titleBar.acceptAction.enabled = false;
        quoteField.enabled = false;
        firstNameField.enabled = false;
    }
} // end of Page

We're now finished with the AddPage custom component. In the next section, we'll create the screen that displays an individual quote.

Create the quote page

At this point, we've created the main UI of our app, along with the page that lets users add a new quote. Now, we're going to implement the custom QML component that actually displays a quote and its author.

Create the QuotePage.qml file and add controls

In the assets folder of your project, create a folder called QuotePage. This subfolder will contain several custom components, which together form the quote page. Inside the QuotePage folder, create a .qml file named QuotePage.qml. This file represents the main screen of the quote page.

We start with a Page as the root control of the component, and add a Container that uses a DockLayout to the page.

Screen showing the quote page of the Quotes app.

We use the background property to set the background of the container to an image asset, which is located in the assets/images folder. It's a good practice to define backgrounds by using an ImagePaintDefinition, which loads the image asset efficiently, so we use that approach here. To learn more about images in Cascades, see Images.

import bb.cascades 1.0

Page {
    Container {
        background: backgroundPaint.imagePaint
        
        attachedObjects: [
            ImagePaintDefinition {
                id: backgroundPaint
                imageSource: "asset:///images/background.png"
            }
        ]

        layout: DockLayout {
        }

The only control inside our container is another custom QML component, QuoteBubble. This component represents the quote bubble and author name. We'll define this component shortly.

    QuoteBubble {
        id: quoteBubble
        horizontalAlignment: HorizontalAlignment.Center
        verticalAlignment: VerticalAlignment.Center
    }
} // end of Container

Customize the back button

Screen showing the custom back button.

For a bit of customization, we change the default back button of this page to use the text "Names" instead. When a user taps this back button, we pop the page off of the main NavigationPane stack to return to the list of quote authors.

We also set the value of the editMode property. This property is a custom property in QuoteBubble and indicates whether edit mode is enabled or not. In edit mode, the user can change the quote text, author first name, and author last name. If edit mode was enabled when the user taps the back button, we want to disable edit mode before returning to the list of quote authors.

paneProperties: NavigationPaneProperties {
    backButton: ActionItem {
        title: "Names"
        onTriggered: {
            nav.pop();
            quoteBubble.editMode = false;
        }
    }
}

Add actions and attached objects

This page includes two actions: an Edit action and a Delete action. The Edit action uses a custom icon and is placed on the action bar. When a user taps Edit, edit mode is enabled. The Delete action uses the predefined DeleteActionItem class and is placed in the action menu. When a user taps this action, we delete the current quote by calling the C++ function deleteRecord(), which we'll define later. Edit mode is also disabled.

actions: [
    ActionItem {            
        title: "Edit"
        imageSource: "asset:///images/Edit.png"
        ActionBar.placement: ActionBarPlacement.OnBar
            
        onTriggered: {
            quoteBubble.editMode = true
        }
    },
    DeleteActionItem {
        objectName: "DeleteAction"
        title: "Delete"
            
        onTriggered: {
            _quoteApp.deleteRecord();
            quoteBubble.editMode = false
        }
    }
]

Lastly, we make a small addition to the attachedObjects list of our page. This page in our app uses a slightly different visual theme than the main page, so we create a TextStyleDefinition that defines a reusable text style. We'll use this text style in the next custom components that we create.

    attachedObjects: [
        TextStyleDefinition {
            id: quoteStyleLightBody
            base: SystemDefaults.TextStyles.BodyText
            color: Color.create("#fafafa")
        }
    ]
} // end of Page

Create the QuoteBubble.qml file and a custom property

The next step is to create the QuoteBubble component. As mentioned above, this component displays the quote text (inside its bubble image) and author name. In the assets/QuotePage folder in your project, create a .qml file called QuoteBubble.qml. This component uses a Container at its root, and includes the custom property editMode and some padding properties.

We've already mentioned editMode, and it's an important part of this component. Custom properties can be very powerful, letting you change many different aspects of your app at the same time. Throughout the QuoteBubble code below, we bind the editMode property to several other properties. When editMode changes, these other properties change as well. Take note of where editMode is used and how it simplifies our code.

import bb.cascades 1.0

Container {
    id: quoteBubble
    property bool editMode: false
    
    topPadding: 30
    bottomPadding: topPadding
    rightPadding: topPadding
    leftPadding: topPadding

Add a custom component and define signal handlers

When edit mode is enabled (that is, editMode is true), two additional buttons appear at the top of the quote area. These buttons, Cancel and Update, are included in another custom component called EditControls. We add an EditControls component to our container, and bind its visible property to the editMode property. This means that the EditControls component is visible only when edit mode is enabled.

Diagram showing edit mode.
EditControls {
    id: editControls
    visible: quoteBubble.editMode

The EditControls component contains two custom signals, cancel() and update(), which are emitted when a user taps Cancel and Update, respectively. When the user taps Cancel, we want to revert the quote and author fields back to the data that's stored in the _contentView property. Remember that the _contentView property includes the full data for the selected quote from the data model. When the user taps Update, we want to update the data model with the values that the user typed in the fields. To update the values, we use the C++ function updateSelectedRecord(), which we'll define later. Also, regardless of which button the user taps, we disable edit mode.

    onCancel: {
        longText.text = _contentView.quote;
        _quoteApp.updateSelectedRecord(_contentView.firstname,
                                       _contentView.lastname,
                                       _contentView.quote);
        quoteBubble.editMode = false;
    }
        
    onUpdate: {
        _quoteApp.updateSelectedRecord(editName.firstName,
                                       editName.lastName,
                                       longText.text);
        quoteBubble.editMode = false;
    }
} // end of EditControls

Add controls for the quote

Next, we add a Container to hold the actual quote. This container uses a DockLayout, and we add an ImageView that represents the bubble image behind the quote text. This image scales automatically depending on the length of the quote, so we use a nine-sliced image to ensure that only the white area in the middle of the image is scaled (not the quotation marks).

Screen showing the quote bubble.

The quote text is placed in a TextArea inside a Container, which lets us add padding around the text. We bind the editable property of the TextArea to our editMode property, and we populate the text using the quote field from our data model.

Container {
    horizontalAlignment: HorizontalAlignment.Center
        
    layout: DockLayout {
    }

    ImageView {
        imageSource: "asset:///images/border_bubble.amd"
        verticalAlignment: VerticalAlignment.Fill
        horizontalAlignment: HorizontalAlignment.Fill
    }

    Container {
        topPadding: 54
        bottomPadding: 85
        rightPadding: 30
        leftPadding: rightPadding
            
        TextArea {
            id: longText
            preferredWidth: 520
            editable: quoteBubble.editMode

            text: _contentView.quote                
        }
    } // end of text area Container
} // end of quote Container

Our QuoteBubble is almost finished. We just need to add a Container with fields that represent the quote author. When edit mode is disabled, the author's name appears as a simple Label. This time, we bind the visible property to the inverse of the editMode property, meaning that the Label is visible only when edit mode is disabled. We also use the text style that we defined at the end of our custom QuotePage component.

Container {
    topPadding: 15
        
    layout: DockLayout {
    }
                
    Label {
       id: nameLabel
       visible: ! quoteBubble.editMode


       text: _contentView.firstname + " " + _contentView.lastname
       textStyle.base: quoteStyleLightBody.style
    }

When edit mode is enabled, we need to change the appearance of the author's name to use editable text fields instead of a Label. To do this, we use another custom component called EditName. Again, we bind the visible property of EditName to editMode. The EditName component includes a custom signal called enableSave(), which indicates whether the Update button in our EditControls should be enabled or not. Depending on the values of the author name fields, we might not want to allow the updated quote to be saved; you'll see why when we define the EditName component a bit later on. For now, we handle this signal by using the onEnableSave signal handler.

        EditName {
            id: editName
            visible: quoteBubble.editMode
            onEnableSave: {
                if (enable) {
                    editControls.updateEnabled = true;
                    longText.enabled = true;
                } else {
                    editControls.updateEnabled = false;
                    longText.enabled = false;
                }
            }
        }
    } // end of quote author Container
} // end of actual quote Container

Define the EditControls custom component

The final task in creating our quote page is to define the supporting components, EditControls and EditName, that we used above. In the assets/QuotePage folder in your project, create a .qml file called EditControls.qml.

The EditControls component consists of a Cancel button, a Label with the text "Edit", and an Update button.

Screen showing the EditControls component.

This component includes a Boolean property called updateEnabled, which indicates whether the Update button should be enabled. This component also includes the custom signals update() and cancel(), and uses a left-to-right stack layout.

import bb.cascades 1.0

Container {
    id: editControls
    property bool updateEnabled: false
    signal update()
    signal cancel()
    bottomPadding: 40
    
    layout: StackLayout {
        orientation: LayoutOrientation.LeftToRight
    }

Let's add the Cancel button to the component. By using a space quota of 1, we ensure that this button receives the same amount of horizontal space in the layout as the Edit label and Update button. When a user taps this button, our component emits the cancel() signal.

Button {
    text: "Cancel"
        
    layoutProperties: StackLayoutProperties {
        spaceQuota: 1
    }
        
    onClicked: {
        editControls.cancel();
    }
}

Next, we add the Edit label. This label uses the same text style as the other components on our quote page.

Label {
    id: editLabel
    text: "Edit"
    horizontalAlignment: HorizontalAlignment.Center
    verticalAlignment: VerticalAlignment.Center

    layoutProperties: StackLayoutProperties {
        spaceQuota: 1
    }

    textStyle.base: quoteStyleLightBody.style
}

Lastly, we add the Update button. We bind its enabled property to the updateEnabled property, and when a user taps this button, our component emits the update() signal.

    Button {
        id: updateButton
        text: "Update"
        enabled: updateEnabled
        
        layoutProperties: StackLayoutProperties {
            spaceQuota: 1
        }
        
        onClicked: {
            editControls.update();
        }
    }
} // end of Container

Define the EditName custom component

Let's move on to our final custom component, EditName. In the assets/QuotePage folder of your project, create a .qml file called EditName.qml.

This component represents an editable version of the author name, which is achieved by using TextField controls for both the first name and last name.

Screen showing the editable version of the author name fields.

We define alias properties called firstName and lastName and bind these properties to the text property of each TextField. This approach lets us access these properties outside of the EditName component. We also include the custom signal enableSave().

import bb.cascades 1.0

Container {
    id: editName
    property alias firstName: firstNameField.text
    property alias lastName: lastNameField.text
    signal enableSave(bool enable)
    
    layout: StackLayout {
        orientation: LayoutOrientation.LeftToRight
    }

We add a TextField that represents the author's first name, and populate its text property with the firstname field from our data model.

TextField {
    id: firstNameField
    hintText: "First name"

    layoutProperties: StackLayoutProperties {
        spaceQuota: 1
    }

    text: _contentView.firstname
}

Now, we add a TextField that represents the author's last name. If you recall, we mentioned above that, depending on the values of the author name fields, we might not want to allow an updated quote to be saved. In our data model, we use the author's last name as the primary key. In other words, every quote entry in the data model must include a non-empty value in the lastname field. So, when a user types in this TextField, we check to see whether the length of the text is greater than zero. If so, we have a valid primary key and we allow the quote to be saved. If not, we disable the Update button in our EditControls component until the user types a valid last name.

    TextField {
        id: lastNameField
        hintText: "Last name"        
        text: _contentView.lastname
                
        layoutProperties: StackLayoutProperties {
            spaceQuota: 1
        }
        
        onTextChanging: {
            if (text.length > 0) {
                editName.enableSave(true);
                firstNameField.enabled = true;
            } else {
                editName.enableSave(false);
                firstNameField.enabled = false;
            }
        }
    }
} // end of Container

At this point, we've finished all of the UI components that our app uses. In the next sections, we'll use C++ to implement the supporting classes and functions that our app needs.

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.

Create the database class

The final task in our tutorial is to create the QuotesDbHelper class, which lets us access the SQL database where our quote data is stored. This class uses Qt functions and classes to access the file system on the device directly. It also uses Cascades data access APIs (such as the SqlDataAccess class) in conjunction with Qt SQL APIs (such as the QSqlDatabase class) to read and update our SQL database.

In this section, we won't go into detail about all of the classes and functions that are used in QuotesDbHelper. Several comments are included in the code samples to help guide you through the code. If you'd like to learn more about accessing the file system and storing data, see Working with the file system and Data.

Create the QuotesDbHelper class

In the src folder of your project, create a class called QuotesDbHelper. Similar to when you created the QuotesApp class, don't worry about adding method stubs. We'll start by implementing our functions in the .cpp file, so open the QuotesDbHelper.cpp file.

At the top of the file, we include the associated header, QuotesDbHelper.h, and use the bb::data namespace. The class constructor is empty. The class destructor performs a couple of clean-up operations on our SQL databases to make sure that our app frees its resources properly. The mDb object is an instance of the QSqlDatabase class, which we use to interact with the database in the file system.

#include "quotesdbhelper.h"

using namespace bb::data;

QuotesDbHelper::QuotesDbHelper()
{
}

QuotesDbHelper::~QuotesDbHelper()
{
    if (mDb.isOpen()) {
        QSqlDatabase::removeDatabase(mDbNameWithPath);
        mDb.removeDatabase("QSQLITE");
    }
}

Implement the copy function

Depending on the location of files in your project, your app might have read-only access to the files, or it might have read-write access. For example, for a file that's included in the assets folder of your project, your app typically has read-only access to this file. In our app, we placed our database file, quotes.db, in the assets/sql folder. Because we need to have read-write access to the database file to insert, update, or delete quote records, we create a function called copyDbToDataFolder(). This function copies our database file from the assets folder to the data folder. After the file is copied to the data folder, our app has full read-write access to the file.

bool QuotesDbHelper::copyDbToDataFolder(const QString
                                                   databaseName)
{
    // First, we check to see if the file already exists in the
    // data folder (that is, the file was copied already).
    QString dataFolder = QDir::homePath();
    QString newFileName = dataFolder + "/" + databaseName;
    QFile newFile(newFileName);

    if (!newFile.exists()) {
        // If the file is not already in the data folder, we copy
        // it from the assets folder (read-only) to the data folder
        // (read-write).
        QString appFolder(QDir::homePath());
        appFolder.chop(4);
        QString originalFileName = appFolder + "app/native/assets/sql/"
                                   + databaseName;
        QFile originalFile(originalFileName);

        if (originalFile.exists()) {
            return originalFile.copy(newFileName);
        } else {
            qDebug() << "Failed to copy file, database file does not
                         exist.";
            return false;
        }
    }

    return true;
}

Implement the load function

Next, we implement the loadDataBase() function. We copy the database file to the data folder using the copyDbToDataFolder() function above, and then set up an SqlDataAccess object that points to the database file. This object lets us execute SQL queries, such as SELECT, on the database. We call execute() to retrieve all of the entries in the specified table (in our case, the quotes table), and we store them in a QVariantList. We also make sure that no errors have occurred.

QVariantList QuotesDbHelper::loadDataBase(const QString
                                                    databaseName,
                                                    const QString table)
{
    QVariantList sqlData;

    if (copyDbToDataFolder(databaseName)) {
        // Load database entries using an SqlDataAccess object into a
        // QVariantList, which can be used in a GroupDataModel to
        // display a sorted list.
        mDbNameWithPath = "data/" + databaseName;

        // Set up an SqlDataAccess object.
        SqlDataAccess sqlDataAccess(mDbNameWithPath);

        // Set a query to obtain all entries in the table and load into
        // our QVariantList.
        sqlData = sqlDataAccess.execute("select * from " + table)
                               .value<QVariantList>();

        if (sqlDataAccess.hasError()) {
            DataAccessError err = sqlDataAccess.error();
            qWarning() << "SQL error: type=" << err.errorType() << ": "
                       << err.errorMessage();
            return sqlData;
        }

We can use our SqlDataAccess object to read entries from the database file, but we need to set up another database connection (using QSqlDatabase) to allow us to insert, update, and delete database entries. By using QSqlDatabase to set up another connection, we won't conflict with the connection that's already set up using SqlDataAccess. We make sure that this second connection was created successfully, and then we open the database using this connection.

        mDb = QSqlDatabase::addDatabase("QSQLITE",
                                    "database_helper_connection");
        mDb.setDatabaseName(mDbNameWithPath);

        if (!mDb.isValid()) {
            qWarning() << "Could not set database name, probably due
                           to an invalid driver.";
            return sqlData;
        }

        bool success = mDb.open();

        if (!success) {
            qWarning() << "Could not open database.";
            return sqlData;
        }

        // Store the name of the table (used in the insert/update/delete
        // functions).
        mTable = table;
    }

    return sqlData;
}

Implement the delete function

Our next function, deleteById(), simply deletes a record by using the DELETE query. To perform the actual deletion, we pass the query to another function, queryDatabase(), which we'll implement a little later on.

bool QuotesDbHelper::deleteById(QVariant id)
{
    // Query for deleting an entry in the table.
    if (id.canConvert(QVariant::String)) {
        QString query = "DELETE FROM " + mTable + " WHERE id=" +
                        id.toString();
        return queryDatabase(query);
    }

    qWarning() << "Failed to delete item with id: " << id;

    return false;
}

Implement the insert and update functions

The two functions that write to the database, insert() and update(), both have a similar structure. We use the prepare() function of the QSqlQuery class to prepare our query. This function makes it easier to construct and prepare an SQL query, especially when the query is complex. For example, a single quotation mark (') inside a double quotation mark (") is difficult to handle if you don't bind your values using the prepare() function. Here's how to implement the insert() function:

QVariant QuotesDbHelper::insert(QVariantMap map)
{

    QSqlQuery sqlQuery(mDb);

    sqlQuery.prepare("INSERT INTO " + mTable + " (firstname, lastname,
                      quote)" "VALUES(:firstName, :lastName, :quote)");

    sqlQuery.bindValue(":firstName", map["firstname"]);
    sqlQuery.bindValue(":lastName", map["lastname"]);
    sqlQuery.bindValue(":quote", map["quote"]);
    sqlQuery.exec();

    QSqlError err = sqlQuery.lastError();

    if (err.isValid()) {
        qWarning() << "SQL reported an error : " << err.text();
    }

    return sqlQuery.lastInsertId();
}

Here's how we construct the update() function:

bool QuotesDbHelper::update(QVariantMap map)
{
    QSqlQuery sqlQuery(mDb);


    sqlQuery.prepare("UPDATE " + mTable + " SET firstname=:firstName,
                      lastname=:lastName, quote=:quote WHERE id=:id");

    sqlQuery.bindValue(":firstName", map["firstname"]);
    sqlQuery.bindValue(":lastName", map["lastname"]);
    sqlQuery.bindValue(":quote", map["quote"]);
    sqlQuery.bindValue(":id", map["id"].toString());
    sqlQuery.exec();

    QSqlError err = sqlQuery.lastError();

    if (!err.isValid()) {
        return true;
    }

    qWarning() << "SQL reported an error : " << err.text();

    return false;
}

Implement the query function

We have one last function to implement, queryDatabase(). This function performs the actual deletion for the deleteById() function above.

bool QuotesDbHelper::queryDatabase(const QString query)
{

    // Execute the query.
    QSqlQuery sqlQuery(query, mDb);

    QSqlError err = sqlQuery.lastError();

    if (err.isValid()) {
        qWarning() << "SQL reported an error for query: "
                   << query << " error: "
                   << mDb.lastError().text();
        return false;
    }

    return true;
}

Complete the QuotesDbHelper header file

Now that we've finished the function implementations, we need to complete the associated QuotesDbHelper.h file. Open the QuotesDbHelper.h file in your project's src folder. Similar to the QuotesApp.h file, the contents of this file are straightforward.

#ifndef _QUOTESDBHELPER_H_
#define _QUOTESDBHELPER_H_

#include <QtSql/QtSql>
#include <bb/data/SqlDataAccess>

using namespace bb::data;

class QuotesDbHelper
{
public:
    QuotesDbHelper();
    ~QuotesDbHelper();

    QVariantList loadDataBase(const QString databaseName,
                              const QString table);
    bool deleteById(QVariant id);
    QVariant insert(QVariantMap map);
    bool update(QVariantMap map);

private:
    bool copyDbToDataFolder(const QString databaseName);
    bool queryDatabase(const QString query);

    QSqlDatabase mDb;
    QString mTable;
    QString mDbNameWithPath;
};

#endif

Add the data library to your project

The code for our Quotes app is complete, but there's one additional thing that we need to do. To use classes in the bb::data namespace, we need to add the appropriate library to our project. You can add additional libraries in the .pro file that's included in the root folder of the project.

Open the .pro file in your project. This file should have the same name as the project itself. In this file, add the following line below the CONFIG line:

LIBS   += -lbbdata

That's it! Build and run the project to see the final result.

Screen showing the main screen of the Quotes app.
Screen showing an individual quote in the Quotes app.

Last modified: 2013-12-21

comments powered by Disqus