QtQuick桌面应用程序开发指南 4)动态管理Note对象_B 5)增强外观 6)许多其余的改进

4.2.2 Stateless(无论状态)JavaScript库
web

为了让开发轻松点, 使用一个JavaScript接口来和数据库交互是个好主意, 它在QML中提供了方便的方法;
数据库

在QtCreator中建立一个新的JavaScript文件 noteDB.js, 保证选择了 State Library选项; 这样使得noteDB.js用起来像一个库, 提供了stateless的helper方法; 这样,在每个QML组件 import noteDB.js以及使用它的时候, 就仅仅有一份实例被载入和使用; 这也保证了仅仅有一个全局变量用来存储数据库实例: _db;数组

Note Non-stateless(非状态无关的)JavaScript文件在导入一个QML组件的时候很是实用, 可以对那个组件运行操做, 所有的变量仅仅有在那份context(上下文)中有效; 每次import都会建立一个独立的JavaScript文件的实例;
安全

noteDB.js应该提供如下的功能:
app

- 打开/建立一个本地数据库实例less

- 建立必须的数据库table(表格)工具

- 从数据库读取notes性能

- 删除所有的notes字体

接下来会看到 关于noteDB.js中的方法怎样被实现的不少的细节--读取, 存储note item到数据库的实现; 先考虑如下的方法实现:
动画

- function openDB() - 假设数据库不存在, 就建立一个, 存在就打开它;

- function createNoteTable() - 假设note table不存在就建立note table; 这种方法仅仅会在 openDB()中调用;

- function clearNoteTable() - 从note table中移除所有的行(row);

- readNotesFromPage(markerId) - 这个helper方法读取所有和特定markerId相关联的note, 返回一个dictionary(字典)类型的数据;

- function saveNotes(noteItems, markerId) - 用来在数据库中保存note item; noteItem參数表明一个note item的list(列表), markerId表明note item隶属的那个对应的page;

Note 所有这些JavaScript方法使用Qt Quick Local Storage API来获取数据库, 声明语句(statement) import QtQuick.LocalStorage 2.0 as Sql, 要写在 noteDB.js的開始; 

4.2.3 读取和存储Note

实现了noteDB.js以后, 可以用法来读取和存储note了; 

一个好的作法是在main.qml文件里, 在初始化的时候或打开数据库链接的时候才去调用; 这样咱们可以使用定义在noteDB.js中的JavaScript方法而不会又一次初始化(reinitializing)数据库;

在main.qml里面导入noteDB.js 和 QtQuick.LocalStorage 2.0, 有个问题是何时调用 openDB()方法; QML提供了helpful attached signal: onCompleted()和 onDestruction(), 会在Component被全然载入全然销毁的时候各自emit(发送);

// main.qml

1
2
3
4
5
6
7
8
9
import QtQuick 2.0
import  "noteDB.js"  as NoteDB
 
Item {
     // this signal is emitted upon component loading completion
     Component.onCompleted: {
         NoteDB.openDB()
     }
//...

如下是openDB方法实现, 它调用 openDatabaseSync()方法来建立数据库, 以后调用 createNoteTable()方法来建立note表;

//noteDB.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.pragma library
.import QtQuick.LocalStorage 2.0 as Sql
// declaring a global variable for storing the database instance
var  _db
 
function  openDB() {
     print( "noteDB.createDB()" )
     _db = openDatabaseSync( "StickyNotesDB" , "1.0" ,
             "The stickynotes Database" , 1000000);
     createNoteTable();
}
 
function  createNoteTable() {
     print( "noteDB.createTable()" )
     _db.transaction(  function (tx) {
         tx.executeSql(
             "CREATE TABLE IF NOT EXISTS note
             (noteId INTEGER PRIMARY KEY AUTOINCREMENT,
             x INTEGER,
             y INTEGER,
             noteText TEXT,
             markerId TEXT)" )
     })
}

在main.qml里面, 初始化了数据库, 安全地開始从Page组件中载入Note item; 上面咱们提到了 readNotesFromPage(markerId)方法, 返回一个data array(数组)list (參照script world--脚本世界里的dictionary), 每个数组表明数据库中的一行--note的数据; 

//noteDB.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//...
function  readNotesFromPage(markerId) {
     print( "noteDB.readNotesFromPage() "  + markerId)
     var  noteItems = {}
     _db.readTransaction(  function (tx) {
         var  rs = tx.executeSql(
             "SELECT FROM note WHERE markerId=?
             ORDER BY markerid DESC" , [markerId] );
         var  item
         for  ( var  i=0; i< rs.rows.length; i++) {
             item = rs.rows.item(i)
             noteItems[item.noteId] = item;
         }
     })
     return  noteItems
}

Page组件会读取note而后建立对应的QML note对象;

// Page.qml

1
2
3
4
5
6
7
8
9
10
11
//...
// when the component is loaded, call the loadNotes()
// function to load notes from the database
Component.onCompleted: loadNotes()
// a Javascript helper function that reads the note data from database
function  loadNotes() {
     var  noteItems = NoteDB.readNotesFromPage(markerId)
     for  ( var  in  noteItems) {
         newNoteObject(noteItems[i])
     }
}

咱们可以看到 newNoteObject()方法在以前的Page.qml定义, 它获得一个data数组做为參数, 实际上就是 x, y, noteText, markerId和 noteId属性的值;

Note 注意note表的name域(field)和Note组件的属性同样; 这可以帮助咱们把table行的数据做为參数传递, 建立note QML对象; 

现在咱们实现了从数据库载入Note item的方法, 下一个逻辑步骤是实现一个将note存储到DB中的方法; PagePanel组件是负责建立Page item的, 所以它应该可以从每张page上读取note, 而后调用 saveNote() JS方法来存储note;

//noteDB.js

1
2
3
4
5
6
7
8
9
10
11
12
//...
function  saveNotes(noteItems, markerId) {
     for  ( var  i=0; i<noteItems.length; ++i) {
         var  noteItem = noteItems[i]
         _db.transaction(  function (tx) {
             tx.executeSql(
             "INSERT INTO note (markerId, x, y, noteText)
             VALUES(?

,?

,?,?

)",

             [markerId, noteItem.x, noteItem.y, noteItem.noteText]);
         })
     }
}

首先咱们定义一个属性alias可以将Note item暴露出来, 这些item是做为在Page组件中container的children而建立的;

// Page.qml

1
2
3
4
5
//...
// this property is used by the PagePanel component
// for retrieving all the notes of a page and storing
// them in the Database.
property alias notes: container.children

在PagePanel中实现在DB中保存note的功能;

// PagePanel.qml

1
2
3
4
5
6
7
8
9
10
11
//...
     Component.onDestruction: saveNotesToDB()
     // a JavaScript function that saves all notes from the pages
     function  saveNotesToDB() {
         // clearing the DB table before populating with new data
         NoteDB.clearNoteTable();
         // storing notes for each individual page
         NoteDB.saveNotes(personalpage.notes, personalpage.markerId)
         NoteDB.saveNotes(funpage.notes, funpage.markerId)
         NoteDB.saveNotes(workpage.notes, workpage.markerId)
     }

为了下降代码复杂度, 咱们在保存note以前清楚DB中所有的数据; 这样可以不用谢代码来更新现存的Note item;

在结束本章前, 用户可建立和删除note, 程序也可以本身主动保存和载入note;

下一步

下一章介绍一些美丽的动画, 以及实现的步骤和技巧;

---4End---


CHAPTER5 外观增强

NoteApp的UI可以看做依照功能和用户交互实现的; 事实上还有很是大的进步空间, 让UI更吸引用户; QML设计成一种声明式语言, 记得用动画和UI的流畅过分; 

这一章会一步步实现动画, 让NoteApp感受更优美(fluid); QML提供了一组QML类型以各类方式实现动画; 这一章会介绍一些新的类型, 在QML组件中使用它们, 让UI更加流畅;

总体上, 本章覆盖如下几点:

- 介绍animation(动画)和transition(过渡)效果;

- 新的QML类型: Behavior, Transition, 各类Animation元素;

- 使用各类动画强化NoteApp的QML组件;

5.1 NoteToolbar动画

咱们来看看怎样改进Note组件, 加入一些基于用户交互的行为; Note有一个toolbar, 可以用Delete tool删除笔记; toolbar是用来按下鼠标四处拖拽note用的;

一个改进是可以让Delete tool仅仅有在需要的时候可见; 好比, 在toolbar上悬停(hover)的时候让Delete工具可见, 使用 fade-in/fade-out(渐入渐出)效果更美丽;

Note opacity属性的值是从parent到child item传播的(propagated); [属性继承]

Behavior类型赞成咱们定义一个基于item的属性变化的行为; 

// NoteToolbar.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//...   
     MouseArea {
         id: mousearea
         anchors.fill: parent
         // setting hoverEnabled property to true
         // in order for the MouseArea to be able to get
         // hover events
         hoverEnabled:  true
     }
 
     // using a Row element for laying out tool
     // items to be added when using the NoteToolbar
     Row {
         id: layout
         layoutDirection: Qt.RightToLeft
         anchors {
             verticalCenter: parent.verticalCenter;
             left: parent.left;
             right: parent.right
             leftMargin: 15;
             rightMargin: 15
         }
         spacing: 20
         // the opacity depends if the mousearea
         // has the cursor of the mouse.
         opacity: mousearea.containsMouse ? 1 : 0
         // using the behavior element to specify the
         // behavior of the layout element
         // when on the opacity changes.
         Behavior on opacity {
             // using NumberAnimation to animate
             // the opacity value in a duration of 350 ms
             NumberAnimation { duration: 350 }
         }
     }
//...

上面代码可以看到, 咱们启用MouseArea的hoverEnabled属性, 接收鼠标悬停事件; 以后, 咱们可以把Row类型的opacity切换到0, 假设MouseArea类型没有悬停(hovered)就设为1; MouseArea的containsMouse属性用来决定opacity的值; 

这样Behavior类型在Row中被建立出来, 基于opacity属性而定义它的行为; 当opacity改变时, NumberAnimation会被应用; 

NumberAnimation类型应用基于一个数字值的改变, 咱们对Row的opacity属性, 在350毫秒时长内显示它;

Note NumberAnimation类型是继承自PropertyAnimation, 它有一个 Easing.Linear做为 easing curve动画的默认值; [动画效果, 执行轨迹等...]

下一步

咱们可以看到怎样使用Transition和其它QML Animation实现动画效果;


5.2 使用State和Transition

前面咱们看到的定义简单的话的技巧是基于属性变化的, 使用了 Behavior和 NumberAnimation;

固然, 也有其它动画是依赖于一组属性变化的--可以用State实现;

现在来看看如何进一步实现NoteApp的UI;

Marker item看起来在用户交互的时候是静态的; 假设基于用户交互的不一样场景给它加上一点动画会如何? 另外, 咱们可以可以让当前激活的marker和当前的page对用户来讲看起来更明明白; 

5.2.1 Marker Items加上动画

咱们差点儿相同可以总结一下在用户交互时改进Marker item的各类可能的场景, 下面是user case(用户用例)描写叙述:

- 当前激活的Marker item应该更加明显; 用户点击的时候, 一个marker要变成激活状态; 激活的marker会略微变大, 可以从左边向右边滑动一些;(像个抽屉)

- 当用户鼠标悬停在一个marker上面, marker从左到右滑动, 但不会像激活的marker同样滑动;

考虑上述场景, 咱们需要在Marker和MarkerPanel组件上进行工做;

当独当上面关于行为需求的描写叙述(左到右的滑动效果), 立刻出现的想法是改变 x的属性, 因为它表明了item在X-axis(轴)上的位置; 另外, 因为Marker item应该要知道它本身是不是当前激活的, 所以加入一个新的属性 active;

为Marker组件引入两个新的state, 表明上面描写叙述的行为:

- hovered: 当用户鼠标悬停的时候会更新marker的x属性值;

- selected: 当marker被激活, 即用户点击的时候, 会更新x属性值;

// Marker.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// this property indicates whether this marker item
// is the current active one. Initially it is set to false
property bool active:  false
 
// creating the two states representing the respective
// set of property changes
states: [
     // the hovered state is set when the user has
     // the mouse hovering the marker item.
     State {
         name:  "hovered"
         // this condition makes this state active
         when: mouseArea.containsMouse && !root.active
         PropertyChanges { target: root; x: 5 }
     },
     State {
         name:  "selected"
         when: root.active
         PropertyChanges { target: root; x: 20 }
     }
]
 
// list of transitions that apply when the state changes
transitions: [
     Transition {
         to:  "hovered"
         NumberAnimation { target: root; property:  "x" ; duration: 300 }
     },
     Transition {
         to:  "selected"
         NumberAnimation { target: root; property:  "x" ; duration: 300 }
     },
     Transition {
         to:  ""
         NumberAnimation { target: root; property:  "x" ; duration: 300 }
     }
  ]

所以咱们声明两个state表明对应基于用户行为的属性变化; 每个state绑定到when属性所描写叙述的状况中;

Note 对于MouseArea的containsMouse属性, hoverEnabled属性必须设为true;

Transition是用来在state以前切换时定义item的行为的; 咱们可以在state激活的时候, 给变化的属性定义各类动画

Note item的默认state是一个空的string--("");

在MarkerPanel组件里, 咱们在它被点击的时候必须设置avtive属性为true; 參考MarkerPanel.qml;

5.2.2 给PagePanel加入Transition

在PagePanel组件, 咱们使用state来管理page之间的navigation(导航); 加入transition是天然的思路; 由于咱们在每个state里面改变了opacity属性, 可以加入给所有的state加入Transition来依据opacity的值执行NumberAnimation, 建立fade-in和fade-out效果;

// PagePanel.qml

1
2
3
4
5
6
7
8
9
10
//...
     // creating a list of transitions for
     // the different states of the PagePanel
     transitions: [
     Transition {
         // run the same transition for all states
         from:  "" ; to:  "*"
         NumberAnimation { property:  "opacity" ; duration: 500 }
     }
     ]

Note 一个item的opacity值也会被传播(propagated)到它的child元素;

下一步

进一步改进UI;

---5End---


CHAPTER6 不少其它改进

这一阶段, 可以以为NoteApp的特性完毕, UI也符合需求; 只是, 这里还有很是大空间改进, 尽管不是最重要(minor)但可以使得程序更美丽, 以及准备部署(deployment);

这一章有些小的改进, 也有些新的主意和特性加入进来; 固然, 咱们可以鼓舞继续开发NoteApp, 也许又一次设计整个UI, 引入不少其它特性;

这里有一个要点列表:

- 不少其它Javascript用来加入功能;

- 使用QML Item的z ordering(z轴次序);

- 使用本身定义的本地字体;

6.1 增强Note Item的功能

一个巧妙的(nifty)功能可以让note依据输入的text而变长; 为了简化, 当不少其它text输入时, 它会将text折叠(wrap)起来, 适应note宽度, 在垂直方向上将note变长; 

Text类型有一个paintedHeight属性给出text在屏幕上绘制时的确切高度; 依据这个值, 咱们可以添加或下降note自身的高度; 

先定义一个Javascript helper方法, 为Item计算height属性的值, 它会放在Note组件的顶层;

// Note.qml

1
2
3
4
5
6
7
8
9
10
// JavaScript helper function that calculates the height of
// the note as more text is entered or removed.
function  updateNoteHeight() {
     // a note should have a minimum height
     var  noteMinHeight = 200
     var  currentHeight = editArea.paintedHeight + toolbar.height +40
     root.height = noteMinHeight
     if (currentHeight >= noteMinHeight)
         root.height = currentHeight
}

由于 updateNoteHeight()方法会依据editArea的paintedHeight属性更新root的height, 咱们需要在paintedHeight变化的时候调用这种方法;

// Note.qml

1
2
3
4
5
6
7
8
TextEdit {
     id: editArea
     //...
     // called when the painterHeight property changes
     // then the note height has to be updated based
     // on the text input
     onPaintedHeightChanged: updateNoteHeight()
//...

Note 每个属性有一个notifier signal(通知信号), 每次property改变的时候会被发送(emit);

updateNoteHeight() JS方法会改变height属性, 因此咱们可以使用Behavior定义一个行为;

// Note.qml

1
2
3
4
//...
// defining a behavior when the height property changes
// for the root element
Behavior on height { NumberAnimation {} }

下一步

展现怎样使用Item的z属性来正确地为note排序;


6.2 Note排序

Page里面的Note没法知道用户当前正在使用哪个note; 默认状况, 所有建立出来的Note item都有同样的z属性默认值, 这样的状况下, QML为item建立默认的栈次序, 依赖于哪个item先被建立;

所需要的行为应该是依据用户交互来改变note的次序; 当用户点击note toolbar, 或者開始编辑note的时候, 当前的note应该跑到前面而不是躲在其它note如下; 这可以经过改变z值来作到, z值比其它的note高就能够;

// Note.qml

1
2
3
//...
// setting the z order to 1 if the text area has the focus
z: editArea.activeFocus ?

1:0

activeFocus属性在editArea有输入焦点(input focus)的时候变为true; 所以让z属性依赖editArea的activeFocus会比較安全; 而后, 咱们需要保证editArea在用户点击toolbar的时候有输入焦点; 咱们在NoteToolbar组件中定义一个pressed()信号, 在鼠标press的时候发送; 

// NoteToolbar.qml

1
2
3
4
5
6
7
8
9
10
11
//...
     // this signal is emitted when the toolbar is pressed by the user
     signal pressed()
 
     // creating a MouseArea item
     MouseArea {
         id: mousearea
//...
         // emitting the pressed() signal on a mouse press event
         onPressed: root.pressed()
     }

在MouseArea中的onPressed信号handler中, 咱们发送NoteToolbar(root)的pressed()信号;

NoteToolbar的pressed()信号在Note组件中会处理;

// Note.qml

1
2
3
4
5
6
     // creating a NoteToolbar that will be anchored to its parent
     NoteToolbar {
         id: toolbar
         // setting the focus on the text area when the toolbar is pressed
         onPressed: editArea.focus =  true
//...

上面代码中, 咱们将editArea的focus属性设为true, editArea会接收到输入焦点; 这样activeFocus属性变为true, 触发(trigger)z属性值的变化;

下一步

如何载入以及使用一个本身定义的本地font(字体)文件;


6.3 载入本身定义字体

在现代程序中使用部署一个本身定义的font而不依赖系统font是非常常见的方式; 对于NoteApp, 咱们想要用QML提供的方法作一样的事情;

FontLoader类型让你可以经过名字或URL路径来载入font; 因为载入的font可以被整个程序普遍使用, 咱们建议在main.qml中载入它, 而后在其它组件中使用;

// main.qml

1
2
3
4
5
6
7
8
9
10
11
//...
     // creating a webfont property that holds the font
     // loading using FontLoader
     property  var  webfont: FontLoader {
         id: webfontloader
         source:  "fonts/juleeregular.ttf"
         onStatusChanged: {
         if  (webfontloader.status == FontLoader.Ready)
             console.log( "font----Loaded" )
         }
     }

这样咱们已经为window item建立了一个webfont属性; 这个属性可以安全地在其它的组件中使用, 可以在editArea的Note组件中使用;

// Note.qml

1
2
3
4
     TextEdit {
         id: editArea
         font.family: window.webfont.name; font.pointSize: 13
//...

要设置editArea的font, 使用font.family属性; 经过window, 咱们使用它的webfont属性来获取font名字, 用来设置;

下一步

将NoteApp准备好部署的细节;

---6End---

---TBC---YCR---

相关文章
相关标签/搜索