4.2.2 Stateless(狀態無關的)JavaScript庫
爲了讓開發輕鬆點, 使用一個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應該提供下面的功能:
- 打開/創建一個本地數據庫實例
- 創建必須的數據庫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 i 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---