Qt Reference Documentation

Scalability: Loading Separate Layouts Based on Screen Configuration

Files:

Using separate top level layout files for different resolutions.

The Using Separate Layouts example shows how to use separate top level layout definitions to support different screen configurations or device form factors with QML Components.

Selecting The Top-Level Layout According to Resolution and Dots Per Inch (DPI)

The example uses the file MainView.qml as its top-level layout definition. There are several versions of MainView.qml in a directory structure where the paths follow the pattern <path>/<resolution>/<DPI>/<fileName>. The loading of the MainView.qml is handled by the CustomLoader component, which inherits from Loader. 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 MainView.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 file MainView.qml with the path value set to "layouts" in an environment with the resolution 800x600 at 103 DPI it would attempt to create the object from files in the following places.

  1. layouts/800x600/100/MainView.qml
  2. layouts/800x600/MainView.qml
  3. layouts/Large/Low/MainView.qml
  4. layouts/Large/MainView.qml
  5. layouts/MainView.qml

The purpose of the search pattern is to enable specific layout definitions, layouts for broader screen categories, and catch-all layouts.

The CustomLoader will emit the signal loaded if the loading of the specified file succeeds, this functionality is inherited from 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.

 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

         CustomLoader {
             id: customLoader

             anchors.fill: parent
             path: "layouts"
             fileName: "MainView.qml"
             onLoadError: errorText.text = "Error: unable to load UI\nTap to quit.";
         }

         Text {
             id: errorText

             color: "white"
             anchors.centerIn: parent
             visible: text
         }

         MouseArea {
             anchors.fill: parent
             enabled: errorText.visible
             onClicked: Qt.quit();
         }
     }
 }

Reusing Components in Top Level Layouts

The top level layout files, named MainView.qml, use the component GradientRectangle. The MainView.qml files define the location of where the file GradientRectangle.qml resides as Root, and then simply refer to the corresponding component as Root.GradientRectangle.

 import QtQuick 1.1
 import "../" as Root

 Rectangle {
     id: rect

     property string sourceInfo: "layouts/MainView.qml"
     property string displayInfo: Math.max(rect.width, rect.height) + "x"
                                  + Math.min(rect.width, rect.height) + ", "
                                  + Math.round(screen.dpi) + " DPI"

     anchors.fill: parent

     Root.GradientRectangle {
         anchors.fill: parent

         Text {
             anchors.centerIn: parent
             horizontalAlignment: Text.AlignHCenter
             text: rect.sourceInfo + "\n" + rect.displayInfo
         }
     }
 }

Reacting to Orientation Changes

The algorithm that selects the correct top-level layout does not take orientation to account. Therefore, the top-level layout file has to specify if/how orientation is a factor in the layout.

The default layout in the example, layouts/MainView.qml does not react to orientation changes, but the layout for the category Normal/High has different states for landscape and portrait. Note the use of AnchorChanges.

 states: State {
     name: "Landscape"
     when: screen.width > screen.height

     AnchorChanges {
         target: output
         anchors {
             left: rect.left
             right: button1.left
             bottom: rect.bottom
         }
     }

     AnchorChanges {
         target: button1
         anchors {
             top: rect.top
             bottom: undefined
             left: undefined
             right: rect.right
         }
     }

     AnchorChanges {
         target: button2
         anchors {
             verticalCenter: rect.verticalCenter
             horizontalCenter: undefined
             right: rect.right
             bottom: undefined
         }
     }

     PropertyChanges {
         target: output
         width: rect.width / 4 * 3
         height: rect.height
     }

     PropertyChanges {
         target: rect
         buttonSide: rect.height / 3
     }
 }

Discussion

It is possible to combine the idea of loading different configuration files with the selection of different top-level layouts based on screen size, see Scalable Configuration.