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

QML编程入门

欢迎来到QML的世界,QML是说明式界面语言。在本文里,我们用QML创建一个简单的文本编辑器。读了本文后,您就可以用QML和Qt C++开发你自己的应用程序了。

用QML来创建用户界面

我们要创建的程序是一个简单的文本编辑器,它可以读,写和其他的文本操作。本教程包含两部份。第一部分是用QML设计程序界面和功能。第二部分是用Qt C++来实现文件读和写。使用Qt的Meta-Object系统,我们可以把C++的函数当做QML元素可以使用的属性。使用QML和Qt C++,我们可以有效地把界面逻辑和程序逻辑分开。

text editor

要想运行QML程序,可以运行qmlviewer再加上QML文件名作为参数。这个教程的C++部分需要你拥有基本的Qt程序编译知识。

教程章节

1 定义按钮和菜单
2 实现菜单条
3 创建文本编辑器
4 装饰文本编辑器
5 用Qt C++扩展QML

定义按钮和菜单

基本的组件 — 按钮

我们现开始创建一个按钮。从功能上来说,一个按钮有一个鼠标感应区和一个标签。当用户按按钮时,按钮执行动作。
i在QML里,基本的可视元素是Rectangle. Rectangle元素有属性来控制外观和位置。

首先要import Qt 4.7,
这样qmlviewer可以导入我们以后需要的QML元素。每一个QML文件必须有这一行。需要注意Qt的版本号要在这一个行里。
这个简单的rectangle有一个属性id, simplebutton,
Rectangle的属性和值绑定在一起,先是属性,冒号,然后是值。在例子程序里,颜色grey绑订到Rectangle的color属性。同理,我们也绑订width和height.

Text元素是不可编辑的区域。我们叫它buttonLabel.为了设置Text区域的字符,我们绑订一个值到text属性。为了使标签在Rectangle的中心,我们使用Text元素的anchor固定到它的parent,这里是simplebutton.Anchor也可以绑订其他元素的anchor.这样界面设计就非常简单了。

我们把代码存到SimpleButton.qml.
运行qmlviewer,用这个文件明作为参数。就可以显示出下面的灰色矩形和文本标签。

simple button

为了实现按钮功能,我们可以使用QML事件处理机制。QML事件处理机制和Qt的signal-slot的机制非常像。信号被出发,连接好的slot函数被调用。

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

simplebutton中包含了MouseArea元素。MouseArea元素描述了检测鼠标移动的交互区域。对于这个按钮,我们把整个MouseArea固定在它的parent – simplebutton. anchors.fill语句可以读取属性anchors的fill属性。QML用基于anchor的布局来把几个项目固定另外一个项目上,这样可以创建稳定的布局。

如果在指定的MouseArea边界里移动鼠标,MouseArea有很多被调用的信号处理函数,其中之一是onClicked,当鼠标按钮被按下时,onClicked被调用。缺省是鼠标左键。我们可以把动作绑定到onClicked处理函数。在我们的例子里,当鼠标在MouseArea被按下的时候,console.log()输出文本。在调试和输出文本时,函数console.log()是一个在非常有用的工具。

SimpleButton.qml的代码可以在屏幕上显示按钮,并且鼠标点击时输出文本。

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

功能完整的按钮代码在Button.qml中。本文中的代码片断有遗漏,有些代码在前面的章节里已经介绍过,有些代码根本文无关。

自定义的属性用property type name语句来声明。在代码中,声明了buttonColor属性,并给它赋值lightblue. buttonColor在后面会在决定按钮的填充颜色条件语句中被用到。注意除了用冒号来赋值绑定,给属性赋值可以等号=。自定义属性允许内部属性被Rectangle的外部来访问。基本的QML类型有int,string, real, 和variant.

通过把颜色绑定到onEntered和onExited信号处理函数,鼠标移到按钮上,按钮的边界会变成黄色,当鼠标移出按钮,边界会变成原来的颜色。
通过把signal放在信号名字的前面来声明buttonClick()信号。所有信号的处理函数都会被自动创建,信号的名字都是以on开始。在结果上onButtonClick就是buttonClick的处理函数。onButtonClick被指定去执行一个动作。在我们的按钮例子里,onClicked鼠标处理函数调用onButtonClick, onButtonClick会显示文本。onButtonClick可以使外部对象很容易访问按钮的鼠标区域。例如,声明多个MouseArea和一个buttonClick信号的对象可以在几个MouseArea信号处理函数中进行更好的区分。

我们现在基本了解用QML来实现可以处理鼠标移动的对象。我在Rectangle中创建一个Text标签,定义它的属性,实现处理鼠标移动的行为。我们在整个文本编辑器中将很多次在对象中创建对象。

如果这个按钮不想一个组件一样执行操作,那是没什么用的。在下一节里我们要创建包含几个按钮的菜单。

button

创建菜单

现在我们介绍了在一个QML文件里创建元素和定制行为。在这部分,我们介绍如何导入QML元素,如何重用元素来创建新的元素。

菜单显示列表的内容,列表中每一项都可以执行操作。在QML中我们有几种方法来创建菜单。第一种创建包含按钮的菜单,每个按钮可以执行特定的操作。菜单的代码在FileMenu.qml中.

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

上面的代码表示如何使用import关键字。这里面需要使用javascript文件或者在另外的目录下的QML文件。因为Button.qml和FileMenu.qml在同一个目录下,所以我们不用导入Button.qml.我们可以直接通过声明Button{}来创建Button元素,这跟Rectangle{}声明类似。

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

在FileMenu.qml中,我们在Row元素里声明了3个Button元素。Row元素里有三个垂直的子对象。Button在我们前面介绍的Button.qml里声明。可以在新建的按钮里声明新的属性绑定,覆盖Button.qml里的属性。名字叫做exitButton被点击时,程序退出。注意除了调用exitButton里的函数onButtonClickon,还有在Button.qml里的信号处理函数onButtonClick也会被调用。

buttons

在Rectangle里声明了Row,为一排按钮创建了一个矩形容器。这个额外的矩形间接地在菜单里组织了按钮。

编辑菜单的声明也非常类似。菜单包含按钮,按钮的标签是拷贝,粘贴和全选。

edit buttons

我们已经知道了如何导入,定制刚才创建的组件,我们可以把这些菜单页组合起来创建一个菜单条。菜单条包含选择菜单的按钮,下面我们看看如何用QML组织数据。

实现菜单条

我们的文本编辑器需要用菜单条来显示菜单。菜单条要在不同的菜单间切换,用户可以选择那个菜单来显示。菜单切换意为着菜单需要比一排菜单更复杂的结构。QML用Models和Views来组织和显示数据。

使用数据的Models和Views

QML有不同的显示data Models的data views. 我们的菜单条在列表中显示菜单,用头来显示一排菜单的名字。在VisualItemModel里面声明菜单列表。VisualItemModel元素包含有视图的条目,比如Rectangle元素和导入的UI元素。其他的类型的Model需要一个代理来显示它们的数据。

我们在menuListModel声明两个可视条目,FileMenu和EditMenu。我们定制了两个菜单,使用一个ListView来显示他们。MenuBar.qml文件包含了QML声明,在EditMenu.qml里定义了一个简单的编辑菜单。

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

ListView元素使用代理来显示Model。代理可以声明Model的条目,然后显示一排或者显示在网格里。在menulistModel里已经有了可视项目,我们不要再声明代理了。

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

另外,ListView从Flickable派生出来,因此列表可以响应鼠标拖放和其他动作。上面代码的最后一部份设置Flickable属性来给视图创建希望的flicking动作。特别地,higlightMOveDuration属性改变flick变化的时间。更大的highlightMoveDuration值会使菜单切换变得更慢。

ListView包含数据项目,通过index以声明顺序来访问每个项目。改变currentIndex可以改变ListView里面被选中的项目。菜单条的头就是这样。在一排里有两个按钮,当点击按钮时,当前的菜单就被改变。fileButton把当前的菜单变成文件菜单,index是0,因为FileMenu在menuListModel第一个被声明。类似地,editButton把当前的菜单变成EditMenu.

labelList矩形的z值是1,表明它被显示在菜单条的前面。有更高z值的项目显示在低z值项目的上面。缺省的z值是0.

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

我们可以点击上面的菜单名字来弹出刚才创建的菜单条。切换菜单的感觉很直接,响应很快。

menu bar

创建文本编辑器

声明文本区域

我们的文本编辑器必须有文本区域。QML的TextEdit元素可以容纳多行可编辑的文本区域。TextEdit和Text元素不同,Text元素部允许编辑文本。

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

编辑器有字体属性,可以自动换行。TextEdit区域在Flickable区域里面,如果光标出了可见区域,TextEdit可以自动滚动。ensureVisible()函数可以查看光标在边界外面,然后相应地移动文本区域。QML用Javascript的语法,Javascript文件可以被导入到QML文件里。

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

把组件整合成文本编辑器

我们现在可以用QML来创建文本编辑器的界面。编辑器有两个组件,菜单条和文本区域。QML可以通过导入和重新定制来重用组件,使我们的代码更简单。文本编辑器把窗口分成两部份,三分之一是菜单条,三分之二是文本区域。菜单条显示在其他元素之上。

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

通过导入重用组件,我们的TextEditor代码非常简单。我们可以定制主程序,而不比担心已经定义的属性。用种方法可以很容易创建UI界面。

text editor

装饰编辑器

实现抽屉接口

我们的编辑器太简单了,我们要装饰它。应用QML我们可以实现渐变和动画。菜单条占了三分之一窗口,应该只有用它的时候才显示它。

我们可以加一个抽屉接口,当点击时,可以扩展菜单条。在我们的程序中,有一个很窄的矩形来响应鼠标点击。抽屉和程序一样,有两个状态,抽屉开和抽屉关。抽屉元素是一条矩形,高度非常小。在抽屉的中心,有一个内嵌的Image元素-箭头图标。当鼠标点击鼠标区域时,抽屉用id screen指定了整个程序的状态.

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

状态就是配置的集合,它声明在State元素里。一系列状态可以显示和帮定在states属性里。在我们的程序里有两个状态DRAWER_CLOSED和DRAWER_OPEN.在PropertyChange元素里声明状态配置。在DRAWER_OPEN状态,有四个条目会有属性变化。第一项,menuBar的y属性变成0。类似地,当状态是DRAWER_OPEN时,textArea会向下移。TextArea, drawer, 和drawer的图标的属性都有相应的变化来符合当前的状态。

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

状态变化是不连贯的,应该是平滑的渐变。状态之间的渐变在Transition元素中定义,绑定到对象的transitions属性。文本编辑器状态变化到当DRAWER_OPEN或DRAWER_CLOSED时,状态是渐变的。要注意,渐变需要一个from状态和to状态,我们这里用*, 说明渐变适用于所有的装太变化。

在渐变中,我们可以在属性变化中使用动画。menuBar从位置y:0变到y:-partition,我们可以用NumberAnimation元素来产生动画。我们定义渐变的时间和曲线缓和来定制动画。曲线缓和在状态渐变过程中来控制动画的速度和插值。我们选择Easing.OutQuint作为曲线缓和,Easing.OutQuint在动画结束时减慢速度。详情请参考QML的动画文档。

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

另外一种属性变化动画的方法是声明Behavior元素。Transition只在属性变化时起作用,Behavior可以设定通用的属性变化动画。在文本编辑器中,箭头可以设置NumberAnimation的旋转动画属性。

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

让我们回到有状态和动画的组件,我们可以使组件的外感更好看。在Button.qml中,我们在按钮被按下时,加上color和scale属性变化。颜色类型动画使用ColorAnimation,数字使用NumberAnimation. 下面的on propertyName语法在对单一属性时非常有用。

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

另外,我们可以加上颜色效果,比如梯度和透明效果。Gradient元素可以覆盖color属性。我们可以用GradientStop元素声明颜色的梯度。梯度位置的值在0.0和1.0之间。

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

菜单条用梯度来显示颜色深度。第一个颜色从位置0.0开始,最后一个颜色在位置1.0结束。

下一步

我们完成了文本编辑器的用户界面。下一步,我们可以用Qt和C++实现程序逻辑。QML是一个非常好原型工具,把程序逻辑和用户界面分开。

text editor

用Qt C++来扩展QML

现在我们有文本编辑器的界面,我们可以用C++来实现编辑器的功能。用QML和C++,我们可以用Qt创建程序。我们可以用Qt的Declarative类在C++程序里创建QML环境。我们也可以创建qmlviewer工具可以读取的C++插件。在我们的程序里,我们用C++来实现读取和存储功能,然后导出插件。这样,我们只要直接读取QML,而不用运行程序文件。

在QML中使用C++的类

我们用Qt和C++来实现文件的读取和存储。在QML中注册C++的类和函数后,可以在QML中使用他们。C++类需要被编译成Qt插件,并且QML需要知道插件的位置。

对于我们的程序,我们需要创建一下东西:
1. Directory类,处理目录的操作
2. File类,从QObject派生,模拟目录的文件列表
3. plugin类,注册到QML的环境中
4. Qt项目文件,编译插件
5. qmldir文件,告诉qmlviewer工具插件的位置

创建Qt插件

为了创建一个插件, 我们要在Qt项目文件里设置下列东西。首先,必须的源程序,头文件,Qt模块。所有的C++代码和项目文件都在filedialg目录里。

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

特别地,我们用declarative模块编译Qt插件,所以模板是lib. 我们把编译好的插件放在父目录的plugins目录里。

把类注册到QML中。

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

插件类DialogPlugin是QDeclarativeExtensionPlugin的派生类。我们需要实现虚函数registerTypes(). 下面是dialogplugin.cpp:

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

registerTypes()函数把File类和Diretory类注册到QML里。这个函数需要类模板,重要版本号好,次要版本号和类名字。

我们用Q_EXPORT_PLUGIN2宏来导出插件。注意dialogPlugin.h文件,Q_OBJECT宏必须在类的最前面。此外,我们必须运行qmake来产生必须的元代码。

在C++类中创建QML属性

我们可以用C++和Qt的元对象系统来创建QML元素和属性。我们可以用slots和signal来实现属性,使Qt知道这些属性,这些属性可以用在QML里。

文本编辑器需要有文件读取和存储功能。这些功能存在于文件对话框里。幸运的是,我们可以用QDir,QFile和QTextStream来实现目录读取和输入输出流。

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

Directory类用Qt的元数据系统来注册文件处理所需要的属性。把Directory类导出成插件,在QML里作为Directory元素使用。每一个用Q_PROPERTY宏定义的属性也是QML里的属性。

Q_PROPERTY声明了属性,同时也声明了元对象系统的读和写的函数。例如,filename属性,是QString类型,用filename()函数来读,用setFilename()来写。另外还声明了一个与filename属性关联的信号叫filenameChanged(),当属性变化时就发出这个信号。在头文件里读和写的函数都是公有函数。

类似地,我们还声明了其他属性。filesCount属性表示目录的文件数。filename属性就是当前所选的文件的名字,读取或存储的文件内容存在fileContent属性里。

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

files列表属性是一个目录下所有经过过滤的文件列表。Directory类实现了滤掉无效文件的功能,只有.txt结尾的文件才是有效的。只要在C++中声明为QDeclarativeListProperty,QLists可以用在QML文件中使用。模板对象需要从QObject派生出来,因此File类也要从QObject派生出来。在Directory类中,File对象的列表存储在QList中,名字是m_fileList.

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

这些属性可以在QML中作为Directory元素的属性来被使用。注意,我们不必在C++中创建id属性。

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

因为QML使用Javascript的语法和结构,我们可以迭代列表并读取它的属性。如果要读取第一个文件的名字属性,我们用files0.name.

从QML中可以访问通常的C++函数。文件的读取和存储是用C++来实现的,但需要用Q_INVOKABLE宏来声明的。我们也可以把函数声明成slot,这样也可以从QML中访问此函数。

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

Directory类还要在目录内容变化时通知其他对象。我们用signal来实现这个功能。我们前面提过,QML信号有相应的以on开始的处理函数。这个信号叫directoryChanged,当目录更新的时候就会发出这个信号。刷新就是重新读取目录内容,并更新目录的有效文件列表。通过把一个动作关联到onDirectoryChanged信号处理函数来通知QML对象。

列表属性需要进一步讨论。列表属性用回调函数来读取和修改列表的内容。列表属性是QDeclarativeListProperty<File>类型。当列表被读取时,读取函数需要返回QDeclarativeListProperty<File>。File作为模板类型,需要从QObject派生出来。另外,要创建QDeclarativeListProperty,列表的读取和修改需要作为函数指针来传给构造函数。我们的QList的列表需要成为File指针的列表。

下面是QDeclarativeListProperty的构造函数和Directory的实现:

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

构造函数把指针传递给添加列表,列表计数,通过索引来取内容和清空列表的函数。只有添加函数是必不可少的。注意函数指针必须与AppendFunction, CountFunction, AtFunction 和ClearFunction的定义匹配。

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

为了简化我们的文件对话框,Directory类过滤掉了无效的文本文件,无效的文件的扩展名不是.txt的扩展名。如果一个文件名不是.txt的扩展名, 那我们的对话框就看不见。同时要确保被存储的文件名有.txt的扩展名。Directory用QTextStream来读和写文件。

使用Directory元素,我们可以读取文件列表,在目录里文本文件的数目,读取文件名和内容到字符串里,如果目录内容发生变化会接到通知。

要编译插件,对cppPlugins.pro项目文件来运行qmake,运行make编译并把插件拷贝到plugins目录里。

在QML里导入插件

qmlviewer工具可以导入应用程序当前目录下的文件。我们还可以创建qmldir文件,qmldir包含要导入QML文件的位置,插件和其他资源的位置信息。

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

我们刚才创建的插件叫FileDialog,在项目文件里的TARGET里被定义的。编译好的插件在plugins目录里。

把文件对话框集成到文件菜单里

FileMenu要显示FileDialog元素,FileDialog里包含目录里的文本文件列表,这样用户可以通过点击列表来选择文件。我们需要把相应的动作指定到存储,读取和创建按钮. FileMenu包含可编辑的输入文本框,让用户用键盘来输入文件名。

FileMenu.qml用Directory元素来通知FileDialog元素目录内容刷新。在信号处理函数onDirectoryChanged里发出通知。

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

为了是程序简单,文件对话框总是可见的,并且不会显示无效的文件(不是.txt扩展名)。

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

FileDialog元素读取files列表属性并显示目录内容。文件被当作GridView元素的文档,GridView通过代理用网格的形式来显示数据。代理来处理文档的外观,我们的文件对话框创建了文本在中间的网格。点击文件名就会出现一个矩形来选中文件名。当收到notifyRefresh信号时,FileDialog接到通知,重新读取目录的文件。

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

我门的FileMenu现在连接好了相应的操作。saveButton会把文本从TextEdit传到目录的fileContent属性,然后从可编辑文本框拷贝文件名。最后,这个按钮调用saveFile()函数来存储文件。loadButton根前面的很类似。New action会清空TextEdit.

另外,EditMenu按钮与TextEdit函数拷贝,粘贴和全选连接到一起。

text editor

文本编辑器完成

text editor

这个程序可以向简单的编辑器一样,接受文本和存储文件。文本编辑器可以读取文件并执行文件操作。