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. 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.

Last modified: 2014-06-24



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

comments powered by Disqus