原文翻譯自:“Getting Started Programming With QML”:http://doc.qt.nokia.com/4.7/gettingstartedqml.html

QML程式設計入門

歡迎來到QML,這個陳述式使用者介面語言的世界。在這篇入門導引中,我們將會展示如何運用QML建立一個簡單的文字編輯器。讀完本文後,您應該會有足夠的準備來透過QML及Qt C++開發自己的軟體。

用QML建立使用者介面

我們要建立的文字編輯器將會具備讀取(load),儲存(save)及文字編輯(edit)等功能。這篇文章將會包含兩個部分。第一個部分包含使用QML的陳述式語言進行應用程式的介面布局及行為控制。第二個部分,將使用Qt C++實現檔案的存取。透過Qt的Meta-Object系統,我們將可讓C++的函式變成QML可存取的屬性(property)。利用QML與Qt C++,我們可有效的從應用程式邏輯中,將使用者介面邏輯分離出來。

您的瀏覽器不支援圖片顯示

要執行QML範例程式,僅需執行qmlviewer工具,並以範例的QML檔案作為參數。在本文中的C++部分,均假設讀者已經具備編譯Qt程式的基礎知識。

本文章節:

  • 定義按鈕及選單
  • 實現選單列
  • 建立文字編輯器
  • 裝飾文字編輯器
  • 使用Qt C++擴展QML

定義按鈕及選單

基本元件 – 按鈕

我們從建立一個按鈕作為文字編輯器的開始。從功能上來說,按鈕是一個滑鼠感知區及一個標籤所組成。而且,按鈕會在使用者按下它時有所動作。

在QML中,基本的可視項目是 Rectangle [doc.qt.nokia.com] 元素。一個 Rectangle 有可控制外觀及位置的屬性。

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

首先, import QtQuick 1.0 讓qmlviewer工具將我們稍後所需的QML元素引入。所有的QML都必須有這一行指令。注意Qt模組的版本也要於import敘述中一併指定。

這個簡單的矩形有一個唯一的識別字, simplebutton 。這個識別字是由 id 屬性所指定的。矩形元素的屬性是由一系列的屬性後接著冒號,再接著屬性值的方式來指定。在範例中,矩形的灰色是由 color 屬性所指定。同樣的,我們可藉由 widthheight 來指定矩形的寬與高。

Text [doc.qt.nokia.com] 元素是一個不可編輯的文字欄位。我們將這個 Text 元素命名為 buttonLabel 。設定 Text 欄位的內容是透過 value 屬性。這個標籤被包含在一個 Rectangle 之內,為了要讓它能夠對齊中央,我們指定 Text 元素的 anchors 設定到他的父項目 simplebutton 上。 Anchors 可與其他項目的 anchors 綁定,這會讓元件布局更簡單。

我們應將這個程式碼存成 Simplebutton.qml ,並且將這個檔名作為 qmlviewer 工具的參數。接著,我們就會得到一個包含文字標籤的灰色矩形。

您的瀏覽器不支援圖片顯示

要實現按鈕的單擊功能,我們可使用 QML 的事件處理。 QML的事件處理與 Qt 的 signal及slot [doc.qt.nokia.com] 機制極為類似。 Signals 被發射,而連結的 slot 會被呼叫。

  1. Rectangle{
  2.      id:simplebutton
  3.      ...
  4.  
  5.      MouseArea{
  6.          id: buttonMouseArea
  7.  
  8.          anchors.fill: parent // 將滑鼠偵測區釘到整個矩形的範圍。
  9.          //onClicked會處理滑鼠按鈕按下的事件。
  10.          onClicked: console.log(buttonLabel.text + " clicked" )
  11.      }
  12. }

在我們的 simplebutton 元件中,我們引入了 MouseArea 元素。 MouseArea 元素描述了可偵測滑鼠移動的互動區域。對於我們的按鈕,我們將 MouseArea 固定在他的父元件 simplebutton 上。 anchors.fill 的語法是用來存取 anchors 的屬性群組中的 fill 這個特定屬性。QML使用 anchor 為基礎的的布局方式。一個項目會固定於另外一個項目上,由此建立一個強固的布局。

MouseArea有許多的訊號處理函式,當滑鼠於 MouseArea 所指定的範圍內移動時將會被呼叫。其中,onClicked 是當滑鼠按鍵按下時會被呼叫,預設是用左鍵單擊。我們可以將動作綁定到onClicked的處理函式。在我們的範例中,只要這個指定的滑鼠區域被按下,console.log() 就會輸出文字。console.log()在除錯及輸出文字上是很有用的。

在SimpleButton.qml的程式已經足以在螢幕上顯示一個按鈕並且當滑鼠在區域內被按下時就會輸出文字。

  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.      // 用條件運算子來決定按鈕的顏色。
  22.      color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor
  23.  }

在 Button.qml 中是一個功能完整的按鈕。本文中只擷取其中一部分,其他部分都被省略。那些程式因為是前面已經介紹過了或是與目前的討論無關,所以使用省略符號來代表他們。

自訂的屬性可用一般屬性的語法來宣告。在這個程式中,宣告了 buttonColor 這個型態為 color 的屬性,並且賦予 “lightblue” 的值。在後面,buttonColor使用了條件運算來決定實際要繪製的按鈕顏色。注意屬性除了使用一般冒號方式來指定值,也可以用等號來指定。自訂的屬性允許在 Rectangle 程式範圍之外存取其內部的項目。一些基本的QML型態,像是 int,string,real及variant都可以使用。

藉由onEntered及onExited訊號與顏色設定的結合,當滑鼠移入及移出按鈕邊界時,按鈕的顏色也將隨之改變。

在Button.qml中藉著在buttonClick()前面加上 signal 的關鍵字,將它宣告成一個訊號。所有的訊號的處理函式都會自動的被建立,且名稱都是 on 開頭。因此,onButtonClick是buttonClick的處理函式。之後,onButtonClick將被指定一些動作。在我們的範例中, onClicked 的滑鼠處理將會呼叫 onButtonClick函式,這個函式只是簡單的顯示一些文字。onButtonClick讓外部物件得以容易的存取按鈕的滑鼠區域。例如:如果有一個以上的MouseArea宣告,一個buttonClick訊號可以更好的區分不同的MouseArea訊號。

現在,我們有基礎知識在QML中實現基本的滑鼠移動處理。我們在一個矩形中建立一個 Text 標籤 ,自訂它的屬性及實現反應滑鼠移動的行為。在元素中建立元素的概念將會在我們的文字編輯器軟體中一再的出現。

在沒有能夠進行一些動作前,這個按鈕目前還不算太有用。下一節中,我們將會建立一個包含這些按鈕的選單。

您的瀏覽器不支援圖片顯示

建立選單頁面

直到目前為止,我們說明了如何在單一的QML檔案中,建立畫面元素及指定行為。在本節中,我們將會說明如何載入QML元素,並且重複利用這些建立好的元件來建構其他的元件。

選單可用來顯示列表中的內容,其中每個項目都可以進行一個動作。在QML中,我們可以用好幾種方式建立選單。首先,我們將會會先建立一個包含按鈕的選單,這些按鈕最終將會用來執行不同的動作。這個選單的程式碼放在FileMenu.qml中。

  1.  import QtQuick 1.0               // 載入主要的Qt QML模組
  2.  import "folderName"            // 載入指定資料夾的內容
  3.  import "script.js" as Script        // 載入一個Javascript檔案並且將他命名成Script

上面的語法展示了 import 關鍵字的用法。這是用於不在同一個目錄下的 Javascript或QML 檔案時。由於 Button.qml 與 FileMenu.qml 是在同一個目錄下,我們無須在指定載入 Button.qml 就可以使用它。只需要直接透過 Button{} 的語法建立一個按鈕元素,就像是 Rectangle{} 的用法一般。在 FileMenu.qml 的內容如下:

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

在 FileMenu.qml 中,我們宣告三個 Button 元素。它們被宣告在一個 Row 元素內,定位器會將這些子元素排列成垂直一列。Button 宣告在Button.qml中,跟我們前一節中使用的 Button.qml 相同。新屬性可以綁定在新建立的按鈕中,並且覆蓋原來定義於Button.qml中的設定。exitButton按鈕在被按下的時候會離開並且關閉視窗。注意,除了exitButton按鈕中的onButtonClick函式會被呼叫外,在Button.qml 中的 onButtonClick 訊號處理函式也會一併被呼叫。

您的瀏覽器不支援圖片顯示

Row 是宣告在 Rectangle之內,並且建立一個矩形的容器,用來將按鈕排成一列。這個矩形建立了一種間接的方式在選單內組織一列按鈕。編輯選單的宣告非常類似於我們在這裡所指出的方法。選單有按鈕,而按鈕上還有文字標籤:Copy, Paste及Select All。

您的瀏覽器不支援圖片顯示

具備了前述關於載入及客製化元件的知識,我們現在或許已經可以結合這些選單頁面來建立一個選單列,在上面包含可用來選擇選單的按鈕。並且可以看看如何用QML來組織資料。

實現選單列

我們的文字編輯器軟體將會需要能夠使用選單列顯示選單的方法。選單列可以切換不同的選單,而使用者可以選擇顯示想要的選單。選單的切換按試著選單需要更多的結構,而不僅僅是將他們放在一列顯示而已。QML使用模型與視圖(model and view)的方式來構成資料以及顯示他們。

使用資料模型與檢視

QML有不同的資料視圖(view)來顯示資料模型。我們的選單列將會以列表的方式來顯示選單,並且有一列標頭用來顯示選單名稱。這列選單是宣告在 VisualItemModel 中。 VisualItemModel 元素中的項目已經包含了諸如 Rectangle 元素等視圖及已載入的UI元素。其他的模型型態像是ListModel元素需要有一個 delegate 才能夠顯示他們的資料。

我們在menuListModel中宣告了兩個可見的項目,FileMenu及EditMenu。我們自定兩個選單並且用ListView來顯示他們。MenuBar.qml檔案包含這些QML的宣告。此外,一個簡單的編輯選單則定義在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.      }

ListView元素將會根據delegate來顯示一個資料模型。這個delegate可以將模型內的項目以Row元素的方式顯示,或是也表格的方式顯示。我們的menuListModel已經有可見的項目,因此,我們將不需要宣告一個delegate。

  1.      ListView{
  2.          id: menuListView
  3.  
  4.          // 定位器設定為與視窗定位器連動
  5.          anchors.fill:parent
  6.          anchors.bottom: parent.bottom
  7.          width:parent.width
  8.          height: parent.height
  9.  
  10.          // 指定包含資料的模型
  11.          model: menuListModel
  12.  
  13.          // 控制選單切換的移動
  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.      }

另外,因為ListView繼承自Flickable,列表會對滑鼠拖曳與手勢有所反應。上面程式的最後指定了Flickable屬性以為我們列表建立需要的翻轉移動。尤其,highlightMoveDuration屬性可用來影響翻轉時所需的時間。較高的highlightMoveDuration屬性值會造成較慢的選單切換。

ListView透過索引來對項目進行維護,在模型中每個可見的項目都可以依照宣告的順序透過索引進行存取。變更currentIndex就會更換目前顯示高亮度的項目。
我們選單的標題列正是這個效果的示範。兩個同在一個列中的按鈕在被按下時,兩者都會變更目前的選單。當fileButton被按下時,會修改目前選單為檔案選單。之所以指定 index 為 0,是因為 FileMenu 在menuListModel中宣告時是第一個。同樣的,editButton被按下時,會將目前選單設定為EditMenu。

labelList的矩形有一個為1的z值。這表示他會被顯示在選單列的前面。有較高z值的項目會被顯示在較低z值項目的前面。項目的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.      }

我們所建立的選單列可以透過翻轉或是按下選單名稱的方式來存取選單。切換選單螢幕感覺起來是直覺而且反應靈敏。

您的瀏覽器不支援圖片顯示

建立文字編輯器

宣告文字編輯區

如沒有一個可編輯的文字區域,我們的編輯器就稱不上是一個文字編輯器了。QML的 TextEdit:http://doc.qt.nokia.com/4.7/qml-textedit.html 元素允許宣告一個多行文字編輯區域。TextEdit與Text元素不同,後者不允許使用者直接編輯文字。

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

這個編輯器有自己的字體顏色屬性集合,已及換行的設定。TextEdit區域是在一個可翻轉的區域內,當文字游標超出可視區域時將會捲動文字。 ensureVisible() 函式會檢查游標矩形是否超出可視範圍,並據此移動文字區域。QML使用JavaScript語法來撰寫其腳本,如前面所提及的,JavaScript檔案可以被載入並且在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.      }

組合文字編輯器的元件

現在,我們已經準備好了使用QML來建立文字編輯器的排版。這個文字編輯器有兩個元件,我們所建立的選單列及文字區域。QML允許我們重複使用元件,如此一來,我們可以透過載入元件並且視需要客製化來讓程式碼簡單點。我們的文字編輯器將視窗一分為二,其中三分之一的部份給選單列使用,三分之二則留給文字區域顯示內容。我們的選單列顯示在任何其他元素之前。

  1.      Rectangle{
  2.  
  3.          id: screen
  4.          width: 1000; height: 1000
  5.  
  6.          // 螢幕分割為MenuBar及TextArea。1/3部分給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.      }

藉由載入可重用的元件,我們的 TextEditor 程式碼看起來更簡單了。我們可以再次客製化主程式,不用擔心那些已經定義行為的屬性。透過這樣的方式,應用軟體的配置及UI元件可以簡單的建立出來。

您的瀏覽器不支援圖片顯示

裝飾文字編輯器

實現抽屜介面

我們的文字編輯器看起來有點簡單,現在我們要來裝飾它。使用QML,我們可以宣告轉換行為而讓文字編輯器出現動畫效果。我們的選單列佔用了1/3的螢幕,比較好的方式應該是當我們有需要的時候才顯示這些區域。

我們可以加入一個抽屜介面,它會在被按下的時候,放大或縮小選單列。在我們的實作中,我們有一個細窄的矩形區域來回應滑鼠的點擊。我們的抽屜及整個應用程式都有兩個狀態,“抽屜打開“及“抽屜關閉“。抽屜項目是一個具有很小高度的矩形。內部有一個嵌入的Image元素顯示一個箭頭圖示,並且置於抽屜的中央位置。只要使用者在滑鼠區域進行點擊,抽屜會透過screen這個識別符,指定一個狀態給整個應用程式。

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

一個狀態就是一組組態設定,透過State元素來描述它。一系列的狀態可以綁定在states屬性上。在我們的應用程式中,有兩個命名為DRAWER_CLOSED與DRAWER_OPEN的狀態。項目的組態是透過PropertyChanges元素來指定。在DRAWER_OPEN狀態下,有四個項目會收到狀態改變的通知。第一個是menuBar會把 y 屬性設成 0。與此類似的,textArea也會在DRAWER_OPEN狀態下降低位置。textArea,drawer及它的icon將會遭遇屬性變更以符合目前的狀態。

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

狀態的改變是突然發生的,但是我們需要能夠比較平順的轉換行為。狀態之間的轉換是透過使用Transition元素來定義,並且可於稍後綁定在項目的transitions屬性上。我們的文字編輯器狀態的轉換,最後都會是DRAWER_OPEN及DRAWER_CLOSED的其中之一。重要的是,轉換需要一個來源狀態與目的狀態,但對於我們來說,我們可以使用萬用字元 * 來表示這個轉換是給全部的狀態變化用的。

在轉變的過程中,我們可以指定動畫效果到屬性變動上。我們的menuBar開關位置從y:0變成y:-partition,我們可以使用 NumberAnimation [doc.qt.nokia.com] 元素來讓這個變化有動畫的效果。我們可以宣告目標的屬性將依據某段指定的時間及某種消退曲線(easing curve)來進行動畫效果的變化。一個消退曲線可以控制動畫的速率並且依據狀態之間的變化進行必要的內插。我們選擇的消退曲線是 Easing.OutQuint [doc.qt.nokia.com] ,這會讓移動的速度在動畫接近結束時減緩。詳見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.      ]

另一種動畫屬性變更的方法是透過宣告一個 Behavior [doc.qt.nokia.com] 元素。轉換僅發生在狀態的變化上,但是 Behavior 可以針對一般的屬性變化設定一個動畫。在文字編輯器中,箭頭有設定 NumberAnimation 在屬性變化的時候驅動 rotation 屬性。

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

有了狀態及動畫的相關知識後,讓我們回到我們的元件上。現在,我們可以強化元件的顯示。在 Button.qml 裡,我們在按鈕被按下時,加入 color 及scale 屬性變化。顏色型態可以使用 ColorAnimation。而數字屬性可以使用 NumberAnimation。 下面的 on propertyName 語法在針對單一的屬性時會很有用。

在 Button.qml:

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

此外,我們可以透過加入顏色效果增強我們 QML 元件的顯示,如 漸層(gradients) 及透明(opacity)等效果。宣告一個 Gradient 元素可以覆蓋元件的顏色屬性設定。你也可以在漸層設定中透過 GradientStop 指定一個顏色。漸層的位置(position)是透過介於 0.0 到 1.0 的數值來指定。

在 MenuBar.qml

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

gradient 被用於選單列中以顯示一個具有模擬深度的漸層。第一個顏色由0.0開始到1.0為止。

下一步該怎麼做

我們已經完成了一個簡單的文字編輯器的使用者介面的建造。使用者介面已經完成,接著我們就可以透過標準的Qt與C++實現應用軟體邏輯。QML是一個很好的原型建構工具並且能將城市邏輯與使用者介面分隔開來。

您的瀏覽器不支援圖片顯示

使用Qt C++擴展QML

現在我們有了自己的編輯器布局,我們可以開始使用C++來實現編輯器功能。在QML中搭配使用C++讓我們得以使用Qt來建立應用軟體的邏輯。我們可以在C++中使用Qt的Declarative類別建立一個QML的環境,並且使用Graphics Scene來顯示 QML 元素。此外,我們也可以選擇匯出我們的C++程式碼到一個qmlviewer可以讀取的plugin中。對我們的應用程式而言,我們應該在C++實現載入及儲存的功能,並且將他們匯出成為一個plugin。如此一來,我們只需要直接載入QML檔案而不需要執行整個可執行檔。

將C++類別透露給QML

我們使用Qt及C++來實作檔案存取的功能。C++的類別與函式可透過註冊的方式讓QML使用。這些類別也需要編譯成Qt plugin的形式,同時QML檔案也需要知道去哪找到這個plugin。

對我們的軟體而言,我墳需要建立下面的項目:

1. 用來處理目錄相關操作的Directory類別。 2. 用來模擬在一個目錄中的檔案列表的File類別,這個類別是一個QObject。 3. plugin類別將會將這些類別註冊到QML的環境下。 4. 會將plugin進行編譯的Qt專案檔。 5. 一個qmldir檔案用來告訴qmlviewer工具去哪找到這些plugin。

建立一個Qt Plugin

要建立一個plugin,我們需要設定一個下面所列的Qt專案檔。首先,指定需要的原始程式檔,標頭檔及Qt模組。所有的C++程式碼與專案檔案都放在 filedialog 的目錄中。

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

在我們將Qt與declarative模組一起編譯,且指定將它編譯成plugin時,特別要注意的事需要指定lib作為我們的樣板。我們應該將編譯好的plugin放到父目錄中的plugins子目錄。

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

我們的plugin類別 DialogPlugin 是一個QDeclarativeExtensionPlugin的子類別。現在我們需要實現繼承的函式 registerTypes()。
dialogPlugin.cpp看起來如下:

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

registerTypes()函式會將我們的 File 及 Directory 類別註冊到QML中。這個函式需要一個類別名稱作為樣板,以及一個主版本,副版本還有一個自訂的名稱。

我們需要使用 Q_EXPORT_PLUGIN2 這個巨集來將plugin匯出。注意,在我們的 dialogPlugin.h 中,在類別的開頭處使用了 Q_OBJECT 巨集。而且,我們需要執行 qmake 來從專案檔案產生需要的 meta-object 程式碼。

在C++類別中建立QML屬性

我們可以使用C++及Qt的Meta-Object系統建立QML的元素及屬性。我們可以使用 slots 及 signals 實現屬性,讓Qt可以知道這些屬性的變化。稍後,這些屬性就可以在QML中使用。

在文字編輯器中,我們需要能夠存取檔案。通常這些功能都包含在一個檔案對話框中。幸運的是,我們可以使用QDir, QFile 及 QTextStream 來實現目錄的讀取及輸出入串流。

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

這個 Directory 類別使用 Qt 的 Meta-Object 系統來註冊用於檔案處理的屬性。這個類別是透過如同plugin一般的方式匯出的,可以在QML當成一個Directory元素來使用。每個使用 Q_PROPERTY 巨集所列出的屬性都是一個QML屬性。

Q_PROPERTY 宣告了一個屬性及他的讀寫函式到Qt的Meta-Object系統。舉例來說,filename屬性是QString型態,它是透過filename()函式讀取其內容,使用setFilename()函式來寫入內容。此外,有一個signal稱為filenameChanged()綁定在 filename屬性上,只要這個屬性有所變化,就會發射訊號。讀寫函式在標頭檔中都是宣告成public的。

同樣的,我們可以依照需要來宣告其他的屬性。 filesCount屬性代表在目錄中的檔案數目。filename屬性是代表目前所選擇的檔案,至於載入或是儲存的檔案內容則是放在fileContent屬性中。

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

這個 files 的屬性是在指定目錄下經過篩選的檔案列表。 Directory 類別是被設計能夠篩選掉無效的文字檔案; 只有附檔名是 .txt 的才被認為是有效的。近一步的,QLists 可以透過在 C++ 中宣告成 QDeclarativeListProperty 後,就可以在QML中使用它。這個樣板的物件需要由QObject繼承而來,因此,File類別必須也繼承自QObject。在Directory類別中,File物件的列表是被儲存於稱為m_fileList的QList變數中。

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

這個屬性在稍後可以當成Directory元素屬性的一部分而在QML使用。注意,在我們的C++程式碼中,並沒有一定要建立一個 id 識別字屬性。

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

因為 QML 使用 Javascript的語法及結構,我們可以遞迴的尋訪檔案列表並且取得其屬性。要取得第一個檔案的屬性,我們可以呼叫 files0.name。

普通的C++函式也可以在QML中被存取。像是檔案的儲存與載入函式就是在C++被實現的,並且透過Q_INVOKABLE巨集來宣告。另外,也可以宣告函式成為slot,這樣也可以在QML中存取它。

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

Directory類別也必須在目錄內容有所變動時通知其他的物件。這個特性是透過signal的方式達成。如前眠所提及的,QML signal有對應的處理函式存在,這個函式名稱就是在signal的前面加上 on 字串。這個signal叫做directoryChanged,它會在一個目錄被刷新的時候發出。所謂的刷新就是簡單的重新載入目錄內容並且更新有效的檔案列表。QML項目可以在稍後透過將一個動作綁在onDirectoryChanged訊號處理函式上來得到這個通知。

list 屬性需要在稍後能夠被使用。因為 list 屬性使用callback來存取及修改其內容。list屬性是QDeclarativeListProperty<File>的形態。無論何時,只要這個屬性被存取,存取函式就需要能夠傳回一個QDeclarativeListProperty<File>。其中,樣板的形態 File 必須衍生自 QObject。接著,要建立這個QDeclarativeListProperty,list的存取函式及修改函式需要將建構子當成函數指標傳入。 list(在我們的範例中是一個 QList)也需要是一個 File 指標的列表。

QDeclarativeListProperty的建構子集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 );

建構子會傳入那些新增列表,計算列表內容數目,透過索引存取項目及清空列表的函式指標。其中,只有新增列表函式是必須的。注意,函式指標需符合AppendFunction,CountFunction,AtFunction及ClearFunction的定義。

  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)

要簡化我們的檔案對話框,Directory類別篩掉無效的文字檔案(那些附檔名不是.txt的檔案)。如果一個檔案名稱沒有.txt作為附檔名,它將無法在我們的檔案對話框中被看到。也就是說,這個實作確保了檔案一定會被存在一個附檔名為.txt的檔案中。Directory使用QTextStream來讀取及輸出檔案內容到實際檔案中。

在我們的Directory元素中,我們能夠用列表的方式取得檔案,並且查知多少檔案在目錄中,以字串的形式取得檔案的名稱及內容,以及在目錄內容有變動的時候被通知到。

要建立這個plugin,對cppPlugins.pro執行qmake程式,接著使用make來建立及傳送這個plugin到plugins的目錄中。

在QML中載入一個Plugin

qmlviewer工具會載入與應用程式相同路徑下的檔案。我們也可以建立一個名為qmldir的檔案,並且在其中放入我們所希望載入的QML檔案的位置。這個qmldir檔案可以儲存plugin及其他資源的位置。

  1. 在 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叫做FileDialog,也就是專案檔中的TARGET欄位所指定的名稱。編譯過的plugin會放在plugins的目錄下。

整合檔案對話框到File選單中

我們的FileMenu需要顯示FileDialogh的元素,並且包含一系列的在目錄中的文字檔案,以便讓使用者可以在列表中選擇所要的檔案。我們也需要指定save,load及new按鈕到個別所對應的動作上。FileMenu包含一個可編輯的文字輸入框讓使用者透過鍵盤輸入檔案名稱。

Directory元素用於FileMenu.qml中,它會在目錄刷新後通知FileDialog元素。這個通知是透過onDirectoryChanged達成。

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

為了讓我們的程式保持簡單,檔案對話框將會永遠處於可見狀態,也不會顯示那些附檔名不是.txt的無效文字檔案。

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

FileDialog元素會透過讀取files列表屬性來顯示目錄的內容。files屬性被當成GridView元素的資料模型來使用,這將會讓delegate能夠在一個表格中顯示資料元素。delegate處理模型的顯示外觀,我們的檔案對話框將會簡單的建立一個具有置中格式文字的表格。用滑鼠在檔名上單擊將會導致一個矩形出現在檔名四周用來強調被選擇的項目。任何時候只要notifyRefresh訊號發生,FileDialog都會被通知,並且重新載入目錄下的檔案。

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

現在,我們的FileMenu可以連結到他們的個別的動作上。saveButton會將TextEdit上的文字傳送到directory的fileContent屬性中,接著將它的檔名複製到可編輯的文字輸入視窗內。最後,這個按鈕會呼叫saveFile()函式來儲存檔案。sloadButton也是相似的動作。此外,New action將會清除TextEdit的內容。

EditMenu按鈕也連接到TextEdit的函式,可以對文字編輯器進行複製,貼上及選擇全部文字等動作。

您的瀏覽器不支援圖片顯示

文字編輯器完工

您的瀏覽器不支援圖片顯示

這個軟體可以像個簡單的文字編輯器工作,它能夠接受文字,並且儲存文字到檔案中。這個文字編輯器也可以從檔案中載入內容並且進行文字操作。