Iniciando o desenvolvimento com QML

Bem-vindo ao mundo do QML, a linguagem declarativa para interfaces com usuário. Neste guia introdutório nós criaremos um editor de texto simples usando QML. Uma vez lido este guia, você estará pronto para criar suas próprias aplicações usando QML e Qt C++.

QML para criar interfaces com usuário

A aplicação que vamos criar é um editor de text simples que irá carregar, salvar e fará algumas manipulações no texto. Este guia consite de duas partes. A primeira parte envolve design do layout da aplicação e seu comportamento, usando a linguagem declarativa QML. Na segunda parte, processos de carregar e salvar arquivos serão implementados com Qt C++. Utilizando o Sistema de Meta-Objetos do Qt [doc.qt.nokia.com], é possível expor funções e propriedades do escritas em C++ que elementos QML conseguirão usar. Utilizando QML e Qt C++, é possível manter separadas, de uma forma eficiente, a interface com usuário e a lógica da aplicação.

Para executar o exemplo de código QML, basta usar a ferramenta qmlviewer com o arquivo QML como argumento. A porção C++ deste tutorial assume que o leitor tem conhecimentos básicos do procedimento de compilação de código Qt.

Capítulos do Tutorial:
  1. Definindo um Botão e um Menu
  2. Implementando uma Barra de Menu
  3. Construindo um Editor de Texto
  4. Decorando o Editor de Texto
  5. Estendendo QML usando Qt C++

Definindo um Botão e um Menu

Componente Básico – um Botão

Comecemos nosso editor de texto criando um botão. Em termos de funcionalidade, um botão é uma área sensível ao mouse /rato com um texto. Botões reagem quando um usuário os pressionam.
Em QML, o item visual básico é o elemento Rectangle [doc.qt.nokia.com] (Retângulo). O elemento retângulo tem propriedades para controlar sua a aparência e localização.

Primeiramente, o import Qt 4.7 permite que a ferramenta qmlviewer importe os elementos QML que usaremos em seguida. Tal linha deve existir em todo arquivo QML. Observe que a versão dos módulos Qt é incluida na sentenção de importação.
Esse retângulo simples tem um identificador único, simplebutton, quue é atribuído a propriedade id. As propriedades do elemento Retângulo são ligadas aos valores através da listagem da propriedade, seguida de dois pontos, então o valor. Nesse exemplo de código, a cor cinza é associada à propriedade cor (color) do rectângulo. De modo similar, nós associamos a largura (width) e altura (height) do retângulo.
O elemento Text [doc.qt.nokia.com] (Texto) é um campo de texto não editável. Nós batizamos esse elemento texto de buttonLabel. Para configurar o conteúdo desse campo de texto, associamos o valor desejado à propriedade texto (text). Essa “etiqueta” fica no interior do retângulo e para centralizá-la, associamos as âncotas do elemento Text às de seu pai (parent), chamado simplebutton. Âncoras podem ser associadas às âncoras de outro item, simplificando assim o layout.
Salvemos esse código como SimpleButton.qml. Executando o qmlviewer com tal arquivo como argumento exibirá um retângulo cinza com um texto em seu interior.

Para implementar a funcionalidade de clique no botão, podemos usar o manipulador de eventos do QML. O manipulador de eventos do QML é bastante parecido com o mecanismo de sinais e slots do Qt [doc.qt.nokia.com]. Sinais são emitidos e os slots conectados são chamados.

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

Adicionamos um elemento MouseArea [doc.qt.nokia.com] ao nosso simplebutton. Elementos MouseArea descrevem a área interativa onde movimentos de mouse/rato são detectados. Para nosso botão, nós ancoramos a MouseArea [doc.qt.nokia.com] ao seu pai, que é o simplebutton. A sintaxe anchors.fill é uma forma de acessar uma propriedade específica, chamada fill, dentro de um grupo de propriedades chamadas âncoras. QML usa layout baseado em âncoras [doc.qt.nokia.com] onde os itens podem ser acorados uns aos outros, criando assim layouts robustos.
A MouseArea tem vários manipuladores de sinais que são chamados durante os movimentos do mouse/rato dentro dos limites da MouseArea. Um deles é o onClicked (“ao clicar”) e é chamado toda vez que um botão do mouse/rato é clicado, sendo que o botão esquerdo é o padrão. É possível associar ações ao onClicked. No nosso exemplo, console.log() exibe texto toda vez que a MouseArea recebe cliques. A função console.log() é útil para depuração e para saídas de texto.
O código em SimpleButton.qml é suficiente para exibir um botão e escrever texto na saída padrão toda vez que receber um clique.

  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.      //determina a cor do botão usandoo operador condicional
  22.      color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor
  23.  }

Um botão funcional está em Button.qml. Os trechos de código deste arquivo tem partes omitidas, denotadas por reticências porque elas ou foram apresentados em seções anteriores ou são irrelevantes para o código em discussão atualmente.
Propriedades personalizadas são declaradas usando a sintaxe de tipo property. No código, a propriedade buttonColor, do tipo color, é declarada e recebe o valor “lightblue”. buttonColor é mais tarde usada em uma operação condicional para determinar a cor de preenchimento do botão. Note que atribuição de valores às propriedades é feita usando o sinal de igual =, enquanto associação de valores é feita usando dois pontos :. Propriedades personalizadas permitem que itens internos sejam acessíveis fora do escopo do nosso elemento Rectangle. Existem tipos básicos QML [doc.qt.nokia.com] tais como int, string, real, assim como um tipo chamado variant.
Associando os manipuladores de sinais onEntered e onExited a cores, as bordas do botão ficarão amarelas quando o ponteiro do mouse/rato estiver sobre a área do botão e voltarão à cor original quando o ponteiro sai da área do botão.
Um sinal buttonClick() é declarado em Button.qml colocando a palavra-chave signal em frente ao nome do sinal. Todos os sinais tem seus manipuladores automaticamente criados, com nomes começando em on. Como resultado, onButtonClick é o manipulador do sinal buttonClick. Com isso, atribui-se uma ação executada por onButtonClick. No nosso botão de exemplo, o manipulador de mouse/rato onClicked simplesmente chamará onButtonClick, que por sua vez exibirá um texto. onButtonClick possibilita objetos externos acessarem a MouseArea do Button de uma forma simples. Por exemplo, itens podem possuir mais que uma declaração de MouseAreae um sinal buttonClick pode identificar qual manipulador é o mais adequado para determindada situação.
Nós agora temos o conhecimento básico para implementar itens em QML que lidam com movimentos básicos de mouse/rato. Nós criamos um elemento Text dentro de um elemento Rectangle, personalizamos suas propriedades, e implementamos comportamentos que respondem aos movimentos de mouse. A idéia de criar elementos dentro de elementos acontece repidamente ao longo do editor de texto que criaremos.
O botão que temos é inútil a menos que o utilizemos como um componente para executar uma ação. Na próxima seção, criaremos um menu contendo vários desses botões.

Criando uma página de Menu

Até agora, vimos como criar elementos e associar comportamentos em apenas um arquivo QML. Nesta seção, veremos como importar elementos QML e como reutilizar alguns dos componentes criados para criação de outros componentes.
Menus exibem o conteúdo de uma lista, cada item sendo capaz de realizar uma ação. Com QML, podemos criar um menu de diferentes maneiras. Primeiramente, criaremos um menu contendo botoões que eventualmente realizarão alguma ação. O código do está no arquivo FileMenu.qml.

  1.  import Qt 4.7                   \\importa o módulo principal Qt QML
  2.  import "folderName"           \\importa conteúdos do diretório folderNamefolder
  3.  import "script.js" as Script   \\importa um arquivo Javascript como Script

A sintaxe mostrada acima apresenta como usar a palavra-chave import. Isso é necessário para usar arquivos JavaScript [developer.mozilla.org], ou arquivos QML que não estejam no mesmo diretório. Como Button.qml está no mesmo diretório que FileMenu.qml, não precisamos importá-lo para que Buton seja usado. Para criar um elemento Button, basta declarar Button{}, similar a uma declaração Rectangle{}.

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

Em FileMenu.qml, nós declaramos três elementos Button. Eles são declarados dentro de um elemento Row [doc.qt.nokia.com], um posicionador que colocará seus filhos em uma fileira horizontal. A declaração deButton mora em Button.qml, que é o mesmo Button.qml que nós usamos na seção anterior. Novas associações de propriedades podem ser declaradas dentro dos novos botões criados, efetivamente sobrescrevendo as propriedades de Button.qml. O botão chamado exitButton fechará a janela quando for clicado. Observe que o manipulador de sinal onButtonClick de Button.qml será chamado além do manipulador onButtonClick em exitButton.

Row é declarado em um Rectangle, criando um envoltório para a linha de botões. Esse botão adicional cria uma forma indireta de organizar a linha de botões dentro de um menu.
A declaração do menu de edição é semelhante. O menu tem botões com as seguintes legendas: Copy, Paste, e Select All.

Munidos de nosso conhecimento de importar e personalizar componentes previamente criados, nós agora combinaremos essas páginas de menu para criar uma barra de menus, consistindo de botões buttons para seleção, e veremos como organizar dados usando QML.

Implementando uma Barra de Menu

Nossa aplicação de editor de texto precisará mostrar menus usando uma barra de menu. Essa barra alternará os diferentes menus e o usuário poderá selecionar qual menu será exibido. Alternar menu implica que os menus precisam de mais estrutura do que a simples exibição em linha. QML usa modelos e visualizadores para estruturar exibir dados.

Usando modelos de dados e visualizadores

QML tem diferentes visualizadores de dados [doc.qt.nokia.com] para mostrar os modelos de dados [doc.qt.nokia.com]. Nossa barra de menu exibirá os menus em uma lista, com um cabeçalho mostrando uma linha de nomes de menus. A lista de menus é declarada dentro de VisualItemModel [doc.qt.nokia.com]. O elemento VisualItemModel contém itens que já tem representação visual, tais como Rectangle e outros elementos de interface importados. Outros tipos de modelo, como o elemento ListModel [doc.qt.nokia.com], precisam de um delegate para exibirem seus dados.
Nós declaramos dois itens visuais no menuListModel: FileMenu e EditMenu. Nós personalizamos os dois menus e os exibimos usando um ListView [doc.qt.nokia.com]. O arquivo MenuBar.qml contém as declarações QML e um menu simples de edição é definido em 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.      }

O elemento ListView [doc.qt.nokia.com] mostrará dados de um modelo de acordo com um delegate. O delegate declara os itens do modelo de forma a mostrá-los em um elemeto Row ou os mostra em um grid. Nosso menuListModel possui itens visíveis, por isso, não precisa um delegate.

  1.      ListView{
  2.          id: menuListView
  3.  
  4.          //Âncoras são configuradas para reagir às âncoras da janela
  5.          anchors.fill:parent
  6.          anchors.bottom: parent.bottom
  7.          width:parent.width
  8.          height: parent.height
  9.  
  10.          //modelo que contém os dados
  11.          model: menuListModel
  12.  
  13.          //controla o movimento da troca de menus
  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.      }

Adicionalmente, ListView herda do elemento Flickable [doc.qt.nokia.com], o que faz com que a lista responda a arrastos e outros gestos de mouse. A última porção de código configura as propriedades de Flickablepara criar o movimento desejado. Em particular, a propriedade highlightMoveDuration muda a duração da transição de flick. Quanto maior o valor de highlightMoveDuration, menor a velocidade das transições do menu.
ListView controla os itens do modelo através de um índice e cada item visual do modelo é acessível através desse index, seguindo a ordem de declaração. Alterando a propriedade currentIndex efetivamente altera o item destacado no ListView. O cabeçalho do nosso menu exemplifica esse efeito. Exitem dois botões em linha, ambos mudando o menu atual quando clicados. O item fileButton muda o menu atual para o menu de arquivos, com índice sendo 0 pois FileMenu é declaredo primeiro em menuListModel. De modo análogo, editButton mudará o menu atual para EditMenu.
O retângulo labelList tem valor z igual a 1, denotando tque é mostrado à frente da barra de menus. Itens com maior z são exibidos à front dos itens com menores valores de z. O valor padrão de 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.      }

A barra de menu que acaba de ser criada pode ser movida para acessar os menus ou através de cliques nos títulos dos menus no topo. A troca de menus é intuitiva e responsiva.

construindo um Editor de Texto

Declarando um TextArea

Nosso editor de texto não é um editor de texto se não possuir uma área para edição de texto. O elemento TextEdit [doc.qt.nokia.com] provê a declaração de uma área de edição de texto de múltiplas linhas. TextEdit [doc.qt.nokia.com] é diferente do elemento Text [doc.qt.nokia.com], que não permite a edição direta de seu conteúdo.

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

O editor tem as propriedades de cor de fonte configuradas, bem como a quebra de linha. A área do TextEdit está dentro de uma área flickable que rola o texto se o cursor estiver fora da área visível. A função ensureVisible() verificará se o retângulo do cursor está fora dos limites da área visível e faz a movimentação quando necessária. QML usa sintaxe Javascript para seus scripts e, conforme dito anteriormente, arquivos Javascript podem ser importados e usados dentro de um arquivo 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.      }

Combinando componentes para o Editor de Texto

Agora estamos prontos para criar o layout do nosso editor de texto usando QML. O editor de texto tem dois componentes, a barra de menu que criamos e a área de texto. QML nos permite reutilizar elementos, consequentemente tornando nosso código mais simples, através da importação de componentes e personalizando quando necessário. Nosso editor de texto divide a janela em dois; um terço da tela fica dedicada à barra de menu e o restante para a área de texto. A barra de menu é exibida à frente de qualquer outro elemento.

  1.      Rectangle{
  2.  
  3.          id: screen
  4.          width: 1000; height: 1000
  5.  
  6.          //a tela é dividida entre MenuBar e TextArea. 1/3 fica com 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.      }

Importando componentes reutilizáveis, nosso arquivo TextEditor parece bem mais simples. Nós podemos então modificar a aplicação sem termos que nos preocupar com propriedades e comportamentos que já foram definidos. Usando essa abordagem, o layout e a interface com usuário podem ser criados de uma forma simples.

Decorando o Editor de Texto

Implementando uma interface de desenho

Nosso editor de texto parece simples, nós precisamos incrementá-lo. Usando QML, nós podemos declarar transições e animar nosso editor. Nossa barra de menu ocupa um terço da tela e seria bom que ela aparecesse apenas quando nós queremos usá-la.

Podemos adicionar uma interface de desenho, que irá contrair ou expandir a barra de menu quando clicada. Em nossa implementação, nós temos um retângulo fino que responde aos cliques de mouse. A área de desenho, assim como a aplicação, possui dois estados: “área de desenho aberta” e “área de desenho fechada”. O item área de desenho é uma retângulo estreito com pequena altura. Dentro dele um elemento Image [doc.qt.nokia.com] declara que um ícone seta fica centralizado dentro da área de desenho. Esta por sua vez associa um estado à aplicação como um todo através do identificador screen, toda vez que o usuário clicar sobre a MouseArea.

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

Um estado é simplesmente uma coleção de configurações e é declarado em um elemento State [doc.qt.nokia.com]. Uma lista de estados pode ser listada e associada a propriedade states. Em nossa aplicação, os dois estados são chamados DRAWER_CLOSED e DRAWER_OPEN (área de desenho fechada e aberta, respectivamente). As configurações de itens são declaradas em elementos PropertyChanges [doc.qt.nokia.com]. no estado DRAWER_OPEN, 4 itens receberão modificações de propriedades. O primeiro alvo, menuBar, mudará sua propriedade y para 0. Similarmente, textArea descerá para uma nova posição quando o estado for DRAWER_OPEN. Os elementos textArea, drawer e o ícone sofrerão alterações para se ajustarem ao estado 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.      ]

Mudanças de estado são abruptas e precisam de transições suaves. Transições entre estados são definidos usando o elemento de transição Transition [doc.qt.nokia.com], que pode ser associado a propriedade transitions. Nosso editor de texto tem uma transição de estado sempre que ha mudança para DRAWER_OPEN ou DRAWER_CLOSED. Importante observar que a transição precisa de um estado from (origem) e de um estado to (destino), mas para a nossa transição, podemos usar o símbolo coringa * para indicar que a transição se aplica a todas as mudanças de estado.

Durante as transições, podemos atribuir animações às mudanças da propriedade. Nosso menuBar muda de posição de y:0 para y:-partition e podemos animar essaa transição usando o elemento NumberAnimation [doc.qt.nokia.com]. Nós declaramos que as propriedades dos alvos serão animadas durante um certo período de tempo usando uma determinada curva de atenuação (easing curve). Uma curva de atenuação controla as taxas de animação e interpolação de comportamento durante as transições de estado. A curva de atenuação que nós escolhemos é Easing.OutQuint [doc.qt.nokia.com], que retarda o movimento perto do fim da animação. Por favor leia o artigo sobre Animações no QML [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.      ]

Outra maneira de animar as alterações de propriedade é declarar um elemento de comportamento: Behavior. Uma transição só funciona durante as mudanças de estado e Behavior [doc.qt.nokia.com] pode definir uma animação para a mudança de uma propriedade geral. No editor de texto, a seta tem uma NumberAnimation animando sua propriedade rotação sempre que essa propriedade sofre alterações.

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

Voltando aos nossos componentes, com o conhecimento dos estados e animações, podemos melhorar a aparência dos componentes. Em Button.qml, podemos adicionar as alterações de propriedades de cor e escala quando o botão é clicado. Tipos de cores são animados usando ColorAnimation [doc.qt.nokia.com] e os números são animados usando NumberAnimation [doc.qt.nokia.com]. O propertyName na sintaxe mostrada abaixo é útil quando nosso alvo é uma única propriedade.

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

Além disso, podemos melhorar a aparência dos nossos componentes QML adicionando efeitos de cores, tais como gradientes e efeitos de opacidade. Declarando um elemento Gradient [doc.qt.nokia.com] vai substituir a propriedade de cor do elemento. Você pode declarar uma cor no gradiente usando o elemento GradientStop [doc.qt.nokia.com]. O gradiente é posicionado usando uma escala entre 0.0 e 1.0.

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

Este gradiente é usado pela barra de menu para exibir um gradiente simulando profundidade. A primeira cor começa em 0.0 e que a última cor é de 1.0.

Para onde seguir

Estamos terminando a construção da interface com usuário de um editor de texto muito simples. Indo adiante, a interface do usuário está completa, e nós podemos implementar a lógica do aplicativo usando Qt e C++. QML funciona bem como uma ferramenta de prototipagem, separando a lógica da aplicação do design de interface com usuário.

Estendendo QML usando Qt C++

Agora que temos o layout do nosso editor de texto, podemos agora implementar as funcionalidades do editor de texto em C++. Usando QML com C++ nos permite criar a lógica da aplicação usando Qt. Podemos criar um contexto QML em uma aplicação C++ usando as classes Qt Declarative [doc.qt.nokia.com] e exibir os elementos QML usando uma QGraphicsScene. Alternativamente, nós podemos exportar o nosso código C++ em um plugin que a ferramenta qmlviewer [doc.qt.nokia.com] consegue ler. Para a nossa aplicação, vamos implementar em C++ as funções de carregar e salvar e exportá-las como um plugin. Desta forma, só precisamos carregar o arquivo QML diretamente em vez de rodar um binário executável.

Expondo classes C++ ao QML

Nós implementaremos carregar e salvar arquivos usando Qt e C++. Classes C++ e funções podem ser utilizadas em QML, bastando registrá-las. A classe também deve ser compilado como um plugin_ Qt e o arquivo QML precisa saber onde o plugin está localizado.

Para a nossa aplicação, precisamos criar os seguintes itens:

  1. Classe diretório que cuidará de operações relacionados com diretórios
  2. Classe arquivo que é um QObject [doc.qt.nokia.com], simulando a lista de arquivos de um diretório
  3. Classe plugin que registrará a classe no contexto QML
  4. Arquivo de projeto Qt que compilará o plugin
  5. Um arquivo qmldir dizendo ao qmlviewer onde encontrar o plugin

Construindo um plugin Qt

Para criar um plugin, precisamos definir o seguinte em um arquivo de projeto Qt. Primeiramente, os arquivos de código-fonte necessários, cabeçalhos e módulos Qt precisam ser adicionados em nosso arquivo de projeto. Todos os arquivos de código C++ e arquivos de projeto estão no diretório 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

Em particular, nós compilamos Qt com o módulo declarative e o configuramos como um plugin, precisando de um modelo (template) de lib. Vamos colocar o plugin compilado dentro do plugins do diretório pai do atual.

Registrando uma classe no 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.      };

Nossa classe plugin, DialogPlugin, é uma subclasse de QDeclarativeExtensionPlugin [doc.qt.nokia.com]. Precisamos implementar a função herdada, registerTypes() [doc.qt.nokia.com]. O arquivo dialogPlugin.cpp parece com o seguinte:

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

A função registerTypes() registra nossas classes File e Directory no QML. Esta função precisa do nome da classe para o seu template, um número de versão principal, um número de versão menor, e um nome para nossas classes.
Precisamos exportar o plugin usando a macro Q_EXPORT_PLUGIN2 [doc.qt.nokia.com]. Note que em nosso arquivo dialogPlugin.h, temos a macro Q_OBJECT [doc.qt.nokia.com] no topo da nossa classe. Assim sendo, precisamos executar qmake no arquivo de projeto para gerar o código de meta-objetos necessários.

Criando propriedades QML em uma classe C++

Podemos criar elementos QML e propriedades usando C++ e o sistema de meta-objetos do Qt [doc.qt.nokia.com]. Podemos implementar propriedades usando sinais e slots, fazendo Qt conhecer essas propriedades. Estas propriedades podem ser utilizadas em QML.
Para o editor de texto, precisamos salvar e carregar arquivos. Normalmente, esses recursos estão contidos em um diálogo de arquivo. Felizmente, nós podemos usar QDir [doc.qt.nokia.com], QFile [doc.qt.nokia.com] e QTextStream [doc.qt.nokia.com] para implementar a leitura de diretório e dos fluxos de entrada e saída.

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

A classe Directory usa o sistema de meta-objetos do Qt para registrar propriedades que necessita para realizar tratamento de arquivos. A classe Directory é exportada como um plugin e é utilizável em QML como um elemento Directory. Cada propriedades listadas usando a macro Q_PROPERTY [doc.qt.nokia.com] é uma propriedade QML.
Q_PROPERTY [doc.qt.nokia.com] declara uma propriedade, bem como as suas funções de leitura e escrita no sistema de meta-objetos. Por exemplo, a propriedade filename, do tipo QString [doc.qt.nokia.com], pode ser lido usando filename() e escrita utilizando a função setFileName(). Além disso, há um sinal associado à propriedade filename chamado filenameChanged(), que é emitido sempre que a propriedade sofre alterações. As funções de leitura e escrita são declaradas como públicas no cabeçalho do arquivo.
Da mesma forma, temos as outras propriedades declaradas de acordo com seus usos. A propriedade filesCount indica o número de arquivos em um diretório. A propriedade filenameé definida para o nome do arquivo selecionado atualmente e o conteúdo carregado/salvo é armazenado na propriedade fileContent.

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

A propriedade da lista de arquivos (files) é uma lista de todos os arquivos filtrados em um diretório. A classe Directory é implementada para filtrar os arquivos de texto inválidos; apenas arquivos com extensão .txt são válidos. Além disso, QList [doc.qt.nokia.com] podem ser usadas em arquivos QML declarando-as como QDeclarativeListProperty [doc.qt.nokia.com] em C++. O objeto da lista precisa herdar de um QObject [doc.qt.nokia.com], portanto, a classe File também deve herdar QObject [doc.qt.nokia.com]. Na classe Directory, a lista de objetos File é armazenada em uma QList [doc.qt.nokia.com] chamada 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.      };

As propriedades podem ser usadas em QML como parte de uma propriedade do elemento Directory. Note que não temos que criar uma propriedade identificadora id em nosso código C++.

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

Como QML usa a sintaxe e estrutura de Javascript, podemos percorrer a lista de arquivos e recuperar suas propriedades. Para recuperar a propriedade do arquivo do primeiro arquivo, podemos chamar files0.name.
Funções regulares de C++ também são acessíveis no QML. As funções para carregar e salvar arquivos são implementadas em C++ e declaradas usando a macro Q_INVOKABLE [doc.qt.nokia.com]. Alternativamente, podemos declarar as funções como slots que serão acessíveis a partir do QML.

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

A classe Directory também tem de notificar outros objetos quando o conteúdo do diretório for alterado. conseguimos isso utilizado um sinal. Como mencionado anteriormente, os sinais QML tem um manipulador correspondente com os seus nomes prefixados com on. O sinal é chamado directoryChanged e é emitido sempre que houver uma atualização de diretório. A atualização simplesmente recarrega o conteúdo do diretório e atualiza a lista de arquivos válidos no diretório. Itens QML podem ser notificados anexando uma ação para o manipulador de sinal onDirectoryChanged.
A lista de propriedades precisam ser exploradas mais a fundo. Isso acontece pois as propriedades lista usam callbacks para acessar e modificar o conteúdo da lista. A propriedade lista é do tipo QDeclarativeListProperty<File>. Sempre que a lista é acessada, a função de acesso precisa retornar uma QDeclarativeListProperty<File>. O tipo do modelo (template), arquivo, precisa ser derivado QObject. Além disso, para criar uma QDeclarativeListProperty [doc.qt.nokia.com], o método de acesso e modificadores da lista precisam ser passados para o consructor como ponteiros de função. A lista, um QList no nosso caso, também precisa ser uma lista de File.

O construtor de QDeclarativeListProperty [doc.qt.nokia.com] e a implementação de 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 );

O construtor passa ponteiros para funções que irã acrescentar à lista, contar a lista, recuperar o item usando um índice e esvaziar a lista. Apenas a função de acréscimo é obrigatória. Observe que os ponteiros de função devem coincidir com as definições de AppendFunction [doc.qt.nokia.com], CountFunction [doc.qt.nokia.com], AtFunction [doc.qt.nokia.com] ou 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)

Para simplificar o nosso diálogo de arquivo, a classe Directory filtra arquivos de texto inválidos, que são arquivos que não tem um extensão .txt. Se um arquivo não tem a extensão .txt, então não vai aparecer nosso diálogo de arquivo. Além disso, a implementação garante que os arquivos salvos tem uma extensão txt. Directory usa QTextStream [doc.qt.nokia.com] para ler o arquivo e também para gravar a saída do conteúdo em um arquivo.
Com o nosso elemento Directory, podemos recuperar os arquivos como uma lista, saber quantos arquivos de texto estão no diretório do aplicativo, obter o nome do arquivo e o conteúdo como uma string, e ser notificado sempre que houver alterações no conteúdo do diretório.
Para criar o plugin, execute qmake no arquivo de projeto cppPlugins.pro e em seguida, execute make para compilar e transferir o plugin para o diretório de plugins.

Importando um plugin no QML

A ferramenta qmlviewer importa arquivos que estão no mesmo diretório da aplicação. Podemos também criar um arquivo qmldir contendo os locais dos arquivos QML que desejamos importar. O arquivo qmldir também pode guardar os locais de plugins e outros recursos.

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

O plugin que acabamos de criar é chamado FileDialog, conforme indicado pelo campo TARGET no arquivo de projeto. O plugin é compilado no diretório de plugins.

Integrando um Diálogo de Arquivos ao Menu Arquivo

Nosso FileMenu precisa exibir o elemento FileDialog, contendo uma lista de arquivos de texto em um diretório, permitindo assim que o usuário selecione o arquivo, clicando na lista. Precisamos também atribuir salvar, carregar e novos botões para suas respectivas ações. O FileMenu contém uma entrada de texto editável para permitir que o usuário digite um nome de arquivo usando o teclado.
O elemento Directory é usado no arquivo FileMenu.qml e notifica o elemento FileDialog que o diretório atualizou seu conteúdo. Esta notificação é realizada em um manipulador de sinal, onDirectoryChanged.

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

Mantendo a simplicidade da nossa aplicação, o diálogo de arquivos será sempre visível e não mostrará arquivos inválidos, que não tenha uma extensão .txt,

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

O elemento FileDialog exibirá o conteúdo de um diretório lendo sua propriedade lista chamada de files. Os arquivos são usados como modelo de um elemento GridView [doc.qt.nokia.com], que exibe itens de dados em uma grade de acordo com um delegate. O delegate lida com a aparência do modelo e nosso diálogo de arquivo simplesmente criará uma grade com texto centralizado no meio. Clicando no nome do arquivo resulta no aparecimento de um retângulo para destacar o nome do arquivo. O FileDialog é notificado sempre que o sinal notifyRefresh é emitido, recarregando os arquivos no diretório.

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

Nosso FileMenu agora pode se conectar às suas respectivas ações. O saveButton transferirá o texto do TextEdit para a propriedade fileContent do diretório e então copiar o seu nome de arquivo da entrada de texto editável. Finalmente, o botão chama a função saveFile();, salvando o arquivo. O sloadButton tem execução semelhante. Também, a ação New esvazia o conteúdo do TextEdit.
Além disso, os botões EditMenu estão ligados às funções d TextEdit para copiar, colar e selecionar todo o texto no editor de texto.

Concluindo o Editor de Texto

A aplicação funciona como um editor simples de texto, capaz de aceitar texto e salvá-lo em um arquivo. O editor de texto também carrega arquivos e manipula formatação de texto.