Files:
Loading configuration files based on screen configuration
This example demonstrates how to load different configuration files that modify UI layout and app logic depending on the DPI and screen resolution.
The example application implements a simple game, where four different colored "buttons" flash in random order. The player must push the buttons in the same sequence that the buttons flash. Each correct button press yields one point, but pressing the buttons in the wrong sequence ends the game.
This example uses two loaders to initialize the application. First, the main file scalableconfiguration.qml loads one of several alternative configuration objects, selecting the appropriate file to load by the DPI and screen dimensions of the application runtime. Then the example loads the GameView object that uses the data in the configuration object to layout the UI and adjust game settings.
import QtQuick 1.1 // Note: This example imports the ?Qt.labs.components.native 1.0" module that allows the same // Qt Quick Components example code to run as is in both MeeGo 1.2 Harmattan and Symbian platforms // during the application development. However, real published applications should not import this // module but one of the following platform-specific modules instead: // import com.nokia.symbian 1.1 // Symbian components // import com.nokia.meego 1.1 // MeeGo components import Qt.labs.components.native 1.0 Window { id: window Item { anchors.fill: parent Component { id: gameViewComponent GameView { configuration: customLoader.item } } Loader { id: gameViewLoader anchors.fill: parent } Text { id: errorText color: "white" anchors.centerIn: parent visible: errorText.text.length > 0 text: "" } MouseArea { anchors.fill: parent enabled: errorText.visible onClicked: Qt.quit() } } CustomLoader { id: customLoader path: "configurations" fileName: "Configuration.qml" onLoaded: gameViewLoader.sourceComponent = gameViewComponent; onLoadError: errorText.text = "Error: unable to load configuration\nTap to quit."; } }
In this setup it is important to first load the configuration object, and only then create any objects that use data in the configuration object; any attempt to create an object that relies on non-existent configuration information will fail.
The loading sequence starts when the application starts and the CustomLoader element is intialized. The CustomLoader inherits from Loader, it is an element that encapsulates the logic for loading different files for different resolutions.
The configuration objects are implemented by the files named Configuration.qml, the file name is assigned to CustomLoader property fileName in scalableconfiguration.qml.
The different versions of Configuration.qml exist in a directory structure where the paths follow the pattern <path>/<resolution>/<DPI>/<fileName>. The <path> is the root directory of the structure, the CustomLoader property path defines the value of <path>, in the example the directory is named layouts. The part <fileName> represents the file that should be loaded, the CustomLoader property fileName stores this value, which is set to Configuration.qml in example code.
The <resolution> part of the path can be expressed in two ways. The most specific way to express <resolution> is to follow the pattern <max(width,height)>x<min(width,height)>, which uses properties screen.displayWidth and screen.displayHeight to parse a directory name. Note that the larger of the two values is always used first, the CustomLoader does not support loading different files for different orientations. The alternative way to define <resolution> is to use one of the screen size categories Small, Normal, Large, or ExtraLarge. The CustomLoader defines the screen size categories in the function displayCategory() by using display size category information provided by screen.displayCategory and Screen.
function displayCategory() { switch (screen.displayCategory) { case Screen.Small: return "Small"; case Screen.Normal: return "Normal"; case Screen.Large: return "Large"; default: return "ExtraLarge"; } }
The <DPI> part of the path represents the dots per inch value of the display, the <DPI> can be an integer DPI value or one of the density categories defined in the CustomLoader function densityCategory(). Similarly to the display size category, the screen.density property and the Screen object provides DPI information.
function densityCategory() { switch (screen.density) { case Screen.Low: return "Low"; case Screen.Medium: return "Medium"; case Screen.High: return "High"; default: return "ExtraHigh"; } }
The path that the CustomLoader is trying to load is maintained in the property mySource. Note that the CustomLoader rounds the DPI value that it obtains from screen.dpi to the nearest 10 and stores the value in roundedDpi.
property int attempt: 0 property int largerDimension: Math.max(screen.displayWidth, screen.displayHeight) property int smallerDimension: Math.min(screen.displayWidth, screen.displayHeight) property int roundedDpi: Math.round(screen.dpi / 10) * 10 property string mySource: path + "/" + largerDimension + "x" + smallerDimension + "/" + roundedDpi + "/" + fileName;
The actual loading starts when the onMySourceChanged signal handler is triggered as the CustomLoader component is initialized. The CustomLoader tries first to create an object from the most specific path. If the path does not exist, then CustomLoader starts to generalize the path by first replacing screen dimension and DPI information with corresponding categories, then by dropping DPI information, and finally the resolution part of the path, see onStatusChanged in the following snippet.
onMySourceChanged: { attempt = 0; if (customLoader.smallerDimension > 0 && customLoader.largerDimension > 0) source = mySource; } onStatusChanged: { if (customLoader.status == Loader.Error) { customLoader.attempt++; switch (customLoader.attempt) { case 1: source = path + "/" + largerDimension + "x" + smallerDimension + "/" + fileName; break; case 2: source = path + "/" + displayCategory() + "/" + densityCategory() + "/" + fileName; break; case 3: source = path + "/" + displayCategory() + "/" + fileName; break; case 4: source = path + "/" + fileName; break; default: customLoader.loadError(); source = ""; } } else { if (debug) console.log("CustomLoader: successfully loaded file: " + source); } }
If the CustomLoader would look for the configuration file Configuration.qml with the pathPrefix "configurations" in an environment with the resolution 800x600 at 103 DPI it would attempt to create the object from files in the following places.
The purpose of the search pattern is to enable specific configurations definitions, configurations for broader screen categories, and catch-all configurations.
The CustomLoader will emit the signal loaded if the loading of the specified file succeeds, this functionality is inherited from Loader. The signal handler onLoaded is used in scalableconfiguration.qml to initailize the loading of the GameView element with a regular Loader.
If the CustomLoader fails to load the specified file, then it emits the signal loadError. There is an handler for the loadError signal in separatelayouts.qml, which displays an error message.
The data in the Configuration.qml files store information such as about how many rows and columns of game controls to use, font specifications, and game speed information. The following snippet is from the configuration file configurations/640x360/Configuration.qml.
import QtQuick 1.1 // Note: This example imports the ?Qt.labs.components.native 1.0" module that allows the same // Qt Quick Components example code to run as is in both MeeGo 1.2 Harmattan and Symbian platforms // during the application development. However, real published applications should not import this // module but one of the following platform-specific modules instead: // import com.nokia.symbian 1.1 // Symbian components // import com.nokia.meego 1.1 // MeeGo components import Qt.labs.components.native 1.0 Item { property bool inPortrait: screen.width < screen.height property alias largeFont: largeText.font property alias smallFont: smallText.font property int columns: inPortrait ? 2 : 4 property int rows: inPortrait ? 2 : 1 property int spacing: inPortrait ? 5 : 10 property int interval: 1000 property int intervalDecrease: 10 property int radius: 10 Text { id: largeText font.family: "Nokia Sans" font.weight: Font.DemiBold font.pixelSize: 130 } Text { id: smallText font.family: "Nokia Sans" font.weight: Font.Bold font.pixelSize: inPortrait ? 30 : 50 } }
The GameView element gets the access to the Configuration object from the assignment customLoader.item to its configuration property. In GameView.qml the property is referred to as root.configuration, it is used in various locations. The function startGame() sets the interval of a Timer to root.configuration.interval.
function startGame() { GameEngine.reset(); scoreText.text = "0000"; gameTimer.interval = root.configuration.interval; gameTimer.start(); }
The Timer uses the property root.configuration.intervalDecrease to speed up the game incrementally.
Timer { id: gameTimer repeat:true onTriggered: { gameTimer.interval -= root.configuration.intervalDecrease; GameEngine.blink(); } }
The GameView element lays out game controls in a Grid, whose column and row setup is defined in the configuration object.
Grid { id: grid property int cellWidth: (width - ((columns - 1) * spacing)) / columns anchors { left: parent.left; bottom: parent.bottom; right: parent.right } columns: root.configuration.columns rows: root.configuration.rows spacing: root.configuration.spacing
The colored "buttons" are defined in GradientRectangle.qml, window.config is used to control the radius of the buttons.
GameButton { id: greenButton width: grid.cellWidth radius: root.configuration.radius bottomColor: GameEngine.GREEN enabled: gameTimer.running onClicked: GameEngine.check(GameEngine.GREEN) }
The GameView element has a Text element that displays status messages in a font defined by the configuration object.
Text { id: statusText text: statusRect.text anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter font: root.configuration.smallFont color: "red" }
It is possible to combine the idea of selecting different top-level layouts with the loading of different configuration files based on screen size, see Loading Separate Layouts Based on Screen Configuration.