Qt Reference Documentation

Scalable Configuration

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.

Loading Configuration Data According to Resolution and Dots Per Inch (DPI)

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.

  1. configurations/800x600/100/Configuration.qml
  2. configurations/800x600/Configuration.qml
  3. configurations/Large/Low/Configuration.qml
  4. configurations/Large/Configuration.qml
  5. configurations/Configuration.qml

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.

Using Data Defined in the Configuration

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"
 }

Discussion

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.