Autor des Abschnitts: jryannel
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.
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.
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.
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:
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).
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.
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
.
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.
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.
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.
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 }
.
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>
.
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.
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
}
}
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.
Immer wenn das Textelement die Leertaste empfängt (weil der Benutzer eben die Tastatur bedient hat) rufen wir die JavaScript funktion increment()
auf.
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).
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
.
Item
Element¶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 |
|
Layout |
|
Tastatur |
attached Key and KeyNavigation properties to control key handling and the input focus property to enable key handling in the first place |
Transformationen |
|
Sichtbarkeit |
|
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.
Rectangle
Element¶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
}
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"
}
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.
Text
Element¶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
}
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.
Image
-Element¶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
}
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.
MouseArea
Element¶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
}
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.
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.
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.
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()
}
}
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
}
}
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.
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.
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.
// 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.
// 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.
// 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.
// 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.
// 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.
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.
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.
Ein Element füllt ein Elternelement aus.
GreenSquare { BlueSquare { width: 12 anchors.fill: parent anchors.margins: 8 text: '(1)' } }
in Element wird linksbündig am Elternelement ausgerichtet.
GreenSquare { BlueSquare { width: 48 y: 8 anchors.left: parent.left anchors.leftMargin: 8 text: '(2)' } }
Eines Elementes linke Seite wird an des Elternelements rechter Seite ausgerichtet.
GreenSquare { BlueSquare { width: 48 anchors.left: parent.right text: '(3)' } }
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)' } }
Ein Element wird zentriert bezüglich des Elternelements.
GreenSquare { BlueSquare { width: 48 anchors.centerIn: parent text: '(5)' } }
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.
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
.
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"
}
}
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
...
}
}
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.
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.
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"
}
}
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;
}
}
}