Ein Schneller Einstieg (teilweise dt.)

Autor des Abschnitts: jryannel

Issues: Create | View

Bemerkung

Letzter Build: März 14, 2018 at 02:55 CET

Der Quellcode für dieses Kapitel befindet sich im assets Verzeichnis.

Dieses Kapitel gibt einen Überblick über QML, die Beschreibungssprache für das Benutzerinterface in Qt 5. Wir diskutieren die QML Syntax, die aus einer baumartigen Struktur von Elementen besteht und es folgt ein Überblick über die wichtigsten Grundelemente. Später schauen wir uns kurz an, wie man eigene Elemente erstellt, so genannte “components” und wie man Elemente verändert mit Hilfe von Manipulationen von Eigenschaften. Zum Schluss behandeln wir Layouts, die Elemente arrangieren können und in welche Elemente der Benutzer Eingaben machen kann.

Die QML Syntax

Issues: Create | View

QML ist eine Auszeichnungs- oder Beschreibungssprache mit deren Hilfe das Benutzerinterface deiner Anwendung beschrieben wird. Das Benutzerinterface wird durch kleine Elemente beschrieben, die in größere Komponenten zusammengefasst werden. QML beschreibt dabei das Aussehen und das Verhalten der Interfaceelemente. Das Benutzerinterface kann dann mit Hilfe von JavaScript Code erweitert werden um einfache oder komplexe Programmlogik hinzuzufügen. Diese Art und Weise folgt dem HTML-JavaScript Muster, aber QML ist im Gegensatz zu HTML von Grund auf darauf ausgelegt, Benutzerschnittstellen statt Text-Dokumente zu beschreiben.

In ihrer einfachsten Form ist QML eine Hierarchie von Elementen. Kinder-Elemente erben das Koordinatensystem von ihren Eltern. Eine x,y Koordinate ist immer relativ zum Elternelement.

../_images/scene1.png

Beginnen wir mit einem einfachen Beispiel einer QML-Datei um die zu HTML unterschiedliche Syntax zu erklären:

// RectangleExample.qml

import QtQuick 2.5

// The root element is the Rectangle
Rectangle {
    // name this element root
    id: root

    // properties: <name>: <value>
    width: 120; height: 240

    // color property
    color: "#4A4A4A"

    // Declare a nested element (child of root)
    Image {
        id: triangle

        // reference the parent
        x: (parent.width - width)/2; y: 40

        source: 'assets/triangle_red.png'
    }

    // Another child of root
    Text {
        // un-named element

        // reference element by id
        y: triangle.y + triangle.height + 20

        // reference root element
        width: root.width

        color: 'white'
        horizontalAlignment: Text.AlignHCenter
        text: 'Triangle'
    }
}
  • Der import Befehl importiert ein Modul in einer speziellen Version. Grundsätzlich ist der Import von QtQuick 2.0 ein guter Start.

  • Einzeilige Kommentare kann man mit Hilfe von // oder mehrzeilige mit Hilfe von /* */ eingeben, genau wir in C/C++ und JavaScript

  • Jede QML-Datei benötigt genau ein Wurzelelement wie in HTML (hier: “Rectangle”)

  • Ein Element wird über sein Typ gefolgt von { } definiert.

  • Elemente können Eigenschaften haben, die in der Form name : value geschrieben werden.

  • Beliebige Elemente innerhalb eines QML-Dokuments können über ihre id (ein Bezeichner ohne Anführungszeichen) angesprochen werden.

  • Elemente können verschachtelt werden, was dann bedeutet, dass ein Elternelement Kindelemente hat. Die Elternelemente kann man über das Schlüsselwort parent ansprechen.

Tipp

Oft möchte man ein bestimmtes Element über die ID oder das Elternelement mit dem parent-Schlüsselwort ansprechen. Daher ist es gute Praxis das Wurzelelement auch “root” mit Hilfe von id: root zu nennen. Dann weiß man von überall im QML-Dokument wie das Wurzelelement heißt.

Hinweis

Jetzt kann man das Beispiel mit der Qt Quick runtime von der Kommandozeile starten

$ $QTDIR/bin/qmlscene RectangleExample.qml

wobei du $QTDIR durch den Pfad zu deiner Qt installation ersetzen musst, <version durch die Version von Qt und <target> z.B durch gcc_64. Die qmlscene Date initialisiert die Runtime und interpretiert die angegebene QML Datei.

In Qt Creator kannst du das ensprechende Projekt öffnen und das Dokument RectangleExample.qml ausführen.

Eigenschaften

Issues: Create | View

Elemente werden über deren Elementname deklariert, aber erst die Benutzung der Eigenschaften oder durch selbst erstellte Eigenschaften sind Elemente richtig definiert. Eine Eigenschaft ist eine einfaches Schlüssel-Wert Paar, z.B. width : 100, text: 'Hallo' oder color: '#FF0000'. Eine Eigenschaft hat einen wohldefinierten Datentyp und kann einen Initialwert zugewiesen bekommen (Im Beispiel jeweils die rechte Seite).

    Text {
        // (1) identifier
        id: thisLabel

        // (2) set x- and y-position
        x: 24; y: 16

        // (3) bind height to 2 * width
        height: 2 * width

        // (4) custom property
        property int times: 24

        // (5) property alias
        property alias anotherTimes: thisLabel.times

        // (6) set text appended by value
        text: "Greetings " + times

        // (7) font is a grouped property
        font.family: "Ubuntu"
        font.pixelSize: 24

        // (8) KeyNavigation is an attached property
        KeyNavigation.tab: otherLabel

        // (9) signal handler for property changes
        onHeightChanged: console.log('height:', height)

        // focus is need to receive key events
        focus: true

        // change color based on focus value
        color: focus?"red":"black"
    }

Schauen wir uns die verschiedenen Eigenheiten von Eigenschaften an:

  1. id ist ein sehr spezieller Wert mit dem man innerhalb einer QML-Datei (“document”) genau ein Element ansteuern (“referenzieren”) kann. Die id ist nicht vom Typ einer Zeichenkette sondern ein Bezeichner und Teil der QML Syntax. Eine id muss einmalig in einem Dokument sein und ihm kann nicht ein anderer Wert zugewiesen werden, noch kann nach ihm gesucht werden. (Es benimmt sich eher wie ein Zeiger aus der C++-Welt).

  2. Eine Eigenschaft kann auf einen Wert gesetzt werden, abhängig von seinem Datentyp. Wenn kein Wert für eine Eigenschaft vergeben wurde, wird vom System ein erstmaliger Wert gewählt. Man muss die Dokumentation des jeweiligen Elements anschauen um diesen Initialwert herauszubekommen.

  3. Eine Eigenschaft kann von einer oder vielen anderen Eigenschaften abhängen. Das nennt man binding (“Bindung”). Eine gebundene Eigenschaft wird immer aktualisiert wenn sich die andere Eigenschaft (von der der Wert abhängt) ändert. Man kann es sich wie einen Vertrag vorstellen, in diesem Beispiel ist die Höhe height immer zwei mal der Breite width.

  4. Eigene Eigenschaft kann man zu einem Element hinzufügen in dem man den Bezeichner property gefolgt von dem Datentyp, dem Namen der Eigenschaft und - wenn man will - einem initialen Wert verwendet (property <type> <name> : <value>). Falls kein Wert angegeben wird, wählt das System einen.

    Bemerkung

    You can also declare one property to be the default property if no property name is given by prepending the property declaration with the default keyword. This is used for example when you add child elements, the child elements are added automatically to the default property children of type list if they are visible elements.

  5. Eine weitere Möglichkeit, Eigenschaften zu definieren ist über das Schlüsselwort alias (property alias <name> : <reference>). Das alias Schlüsselwort erlaubt uns eine Eigenschaft eines anderen Elements oder ein Element selbst weiterzuleiten, d.h. hier verwendbar zu machen. Wir werden diese Technik später nutzen, wenn wir Komponenten definiteren, die innere Eigenschaften besitzen, die wir aber im Wurzelelement verwenden wollen. Ein property alias braucht keinen Typ, denn es verwendet den Typ der Referenzeigenschaft oder des Referenzelements.

  6. Die text-Eigenschaft hängt hier von der erfundenen Eigenschaft times ab, welche vom Typ int (Ganzzahl) ist. Der int-Wert wird dann automatisch in einen string-Typ konvertiert. Der Ausdruck ist ein Beispiel einer Bindung und hat zur Folge, dass immer automatisch wenn times sich ändert, der Text geändert wird.

  7. Einige Eigenschaften können gruppiert werden. Dieses Feature wird verwendet, wenn eine Eigenschaft strukturiert werden soll und verwandte Eigenschaften gruppiert werden sollen. Gruppen von Eigenschaften kann man auch so schreiben: font { family:  "Ubuntu"; pixelSize: 24 }.

  8. Einige Eigenschaften sind an ein Element geheftetet. Das macht man für globale Elemente, die nur einmalig in der Applikation vorkommen (z.B: Tastatureingaben). Die Syntax dazu ist <Element>.<property>: <value>.

  9. Für jede Eigenschaft kann man signal handler zur Verfügung stellen. Diese Funktion wird dann aufgerufen, sobald die Eigenschaft sich ändert. Hier beispielsweise wird man über die eingebaute Funktion console.log immer dann informiert, wenn sich de Höhe ändert.

Warnung

Eine Element-ID sollte man nur innerhalb seines Dokumens (also der aktuellen Datei) verwenden. QML erlaubt zwar mit dem Mechanismus “dynamic-scoping”, dass später geladene Dokumente die IDs von früher geladenen Dokumenten überschreiben und entspricht so etwas wie globalen Variablen. Leider führt das häufig zu sehr schlecht funktionierendem Code, bei dem der Ausgang von der Reihenfolge der Ausführung abhängt. Daher sollte man eine ID von außerhalb nicht direkt verwenden, sondern besser ein Element, dass man nach außen exporieren will über Eigenschaften des Wurzelelements des aktuellen Dokuments zur Verfügung stellen.

Mit Skripten programmieren

Issues: Create | View

QML und JavaScript (eigentlich offiziell “ECMAScript”) passen bestens zusammen. Im Kapitel JavaScript werden wir zu dieser Symbiose mehr ins Detail gehen. An dieser Stelle wollen wir bloß mal auf diese Beziehung aufmerksam machen.

    Text {
        id: label

        x: 24; y: 24

        // custom counter property for space presses
        property int spacePresses: 0

        text: "Space pressed: " + spacePresses + " times"

        // (1) handler for text changes
        onTextChanged: console.log("text changed to:", text)

        // need focus to receive key events
        focus: true

        // (2) handler with some JS
        Keys.onSpacePressed: {
            increment()
        }

        // clear the text on escape
        Keys.onEscapePressed: {
            label.text = ''
        }

        // (3) a JS function
        function increment() {
            spacePresses = spacePresses + 1
        }
    }
  1. Der Handler, der bei Textänderungen aufgerufen wird, onTextChanged gibt den aktuellen Text jedes Mal dann aus, wenn der Text durch ein Betätigen der Leertaste geändert wurde.

  2. Immer wenn das Textelement die Leertaste empfängt (weil der Benutzer eben die Tastatur bedient hat) rufen wir die JavaScript funktion increment() auf.

  3. Die Definition der JavaScript Funktion in der Form function () { ... }, die unseren Zähler spacePressed erhöht. Jedes Mal wenn spacePressed erhöht wird, werden auch die angebundenen Eigenschaften aktualisiert.

Bemerkung

Der Unterschied zwischen dem QML : (Bindung) und dem JavaScript = (Zuweisung) ist, dass die Bindung ein fester Vertrag ist und über die Laufzeit der Programms so bleibt, während eine JavaScript-Zuweisung eine einmallige Wertzuweisung. Die Bindung endet erst, wenn eine neue Bindung für die Eigenschaft gesetzt wird oder sogar, wenn ein JavaScript-Wert der Eigenschaft zugewiesen wird. Das Setzen eines Tasten-Handlers beispielsweise auf eine leere Zeichenkette würde unser bisheriges Anzeigen des Zählerstandes zerstören:

Keys.onEscapePressed: {
    label.text = ''
}

Nachdem man Escape gedrückt hat, wird die Leertaste die Anzeige nicht mehr aktualisieren, weil die Bindung der text Eigenschaft (text: “Space pressed: “+ spacePresses + “times”) aufgelöst wurde.

Wenn man zwei verschiedene Strategien hat, eine Eigenschaft zu ändern (Textaktualisierung über eine Bindung und Textlöschen über eine JavaScript-Zuweisung) dann kannst du keine Bindungen verwenden. Du musst dann immer Zuweisungen bei beiden Veränderung verwenden, weil durch eine Zuweisung eine Bindung aufgelöst wird (der Vertrag wird gebrochen).

Basiselemente

Issues: Create | View

Elemente kann man in visuelle und nicht-visuelle Elemente gruppieren. Ein visuelles Elemente (wie z.B. Rectangle) hat eine Geometrie und wird üblicherweise auf einem Gebiet des Bildschirms angezeigt. Ein nicht-visuelles Elemente (wie z.B. ein Timer) bringt allgemeine Funktionalität mit sich, die man oft verwendet um die visuellen Elemente zu manipulieren.

Wir fokussieren uns hier mal auf die fundamentalen visuellen Elemente, wie Item, Rectangle, Text, Image und MouseArea.

Das Item Element

Issues: Create | View

Item ist das Basiselement für alle visuellen Elemente weil alle anderen visuellen Elemente von ihm abstammen. Es zeichnet zunächst nichts selbst auf den Schirm, aber es definiert Eigenschaften, die für alle visuellen Elemente gelten:

Gruppe

Eigenschaften

Geometrie

x und y definieren die Position oben-links, width und height stehen für die Ausmaße des Elements und z beschreibt wie die Elemente gestapelt werden und welches vor anderen angezeigt wird.

Layout

anchors (left, right, top, bottom, vertical und horizontal center) werden verwendet um die Elemente relativ zu anderen Elementen mit deren jeweiligen Rändern (margins) zu positionieren

Tastatur

attached Key and KeyNavigation properties to control key handling and the input focus property to enable key handling in the first place

Transformationen

scale und rotate transformieren wie der Name es sagt, generell ist die transform-Eigenschaftenliste für x,y,z beliebige Transformation um deren transformOrigin Punkt zuständig

Sichtbarkeit

opacity verwaltet die Transparenz, visible um Elemente anzuzeigen/zu verbergen, clip um die Zeichentätigkeit auf die Elementmaße einzugrenzen und smooth um die Qualität der Darstellung zu verbessern

Zustandsdefinition

states list property with the supported list of states and the current state property as also the transitions list property to animate state changes.

Um die verschiedenen Eigenschaften besser zu verstehen, werden wir sie im Zusammenhang mit einem Element genauer vorstellen. Merke dir, dass dieses Basiseigenschaften in jedem sichtbaren Element vorhanden sind und überall dort gleich funktionieren.

Bemerkung

Das Item-Element wird oft als Container für andere Elemente verwendet, genau wie das div-Element in HTML.

Das Rectangle Element

Issues: Create | View

Das Rechteckelement Rectangle beerbt und erweitert das Item Element und fügt eine Füllfarbe und Ränder hinzu über border.color and border.width. Für abgerundete Ecken ist die radius Eigenschaft zuständig.

    Rectangle {
        id: rect1
        x: 12; y: 12
        width: 76; height: 96
        color: "lightsteelblue"
    }
    Rectangle {
        id: rect2
        x: 112; y: 12
        width: 76; height: 96
        border.color: "lightsteelblue"
        border.width: 4
        radius: 8
    }
../_images/rectangle2.png

Bemerkung

Gültige Farbwerte sind Farben der SVG Farbnamen (siehe http://www.w3.org/TR/css3-color/#svg-color). Man kann Farben in QML auf verschiedene Weisen produzieren, der übliche Weg ist ein RGB-Farbwert (‘#FF4444’) oder über einen englischen Farbnamen (z.B. ‘white’).

Neben der Füllfarbe und einem Rand unterstützt ein Rechteck noch selbsterstellte Gradienten.

    Rectangle {
        id: rect1
        x: 12; y: 12
        width: 176; height: 96
        gradient: Gradient {
            GradientStop { position: 0.0; color: "lightsteelblue" }
            GradientStop { position: 1.0; color: "slategray" }
        }
        border.color: "slategray"
    }
../_images/rectangle3.png

Ein Gradient wird über eine Folge von Gradientenmarken GradientStop definiert. Jede Marke hat eine Position und eine Farbe. Die Position legt die Marke auf der y-Achse fest (0=oben, 1=unten) und color die Farbe an dieser Position.

Bemerkung

Ein Rechteck mit keiner Höhe/Breite wird nicht sichrbar sein. Das kann oft dann passieren, wenn man mehrere Rechtecksbreiten (-höhen) von einander abhängig definiert hat und irgendetwas in der Logik des Zusammenbaus schief ging.

Bemerkung

Man kann keinen schrägen Gradienten erzeugen. Dann ist es besser, vordefinierte Bilder zu verwenden. Man könnte zwar die Rechtecke mit Gradient drehen, aber dann stimmt die Geometrie des gedrehten Rechtecks wird nicht geändert und wird daher zu Verwirrung führen, weil die Geometrie des Elements dann nicht mehr mit der Geometrie der sichtbaren Fläche zusammenpasst. Der Autor meint, es sei besser, vordesignte Gradientenbilder zu verwenden.

Das Text Element

Issues: Create | View

Um Text anzuzeigen, ist das Text Element geeignet. Seine wichtigste Eigenschaft ist die text-Eigenschaft vom Datentyp string. Das Element berechnet seine Breite und Höhe erstmals aus dem anfangs gesetzten Text und der verwendeten Schriftart. Die Schriftart kann mit der Eigenschaftengruppe font beeinflusst werden (e.g. font.family, font.pixelSize, ...). Die Farbe des Textes wird mit der color-Eigenschaft geändert.

    Text {
        text: "The quick brown fox"
        color: "#303030"
        font.family: "Ubuntu"
        font.pixelSize: 28
    }

../_images/text.png

Text kann mit den Eigenschaften horizontalAlignment und verticalAlignment links-,unten-, rechts- oder obenbündig oder zentriert angeordnet werden. Um die Textgestaltung noch zu verbessern kann man style und styleColor-Eigenschaften verwenden, die den Text umrahmt, versunken oder erhöht darstellen. Für einen längeren Text möchte man ihn entweder verkürzt darstellen und Auslassungspunke A very ... long text mit der elide-Eigenschaft entweder auf links, rechts oder mittig festlegen. Alternativ kann man den gesamten Text umgebrochen anzeigen lassen mit Hilfe der wrapMode-Eigenschaft. Diese funktioniert aber nur, wenn die Breite explizit gesetzt wurde:

Text {
    width: 40; height: 120
    text: 'A very long text'
    // '...' shall appear in the middle
    elide: Text.ElideMiddle
    // red sunken text styling
    style: Text.Sunken
    styleColor: '#FF4444'
    // align text to the top
    verticalAlignment: Text.AlignTop
    // only sensible when no elide mode
    // wrapMode: Text.WordWrap
}

Ein Text-Element zeigt nur den angegebenen Text an. Es wird kein Hintergrund gestaltet. Außer dem gerenderten Text ist das Element transparent. Das darunterliegende Design ist verantwortlich einen sinnvollen Hintergrund für das Textelement zu bieten.

Bemerkung

Vorsicht bei der Anfangsbreite (und -höhe) eines Textelements. Sie hängt von der Zeichenkette und der Schriftgestaltung ab. Ein Text-Element ohne gesetzte Breite und ohne Textinhalt wird nicht sichtbar sein, weil die Anfangsbreite auf 0 gesetzt ist.

Bemerkung

Wenn man ein Text-Element in ein Layout einpasst muss man zwischen der Ausrichtung der Schrift innerhalb der Abgrenzungsbox des Text-Elements und der Ausrichtung der Abgrenzung selbst unterscheiden. Für erstere verwendet man die horizontalAlignment und verticalAlignment-Eigenschaften und im letzeren Fall sollte man die Elementgeometrie beeinflussen oder Anker verwenden.

Das Image-Element

Issues: Create | View

Ein Image-Element kann Bilder verschiedener Formate anzeigen (z.B. PNG, JPG, GIF, BMP, WEBP). Für eine vollständige Liste, konsultiere bitte die Qt Dokumentation. Neben der source-Eigenschaft die den URL des Bildes enthalten sollte, gibt es einen Modus fillMode, der das Verhalten bei Größenänderungen festlegt.

    Image {
        x: 12; y: 12
        // width: 72
        // height: 72
        source: "assets/triangle_red.png"
    }
    Image {
        x: 12+64+12; y: 12
        // width: 72
        height: 72/2
        source: "assets/triangle_red.png"
        fillMode: Image.PreserveAspectCrop
        clip: true
    }
../_images/image.png

Bemerkung

Ein URL kann ein lokaler Pfad mit Schrägstrichen (z.B. ”./images/home.png”) oder ein HTTP-Link (z.B. “http://example.org/home.png”).

Bemerkung

Image-Elemente die PreserveAspectCrop verwenden, sollten auch Abschneiden (clipping) verwenden, um zu vermeiden, dass das Bild außerhalb der Grenzen des Elements angezeigt wird. Standardmäßig ist Abschneiden ausgeschalten (clip : false), die muss man mit (clip : true) einschalten. Das gilt im Übrigen für jegliche sichtbare Elemente.

Tipp

Verwendest du C++, dann kannst du deine eigenen Bilder “on the fly” und auch multithreaded erstellen. Verwende dafür den Provider QQmlImageProviderBase.

Das MouseArea Element

Issues: Create | View

Um mit einem der bisherigen Elemente zu interagieren wird man oft ein MouseArea-Element verwenden. Das ist ein rechteckiges unsichtbares Objekt in dem man Maussignale auffangen kann. Dieses maussensitive Gebiet wird oft zusammen mit einem sichtbaren Element verwendet um Befehle auszuführen, wenn der Benutzer mit dem sichtbaren Teil interagieren will.

    Rectangle {
        id: rect1
        x: 12; y: 12
        width: 76; height: 96
        color: "lightsteelblue"
        MouseArea {
            id: area
            width: parent.width
            height: parent.height
            onClicked: rect2.visible = !rect2.visible
        }
    }

    Rectangle {
        id: rect2
        x: 112; y: 12
        width: 76; height: 96
        border.color: "lightsteelblue"
        border.width: 4
        radius: 8
    }
../_images/mousearea1.png
../_images/mousearea2.png

Bemerkung

Ein wichtiger Bestandteil von Qt Quick ist, dass die Verarbeitung von Eingaben von der visuellen Darstellung getrennt ist. Dadurch kann man dem Benutzer ein Element anzeigen, aber das Gebiet der Interaktion kann größer sein.

(Eigene) Komponenten

Issues: Create | View

Eine Komponente ist ein wiederverwendbares Element und QML bietet mehrere Möglichkeiten, diese zu erzeugen. Hier schauen wir uns die einfachste Möglichkeit an - eine Komponente, die über eine Datei erzeugt wird. Dazu wird ein QML Element in einer Datei angelegt und der Datei den Elementnamen gegeben (z.B. Button.qml). Man kann die Komponente dann wie jedes andere Element aus den QtQuick Modulen verwenden, in unserem Fall würde man in seinem Code Button { ... } verwenden.

Lass’ uns beispielsweise ein Rechteck mit einem Textelement und einer “MouseArea”. Das erinnert an einen einfachen Knopf und muss für die Demonstrationszwecke nicht komplizierter sein.

    Rectangle { // our inlined button ui
        id: button
        x: 12; y: 12
        width: 116; height: 26
        color: "lightsteelblue"
        border.color: "slategrey"
        Text {
            anchors.centerIn: parent
            text: "Start"
        }
        MouseArea {
            anchors.fill: parent
            onClicked: {
                status.text = "Button clicked!"
            }
        }
    }

    Text { // text changes when button was clicked
        id: status
        x: 12; y: 76
        width: 116; height: 26
        text: "waiting ..."
        horizontalAlignment: Text.AlignHCenter
    }

Das Userinterface wird so oder ähnlich aussehen. Auf der linken Seite das Userinterface im Anfangszustand, auf der rechten Seite, nachdem die Schaltfläche geklickt wurde.

../_images/button_waiting.png
../_images/button_clicked.png

Unsere Aufgabe ist es jetzt dieses Userinterface der Schaltfläche in eine wiederverwendbare Komponente zu extrahieren. Daher denken wir mal kurz darübernach, welche mögliche API (welche Funktionalität) die Schaltfläche haben sollte. Am besten überlegt man sich, wie jemand anderes deine Schaltfläche verwenden sollte. Hier sind mal meine Ideen:

// minimal API for a button
Button {
    text: "Click Me"
    onClicked: { // do something }
}

Ich würde als Benutzer gerne den Text der Schaltfläche über eine text-Eigenschaft und meinen eigenen Handler für die Klickaktion festlegen können. Außerdem würde ich erwarten, dass die Schaltfläche eine sinnvolle Anfangsgröße besitzt, die ich aber überschreiben kann, z.B. mit width: 240.

Um das zu erreichen erstellen wir eine Datei Button.qml und kopieren unser Schaltflächen-UserInterface hinein. Zusätzlich müssen wir noch die Eigenschaften exportieren, die ein Benutzer auf unterster Ebene eben ändern können möchte.

// Button.qml

import QtQuick 2.5

Rectangle {
    id: root
    // export button properties
    property alias text: label.text
    signal clicked

    width: 116; height: 26
    color: "lightsteelblue"
    border.color: "slategrey"

    Text {
        id: label
        anchors.centerIn: parent
        text: "Start"
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            root.clicked()
        }
    }
}

Wir haben jetzt den Text exporiert und eine Klick-Signal auf Wurzelebene. Typischerweise bennen wir unser Wurzelelement “root” um es mit dem Bezug auf unsere Kompontente einfacher zu haben. Wir verwenden dazu das alias Feature von QML als ein Weg die Eigenschaften von innerhalb des verschachtelten QML-Elements auf Wurzelebene zu haben und für die Außenwelt verfügbar zu machen, denn nur auf Eigenschaften auf Wurzelebene der Komponente können von außen zugegriffen werden.

Um unsere neue Schaltfläche als Element verwenden zu können, müssen wir es einfach nur in unserer Datei deklarieren. Auf diese Weise ist unsere ursprüngliches Beispiel ein bisschen übersichtlicher geworden.

    Button { // our Button component
        id: button
        x: 12; y: 12
        text: "Start"
        onClicked: {
            status.text = "Button clicked!"
        }
    }

    Text { // text changes when button was clicked
        id: status
        x: 12; y: 76
        width: 116; height: 26
        text: "waiting ..."
        horizontalAlignment: Text.AlignHCenter
    }

Jetzt kann man so viele Schaltflächen wie man will im Userinterface verwenden indem man einfach Button { ... } verwendet. Eine echte Schaltfläche würde etwas komplexer sein, z.B. etwas rückmelden wenn sie angeklickt wurde oder etwas schöner dekoriert sein.

Bemerkung

Ich finde es noch besser einen Schritt weiter zu gehen und ein Item-Element als das Wurzelelement zu definieren. Das gibt uns mehr Kontrolle über die exportierten Funktionen (die API der Komponente) und verhindert z.B. dass die Farbe unserer Schaltfläche verändert werden kann. Das Ziel sollte es sein, einen kleinstmöglichen Funktionsumfang zu exportieren. Das bedeutet ganz praktisch, das Rectangle-Element in ein Item-Element zu packen.


Item {
    id: root
    width: 116; height: 26

    property alias text: label.text
    signal clicked

    Rectangle {
        anchors.fill parent
        color: "lightsteelblue"
        border.color: "slategrey"
    }
    ...
}

Mit Hilfe dieses Verfahrens ist es sehr einfach, eine eigene Serie von wiederverwendbaren Komponenten herzustellen.

Simple Transformations

Issues: Create | View

A transformation manipulates the geometry of an object. QML Items can in general be translated, rotated and scaled. There is a simple form of these operations and a more advanced way.

Let’s start with the simple transformations. Here is our scene as our starting point.

A simple translation is done via changing the x,y position. A rotation is done using the rotation property. The value is provided in degrees (0 .. 360). A scaling is done using the scale property and a value <1 means the element is scaled down and >1 means the element is scaled up. The rotation and scaling does not change your geometry. The items x,y and width/height haven’t changed. Just the painting instructions are transformed.

Before we show off the example I would like to introduce a little helper: The ClickableImage element. The ClickableImage is just an image with a mouse area. This brings up a useful rule of thumb - if you have copied a chunk of code three times, extract it into a component.

// ClickableImage.qml

// Simple image which can be clicked

import QtQuick 2.5

Image {
    id: root
    signal clicked

    MouseArea {
        anchors.fill: parent
        onClicked: root.clicked()
    }
}
../_images/objects.png

We use our clickable image to present three objects (box, circle, triangle). Each object performs a simple transformation when clicked. Clicking the background will reset the scene.

// transformation.qml


import QtQuick 2.5

Item {
    // set width based on given background
    width: bg.width
    height: bg.height

    Image { // nice background image
        id: bg
        source: "assets/background.png"
    }

    MouseArea {
        id: backgroundClicker
        // needs to be before the images as order matters
        // otherwise this mousearea would be before the other elements
        // and consume the mouse events
        anchors.fill: parent
        onClicked: {
            // reset our little scene
            circle.x = 84
            box.rotation = 0
            triangle.rotation = 0
            triangle.scale = 1.0
        }
    }

    ClickableImage {
        id: circle
        x: 84; y: 68
        source: "assets/circle_blue.png"
        antialiasing: true
        onClicked: {
            // increase the x-position on click
            x += 20
        }
    }

    ClickableImage {
        id: box
        x: 164; y: 68
        source: "assets/box_green.png"
        antialiasing: true
        onClicked: {
            // increase the rotation on click
            rotation += 15
        }
    }

    ClickableImage {
        id: triangle
        x: 248; y: 68
        source: "assets/triangle_red.png"
        antialiasing: true
        onClicked: {
            // several transformations
            rotation += 15
            scale += 0.05
        }
    }

    function _test_transformed() {
        circle.x += 20
        box.rotation = 15
        triangle.scale = 1.2
        triangle.rotation = -15
    }

    function _test_overlap() {
        circle.x += 40
        box.rotation = 15
        triangle.scale = 2.0
        triangle.rotation = 45
    }

}
../_images/objects_transformed.png

The circle increments the x-position on each click and the box will rotate on each click. The triangle will rotate and scale the image down on each click, to demonstrate a combined transformation. For the scaling and rotation operation we set antialiasing: true to enable anti-aliasing, which is switched off (same as the clipping property clip) for performance reasons. In your own work, when you see some rasterized edges in your graphics, then you should probably switch smooth on.

Bemerkung

To achieve better visual quality when scaling images it is recommended to scale images down instead of up. Scaling an image up with a larger scaling factor will result into scaling artifacts (blurred image). When scaling an image you should consider using antialiasing : true to enable the usage of a higher quality filter.

The background MouseArea covers the whole background and resets the object values.

Bemerkung

Elements which appear earlier in the code have a lower stacking order (called z-order). If you click long enough on circle you will see it moves below box. The z-order can also be manipulated by the z-property of an Item.

../_images/objects_overlap.png

This is because box appears later in the code. The same applies also to mouse areas. A mouse area later in the code will overlap (and thus grab the mouse events) of a mouse area earlier in the code.

Please remember: The order of elements in the document matters.

Positionierungselemente

Issues: Create | View

Es gibt eine Reihe von QML Elementen, die man zur Positionierung von anderen Elementen verwendet. Diese nennt man “positioner” und die folgenden Row, Column, Grid und Flow sind in QtQuick enthalten. In der nächsten Illustration kann man deren Wirkung an der Darstellung desselben Inhalts erkennen.

Bemerkung

Bevor wir ins Detail gehen, lass mich ein paar Hilfselemente einführen. Die roten, blauen, grünen, helleren und dunkleren Quadrate. Jedes dieser Komponenten enthält ein 48x48 Pixel großes gefärbtes Rechteck. Als Referenz hier der Quellcode für den RedSquare:

// RedSquare.qml

import QtQuick 2.5

Rectangle {
    width: 48
    height: 48
    color: "#ea7025"
    border.color: Qt.lighter(color)
}

Bitte beachte die Verwendung von Qt.lighter(color) um einen helleren, aber ansonsten gleichfarbigen Rahmen zu erstellen. Wir werden diese Hilfselemente in den kommenden Kapitel verwenden um den Quellcode kompakter und hoffentlich lesbarer zu machen. Bitte beachte, dass jedes Rechteck zunächst 48x48 Pixel groß ist.

Das Column-Element arrangiert die Kindelemente vertikal übereinander in einer Spalte. Die Eigenschaft spacing setzt die Abstände der Kindelemente untereinander.

../_images/column.png
// column.qml

import QtQuick 2.5

DarkSquare {
    id: root
    width: 120
    height: 240

    Column {
        id: row
        anchors.centerIn: parent
        spacing: 8
        RedSquare { }
        GreenSquare { width: 96 }
        BlueSquare { }
    }
}

Das Row-Element setzt die Kindelemente horizontal nebeneinander entweder von links nach rechts oder umgekehrt, abhängig von der Eigenschaft layoutDirection. Wieder wird spacing für die Abstände verwendet.

../_images/row.png
// row.qml

import QtQuick 2.5

BrightSquare {
    id: root
    width: 400; height: 120

    Row {
        id: row
        anchors.centerIn: parent
        spacing: 20
        BlueSquare { }
        GreenSquare { }
        RedSquare { }
    }
}

Das Grid-Element arrangiert die Kiner in einer Tabelle. Über die Eigenschaften rows und columns können Zeilen- und Spaltenanzahlen eingeschränkt werden. Wenn eines der Eigenschaften nicht gesetzt wird, wird es jeweils von der Anzahl der Kindelemente errechnet. Setzt man die Zeilenanzahl beispielsweise auf 3 und fügt 6 Kindelemente hinzu, werden daraus 2 Spalten. Die Eigenschaften flow und layoutDirection werden verwendet um die Reihenfolge der hinzugefügten Elemente zu steuern, während spacing die Größe der Zwischenabstände steuert.

../_images/grid.png
// grid.qml

import QtQuick 2.5

BrightSquare {
    id: root
    width: 160
    height: 160

    Grid {
        id: grid
        rows: 2
        columns: 2
        anchors.centerIn: parent
        spacing: 8
        RedSquare { }
        RedSquare { }
        RedSquare { }
        RedSquare { }
    }

}

Der letzte Positionierer ist Flow. Es fügt Kindelemente in einem Fluss hinzu. Die Richtung des Flusses wird über flow und layoutDirection gesteuert. Dies kann seitlich oder von oben nach unten erfolgen. Es kann auch von links nach rechts oder umgekehrt erfolgen. Während Elemente im Fluss hinzugefügt werden, werden sie umgebrochen um neue Zeilen oder Spalten zu erzeugen, je nachdem was gebraucht wird. Damit ein Fluss funktionieren kann benötigt es eine Breite oder eine Höhe. Dies kann entweder direkt oder über Anker-Layouts gesetzt werden.

../_images/flow.png
// flow.qml

import QtQuick 2.5

BrightSquare {
    id: root
    width: 160
    height: 160

    Flow {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 20
        RedSquare { }
        BlueSquare { }
        GreenSquare { }
    }
}

Ein Element, das oft in Verbindung mit den Positionierern verwendet wird ist Repeater. Das funktioniert wie eine for-Schleife und iteriert über ein “Datenmodell”. Im einfachsten Fall ist das Modell einfach ein Wert, das die Anzahl der Schleifenwiederholungen angibt.

../_images/repeater.png
// repeater.qml

import QtQuick 2.5

DarkSquare {
    id: root
    width: 252
    height: 252
    property variant colorArray: ["#00bde3", "#67c111", "#ea7025"]


    Grid{
        anchors.fill: parent
        anchors.margins: 8
        spacing: 4
        Repeater {
            model: 16
            Rectangle {
                width: 56; height: 56
                property int colorIndex: Math.floor(Math.random()*3)
                color: root.colorArray[colorIndex]
                border.color: Qt.lighter(color)
                Text {
                    anchors.centerIn: parent
                    color: "#f0f0f0"
                    text: "Cell " + index
                }
            }
        }
    }
}

In diesem Repeater-Beispiel verwenden wir etwas Neumagisches. Wir definieren unsere eigene Farbeigenschaft, die wir als ein Feld von Farben verwenden. Der Repeater erzeugt eine Reihe von Rechtecken (16, wie vom Modell vorgegeben). In jeder Schleife erstellt er ein Rechteck, wie es im Kindelement (eigentlich: im Delegat) des Repeaters vordefiniert wird. Im Rechteck wählen wir die Farbe über die JS Mathematik Funktionen Math.floor(Math.random()*3). Dies ergibt eine Zahl im Bereich von 0..2, welche wir dann verwenden um eine Farbe aus unserem Farbfeld auszuwählen. Wie früher schon erklärt ist JavaScript Kernbestandteil von Qt Quick, daher sind dessen Standardbibliotheken auch für uns verfügbar.

Ein Repeater fügt die index-Eigenschaft hinzu. Diese Eigenschaft enthält den aktuellen Schleifendurchlauf (0,1,..15). Wir können diesen Index für unsere Entscheidungen verwenden oder in unserem Fall einfach nur um ihn mit Hilfe des Text-Elementes darzustellen.

Bemerkung

Wie man größere Modelle und dynamische Ansichten mit dynamischen Delegaten verwendet wird in einem eigenen Modell-Ansicht-Delegaten Kapitel behandelt. Repeaters werden am besten verwendet, wenn man eine kleine Menge von unveränderlichen Daten präsentieren muss.

Layout Elemente

Issues: Create | View

Zu tun

do we need to remove all uses of anchors earlier?

QML bietet eine flexible Möglichkeit Elemente mit Hilfe von Fixpunkten (anchor = Anker) festzulegen. Das Konzept der Fixpunkte ist Teil der Basiseigenschaften eines Item-Elements und für alle sichtbaren QML Elemente verfügbar. Ein Fixpunkt wirkt wie ein Vertrag und ist stärker als andere konkurrierende Geometrieänderungen. Fixpunkte sind Ausdrücke der relativen Beziehungen, man braucht immer ein Element an das man etwas fixiert.

../_images/anchors.png

Ein Element hat 6 hauptsächliche fixierende Achsen (“top”, “bottom”, “left”, “right”, “horizontalCenter”, “verticalCenter”). Zusätzlich gibt es eine Achse (“baseline”) für Text in Text-Elementen. Jede der fixierenden Achesn hat einen offset. Im Fall von “top”, “bottom”, “left” und “right” werden sie “margins” genannt. Für “horizontalCenter”, “verticalCenter” und “baseline” werden sie “offsets” genannt.

../_images/anchorgrid.png
  1. Ein Element füllt ein Elternelement aus.

            GreenSquare {
                BlueSquare {
                    width: 12
                    anchors.fill: parent
                    anchors.margins: 8
                    text: '(1)'
                }
            }
    
  2. in Element wird linksbündig am Elternelement ausgerichtet.

            GreenSquare {
                BlueSquare {
                    width: 48
                    y: 8
                    anchors.left: parent.left
                    anchors.leftMargin: 8
                    text: '(2)'
                }
            }
    
  3. Eines Elementes linke Seite wird an des Elternelements rechter Seite ausgerichtet.

            GreenSquare {
                BlueSquare {
                    width: 48
                    anchors.left: parent.right
                    text: '(3)'
                }
            }
    
  4. Mittig ausgerichtete Elemente. blue1 wird horizontal bezüglich des Elternelements zentriert. blue2 wird auch horizontal zentriert aber bezüglich blue1 und die Oberseite von blue2 wird an der Unterseite von blue1 ausgerichtet.

            GreenSquare {
                BlueSquare {
                    id: blue1
                    width: 48; height: 24
                    y: 8
                    anchors.horizontalCenter: parent.horizontalCenter
                }
                BlueSquare {
                    id: blue2
                    width: 72; height: 24
                    anchors.top: blue1.bottom
                    anchors.topMargin: 4
                    anchors.horizontalCenter: blue1.horizontalCenter
                    text: '(4)'
                }
            }
    
  5. Ein Element wird zentriert bezüglich des Elternelements.

            GreenSquare {
                BlueSquare {
                    width: 48
                    anchors.centerIn: parent
                    text: '(5)'
                }
            }
    
  6. Ein Element wird zentriert mit einem linken offset bezüglich des Elternelements und verwendet dabei die horizontalen und vertikalen Achsen.

            GreenSquare {
                BlueSquare {
                    width: 48
                    anchors.horizontalCenter: parent.horizontalCenter
                    anchors.horizontalCenterOffset: -12
                    anchors.verticalCenter: parent.verticalCenter
                    text: '(6)'
                }
            }
    

Bemerkung

Unsere Quadrate wurden erweitert um sie ziehbar zu machen. Teste die Beispiele aus und ziehe ein paar Quadrate herum. Du wirst sehen, dass (1) nicht gezogen werden kann, weil es auf allen Seiten fixiert wurde, dagegen kannst du natürlich das Elternelement von (1) ziehen, weil es überhaupt nicht verankert wurde. (2) kann man vertikal ziehen weil nur die linke Seite verankert wurde. Ähnliches gilt für (3). (4) kann nur vertikal gezogen werde weil beide Quadrate horizontal zentriert wurden. (5) ist mittig bezüglich des Elternelements fixiert und kann als solches nicht verschoben werden, ähnliches gilt für (6). Ziehen eines Elements bedeutet, die x,y Position verändern zu wollen. Weil Verankern stärker ist als geometrische Änderungen, wie x,y-Änderungen, ist das Ziehen durch die fixierenden Achsen eingeschränkt möglich. Wir werden diesen Effekt später sehen, wenn wir Animationen diskutieren.

Input Elements

Issues: Create | View

We have already used the MouseArea as a mouse input element. Next, we’ll focus on keyboard input. We start off with the text editing elements: TextInput and TextEdit.

TextInput

Issues: Create | View

The TextInput allows the user to enter a line of text. The element supports input constraints such as validator, inputMask, and echoMode.

// textinput.qml

import QtQuick 2.5

Rectangle {
    width: 200
    height: 80
    color: "linen"

    TextInput {
        id: input1
        x: 8; y: 8
        width: 96; height: 20
        focus: true
        text: "Text Input 1"
    }

    TextInput {
        id: input2
        x: 8; y: 36
        width: 96; height: 20
        text: "Text Input 2"
    }
}
../_images/textinput.png

The user can click inside a TextInput to change the focus. To support switching the focus by keyboard, we can use the KeyNavigation attached property.

// textinput2.qml

import QtQuick 2.5

Rectangle {
    width: 200
    height: 80
    color: "linen"

    TextInput {
        id: input1
        x: 8; y: 8
        width: 96; height: 20
        focus: true
        text: "Text Input 1"
        KeyNavigation.tab: input2
    }

    TextInput {
        id: input2
        x: 8; y: 36
        width: 96; height: 20
        text: "Text Input 2"
        KeyNavigation.tab: input1
    }
}

The KeyNavigation attached property supports a preset of navigation keys where an element id is bound to switch focus on the given key press.

A text input element comes with no visual presentation besides a blinking cursor and the entered text. For the user to be able to recognize the element as an input element it needs some visual decoration, for example a simple rectangle. When placing the TextInput inside an element you need make sure you export the major properties you want others be able to access.

We move this piece of code into our own component called TLineEditV1 for reuse.

// TLineEditV1.qml

import QtQuick 2.5

Rectangle {
    width: 96; height: input.height + 8
    color: "lightsteelblue"
    border.color: "gray"

    property alias text: input.text
    property alias input: input

    TextInput {
        id: input
        anchors.fill: parent
        anchors.margins: 4
        focus: true
    }
}

Bemerkung

If you want to export the TextInput completely, you can export the element by using property alias input: input. The first input is the property name, where the 2nd input is the element id.

We rewrite our KeyNavigation example with the new TLineEditV1 component.

Rectangle {
    ...
    TLineEditV1 {
        id: input1
        ...
    }
    TLineEditV1 {
        id: input2
        ...
    }
}
../_images/textinput3.png

And try the tab key for navigation. You will experience the focus does not change to input2. The simple use of focus:true is not sufficient. The problem arises, that the focus was transferred to the input2 element the top-level item inside the TlineEditV1 (our Rectangle) received focus and did not forward the focus to the TextInput. To prevent this QML offers the FocusScope.

FocusScope

Issues: Create | View

A focus scope declares that the last child element with focus:true receives the focus if the focus scope receives the focus. So it’s forward the focus to the last focus requesting child element. We will create a 2nd version of our TLineEdit component called TLineEditV2 using the focus scope as root element.

// TLineEditV2.qml

import QtQuick 2.5

FocusScope {
    width: 96; height: input.height + 8
    Rectangle {
        anchors.fill: parent
        color: "lightsteelblue"
        border.color: "gray"

    }

    property alias text: input.text
    property alias input: input

    TextInput {
        id: input
        anchors.fill: parent
        anchors.margins: 4
        focus: true
    }
}

Our example will now look like this:

Rectangle {
    ...
    TLineEditV2 {
        id: input1
        ...
    }
    TLineEditV2 {
        id: input2
        ...
    }
}

Pressing the tab key now successfully switches the focus between the 2 components and the correct child element inside the component is focused.

TextEdit

Issues: Create | View

The TextEdit is very similar to TextInput and support a multi-line text edit field. It doesn’t have the text constraint properties as this depends on querying the painted size of the text (paintedHeight, paintedWidth). We also create our own component called TTextEdit to provide a edit background and use the focus scope for better focus forwarding.

// TTextEdit.qml

import QtQuick 2.5

FocusScope {
    width: 96; height: 96
    Rectangle {
        anchors.fill: parent
        color: "lightsteelblue"
        border.color: "gray"

    }

    property alias text: input.text
    property alias input: input

    TextEdit {
        id: input
        anchors.fill: parent
        anchors.margins: 4
        focus: true
    }
}

You can use it like the TLineEdit component

// textedit.qml

import QtQuick 2.5

Rectangle {
    width: 136
    height: 120
    color: "linen"

    TTextEdit {
        id: input
        x: 8; y: 8
        width: 120; height: 104
        focus: true
        text: "Text Edit"
    }
}
../_images/textedit.png

Keys Element

Issues: Create | View

The attached property Keys allows executing code based on certain key presses. For example to move a square around and scale we can hook into the up, down, left and right keys to translate the element and the plus, minus key to scale the element.

// keys.qml

import QtQuick 2.5

DarkSquare {
    width: 400; height: 200

    GreenSquare {
        id: square
        x: 8; y: 8
    }
    focus: true
    Keys.onLeftPressed: square.x -= 8
    Keys.onRightPressed: square.x += 8
    Keys.onUpPressed: square.y -= 8
    Keys.onDownPressed: square.y += 8
    Keys.onPressed: {
        switch(event.key) {
            case Qt.Key_Plus:
                square.scale += 0.2
                break;
            case Qt.Key_Minus:
                square.scale -= 0.2
                break;
        }

    }
}
../_images/keys.png

Advanced Techniques

Issues: Create | View

Zu tun

To be written