Introducere în programarea cu QML

Bun venit in lumea QML, limbajul declarativ pentru interfațe utilizator. În aceast ghid, vom crea un editor de texte folosing QML. După parcurgerea acestui ghid, veți fi gata sa dezvoltați propriile aplicații folosind QML și Qt C++.

Construirea interfeței cu utilizatorul folosind QML

Aplicația pe care o construim este un editor de texte care va încarca, salva și realiza manipulări de text. Acest ghid are două părți. Prima parte implică proiectarea aspectului aplicației și a comportamentului său folosind limbajul declarativ QML. În partea a doua, încarcarea si salvarea fișierelor va fi implementată folosing Qt C++. Folosind sistemul Qt Meta-Object [doc.qt.nokia.com], putem expune funcții C++ ca proprietăți pe care elementele QML le pot folosi. Folosind QML și Qt C++, putem decupla logica interfeței grafice de logica aplicației.

Pentru a rula exemplul de cod QML, porniți qmlviewer [doc.qt.nokia.com] oferindu-i fișierul QML drept argument. Porțiunea C++ a acestui tutorial presupune că cititorul are cunoștiințe de bază despre procedurile de compilare Qt.

Definirea unui buton și a unui meniu

Componenta de bază: un buton

Începem editorul de texte prin construirea unui buton. Din punct de vedere funcțional, un buton are o zonă sensibilă la acțiunile mouse-ului și o etichetă. Butoanele execută acțiuni atunci când utilizatorul le apasă.

În QML, elemenul vizual de bază este elementul Rectangle [doc.qt.nokia.com]. Elementul Rectangle are proprietăți care controlează aspectul și locația sa.

În primul rând, import QtQuick 1.0 determină qmlviewer să importe elementele QML ce vor fi folosite mai târziu. Aceasta linie trebuie să existe în orice fișier QML. Observați că versiunea modulelor Qt e inclusă in propoziția import.

Acest simplu dreptunghi are un identificator unic, simplebutton, care este asociat proprietății id. Proprietăților elementului Rectangle le sunt asociate valori prin enunțarea proprietății, urmata de două puncte și apoi de valoarea proprietății. În exemplul de cod, culoarea gri este asociată proprietății color a dreptunghiului. Similar, asociem valori lățimii și înălțimii dreptunghiului.

Elementul Text [doc.qt.nokia.com] este un câmp text needitabil. Numim acest element Text buttonLabel. Pentru a stabili conținutul câmpului Text ca șir de caractere, asociem o valoare proprietății text. Eticheta este inclusa în elementul Rectangle și pentru a o centra în mijlocul acestuia, atribuim ancorele elementului Text părintelui său, numit simplebutton. Ancorele pot fi asociate cu ancorele altor elemente, permițând simplificarea atribuirilor.

Vom salva codul actual ca SimpleButton.qml. Rularea qmlviewer cu fișierul salvat ca argument va afisa dreptunghiul gri cu o etichetă text.

Pentru a implementa funcționalitatea apasării butonului, putem folosi manipularea evenimentelor QML. Manipularea evenimentelor QML este foarte asemanatoare cu mecanismul Qt signal and slot [doc.qt.nokia.com].

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

Includem un element MouseArea [doc.qt.nokia.com] în simplebutton. Elementele MouseArea descriu zona în care mișcările mouse-ului sunt detectate. Pentru butonul nostru, ancorăm întregul MouseArea de părintele său care e simplebutton. Sintaxa anchors.fill este un mod de a accesa o proprietate numită fill, în cadrul grupului de proprietăți numit anchors. QML folosește ancore [doc.qt.nokia.com] pentru a specifica amplasarea elementelor; elementele se ancorează de alte elemente, creînd astfel amplasări robuste.

MouseArea are multe handlere pentru semnale care sunt chemate în timpul mișcărilor mouse-ului în cadrul granițelor sale. Unul dintre ele este onClicked care este chemat atunci când butonul acceptat al mouse-ului este apăsat, butonul acceptat implicit fiind cel stâng. Putem atribui acțiuni handler-ului onClicked. În exemplul nostru, console.log() tipărește text atunci când se dă click pe elementul MouseArea. Funcția console.log() este folositoare pentru depanarea programelor și pentru tipărirea de text.

Codul din SimpleButton.qml e suficient pentru a afișa un buton pe ecran și a tipărește text atunci când este apăsat cu mouse-ul.

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

Un buton complet funcțional este prezentat in Button.qml. Exemplele de cod din acest articol nu conțin anumite părți din cod, care sunt înlocuite de … fie pentru că au fost deja prezentate în secțiunile anterioare sau pentru că nu sunt relevante pentru codul discutat.

Proprietățile custom sunt declarate folosind sintaxa: property type name. În cod, proprietatea buttonColor, de tipul color, este declarată si atribuită valorii “lightblue”. buttonColor e folosită mai târziu într-o operație condițională pentru a determina culoarea cu care butonul este umplut. Observați că atribuirea valorilor unei proprietăți e posibilă folosind semnul =, în afară de atribuirea folosind caracterul :. Proprietățile custom permit ca elementele interne să fie accesibile în afara domeniului elementului Rectangle. Există tipuri QML [doc.qt.nokia.com] de bază, ca int, string, real, precum și un tip numit variant.

Prin atribuirea de culori handlerelor pentru semnalele onEntered și onExited, bordura butonului va deveni galbenă când mouse-ul este deasupra butonului și culoarea va reveni la cea anterioară atunci când mouse-ul iese de pe suprafața butonului.

Un semnal buttonClick() este declarat în Button.qml plasând cuvântul cheie signal în fața numelui semnalului. Toate semnalele au handlere create automat, numele lor începând cu on. Drept consecință, onButtonClick este handlerul pentru buttonClick. Lui onButtonClick îi este atribuită o acțiune. În exemplul nostru, handlerul onClicked va chema onButtonClick, care afișează text. onButtonClick permite obiectelor externe să acceseze ușor zona butonului. De exemplu, elementele pot avea mai mult de o declarație MouseArea si un semnal buttonClick poate face mai bine distincția între mai multe handlere pentru semnalele MouseArea.

Avem acum cunoștiințele de bază pentru a implementa elemente în QML care pot gestiona mișcări simple ale mouse-ului. Am creat o etichetă Text în interiorul unui Rectangle, definit proprietăți custom, și implementat comportamente ca răspuns la mișcările mouse-ului. Idea de a crea elemente în interiorul altor elemente este repetată de-a lungul aplicației editor de texte.

Butonul nu este folositor dacă nu este folosit ca o componentă pentru a îndeplini o acțiune. În secțiunea următoare, vom crea un meniu ce conține mai multe butoane.

Crearea unui Menu Page

Până acum, am văzut cum să cream elemente și să atribuim comportamente acestora în cadrul unui singur fișier QML. În această secțiune, vom arăta cum să importăm elemente QML și cum să reutilizăm componentele deja create pentru a construi alte componente.

Meniurile afișează conținutul unei liste, fiecare element având posibilitatea de a realiza o acțiune. În QML putem crea un meniu în mai multe moduri. Mai întâi vom crea un menu care conține butoane care în final vor realiza diverse acțiuni. Codul pentru meniu se găsește in fișierul FileMenu.qml.

  1. import QtQuick 1.0                \\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

Sintaxa de mai sus arată cum să folosiți cuvântul cheie import. Acest cuvânt este necesar pentru a folosi fișiere Javascript [developer.mozilla.org], sau fișiere QML care nu se află în același director. Deoarece Button.qml e în același director cu FileMenu.qml, nu e nevoie să importăm fișierul Button.qml pentru a-l folosi. Putem crea un buton folosind o declarație Button{}, similar cu o declarație Rectangle{}.

  1. În 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.      }

În FileMenu.qml declarăm trei elemente Button. Ele sunt declarate în interiorul unui element Row [doc.qt.nokia.com], un element care poziționează copii săi de-a lungul unui rând vertical. Declarația elementului Button se găsește în Button.qml, același Button.qml pe care l-am folosit în secțiunea anterioară. Noi atribuiri ale proprietăților pot fi declarate în cadrul butoanelor nou create, ele suprascriu proprietățile din Button.qml. Butonul numit exitButton va închide aplicația și fereastra sa atunci când este apăsat. Observați că handlerul pentru semnal onButtonClick din Button.qml va fi chemat, alături de handlerul onButtonClick din exitButton.

Declarația Row se află în cadrul unui Rectangle, creând un container dreptunghi pentru rândul de butoane. Acest nou dreptunghi crează un mod indirect de a organiza rândul de butoane într-un meniu.

Declarația meniului pentru editare e foarte asemănătoare în această etapă. Meniul are butoane cu etichetele: Copy, Paste și Select All.

Folosind cunoștințele legate de importarea și personalizarea componentelor create anterior, putem combina aceste pagini cu meniuri pentru a crea o bară de meniuri ce constă în butoane pentru a selecta meniul și putem urmări cum sa structurăm date folosind QML.

Implementarea unei bare de meniuri

Editorul de texte are nevoie de o modalitate de a afișa meniuri folosind o bară de meniuri. Aceasta comută între diferite meniuri, iar utilizatorul poate alege care dintre ele e afișat. Pentru a putea comuta între meniuri, acestea trebuie să fie structurate și nu doar afișate la rând. QML folosește modele și vizualizări pentru a structura datele și pentru a le vizualiza.

Folosirea modelelor de date și a vizualizărilor

QML folosește diverse vizualizări de date [doc.qt.nokia.com] pentru a afișa modele de date [doc.qt.nokia.com]. Bara de meniuri va afișa meniurile într-o listă, având un antet pentru a afișa numele meniurilor. Lista cu meniuri e declarată în cadrul unui VisualItemModel. Elementul VisualItemModel [doc.qt.nokia.com] conține itemi care au deja vizualizări, precum elemente Rectangle și elemente UI importate. Alte tipuri de modele precum ListModel [doc.qt.nokia.com] au nevoie de un delegat pentru a-și afișa datele.

Declarăm două elemente vizuale în menuListModel, FileMenu și EditMenu. Personalizăm cele două meniuri și le afișăm folosind un ListView [doc.qt.nokia.com]. Fișierul MenuBar.qml conține declarațiile QML, iar un meniu simplu pentru editare e definit în 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.      }

Elementul ListView [doc.qt.nokia.com] afișează un model conform unui delegat. Delegatul poate declara itemii modelului ce trebuie afișați într-un element Row sau poate afișa itemii într-o grilă. menuListModel are deja elemente vizibile deci nu este nevoie să declarăm un delegat.

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

În plus, ListView moștenește Flickable [doc.qt.nokia.com] ceea ce face ca lista sa raspundă la “trageri” cu mouse-ul și alte gesturi. Ultima parte a codului de mai sus setează proprietăți Flickable pentru a crea mișcarea dorită vizualizării noastre. Mai precis, proprietatea highlightMoveDuration schimbă durata tranziției flick. O valoare mai mare pentru highlightMoveDuration rezultă într-o schimbare mai lentă a meniului.

ListView păstrează itemii modelului prin intermediul unui index și fiecare item vizual al modelului este accesibil prin intermediul indexului, în ordinea declarației. Schimbarea lui currentIndex schimbă practic elementul evidențiat în ListView. Header-ul barei de meniuri exemplifică acest efect. El conține două butoane într-un rând, fiecare din ele schimbând meniul curent când este apăsat. Apăsarea fileButton face ca meniul curent să fie meniul file, indexul fiind 0 deoarece FileMenu e declarat primul în cadrul menuListModel. Similar, apăsarea editButton face ca meniul curent sa fie EditMenu.

Dreptunghiul labelList are valoarea z egala cu 1, ceea ce face ca el să fie afișat în fața barei de meniuri. Elementele cu o valoare z mai mare sunt afișate în fața elementelor cu valoare z mai mica. Implicit, valoarea z este 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.      }

Bara de meniuri creată folosește poate folosi efectul “flick” sau apăsarea pe numele meniurilor pentru a accesa meniurile. Schimbarea meniurilor e intuitivă și răspunde acțiunilor utilizatorului.

Construirea unui editor de texte

Declararea unei zone pentru text

Editorul nostru de texte nu ar fi un editor dacă nu ar conține o zonă editabilă de text. Elementul TextEdit [doc.qt.nokia.com] din QML permite declararea unei zone de text formată din mai multe linii. Elementul TextEdit [doc.qt.nokia.com] e diferit de elementul Text [doc.qt.nokia.com], acesta din urmă nu permite utilizatorului să editeze textul.

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

Editorul are culoarea font-ului setată și este setat sa continue textul pe linia următoare. Zona TextEdit se afla în cadrul unei zone “flickable” care va face scroll textului dacă cursorul este în afara zonei vizibile. Funcția ensureVisible() verifică dacă cursorul este în afara limitelor vizibile caz în care zona de text e mutată corespunzător. QML folosește sintaxa Javascript pentru script-urile sale; așa cum s-a menționat, fișiere Javascript pot fi importate și folosite în cadrul unui fișier 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.      }

Combinarea componentelor pentru editorul de texte

Putem acum să creăm amplasarea elementelor editorului nostru de texte folosind QML. Editorul de texte are două componente, bara de meniuri pe care am creat-o și zona de text. QML ne permite să reutilizăm componente, și deci să simplificăm codul, prin importarea componentelor și personalizarea lor, atunci când aceasta este necesară. Editorul de texte împarte fereastra în două: o treime a ecranului e dedicată barei de meniuri și două treimi afișează zona de text. Bara de meniuri este afișată în fața oricăror alte elemente.

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

Prin importarea componentelor reutilizabile, codul pentru editorul de texte este mult mai simplu. În plus, putem personaliza aplicația principală, fără a ne preocupa de proprietățile care au deja definite comportamente. Folosind această abordare, amplasarea elementelor unei aplicații și a componentelor interfeței grafice pot fi ușor realizate.

Înfrumusețarea editorului de texte

Implementarea interfeței Drawer

Editorul de texte e încă destul de simplu și poate fi îmbunătățit. Folosind QML putem declara tranziții și animații în editor. Bara de meniuri ocupă o treime din ecran și ar fi frumos dacă ar apărea doar la cerere.

Putem adăuga o interfață drawer (sertar) care va compacta sau expanda bara de meniuri la apăsare. În implementarea curentă există un dreptunghi îngust ce răspunde la apăsări cu mouse-ul. Ca și aplicația, sertarul are două stări: “sertar deschis” și “sertar închis”. Obiectul sertar e o fâșie de dreptunghi de înălțime mică. În interior se găsește un element de tip Image [doc.qt.nokia.com] specificând că o iconiță de tip săgeată e centrată în cadrul sertarului. Obiectul sertar atribuie o stare întregii aplicații, în cadrul ecranului identificator, de fiecare dată când un utilizator apasă în zona mouse-ului.

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

O stare e pur și simplu o colecție de configurații și se declară printr-un element State [doc.qt.nokia.com]. O listă de stări poate fi enumerată și legată de proprietatea states. În aplicația noastră, cele două stări sunt numite DRAWER_CLOSED și DRAWER_OPEN. Configurațiile elementelor sunt declarate în elemente PropertyChanges [doc.qt.nokia.com]. În starea DRAWER_OPEN există patru elemente ale căror proprietăți se schimbă. Primul dintre ele, menuBar, își va schimba proprietatea y în 0. Similar, textArea va coborî într-o poziție nouă când starea e DRAWER_OPEN. Elementele textArea, drawer și iconița obiectului drawer vor suferi schimbări ale proprietăților pentru a se conforma stării curente.

  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.      ]

Schimbările de stare sunt bruște și necesită tranziții mai fine. Tranzițiile între stări sunt definite cu ajutorul elementului Transition [doc.qt.nokia.com], care poate fi conectat de proprietatea transitions a obiectului. Editorul nostru de texte suferă o tranziție de fiecare dată când starea se schimbă fie în DRAWER_OPEN, fie în DRAWER_CLOSED. De remarcat e faptul că tranziția necesită o stare from și o stare to; însă, pentru tranzițiile noastre, putem folosi simbolul wildcard * pentru a preciza că tranziția se aplică tuturo schimbărilor de stare.

În timpul tranzițiilor putem asigna animații schimbărilor de proprietăți. Elementul menuBar își comută poziția de la y:0 la y:-partition și putem anima această tranziție folosind NumberAnimation [doc.qt.nokia.com]. Specificăm că proprietățile elementelor țintă vor fi animate pentru o anumită durată de timp și că vor folosi o curbă de atenuare. O curbă de atenuare reglementează gradul de animare și interpolare în timpul tranzițiilor între stări. Alegem curba de atenuare Easing.OutQuint [doc.qt.nokia.com], ce încetinește mișcarea către sfârșitul animației. Pentru detalii suplimentare, vă rugăm să citiți articolul QML 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.      ]

O altă modalitate de a anima schimbările proprietăților e de a declara un element Behavior [doc.qt.nokia.com]. O tranziție apare doar în timpul schimbărilor de stare, iar Behavior poate stabili o animație pentru o schimbare generală a unei proprietăți. În editorul de texte, săgeata are un element NumberAnimation care îi animează proprietatea rotation de fiecare dată când aceasta se schimbă.

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

Revenind la componentele noastre după ce ne-am familiarizat cu conceptele de stare și animație, putem îmbunătăți aspectul acestora. În Button.qml putem adăuga schimbări ale proprietăților color și scale de fiecare dată când butonul e apăsat. Culorile se animează folosind ColorAnimation [doc.qt.nokia.com], iar numerele se animează cu ajutorul NumberAnimation [doc.qt.nokia.com]. Sintaxa on-numele-proprietății prezentată mai jos e folositoare când avem de-a face cu o singură proprietate.

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

În plus, putem îmbunătăți aspectul componentelor QML adăugând efecte coloristice precum gradienți și opacitate. Declararea unui element Gradient [doc.qt.nokia.com] suprascrie proprietatea color a elementului respectiv. În cadrul gradientului, o culoare poate fi declarată folosind un element GradientStop [doc.qt.nokia.com]. Gradientul e precizat folosind o scală între 0.0 și 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.      }

Acest gradient e folosit de către bara de meniuri pentru a afișa un gradient ce simulează adâncimea. Prima culoare începe la 0.0, iar ultima culoare e la 1.0.

Cum să continuați

Suntem pe punctul de a termina construirea unei interfețe utilizator pentru un editor de texte foarte simplu. Interfața utilizator e completă; putem, deci, să implementăm logica aplicației folosind Qt și C++. QML poate fi folosit destul de bine ca instrument pentru crearea de prototipuri, separând logica aplicației de proiectarea interfeței utilizator.

Extinderea QML folosind Qt C++

Acum că avem amplasarea elementelor editorului de texte, putem implementa funcționalitățile în C++. Folosirea QML împreună cu C++ permite crearea logicii aplicației folosind Qt. Putem crea un context QML într-o aplicație C++ folosind clasele din Qt Declarative [doc.qt.nokia.com] și putem afișa elementele QML folosind Graphics Scene. Ca alternativă, putem exporta codul C++ ca un plugin pe care qmlviewer [doc.qt.nokia.com] îl poate folosi. Pentru aplicația noastră, vom implementa funcțiile de încarcare si salvare a textului în C++ și le vom exporta ca plugin. În acest fel, e nevoie doar să încărcăm fișierul QML, în loc să rulăm un executabil.

Expunerea claselor C++ către QML

Vom implementa încărcarea și salvarea fișierelor folosind Qt și C++. Clasele și funcțiile C++ pot fi folosite în QML dacă sunt înregistrate ca atare. Clasa trebuie de asemenea compilată ca un plugin Qt și fișierul QML trebuie să știe unde se află plugin-ul.

Pentru aplicația noastră, avem nevoie de următoarele:

  1. clasa Directory care se va ocupa de operațiile legate de directoare
  2. clasa File care este un QObject [doc.qt.nokia.com] și simulează lista de fișiere dintr-un director
  3. o clasa plugin care va înregistra clasa în context QML
  4. un fișier proiect Qt care va compila plugin-ul
  5. un fișier qmldir care va indica qmlviewer unde se găsește plugin-ul

Compilarea unui plugin pentru Qt

Pentru a compila un plugin, e nevoie de următoarele într-un fișier proiect Qt. Mai întâi, sursele, header-ele și modulele Qt necesare trebuie adăugate fișierului proiect. Toate fișierele ce conțin cod C++ și fișierele proiect se găsesc în directorul 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

Mai precis, compilăm Qt cu modulul declarativ și configurăm proiectul ca plugin; pentru aceasta e nevoie template-ul lib. Plugin-ul compilat va fi pus în directorul plugins al directorului părinte.

Înregistrarea unei clase către 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.      };

Clasa plugin, DialogPlugin, e o clasa derivată din QDeclarativeExtensionPlugin [doc.qt.nokia.com]. Trebuie să implementăm funcția moștenită registerTypes() [doc.qt.nokia.com]. Fișierul dialogPlugin.cpp arată ca mai jos:

  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);

Funcția registerTypes() [doc.qt.nokia.com] înregistrează clasele File și Directory către QML. Aceasta funcție are nevoie de numele clasei pentru template, un număr de versiune major, un număr de versiune minor și un nume pentru clasele noastre.

Trebuie să exportăm plugin-ul folosind macro-ul Q_EXPORT_PLUGIN2 [doc.qt.nokia.com]. Observați că în fișierul dialogPlugin.h, folosim macro-ul Q_OBJECT [doc.qt.nokia.com] la începutul clasei. De asemenea, trebuie să rulăm qmake pe fișierul proiect pentru a genera codul meta-object necesar.

Crearea de proprietăți QML într-o clasă C++

Putem crea elemente și proprietăți QML folosind C++ și sistemul Qt Meta-Object [doc.qt.nokia.com]. Putem implementa proprietățile folosind slots și signals; în acest fel Qt recunoaște aceste proprietăți. Proprietățile pot fi apoi folosite în QML.

Pentru editorul de texte, avem nevoie să încărcăm și salvăm fișiere. De obicei, aceste funcționalități sunt conținute de un dialog pentru fișiere. Din fericire, putem folosi QDir [doc.qt.nokia.com], QFile [doc.qt.nokia.com] și
QTextStream [doc.qt.nokia.com] pentru a implementa citirea directorului și a fluxulurilor de intrare/ieșire.

  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.          ...

Clasa Directory folosește sistemul Qt Meta-Object pentru a înregistra proprietățile de care are nevoie pentru a realiza manipularea fișierelor. Clasa Directory este exportată ca plugin și poate fi folosită în QML ca elementul Directory. Fiecare din proprietățile declarate folosind macro-ul Q_PROPERTY [doc.qt.nokia.com] este o proprietate QML.

Macro-ul Q_PROPERTY [doc.qt.nokia.com] declară o proprietate, precum și funcțiile pentru citirea și scrierea ei către sistemul Qt Meta-Object. De exemplu, proprietatea filename, de tipul QString [doc.qt.nokia.com], poate fi citită folosind functia filename() și scrisă folosind funcția setFilename(). De asemenea, există un semnal asociat cu proprietatea filename numit filenameChanged(), semnal care e emis atunci cand proprietatea se schimbă. Funcțiile pentru citire și scriere sunt declarate publice în fișierul header.

Similar, avem celelalte proprietăți declarate conform cu modul în care vor fi folosite. Proprietatea filesCount indică numărul de fișiere dintr-un director. Proprietatea filename conține numele fișierului selectat în acest moment, iar conținutul încărcat/salvat e reținut în proprietatea fileContent.

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

Proprietatea files e o listă ce conține toate fișiere ce ramân în urma filtrării unui director. Clasa Directory e implementata astfel încât să filtreze fișierele text nevalide; numai fișierele cu extensia .txt sunt valide. Mai mult, listele de tipul QList [doc.qt.nokia.com] pot fi folosite în fișiere QML dacă le declarăm ca o QDeclarativeListProperty [doc.qt.nokia.com] în C++. Obiectul cu care este instanțiat template-ul trebuie sa moștenească QObject [doc.qt.nokia.com], deci clasa File trebuie să moștenească QObject [doc.qt.nokia.com]. În clasa Directory lista de obiecte File e reținută într-un QList [doc.qt.nokia.com] numit 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.      };

Proprietățile pot fi folosite în QML ca parte a proprietăților elementului Directory. Observați că nu e necesar să cream o proprietate id în codul C++.

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

Deoarece QML folosește sintaxa și structura Javascript, putem itera lista de fișiere și accesa proprietățile lor. Pentru a obține proprietatea name a primului fișier, folosim files0.name.

Și funcțiile obișnuite C++ sunt accesibile din QML. Funcțiile pentru încărcarea și salvarea fișierelor sunt implementate în C++ și declarate folosind macro-ul Q_INVOKABLE [doc.qt.nokia.com]. Ca alternativă, puteam declara funcțiile ca slot și atunci ele ar fi de asemenea accesibile din QML.

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

Clasa Directory trebuie să și notifice alte obiecte atunci când conținutul directorului se schimbă. Această funcționalitate e implementată folosind un semnal. După cum s-a precizat, semnalele QML au un handler al cărui nume este numele semnalului prefixat cu on. Semnalul este numit directoryChanged și este emis atunci când conținutul directorului este reîmprospătat. Reîmprospătarea pur și simplu recitește conținutul directorului și actualizează lista de fișiere valide din director. Elementele QML pot fi notificate prin atașarea unei acțiuni handler-ului de semnal onDirectoryChanged.

Proprietățile listei trebuie examinate mai îndeaproape. E necesar să facem asta pentru că proprietățile listei folosesc callback-uri pentru a accesa și modifica conținutul listei. Tipul proprietății listă de fișiere este QDeclarativeListProperty<File>. De fiecare data când lista este accesată, funcția accesor trebuie să returneze un object QDeclarativeListProperty<File>. Obiectul folosit pentru instanțierea template-ului, File, trebuie să moștenească QObject. Mai mult, pentru a crea obiectul QDeclarativeListProperty [doc.qt.nokia.com], funcțiile ce vor accesa și modifica lista trebuie trimise constructorului ca pointeri la funcții. Lista, de tipul QList în cazul nostru, trebuie de asemenea să fie o lista de pointeri la File.

Constructorul tipului QDeclarativeListProperty [doc.qt.nokia.com] și implementarea 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 );

Constructorul primește pointeri la funcții care vor adăuga elemente în lista, număra câte elemente sunt în listă, returna un element al listei folosind un index și goli lista. Doar funcția de adăugare în lista este obligatorie. Observați că pointerii la funcții trebuie să corespundă definițiilor pentru AppendFunction [doc.qt.nokia.com], CountFunction [doc.qt.nokia.com], AtFunction [doc.qt.nokia.com], și 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)

Pentru a simplifica dialogul pentru fișiere, clasa Directory filtrează fișierele text nevalide, care sunt fișierele ce nu au extensia txt. Dacă numele unui fișier nu are extensia .txt, acel fișier nu va apărea în dialogul pentru fișiere. De asemenea, implementarea se asigură că fișierele salvate au extensia .txt. Directory folosește QTextStream [doc.qt.nokia.com] pentru a citi și scrie text din și în fișier.

Folosind elementul Directory, putem obține fișierele ca o listă, ști cate fișiere text sunt în directorul aplicației, obține numele fișierului și conținutul său ca șir de caractere și putem fi notificați când sunt schimbări în conținutul directorului.

Pentru a compila plugin-ul, rulați qmake pe fișierul proiect cppPlugins.pro, apoi rulați make pentru a compila și transfera plugin-ul în directorul plugins.

Importarea unui Plugin in QML

qmlviewer importă fișiere care sunt în același director cu aplicația. Putem și sa creăm un fișier qmldir ce conține locațiile fișierelor QML pe care dorim să le importăm. Fișierul qmldir poate reține și locații ale plugin-urilor sau alte resurse.

  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

Plugin-ul pe care tocmai l-am creat e numit FileDialog, după cum indică câmpul TARGET din fișierul proiect. Plugin-ul compilat se găsește în directorul plugins.

Integrarea unui dialogului de fișiere în meniul de fișiere

Elementul FileMenu trebuie să afișeze elementul FileDialog ce conține o listă de fișiere text dintr-un director, permițând astfel utilizatorului să selecteze fișierul dând click pe listă. De asemenea, trebuie să atribuim butoanelor pentru salvare, încărcare și fișier nou acțiunile corespunzătoare. Elementul FileMenu conține un element pentru introducere de text care permite utilizatorului să introducă un nume de fișier folosind tastatura.

Elementul Directory e folosit în fișierul FileMenu.qml și notifică elementul FileDialog că directorul și-a reîmprospătat conținutul. Această notificare e efectuată în handler-ul de semnal onDirectoryChanged.

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

În conformitate cu simplitatea aplicației noastre, dialogul pentru fișiere va fi mereu vizibil și nu va afișa fișierele text nevalide (cele care nu au extensia .txt).

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

Elementul FileDialog va afișa conținutul unui director citind proprietatea sa de tip listă numită files. Această proprietate e folosită ca modelul unui element GridView [doc.qt.nokia.com], care afișează itemi de date într-un tabel, conform unui delegat. Delegatul gestionează aspectul modelului și dialogul nostru de fișiere va crea un simplu tabel cu text centrat în mijlocul său. Un click pe numele fișierului va rezulta în apariția unui dreptunghi pentru a evidenția numele fișierului. FileDialog este notificat atunci când semnalul notifyRefresh este emis, ceea ce cauzează reîmprospătarea fișierelor din director.

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

Acum, FileMenu se poate conecta la acțiunile corespunzătoare. Butonul saveButton va transfera textul din TextEdit în proprietatea fileContent a directorului, apoi va copia numele fișierului din zona editabilă de text. În sfârșit, butonul cheamă funcția saveFile() care salvează fișierul. Execuția butonului loadButton e similară. De asemenea, acțiunea New va goli conținutul zonei editabile de text.

În plus, butoanele meniului de editare sunt conectate la funcțiile TextEdit pentru copiere, lipire și selectare a textului din editor.

Finalizarea editorului de texte

Aplicația poate funcționa ca un simplu editor de texte, poate accepta text pe care îl poate salva într-un fișier. Editorul de texte poate de asemenea încărca text dintr-un fișier și manipula text.