Guida introduttiva alla programmazione con QML

Benvenuto nel mondo di QML, il linguaggio dichiarativo per interfacce utente (UI: User Interface). In questa guida introduttiva, creeremo un semplice editor di testo utilizzando QML. Dopo la lettura di questa guida, dovresti essere in grado di sviluppare le tue applicazioni utilizzando QML e Qt C++.

QML per la costruzione di interfacce utente

L’applicazione che stiamo costruendo è un semplice editor di testo per caricare, salvare ed modificare del testo. Questa guida sarà composta di due parti. La prima parte prevede la progettazione della disposizione (layout) e dei comportamenti dei componenti dell’applicazione, tramite il linguaggio dichiarativo QML. Nella seconda parte procederemo allo sviluppo delle componenti per il caricamento e il salvataggio dei file utilizzando Qt C++. Tramite l’utilizzo del Qt Meta-Object System, siamo in grado di esporre le funzioni C++ come proprietà che gli elementi QML potranno utilizzare in modo nativo. In questo modo, utilizzando QML e Qt C++, possiamo separare, in modo molto efficace, la logica di interfaccia dalla logica dell’applicazione.

http://doc.qt.nokia.com/4.7/images/qml-texteditor5_editmenu.png

Per eseguire il codice QML dell’esempio, è sufficente passare al programma qmlviewer [doc.qt.nokia.com] il nome del file QML come argomento. La parte C++ di questa esercitazione presuppone che il lettore possieda una conoscenza di base delle procedure di compilazione di Qt.

Capitoli dell’esercitazione:

  1. Definire un pulsante e un menu
  2. Implementare una barra dei menu
  3. Costruire un editor di testo
  4. Decorare l’editor di testo
  5. Estendere QML usando Qt C++

Definire un Pulsante e un Menu

Componenti di Base – un Pulsante

Iniziamo il nostro editor di testo con la costruzione di un pulsante. Funzionalmente, un pulsante è costituito da una zona sensibile ad azioni esterne (del mouse o di un’interfaccia touch screen per esempio) e da una etichetta. I pulsanti eseguono delle azioni ogniqualvolta l’utente preme il pulsante.

In QML, l’elemento visuale di base è il rettangolo (Rectangle [doc.qt.nokia.com]). L’elemento Rectangle ha delle proprietà che permettono di controllare l’aspetto dell’elemento e la sua posizione.

  1.   import Qt 4.7
  2.  
  3.   Rectangle{
  4.     id:simplebutton
  5.          
  6.     color: "grey"
  7.     width: 400; height: 400
  8.  
  9.     Text {
  10.         id: buttonLabel
  11.         text: "button label"
  12.         anchors.centerIn:parent
  13.     }
  14.   }

Per prima cosa, nel codice, l’istruzione import Qt 4.7, permette al programma qmlviewer di importare gli elementi QML che useremo in seguito. Questa prima istruzione deve esistere in ogni file QML. Si noti che la versione dei moduli Qt è parte integrante della dichiarazione di importazione.

Questo semplice rettangolo possiede un identificatore univoco, simplebutton, che è associato alla proprietà id. Le proprietà dell’elemento Rectangle sono collegate ai relativi valori in questo modo: elencando il nome della proprietà, seguito da due punti, quindi il suo valore. Nel codice di esempio, il colore grey è legato alla proprietà color del rettangolo. Nello stesso modo colleghiamo le proprietà width (larghezza) e height (altezza) del rettangolo.

L’elemento Text è un campo di testo non modificabile. Chiamiamo buttonLabel questo elemento Text. Per impostare il contenuto della stringa del campo di testo, associamo un valore alla proprietà text. E’ utile far notare che l’elemento QML Text e la sua proprietà text sono entità ben distinte. L’etichetta è contenuta all’interno del rettangolo e, al fine di centrarla nel mezzo, colleghiamo gerarchicamente le anchors (ancore) dell’elemento Text al suo genitore, che nel nostro caso si chiama simplebutton. Ogni ancora può essere collegata alle ancore di altri elementi, in questo modo si semplifica la definizione della disposizione reciproca degli elementi.

Salviamo questo codice come SimpleButton.qml. Se lanciamo il programma qmlviewer con il nome del file come argomento, vedremo visualizzato il rettangolo grigio con una etichetta di testo.

http://doc.qt.nokia.com/4.7/images/qml-texteditor1_simplebutton.png

Per implementare la funzionalità collegata al click sul pulsante, utilizziamo la gestione degli eventi di QML. La gestione degli eventi di QML è molto simile al meccanismo signal e slot di Qt [doc.qt.nokia.com]. Ogni qualvolta viene invocato una signal, viene chiamato il corrispondente slot collegato.

  1. Rectangle{
  2.      id:simplebutton
  3.      ...
  4.  
  5.      MouseArea{
  6.          id: buttonMouseArea
  7.  
  8.          anchors.fill: parent //anchor all sides of the mouse area to the rectangle's anchors
  9.                  //onClicked handles valid mouse button clicks
  10.          onClicked: console.log(buttonLabel.text + " clicked" )
  11.      }
  12. }

Aggiungiamo un elemento MouseArea nel nostro componente simpleButton. Gli elementi che compongono la nostra MouseArea descrivono l’area interattiva in cui vengono rilevati i movimenti del mouse. Nel nostro caso, per il nostro pulsante, ‘ancoriamo’ l’intera MouseArea al suo genitore, che è simpleButton. La sintassi anchors.fill è un modo di esprimere l’accesso, nel nostro caso, ad una proprietà specifica denominata fill all’interno di un gruppo di proprietà denominato anchors. QML utilizza layout basati su ancore, in cui ciascun elemento può ancorarsi ad un altro elemento, in questo modo si creano layout robusti e flessibili.

L’elemento MouseArea prevede molti gestori di segnali (signal handler) che vengono chiamati durante i movimenti del mouse all’interno dei suoi confini specificati. Uno di questi è onClicked ed è chiamato ogni volta che il previsto pulsante del mouse viene cliccato, nel nostro caso il click sinistro è l’azione di default. Possiamo legare delle azioni al gestore dell’evento onClicked. Nel nostro esempio, la chiamata a console.log () emette una stringa di testo ogni volta che l’area attiva viene cliccata. La funzione console.log è un strumento molto utile per il debugging e, in generale, per l’output di testo dell’applicazione.

Il codice in SimpleButton.qml è già sufficiente per visualizzare un pulsante sullo schermo e produrre del testo, ogni volta che lo si clicca con il mouse.

  1. Rectangle {
  2.      id:Button
  3.      ...
  4.  
  5.      property color buttonColor: "lightblue"
  6.      property color onHoverColor: "gold"
  7.      property color borderColor: "white"
  8.  
  9.      signal buttonClick()
  10.      onButtonClick: {
  11.          console.log(buttonLabel.text + " clicked" )
  12.      }
  13.  
  14.      MouseArea{
  15.          onClicked: buttonClick()
  16.          hoverEnabled: true
  17.          onEntered: parent.border.color = onHoverColor
  18.          onExited:  parent.border.color = borderColor
  19.      }
  20.  
  21.      //determines the color of the button by using the conditional operator
  22.      color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor
  23. }

Nel file Button.qml abbiamo il codice per un pulsante completamente funzionante. I frammenti di codice riportati in questo articolo, hanno alcune parti omesse, indicate con dei puntini di sospensione, perché o erano già stati introdotti in precedenza o perché non rilevanti per la discussione di questo codice specifico.

Per dichiarare delle proprietà personalizzate, si utilizza una sintassi del tipo proprietà tipo nome. Nel nostro codice viene dichiarata la proprietà buttonColor, di tipo color, e collegata al valore “lightblue”. La proprietà buttonColor è poi utilizzata in un’operazione condizionale per determinare il colore di riempimento dei pulsanti. Si noti che l’assegnazione del valore della proprietà è possibile sia con il segno di uguale _, sia con l’operatore due punti _:. Le proprietà personalizzate permettono di rendere accessibili elementi interni dell’elemento Rectangle, anche dall’esterno del suo campo di applicazione (scope). Ci sono diversi tipi QML di base come int, string, real, così come un tipo generico chiamato variant.

Collegando i gestori di segnale onEntered e onExited ai colori, è possibile rendere il bordo del pulsante giallo quando il mouse passa sopra il pulsante e ripristinare il colore precedente quando il mouse lascia la sua zona sensibile (MouseArea).

Viene dichiarato un signal buttonClick(), inserendo la keyword signal davanti al nome della funzione. Per ogni signal vengono creati automaticamente le relative funzioni di gestione, i loro nomi iniziano con il prefisso on. Di conseguenza, la funzione onButtonClick è il gestore (handler) di buttonClick. Succesivamente a onButtonClick viene quindi assegnata l’operazione da eseguire. Nel nostro pulsante di esempio, il gestore di mouse onClicked chiama semplicemente onButtonClick, che visualizza un testo. La funzione onButtonClick permette ad oggetti esterni di accedere facilmente alla mouse area del pulsante. Inoltre, gli oggetti possono avere più di una dichiarazione di MouseArea e avere un signal ButtonClick può migliorare la distinzione tra i diversi gestori di segnali della MouseArea.

Adesso abbiamo le conoscenze di base che ci permettono l’implementazione di elementi in QML in grado di gestire i principali movimenti del mouse. Abbiamo creato una etichetta di tipo Text all’interno di un elemento Rectangle, personalizzato le sue proprietà e implementato dei comportamenti che rispondono ai movimenti del mouse. Questa idea di base della creazione di elementi all’interno di altri elementi, si ripresenterà nello sviluppo di tutta questa applicazione di esempio.

Questo pulsante non è molto utile se non viene utilizzato come componente per eseguire qualche azione. Nella sezione successiva, vedremo come creare un menu che utilizza alcuni di questi pulsanti.

http://doc.qt.nokia.com/4.7/images/qml-texteditor1_button.png

Creazione di un Menu

Fino ad ora, abbiamo spiegato come creare degli elementi e definirne i comportamenti all’interno di un singolo file QML. In questa sezione ci occuperemo di come importare elementi QML e come riutilizzare alcuni componenti già realizzati per costruire altri componenti.

Un Menu permette di visualizzare il contenuto di una lista, ogni elemento ha la capacità di eseguire un’azione. In QML, possiamo ottenere un menu in diversi modi. Per prima cosa, creiamo un menu contenente dei pulsanti cui faremo eseguire opportune azioni. Il codice del menu è nel file FileMenu.qml.

  1.  import Qt 4.7                      \\import the main Qt QML module
  2.  import "folderName"            \\import the contents of the folder
  3.  import "script.js" as Script    \\import a Javascript file and name it as Script

La sintassi riportata sopra mostra alcuni utilizzi della keyword import. Questo è necessario per poter utilizzare dei file JavaScript [developer.mozilla.org], o dei file QML che non sono presenti all’interno della directory corrente. Poiché Button.qml è nella stessa directory di FileMenu.qml, non abbiamo la necessità di importare esplicitamente il file Button.qml per poterlo usare. Possiamo creare direttamente un elemento Button di tipo Button{}, allo stesso modo di un Rectangle{}.

  1.  In FileMenu.qml:
  2.  
  3.      Row{
  4.          anchors.centerIn: parent
  5.          spacing: parent.width/6
  6.  
  7.          Button{
  8.              id: loadButton
  9.              buttonColor: "lightgrey"
  10.              label: "Load"
  11.          }
  12.          Button{
  13.              buttonColor: "grey"
  14.              id: saveButton
  15.              label: "Save"
  16.          }
  17.          Button{
  18.              id: exitButton
  19.              label: "Exit"
  20.              buttonColor: "darkgrey"
  21.  
  22.              onButtonClick: Qt.quit()
  23.          }
  24.      }

Nel FileMenu.qml, sono dichiariati tre elementi Button. Questi sono dichiarati all’interno di un elemento di tipo Row, un elemento di posizionamento che allinea i suoi figli lungo una fila verticale. La dichiarazione di Button risiede nel file Button.qml, che è lo stesso file che abbiamo usato per la sezione precedente. E’ possibile definire dei nuovi collegamenti per le proprietà dei pulsanti appena creati, in questo modo si sovrascrivono le proprietà impostate in Button.qml. Il pulsante denominato ExitButton, quando viene cliccato, fa in modo che la finestra dell’applicazione venga abbandonata e chiusa. E’ bene tenere presente che il gestore di segnale onButtonClick nel file Button.qml viene chiamato in aggiunta al gestore onButtonClick di ExitButton.

http://doc.qt.nokia.com/4.7/images/qml-texteditor1_filemenu.png

L’elemeto Row è dichiarato all’interno di un Rectangle, creando un contenitore rettangolare per la fila dei pulsanti. Questo rettangolo aggiuntivo permette di avere un modo indiretto di organizzare la fila dei pulsanti all’interno del menu.

La dichiarazione del menu Modifica (Edit) è molto simile a quanto già visto finora. Il menu è composto da pulsanti che hanno le etichette: Copy (Copia), Paste (Incolla) e Select All (Seleziona tutto).

http://doc.qt.nokia.com/4.7/images/qml-texteditor1_editmenu.png

Grazie alla conoscenza di importazione e personalizzazione dei componenti acquisita finora, possiamo ora combinare queste pagine di menu per creare una barra dei menu, composta da pulsanti per selezionare i menu specifici e quindi passare a vedere come possiamo strutturare i dati utilizzando QML.

Implementare una barra dei menu

Il nostro editor di testo potrà visualizzare i menu disponibili attraverso una barra dei menu. La barra dei menu proporrà i diversi menu e l’utente sceglierà quali menu visualizzare. La funzionalità di cambio dei menu implica che i questi avranno bisogno di una struttura più sofisticata di quella che si limita a mostrarli su di una riga. QML utilizza modelli e viste per gestire e visualizzare i dati strutturati.

Utilizzare modelli dati e viste

QML dispone di diverse viste [doc.qt.nokia.com] (data views) che visualizzano i modelli dati [doc.qt.nokia.com] (data models). La nostra barra dei menu visualizza i menu in una lista, con una testata che mostra una fila di nomi di menu. L’elenco dei menu è dichiarata all’interno di un VisualItemModel. L’elemento “VisualItemModel”:http://doc.qt.nokia.com/4.7/qml-visualitemmodel.html contiene elementi che dispongono già di viste come elementi Rectangle e altri elementi d’interfaccia. Altri tipi di modelli, come l’elemento ListModel [doc.qt.nokia.com], hanno bisogno di un elemento delegato (delegate) per visualizzare i propri dati.

In questo caso sono dichiarati due elementi visuali in menuListModel, il FileMenu e l’EditMenu, e sono visualizzati e personalizzati tramite un controllo ListView [doc.qt.nokia.com]. Il file MenuBar.qml contiene le dichiarazioni QML e un semplice menu modifica è definito in EditMenu.qml.

  1.      VisualItemModel{
  2.          id: menuListModel
  3.          FileMenu{
  4.              width: menuListView.width
  5.              height: menuBar.height
  6.              color: fileColor
  7.          }
  8.          EditMenu{
  9.              color: editColor
  10.              width:  menuListView.width
  11.              height: menuBar.height
  12.          }
  13.      }

L’elemento ListView [doc.qt.nokia.com] visualizza un modello attraverso un elemento delegato. Il delegato può dichiarare che gli elementi del modello siano visualizzati in un elemento di tipo Row o siano posti in una griglia. Nel nostro caso menuListModel ha già gli elementi visibili, quindi, non abbiamo bisogno di dichiarare un delegato.

  1.      ListView{
  2.          id: menuListView
  3.  
  4.          //Anchors are set to react to window anchors
  5.          anchors.fill:parent
  6.          anchors.bottom: parent.bottom
  7.          width:parent.width
  8.          height: parent.height
  9.  
  10.          //the model contains the data
  11.          model: menuListModel
  12.  
  13.          //control the movement of the menu switching
  14.          snapMode: ListView.SnapOneItem
  15.          orientation: ListView.Horizontal
  16.          boundsBehavior: Flickable.StopAtBounds
  17.          flickDeceleration: 5000
  18.          highlightFollowsCurrentItem: true
  19.          highlightMoveDuration:240
  20.          highlightRangeMode: ListView.StrictlyEnforceRange
  21.      }

Inoltre, ListView deriva da Flickable [doc.qt.nokia.com], facendo così in modo l’elenco risponda al trascinamento con il mouse (drag) e altri eventi (gestures). L’ultima parte del codice di cui sopra imposta le proprietà di Flickable per ottenere il movimento desiderato sfogliando la nostra vista. Nello specifico, la proprietà highlightMoveDuration cambia la durata della transizione nello sfoglio. Un valore più alto di highlightMoveDuration causa un passaggio più lento tra i menu.

La ListView getisce gli elementi del modello attraverso un index (indice) e ogni elemento visual nel modello è accessibile tramite questo indice, secondo l’ordine di dichiarazione. Modificare il currentIndex ha come effetto quello di cambiare l’elemento evidenziato nella ListView. L’intestazione nella nostra barra dei menu esemplifica questo effetto. Ci sono due pulsanti nella riga, entrambi cambiano il menu corrente quando vengono cliccati. Il pulsante fileButton cambia il menu corrente nel menu file quando viene cliccato, l’indice in questo caso è 0, perché FileMenu è dichiarato per primo nella menuListModel. Analogamente, il pulsante editButton, quando viene cliccato, cambierà il menù corrente nel menu EditMenu.

Il rettangolo labelList ha valore di z di 1, questo indica che è visualizzato davanti alla barra dei menu. Gli elementi con valori di z maggiori sono visualizzati davanti a oggetti con valori più bassi di z. Il valore di default di z è 0.

  1.      Rectangle{
  2.          id: labelList
  3.          ...
  4.          z: 1
  5.          Row{
  6.              anchors.centerIn: parent
  7.              spacing:40
  8.              Button{
  9.                  label: "File"
  10.                  id: fileButton
  11.                  ...
  12.                  onButtonClick: menuListView.currentIndex = 0
  13.              }
  14.              Button{
  15.                  id: editButton
  16.                  label: "Edit"
  17.                  ...
  18.                  onButtonClick:    menuListView.currentIndex = 1
  19.              }
  20.          }
  21.      }

La barra dei menu che abbiamo appena creato può essere sfogliata per accedere ai menu oppure si può cliccare sui nomi dei menu nella barra in alto. La commutazione delle schermate dei menu è intuitiva e immediata per l’utente.

http://doc.qt.nokia.com/4.7/images/qml-texteditor2_menubar.png

Costruire un Editor di testo

Dichiarare una TextArea

Il nostro editor di testo non può essere un editor di testo finché non contiene una zona di testo modificabile. L’elemento TextEdit di QML permette proprio la dichiarazione di un componente per la modifica di testo multi-linea. TextEdit è diverso da un elemento di tipo Text, che non consente all’utente di modificare direttamente il testo.

  1.      TextEdit{
  2.          id: textEditor
  3.          anchors.fill:parent
  4.          width:parent.width; height:parent.height
  5.          color:"midnightblue"
  6.          focus: true
  7.  
  8.          wrapMode: TextEdit.Wrap
  9.  
  10.          onCursorRectangleChanged: flickArea.ensureVisible(cursorRectangle)
  11.      }

Nell’editor sono impostate le proprietà color del proprio font e del testo a capo (wrap). L’area di TextEdit è all’interno di un area mobile che farà scorrere il testo, se il cursore del testo è al di fuori dell’area visibile. La funzione EnsureVisible() controlla se il rettangolo del cursore si trova al di fuori dei confini visibili e, nel caso, sposta opportunamente l’area di testo. QML utilizza la sintassi Javascript per i suoi script e, come accennato in precedenza, i file Javascript possono essere importati e utilizzati all’interno di un file QML.

  1.      function ensureVisible(r){
  2.          if (contentX >= r.x)
  3.              contentX = r.x;
  4.          else if (contentX+width <= r.x+r.width)
  5.              contentX = r.x+r.width-width;
  6.          if (contentY >= r.y)
  7.              contentY = r.y;
  8.          else if (contentY+height <= r.y+r.height)
  9.              contentY = r.y+r.height-height;
  10.      }

Combinare componenti per l’editor di testo

Ora siamo pronti per creare il layout del nostro editor di testo utilizzando QML. L’editor di testo ha due componenti, la barra dei menu che abbiamo creato e l’area di testo. QML ci permette di riutilizzare i componenti, rendendo quindi il nostro codice più semplice, mediante l’importazione di componenti e la personalizzazione quando ciò sia necessario. Il nostro editor di testo ha la propria finestra divisa in due parti: un terzo dello schermo è dedicata alla barra dei menu e i rimanenti due terzi della schermata al testo. La barra dei menu viene visualizzata davanti a ogni altro elemento.

  1.      Rectangle{
  2.  
  3.          id: screen
  4.          width: 1000; height: 1000
  5.  
  6.          //the screen is partitioned into the MenuBar and TextArea. 1/3 of the screen is assigned to the MenuBar
  7.          property int partition: height/3
  8.  
  9.          MenuBar{
  10.              id:menuBar
  11.              height: partition
  12.              width:parent.width
  13.              z: 1
  14.          }
  15.  
  16.          TextArea{
  17.              id:textArea
  18.              anchors.bottom:parent.bottom
  19.              y: partition
  20.              color: "white"
  21.              height: partition*2
  22.              width:parent.width
  23.          }
  24.      }

Tramite ‘importazione di componenti riutilizzabili, il codice del nostro TextEditor appare molto più semplice. Si può quindi personalizzare l’applicazione principale, senza preoccuparsi delle proprietà che hanno già un comportamenti definito. Usando questo approccio, i layout delle applicazioni e i componenti di interfaccia utente possono essere assemblati più facilmente.

http://doc.qt.nokia.com/4.7/images/qml-texteditor3_texteditor.png

Abbellire l’editor di testo

Implementare una interfaccia a scomparsa

Il nostro editor di testo appare semplice e abbiamo bisogno di abbellirlo. Utilizzando QML, possiamo dichiarare delle transizioni e animare la nostra applicazione. La nostra barra dei menu occupa un terzo dello schermo, sarebbe bello fare in modo che appaia solo quando lo vogliamo.

A tale scopo possiamo aggiungere un elemento di interfaccia (drawer) che si occuperà di contrarre o espandere la barra dei menu quando viene cliccata. Nella nostra caso, abbiamo un piccolo rettangolo che risponde ai clic del mouse. Questo elemento, così come l’applicazione, ha due stati: lo stato “drawer aperto” e lo stato “drawer chiuso”. L’elemento drawer è una sottile striscia rettangolare con all’interno un elemento Imagine che definisce un’icona di una freccia centrata all’interno dell’area. Il drawer assegna uno stato all’intera applicazione, attraverso l’identificatore screen, volta che un utente clicca sulla mouse area.

  1.      Rectangle{
  2.          id:drawer
  3.          height:15
  4.  
  5.          Image{
  6.              id: arrowIcon
  7.              source: "images/arrow.png"
  8.              anchors.horizontalCenter: parent.horizontalCenter
  9.          }
  10.  
  11.          MouseArea{
  12.              id: drawerMouseArea
  13.              anchors.fill:parent
  14.              onClicked:{
  15.                  if (screen.state == "DRAWER_CLOSED"){
  16.                      screen.state = "DRAWER_OPEN"
  17.                  }
  18.                  else if (screen.state == "DRAWER_OPEN"){
  19.                      screen.state = "DRAWER_CLOSED"
  20.                  }
  21.              }
  22.              ...
  23.          }
  24.      }

Uno stato è semplicemente un insieme di configurazioni predefinite e si dichiara tramite un elemento di tipo State. Un lista di stati possono essere collegati alla proprietà states e successivamente iterati. Nella nostra applicazione, i due stati sono chiamati DRAWER_CLOSED e DRAWER_OPEN. Tramite i PropertyChanges sono definite le nuove configurazioni degli elementi. Nello stato DRAWER_OPEN, ci sono quattro elementi che riceveranno delle modifiche alle proprietà. Il primo, menuBar, cambierà la sua proprietà y in 0. Analogamente, la textArea si abbasserà in una nuova posizione quando lo stato è DRAWER_OPEN. Anche la textarea, il drawer, e la sua icona subiranno dei cambiamento delle loro proprietà per adeguarsi allo stato corrente.

  1.      states:[
  2.          State {
  3.              name: "DRAWER_OPEN"
  4.              PropertyChanges { target: menuBar; y: 0}
  5.              PropertyChanges { target: textArea; y: partition + drawer.height}
  6.              PropertyChanges { target: drawer; y: partition}
  7.              PropertyChanges { target: arrowIcon; rotation: 180}
  8.          },
  9.          State {
  10.              name: "DRAWER_CLOSED"
  11.              PropertyChanges { target: menuBar; y:-height; }
  12.              PropertyChanges { target: textArea; y: drawer.height; height: screen.height - drawer.height }
  13.              PropertyChanges { target: drawer; y: 0 }
  14.              PropertyChanges { target: arrowIcon; rotation: 0 }
  15.          }
  16.      ]

I cambiamenti di stato sono in genere repentini e c’è l’esigenza di smorzare le transizioni. Le transizioni tra gli stati sono definite utilizzando l’elemento Transition, che può essere associato alla proprietà transitions di un elemento. Nel nostro editor di testo avviene una transizione di stato ogni volta che il suo stato diventa o DRAWER_OPEN o DRAWER_CLOSED. È importante sottolineare che una transizione ha di solito bisogno di definire uno stato from e uno to, ma nel nostro caso, possiamo usare il simbolo jolly (wild card) * per indicare che la transizione si applica a tutti i cambiamenti di stato.

Durante le transizioni, possiamo assegnare delle animazioni ai cambiamenti di proprietà. La nostra menuBar cambia di posizione da y: 0 a Y:-partition ed è possibile animare questa transizione utilizzando l’elemento NumberAnimation. Possiamo definire che le proprietà degli elementi interessati, si animeranno per un certo periodo di tempo e con una certa curva di andamento (easing curve). Una curva di andamento controlla la frequenza di animazione e il comportamento dell’interpolazione durante le transizioni di stato. La curva di andamento che abbiamo scelto è Easing.OutQuint, che rallenta il movimento verso la fine dell’animazione. Potete approfondire l’argomento nell’articolo QML’s Animation [doc.qt.nokia.com].

  1.      transitions: [
  2.          Transition {
  3.              to: "*"
  4.              NumberAnimation { target: textArea; properties: "y, height"; duration: 100; easing.type:Easing.OutExpo }
  5.              NumberAnimation { target: menuBar; properties: "y"; duration: 100; easing.type: Easing.OutExpo }
  6.              NumberAnimation { target: drawer; properties: "y"; duration: 100; easing.type: Easing.OutExpo }
  7.          }
  8.      ]

Un altro modo per animare il cambiamento dei valori delle proprietà è dichiarare un elemento Behavior. Una transizione funziona solo durante i cambiamenti di stato e un elemento Behavior può impostare un’animazione per un modifica generica della proprietà. Nell’editor di testo, l’icona freccia utilizza una NumberAnimation per animare la sua proprietà rotation (rotazione) ad ogni cambiamento.

  1.  In TextEditor.qml:
  2.  
  3.      Behavior{
  4.          NumberAnimation{property: "rotation";easing.type: Easing.OutExpo }
  5.      }

Tornando ai nostri componenti, tramite l’utilizzo di stati e animazioni, possiamo migliorare l’aspetto di questi elementi. In Button.qml, potremmo aggiungere delle modifiche alle proprietà color e scale quando il pulsante viene premuto. I tipi colore sono animati utilizzando ColorAnimation [doc.qt.nokia.com] ed i numeri sono animati utilizzando NumberAnimation [doc.qt.nokia.com]. La sintassi on propertyName visualizzata più sotto è comoda quando si agisce su di una singola proprietà.

  1.  In Button.qml:
  2.      ...
  3.  
  4.      color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor
  5.      Behavior on color { ColorAnimation{ duration: 55} }
  6.  
  7.      scale: buttonMouseArea.pressed ? 1.1 : 1.00
  8.      Behavior on scale { NumberAnimation{ duration: 55} }

Oltre a questo, possiamo intervenire sull’aspetto dei nostri componenti QML con l’aggiunta di effetti di colore, come sfumature ed effetti di trasparenza. Dichiarando un elemento Gradient [doc.qt.nokia.com] si sovrascrive la proprietà color dell’elemento. Si può quindi dichiarare il colore della sfumatura utilizzando l’elemento GradientStop [doc.qt.nokia.com]. La sfumatura è valorrizata utilizzando una scala tra 0,0 e 1,0.

  1.  In MenuBar.qml
  2.      gradient: Gradient {
  3.          GradientStop { position: 0.0; color: "#8C8F8C" }
  4.          GradientStop { position: 0.17; color: "#6A6D6A" }
  5.          GradientStop { position: 0.98;color: "#3F3F3F" }
  6.          GradientStop { position: 1.0; color: "#0e1B20" }
  7.      }

Questo gradiente è utilizzato dalla barra dei menu per visualizzare un gradiente che simula la profondità. Il primo colore è utilizzato al valore 0,0 e l’ultimo a 1.0.

Prossimi passi

Abbiamo così finito di costruire l’interfaccia utente di un semplice editor di testo. Andando avanti, completata l’interfaccia utente, siamo in grado di implementare la logica dell’applicazione con Qt e C++. Come abbiamo visto QML funziona bene come strumento di prototipazione, separando efficacemente la logica applicativa dalla progettazione dell’interfaccia utente.

Estendere QML usando Qt C++

Ora che abbiamo il layout del nostro editor di testo, possiamo ora implementare le funzionalità dell’editor in C++. L’utilizzo di QML con C++ permette di realizzare la logica della nostra applicazione utilizzando Qt. Siamo in grado di creare un context QML in una applicazione C++ usando le classi dichiarative di Qt [doc.qt.nokia.com] e visualizzare gli elementi QML tramite una Graphic Scene [doc.qt.nokia.com]. In alternativa, possiamo esportare il nostro codice C++ compilato in un plugin che il programma qmlviewer è in grado di leggere. Per la nostra applicazione, implementeremo le funzioni in C++ per caricare e salvare i file e le esporteremo come plugin. In questo modo, abbiamo solo bisogno di lanciare il file QML direttamente anziché eseguire un file eseguibile.

Esportare classi C++ per QML

Vogliamo implementare le funzioni di caricamento e salvataggio file utilizzando Qt e C++. Le classi e le funzioni C++ possono essere utilizzate in QML previa una loro registrazione. La classe ha inoltre bisogno di essere compilata come un plugin Qt e il file QML avrà bisogno di sapere dove si trova il plugin nel file system.

Per la nostra applicazione, abbiamo bisogno di creare i seguenti oggetti:

  1. una classe Directory che consenta di gestire le operazioni sulle directory (cartelle)
  2. una classe File che sia un QObject, che simuli l’elenco dei file in una directory
  3. una classe plugin che registri la classe nel corretto contesto QML
  4. un file di progetto (project file) Qt per compilare il plugin
  5. in file qmldir per indicare al programma qmlviewer dove trovare il plugin

Costruire un pugin Qt

Per costruire un plugin, è necessario impostare le direttive che seguono in project file Qt. Innanzi tutto vanno specificati i file contenenti i sorgenti e i file di include, nonché i moduli Qt utilizzati. Tutti i file sorgenti C++ e di progetto si trovano nella directory FileDialog.

  1.  In cppPlugins.pro:
  2.  
  3.      TEMPLATE = lib
  4.      CONFIG += qt plugin
  5.      QT += declarative
  6.  
  7.      DESTDIR +=  ../plugins
  8.      OBJECTS_DIR = tmp
  9.      MOC_DIR = tmp
  10.  
  11.      TARGET = FileDialog
  12.  
  13.      HEADERS +=     directory.h \
  14.              file.h \
  15.              dialogPlugin.h
  16.  
  17.      SOURCES +=    directory.cpp \
  18.              file.cpp \
  19.              dialogPlugin.cpp

Nello specifico, compiliamo con il modulo Qt declarative e lo configuriamo come un plugin, che necessita di un modello (template) di tipo lib. Dovremmo poi mettere il plugin compilato nella directory plugins del genitore della directory corrente.

Registrare una classe in QML

  1.  In dialogPlugin.h:
  2.  
  3.      #include <QtDeclarative/QDeclarativeExtensionPlugin>
  4.  
  5.      class DialogPlugin : public QDeclarativeExtensionPlugin
  6.      {
  7.          Q_OBJECT
  8.  
  9.          public:
  10.          void registerTypes(const char *uri);
  11.  
  12.      };

La nostra classe plugin, DialogPlugin è una sottoclasse di QDeclarativeExtensionPlugin [doc.qt.nokia.com]. Questa prevede che dobbiamo implementare la funzione ereditata, registerTypes(). Il file dialogPlugin.cpp assomiglia a questo:

  1.  DialogPlugin.cpp:
  2.  
  3.      #include "dialogPlugin.h"
  4.      #include "directory.h"
  5.      #include "file.h"
  6.      #include <QtDeclarative/qdeclarative.h>
  7.  
  8.      void DialogPlugin::registerTypes(const char *uri){
  9.  
  10.          qmlRegisterType<Directory>(uri, 1, 0, "Directory");
  11.          qmlRegisterType<File>(uri, 1, 0,"File");
  12.      }
  13.  
  14.      Q_EXPORT_PLUGIN2(FileDialog, DialogPlugin);

La funzione registerTypes() [doc.qt.nokia.com] registra le nostre classi File e Directory in QML. Questa funzione ha bisogno dell nome della classe per il suo template, un numero di versione principale, un numero di versione minore, e un nome per le nostre classi.

Abbiamo poi bisogno di esportare il plugin utilizzando la macro Q_EXPORT_PLUGIN2 [doc.qt.nokia.com]. Si noti che nel file dialogPlugin.h, abbiamo la macro Q_OBJECT in cima alla nostra classe. Inoltre, abbiamo bisogno di eseguire qmake sul file di progetto per generare il necessario codice meta-object.

Creare proprietà QML in una classe C++

Possiamo ora creare elementi e proprietà QML utilizzando C++ e il Qt Meta-Object System [doc.qt.nokia.com]. Siamo in grado di implementare le proprietà utilizzando gli slot e le signal, rendendole note anche a Qt. Queste proprietà possono poi essere utilizzate direttamente in QML.

Per l’editor di testo, dobbiamo essere in grado di caricare e salvare file. Normalmente, queste funzionalità sono contenute in una finestra di dialogo. Per fortuna, possiamo usare QDir [doc.qt.nokia.com], QFile [doc.qt.nokia.com] e QTextStream [doc.qt.nokia.com] per implementare la lettura di directory e gli stream di input/output.

  1.      class Directory : public QObject{
  2.  
  3.          Q_OBJECT
  4.  
  5.          Q_PROPERTY(int filesCount READ filesCount CONSTANT)
  6.          Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged)
  7.          Q_PROPERTY(QString fileContent READ fileContent WRITE setFileContent NOTIFY fileContentChanged)
  8.          Q_PROPERTY(QDeclarativeListProperty<File> files READ files CONSTANT )
  9.  
  10.          ...

La classe Directory utilizza il Meta-Object System di Qt per registrare le proprietà di cui ha bisogno per realizzare la gestione dei file. La classe Directory è esportata come un plugin ed è utilizzabile in QML come l’elemento Directory. Ciascuna delle proprietà definite utilizzando la macro Q_PROPERTY [doc.qt.nokia.com] è anche una proprietà QML.

La macro Q_PROPERTY [doc.qt.nokia.com] dichiara al Meta-Object System di Qt sia la proprietà, che le sue funzioni di lettura e scrittura. Ad esempio, la proprietà filename, di tipo QString [doc.qt.nokia.com], è leggibile con la funzione filename() e scrivibile utilizzando la funzione setFilename(). Inoltre, vi è una signal associato alla proprietà filename chiamata filenameChanged(), che viene emesso ogni volta che la proprietà cambia. Le funzioni di lettura e scrittura sono dichiarate come public nel file header (.h).

Analogamente, abbiamo le altre proprietà dichiarate secondo il loro utilizzo. La proprietà filesCount indica il numero di file in una directory. La proprietà filename è impostata con il nome del file attualmente selezionato ed il contenuto del file salvato. o caricato, viene memorizzato nella proprietà fileContent.

  1.      Q_PROPERTY(QDeclarativeListProperty<File> files READ files CONSTANT )

La lista di proprietà files è un elenco di tutti i file, filtrati, presenti in una directory. La classe Directory è implementato in modo da filtrare i file di testo non validi; solo i file con estensione .txt sono validi. Inoltre, una QList [doc.qt.nokia.com] può essere utilizzata nei file QML dichiarandola come QDeclarativeListProperty [doc.qt.nokia.com] in C++. L’oggetto deve ereditare da un QObject [doc.qt.nokia.com], quindi, la classe File deve anche ereditare da QObject [doc.qt.nokia.com]. Nella classe Directory, l’elenco di oggetti di tipo File è memorizzato in una QList [doc.qt.nokia.com] chiamata m_fileList.

  1.      class File : public QObject{
  2.  
  3.          Q_OBJECT
  4.          Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
  5.  
  6.          ...
  7.      };

Le proprietà possono quindi essere utilizzate direttamente in QML come parte integrante delle proprietà dell’elemento Directory. Si noti che non abbiamo dovuto creare un proprietà identificatore id nel nostro codice C++.

  1.      Directory{
  2.          id: directory
  3.  
  4.          filesCount
  5.          filename
  6.          fileContent
  7.          files
  8.  
  9.          files[0].name
  10.      }

Poiché QML utilizza la sintassi e la struttura di Javascript, siamo in grado di iterare l’elenco dei file e recuperare le sue proprietà. Per recuperare la proprietà nome del primo file, possiamo usare

  1. files[0].name

Anche le normali funzioni C++ sono accessibili da QML. Le funzioni di caricamento e di salvataggio dei file sono implementate in C++ e dichiarate utilizzando la macro Q_INVOKABLE [doc.qt.nokia.com]. In alternativa, possiamo dichiarare le funzioni come uno slot e renderle in questo modo accessibili da QML.

  1.  In Directory.h:
  2.  
  3.      Q_INVOKABLE void saveFile();
  4.      Q_INVOKABLE void loadFile();

La classe Directory deve anche notificare altri oggetti ogni volta che c’è un cambiamento nel contenuto della directory. Questa funzione viene eseguita utilizzando una signal. Come accennato in precedenza, le signal QML hanno un gestore associato che ha lo stesso nome preceduto dal prefisso on. La signal è denominato directoryChanged e viene emesso ogni volta che c’è un aggiornamento della directory. L’aggiornamento ricarica semplicemente il contenuto della directory e aggiorna l’elenco dei file validi della directory. Gli elementi QML possono poi essere notificati implementand il gestore di signal onDirectoryChanged.

Le proprietà list ha bisogno di essere ulteriormente esplorata. Questo perché le proprietà di tipo list utilizzano delle funzioni di callback per accedere e modificare il contenuto dell’elenco. La proprietà list è di tipo QDeclarativeListProperty<File> . Ogni volta che si accede alla lista, la funzione di accesso (accessor) deve restituire una oggetto di tipo QDeclarativeListProperty<File>. Il tipo del template, File, deve essere un derivato di QObject. Inoltre, per creare la QDeclarativeListProperty [doc.qt.nokia.com], le funzioni di accesso e di modifica (modifier) devono essere passati al costruttore della classe come puntatori a funzione. La lista, una QList nel nostro caso, deve anche essere una lista di puntatori a File.

p.Il costruttore di QDeclarativeListProperty [doc.qt.nokia.com] e l’implementazione in Directory:

  1.      QDeclarativeListProperty  ( QObject * object, void * data, AppendFunction append, CountFunction count = 0, AtFunction at = 0, ClearFunction clear = 0 )
  2.      QDeclarativeListProperty<File>( this, &m_fileList, &appendFiles, &filesSize, &fileAt,  &clearFilesPtr );

Il costruttore passa i puntatori alle funzioni che implementano rispettivamente: aggiungi alla lista, enumera la lista, recupera l’oggetto utilizzando un indice, e svuota la lista. Solo la funzione append è obbligatoria. Si noti che i puntatori a funzione devono corrispondere alla definizione di AppendFunction [doc.qt.nokia.com], CountFunction [doc.qt.nokia.com], AtFunction [doc.qt.nokia.com], e ClearFunction [doc.qt.nokia.com].

  1.      void appendFiles(QDeclarativeListProperty<File> * property, File * file)
  2.      File* fileAt(QDeclarativeListProperty<File> * property, int index)
  3.      int filesSize(QDeclarativeListProperty<File> * property)
  4.      void clearFilesPtr(QDeclarativeListProperty<File> *property)

Per semplificare la nostra finestra di dialogo file, la classe Directory filtra i file di testo non validi, che sono file che non hanno l’estensione .txt. Se un nome di file non ha l’estensione .txt, allora non sarà visibile nella nostra finestra di dialogo file. Inoltre, l’applicazione assicura che i file salvati abbiano l’estensione .txt nel nome del file. La classe Directory utilizza QTextStream [doc.qt.nokia.com] per leggere e scrivere il contenuto del file.

Tramite il nostro elemento Directory, è possibile recuperare tutti i file come una lista, sapere quanti sono i file di tipo testo nella directory dell’applicazione, ottenere il nome del file corrente e il suo contenuto come stringa, ed essere informati quando ci sono cambiamenti nel contenuto delle directory.

Per costruire il plugin, bisogna eseguire qmake con il file di progetto cppPlugins.pro, quindi lanciare make fare per compilare e trasferire il plugin nella directory plugins.

Importare un plugin in QML

Il programma qmlviewer importa i file che si trovano nella stessa directory dell’applicazione. Possiamo anche creare un file qmldir contenente la posizione dei file QML che vogliamo importare. Il file qmldir può anche memorizzare le posizioni di plugin e altre risorse.

  1.  In qmldir:
  2.  
  3.      Button ./Button.qml
  4.      FileDialog ./FileDialog.qml
  5.      TextArea ./TextArea.qml
  6.      TextEditor ./TextEditor.qml
  7.      EditMenu ./EditMenu.qml
  8.  
  9.      plugin FileDialog plugins

Il plugin che abbiamo appena creato si chiama FileDialog, come indicato dal campo TARGET nel file di progetto. Il plugin è compilato e copiato nella directory plugins.

Integrare una finestra di dialogo nel menu File

Il nostro FileMenu ha bisogno di visualizzare l’elemento FileDialog, contenente un elenco dei file di testo in una directory consentendo così all’utente di selezionare un file facendo clic sulla lista. Abbiamo anche bisogno di collegare i pulsanti per salvare, caricare e creare nuovo, alle loro rispettive azioni. Il FileMenu contiene anche un campo di input di testo per consentire all’utente di digitare un nome di file utilizzando la tastiera.

L’elemento Directory viene utilizzato nel file FileMenu.qml e avvisa l’elemento FileDialog quando la directory ha aggiornato il suo contenuto. Questa notifica viene eseguita nel gestore del segnale, onDirectoryChanged.

  1.  In FileMenu.qml:
  2.  
  3.      Directory{
  4.          id:directory
  5.          filename: textInput.text
  6.          onDirectoryChanged: fileDialog.notifyRefresh()
  7.      }

Allineandola con la semplicità della nostra applicazione, la finestra di dialogo file è sempre visibile e non visualizza file di testo non validi, che non hanno una estensione .txt nei loro nomi.

  1.  In FileDialog.qml:
  2.  
  3.      signal notifyRefresh()
  4.      onNotifyRefresh: dirView.model = directory.file

L’elemento FileDialog visualizza il contenuto di una directory leggendo la lista di proprietà chiamata files. I file vengono utilizzati come modello di un elemento GridView [doc.qt.nokia.com], che visualizza gli elementi dati in una griglia su indicazione di un componente delegato. Il delegato gestisce l’aspetto del modello e la nostra finestra di dialogo file utilizza semplicemente una griglia con il testo centrato nel mezzo. Cliccando sul nome del file compare un rettangolo per evidenziare il nome del file. Il FileDialog viene notificato ogni volta che la signal notifyRefresh viene emessa, ricaricando così i file nella directory.

  1.  In FileMenu.qml:
  2.  
  3.      Button{
  4.          id: newButton
  5.          label: "New"
  6.          onButtonClick:{
  7.              textArea.textContent = ""
  8.          }
  9.      }
  10.      Button{
  11.          id: loadButton
  12.          label: "Load"
  13.          onButtonClick:{
  14.              directory.filename = textInput.text
  15.              directory.loadFile()
  16.              textArea.textContent = directory.fileContent
  17.          }
  18.      }
  19.      Button{
  20.          id: saveButton
  21.          label: "Save"
  22.          onButtonClick:{
  23.              directory.fileContent = textArea.textContent
  24.              directory.filename = textInput.text
  25.              directory.saveFile()
  26.          }
  27.      }
  28.      Button{
  29.          id: exitButton
  30.          label: "Exit"
  31.          onButtonClick:{
  32.              Qt.quit()
  33.          }
  34.      }

Il nostro FileMenu può adesso essere connesso alle rispettive azioni. Il pulsante saveButton trasferirà il testo dal TextEdit alla proprietà fileContent della directory, quindi copia il nome del file nel campo di input testo. Infine, il pulsante chiama la funzione savefile(), per salvare il contenuto del file. Il pulsante loadButton ha una comportamento simile. Inoltre, l’azione New svuota il contenuto del TextEdit.

Per finire, i pulsanti EditMenu sono collegate alle funzioni di TextEdit per copiare, incollare e selezionare tutto il testo nell’editor.

http://doc.qt.nokia.com/4.7/images/qml-texteditor5_filemenu.png

L’editor di testo completo

http://doc.qt.nokia.com/4.7/images/qml-texteditor5_newfile.png

L’applicazione può funzionare come un semplice editor di testo, in grado di accettare del testo e salvarlo in un file. L’editor di testo può anche caricare il contenuto da un file ed eseguire manipolazioni del testo.