Tab the page with a repeater
When writing the hextool app I used the Repeater element a lot. When there are many similar elements in the QML user interface, the repeater makes it very easy to instantiate and customize the elements. I have previously done some simple experiments with the tabbed user interface (e.g. see Tab the page!). I was wondering if I could also utilize the repeater when creating the tabbed pages. After some experimenting I found out that the repeater suits very well for this kind of task.
I created a generic application “About” page that could be added to any application by simply customizing the tab names and page contents. Here is how it looks like when running in the simulator. There are currently four pages in this app, but with the repeater it is easy to change the number of pages/tabs. The header line contains the active tab name and the close button.
C++ part
The main.cpp is basically the same as used in many previous apps. I fetch the QML root object and pass it to the C++ MainPage class instance for signal/slot connections.
#include <QtGui/QApplication> #include <QDeclarativeItem> #include "qmlapplicationviewer.h" #include "mainpage.h" Q_DECL_EXPORT int main(int argc, char *argv[]) { QScopedPointer<QApplication> app(createApplication(argc, argv)); QmlApplicationViewer viewer; viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto); viewer.setMainQmlFile(QLatin1String("qml/TestApp15/main.qml")); viewer.showExpanded(); // Get the QML root object for signal-slot connections. QObject *qml = viewer.rootObject(); // Create the back-end processor and pass in the root object. // Make viewer the parent object. new MainPage(qml,&viewer); return app->exec(); }
The MainPage class is defined in the files mainpage.h and mainpage.cpp. The header file defines the signals and slots. I have defined two signals: openTabMain opens the main tab page and setPageContent sets the page content for the tabbed pages. The openTabMain will pass the tab names (defined in QStringList) to the QML side. The setPageContent signal has two parameters: the page number and the page content (as QString). There is one slot: openTabMainRequested which will process the open request for the tabbed page.
#ifndef MAINPAGE_H #define MAINPAGE_H #include <QObject> #include <QStringList> #include <QVariant> class MainPage : public QObject { Q_OBJECT public: explicit MainPage(QObject *qml, QObject *parent = 0); signals: void openTabMain(QVariant); void setPageContent(int,QString); public slots: void openTabMainRequested(); }; #endif // MAINPAGE_H
The mainpage.cpp connects the signals and slots and implements the slot openTabMainRequested. It defines the tab names and content and opens the tab pages and sets the content to each page.
#include "mainpage.h" MainPage::MainPage(QObject *qml, QObject *parent) : QObject(parent) { // connect QML signals to MainPage slots connect(qml, SIGNAL(openTabMainRequested()), this, SLOT(openTabMainRequested())); // connect MainPage signals to QML signals connect(this, SIGNAL(openTabMain(QVariant)), qml, SIGNAL(openTabMain(QVariant))); connect(this, SIGNAL(setPageContent(int,QString)), qml, SIGNAL(setPageContent(int,QString))); } void MainPage::openTabMainRequested() { enum TabName {ABOUT,CREDITS,TRANSLATION,LICENSE}; QStringList tabNames; tabNames << "About" << "Credits" << "Translation" << "License"; QString aboutContent; aboutContent += "<b>TestApp15</b> is a simple example application that "; aboutContent += "creates a tabbed page selector. The tab names and "; aboutContent += "page contents are set from the C++ code.<p>"; aboutContent += "The text size is controlled by the screen orientation.<p>"; aboutContent += "Source code is available at https://github.com/n9dyfi/TestApp15.git"; QString creditsContent; creditsContent +="<b>Thanks To</b><br>"; creditsContent +="<div style='margin-left:15px;'>"; creditsContent +="<u>John Doe</u><br>Main Application development<br>john.doe@example.com<br>"; creditsContent +="<br>"; creditsContent +="<u>Jane Doe</u><br>Main Application Icon<br>jane.doe@example.com<br></div>"; QString translationContent; translationContent += "Currently there are no translations available."; QString licenseContent; licenseContent += "This package is free software; you can redistribute it and/or modify "; licenseContent += "it under the terms of the GNU General Public License as published by "; licenseContent += "the Free Software Foundation; either version 2 of the License, or "; licenseContent += "(at your option) any later version."; licenseContent += "<p>This package is distributed in the hope that it will be useful, "; licenseContent += "but WITHOUT ANY WARRANTY; without even the implied warranty of "; licenseContent += "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the "; licenseContent += "GNU General Public License for more details."; licenseContent += "<p>You should have received a copy of the GNU General Public License "; licenseContent += "along with this package; if not, write to the Free Software "; licenseContent += "Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA"; licenseContent += "<p>On Debian systems, the complete text of the GNU General "; licenseContent += "Public License can be found in `/usr/share/common-licenses/GPL'."; licenseContent += "<p>The Debian packaging is (C) 2018, N9dyfi <n9dyfi@gmail.com> and "; licenseContent += "is licensed under the GPL, see above."; // Open the TabMain page emit(openTabMain(QVariant::fromValue(tabNames))); // Set content for each TabPage emit(setPageContent(ABOUT,aboutContent)); emit(setPageContent(CREDITS,creditsContent)); emit(setPageContent(TRANSLATION,translationContent)); emit(setPageContent(LICENSE,licenseContent)); }
QML part
The main.cpp loads the main QML file main.qml. Here we need to define the counterparts for the C++ signals and slots. Note that the parameter names used in the signal definitions will be used in the signal handlers to access the data coming from the C++ side. In the C++ side we do not need to name the signal parameters. The AppDefaults component contains default values for font sizes, margins, etc. The MainPage contains the main user interface. In addition there is the standard toolbar with a menu.
import QtQuick 1.1 import com.nokia.meego 1.0 PageStackWindow { id: appWindow signal openTabMain(variant tabNames) signal openTabMainRequested signal setPageContent(int pageNumber, string pageContent) initialPage: mainPage AppDefaults { id: appDefaults } MainPage { id: mainPage } ToolBarLayout { id: commonTools visible: true ToolIcon { platformIconId: "toolbar-view-menu" anchors.right: (parent === undefined) ? undefined : parent.right onClicked: (myMenu.status === DialogStatus.Closed) ? myMenu.open() : myMenu.close() } } Menu { id: myMenu visualParent: pageStack MenuLayout { MenuItem { text: qsTr("Quit") onClicked: Qt.quit() } } } }
The AppDefaults component is pretty much the same as what I have been using previously e.g. in the hextool app. The nice thing with this kind of a component based default settings file is that I can dynamically change different dimensions based on the display orientation (e.g. text size or header height). Also the color scheme can be easily set.
import QtQuick 1.1 Item { property int cCOLOR_SCHEME : 4 property int cDEFAULT_MARGIN : 16 property int cHEADER_DEFAULT_HEIGHT_PORTRAIT : 72 property int cHEADER_DEFAULT_TOP_SPACING_PORTRAIT : 15 property int cHEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT : 15 property int cHEADER_DEFAULT_HEIGHT_LANDSCAPE : 64 property int cHEADER_DEFAULT_TOP_SPACING_LANDSCAPE : 9 property int cHEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE : 13 property string cFONT_FAMILY : "Nokia pure Text Light" property int cFONT_SIZE_LARGE : 32 property int cFONT_SIZE_SMALL : 22 property int cFONT_SIZE_TINY : 18 property string cFONT_FAMILY_BUTTON : "Nokia pure Text" property int cFONT_SIZE_BUTTON : 20 property int cTEXT_AREA_HEIGHT : 60 property string cVIEW_HEADER : "image://theme/color"+cCOLOR_SCHEME+"-meegotouch-view-header-fixed" property string cBUTTON_BACKGROUND: "image://theme/color"+cCOLOR_SCHEME+"-meegotouch-button-accent-background" property int cHEADER_HEIGHT : (inPortrait)?cHEADER_DEFAULT_HEIGHT_PORTRAIT: cHEADER_DEFAULT_HEIGHT_LANDSCAPE property int cHEADER_TOP_SPACING : (inPortrait)?cHEADER_DEFAULT_TOP_SPACING_PORTRAIT: cHEADER_DEFAULT_TOP_SPACING_LANDSCAPE property int cHEADER_BOTTOM_SPACING : (inPortrait)?cHEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT: cHEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE property int cHEADER_REDUCED_BOTTOM_SPACING : (inPortrait)? 0.75*cHEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT: 0.5*cHEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE property int cFONT_SIZE : (inPortrait)?cFONT_SIZE_TINY:cFONT_SIZE_SMALL Component.onCompleted: theme.colorScheme = cCOLOR_SCHEME }
The MainPage will just instantiate the Header component and one button to display the tab page.
When the button is clicked the openTabMainRequested signal will be emitted. The signal was connected to the openTabMainRequested slot in the MainPage C++ class. The Connections element connects the onOpenTabMain handler to the openTabMain signal defined in main.qml (and connected to the openTabMain signal coming from the C++ side). When the signal is received the TabMain page will be pushed into the page stack. Note that the TabMain.qml file is not loaded until the tab page is requested. This will speed up the application startup time as we don’t need to load all the components at once. The tabNames parameter coming with the signal will be passed to the TabMain page.
import QtQuick 1.1 import com.nokia.meego Page { tools: commonTools Header { id: header headerText: "TestApp15" } Button{ anchors.centerIn: parent anchors.verticalCenterOffset: header.height/2 text: qsTr("Tab the page!") onClicked: openTabMainRequested() } Connections { target: appWindow onOpenTabMain: pageStack.push(Qt.resolvedUrl("TabMain.qml"),{tabNames:tabNames}) } }
The TabMain component creates the tabbed page with the tabs and contents defined in the MainPage.cpp. The TabGroup element defines the tabbed pages. But instead of instantiating each page explicitly I can use the Repeater element to instantiate any number of pages I want (as defined with the tabNames string list). I will use the TabPage component as my page definition. The Repeater under TabGroup uses the model property to select the number of pages. For each page I will create a TabButton into the ButtonRow element inside the toolbar. Here the buttons are also instantiated with the Repeater element. Here the Repeater model property contains the tab name list which is used to set the button text (using the modelData context property). Finally I will connect the event handler onSetPageContent to the setPageContent signal defined in the main.qml file. When the setPageContent signal is received from the C++ side the selected page content will be set based on the provided signal parameters.
import QtQuick 1.1 import com.nokia.meego 1.0 // This is a generic tab page with a header // API // - property tabNames : a string list containing the tab names // - signal setPageContent(int pageNumber, string pageContent) Page { property int defaultMargin: appDefaults.cDEFAULT_MARGIN property int textSize: appDefaults.cFONT_SIZE property int headerHeight: appDefaults.cHEADER_HEIGHT // Define the tab names property variant tabNames // Page header // Header { id: header headerText: "TestApp15" // Close button ToolIcon { platformIconId: "browser-stop" onClicked: pageStack.pop() anchors { right: parent.right; rightMargin: defaultMargin/2 verticalCenter: parent.verticalCenter} } } // Instantiate the tab pages // Use the tab name as a placeholder text // TabGroup { id: tabGroup Repeater { id: tabPages model: tabNames.length // number of tabs pages to create TabPage { textWidth: header.width-2*defaultMargin // Initial content can be replaced with the setPageContent signal content: tabNames[index] // Copy the tab name to the header when tab is activated onStatusChanged: { if(status===PageStatus.Active) header.headerText = tabNames[index] } } } } // Define the tab buttons in the toolbar // tools: ToolBarLayout { id: tabTools ButtonRow { id: buttonRow Repeater { id: tabButtons model: tabNames TabButton { text: modelData font.pixelSize: textSize tab: tabPages.itemAt(index) } } } } // Select the active tab // Component.onCompleted: { tabGroup.currentTab = tabPages.itemAt(0) buttonRow.checkedButton = tabButtons.itemAt(0) } // Connect the setPageContent signal // Connections { target: appWindow onSetPageContent: { tabPages.itemAt(pageNumber).content = pageContent } } }
The last part of the QML code is the TabPage component. The same component is used for all the tabs. Here we need just a TextEdit element to hold the text and a Flickable element to be able to scroll the text.
import QtQuick 1.1 import com.nokia.meego 1.0 // About - Credits - Translation - License Page { property int textWidth property string content Flickable { id: flick x: defaultMargin y: headerHeight+defaultMargin width: parent.width-2*defaultMargin height: parent.height-headerHeight-defaultMargin contentWidth: textbox.width contentHeight: textbox.height clip: true TextEdit { id: textbox text: content width: textWidth font.pixelSize: textSize wrapMode: TextEdit.Wrap } } }