3.2 把Page Item和Marker Item繫結

之前我們實現了PagePanel元件, 使用了三個state來切換Page元件的opacity屬性; 這一步我們會使用Marker和MarkerPanel元件來實現頁面導航;

在原型階段, MarkerPanel元件十分簡單, 沒有不論什麼功能; 它使用了Repeater型別來產生三個QML Item以及Marker元件作為delegate;

MarkerPanel應該儲存當前啟用的marker(標記), 即那個被使用者點選的marker; 基於MarkerPanel中啟用的marker, PagePanel會更新它的state屬性; 我們須要將PagePanel的state屬性和MarkerPanel新的屬性--持有當前啟用marker的屬性繫結起來;

在MarkerPanel中定義一個string屬性--activeMarker;

// MarkerPanel.qml

1
2
3
4
5
6
7
Item
{
    id:
root
    width:
150;    height: 450
    //
a property of type string to hold
    //
the value of the current active marker
    property
string activeMarker: 
"personal"
//...

我們能夠把一個markerid值儲存起來, 用來唯一地識別marker item; 這樣, activeMarker會持實使用者所點選的marker item的markerid的值,

依據model, Repeater元素能夠產生三個marker item, 因此我們能夠使用一個model來儲存markerid值, 然後在Repeater中使用;

// MarkerPanel.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
Item
{
    id:
root
    width:
150;    height: 450
    //
a property of type string to hold
    //
the value of the current active marker
    property
string activeMarker: 
"personal"
 
    //
a list for holding respective data for a Marker item.
    property
variant markerData: [
        {
markerid: 
"personal" },
        {
markerid: 
"fun" },
        {
markerid: 
"work" }
    ]
 
    Column
{
        id:
layout
        anchors.fill:
parent
        spacing:
5
        Repeater
{
            //
using the defined list as our model
            model:
markerData
            delegate:
Marker {
                id:
marker
                //
handling the clicked signal of the Marker item,
                //
setting the currentMarker property
                //
of MarkerPanel based on the clicked Marker
                //MouseArea
{
                    //anchors.fill:
parent
                    onClicked:
root.activeMarker = modelData.markerid
                //}
            }
        }
    }
}

上述程式碼中我們在onClicked signal handler中設定了 activeMarker屬性; 這意味著我們已經在Marker元件中定義了一個clicked() signal來通知使用者的滑鼠點選事件;

// Marker.qml

1
2
3
4
5
6
7
8
9
10
11
Item
{
    id:
root
    width:
50; height: 90
    signal
clicked()
    MouseArea
{
        id:
mouseArea
        anchors.fill:
parent
        //
emitting the clicked() signal Marker item
        onClicked:
root.clicked()
    }
}

眼下我們以後有了PagePanel元件使用state屬性來管理page, 讓MarkPanel元件能夠識別啟用的marker, 因此, 切換各個page的可見效能夠通過改變page的opacity屬性來做到;

來看看如何使用 activeMarker屬性來相應地更新PagePanel的state;

在main.qml裡面, 已經有了Page item和 MarkerPanel定位好了, 我們會建立以及使用PagePanel item而不是各自使用anchor定位;

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
    //
creating a MarkerPanel item
    MarkerPanel
{
        id:
markerPanel
        width:
50
        anchors.topMargin:
20
        anchors
{
            right:
window.right
            top:
window.top
            bottom:
window.bottom
        }
    }
//...
    //
creating a PagePanel item
    PagePanel
{
        id:
pagePanel
        //
binding the state of PagePanel to the
        //
activeMarker property of MarkerPanel
        state:
markerPanel.activeMarker
        anchors
{
            right:
markerPanel.left
            left:
toolbar.right
            top:
parent.top
            bottom:
parent.bottom
            leftMargin:
1
            rightMargin:
-50
            topMargin:
3
            bottomMargin:
15
        }
    }

上面程式碼中, 我們能夠看到QML的 property binding特性, 能夠把state屬性和activeMarker屬性繫結起來; 這樣不論activeMarker通過使用者操作獲得了什麼值, 同樣的值會被分配給PagePanel的state屬性, 這樣就能開關各個page的可見性了;

下一步

給出如何使用使用圖形來強化UI的細節;

3.3 加入graphics(圖形)

由於QML的特性, 開發和設計全然能夠一起緊密工作, 貫徹整個開發生命期; 現在, 使用graphics讓使用者體驗有了非常大的不同, 這也是程式讓使用者感受到的地方;

QML鼓舞在UI實現過程中儘可能地使用graphics; 使用QML讓圖形設計和開發之間的協作更easy, 設計能夠立馬在主要的UI元素上測試graphics; 這幫助設計來理解在開發新的元件時, 程式猿會須要什麼, 這也讓程式的UI更有吸引力並且某種程度上更易維護;

3.3.1 給元件設定背景圖片

BorderImage型別推薦使用的情況是: 在你想要把一個圖片scale(按比例拉伸), 但它的border(邊界)保持不變的時候; 這樣的型別的一個好的用例(use case)是在QML item上有陰影效果的背景圖片; 你的item可能會在某些時刻scale可是須要保持corners(四角)不變;

來看下如何在元件中將BorderImage設定成背景;

// PagePanel.qml

1
2
3
4
5
6
7
8
9
10
11
//...
    BorderImage
{
        id:
background
        //
filling the entire PagePanel
        anchors.fill:
parent
        source: "images/page.png"
        //
specifying the border margins for each corner,
        //
this info should be given by the designer
        border.left:
68; border.top: 69
        border.right:
40; border.bottom: 80
    }

// Note.qml

1
2
3
4
5
6
7
BorderImage
{
    id:
noteImage
    anchors
{ fill: parent}
    source: "images/personal_note.png"
    border.left:
20; border.top: 20
    border.right:
20; border.bottom: 20
}

Warning: 注意BorderImage型別要以正確的次序在元件裡面使用, 由於實現的次序定義了顯示的順序; 具有同樣 z值的item顯示的次序是按它們被宣告的次序決定的; 很多其它細節參考stack ordering of items-- z property;

當這些item都在MarkerPanel元件中建立的時候, 如何才是給Marker item設定背景的最佳方案--這個方法已經在MarkerPanel中展現了;

這裡有個markerData list, 把它作為model給Repeater來建立Marker item, 當一個marker item被點選的時候, 設定markerid作為activeMarker; 我們能夠擴充套件markerData, 儲存一個影象的的url路徑, 使用Image型別作為Marker元件的頂層型別;

// Marker.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
The Image type as top level is convenient
//
as the Marker component simply is a graphical
//
UI with a clicked() signal.
Image
{
    id:
root
    //
declaring the clicked() signal to be used in the MarkerPanel
    signal
clicked()
    //
creating a MouseArea type to intercept the mouse click
    MouseArea
{
        id:
mouseArea
        anchors.fill:
parent
        //
emitting the clicked() signal Marker item
        onClicked:
root.clicked()
    }
}

這樣就能夠增強MarkerPanel元件;

// MarkerPanel.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
//...
    //
for the markerData, we add the img value pointing to the image url
    property
variant markerData: [
        {
img: 
"images/personalmarker.png",
markerid: 
"personal" },
        {
img: 
"images/funmarker.png",
markerid: 
"fun" },
        {
img: 
"images/workmarker.png",
markerid: 
"work" }
    ]
 
    Column
{
        id:
layout
        anchors.fill:
parent
        spacing:
5
        Repeater
{
            //
using the defined list as our model
            model:
markerData
            delegate:
Marker {
                id:
marker
                //
binding the source property of Marker to that
                //
of the modelData' s img value.
                //
note that the Marker is an Image element
                source:
modelData.img
                //
handling the clicked signal of the Marker item,
                //
setting the currentMarker property
                //
of MarkerPanel based on the clicked Marker
                onClicked:
root.activeMarker = modelData.markerid
            }
        }
    }

上述程式碼中, 能夠看到Marker item的source屬性是怎樣繫結到markerData model的image值的;

我們使用了BorderImage型別來為NoteToolbar元件設定背景, 也作為main.qml的頂層型別;

Note 關於影象的border margins, 以及影象的怎樣anchor和align(對齊), 要和graphics設計討論清楚;

MarkerPanel元件看起來是這種:

然後來看看如何在原型階段使用graphics依照設計來增強toolbar;

3.3.2 建立Tool元件

基於程式碼重用考慮, 定義一個新元件給toolbar中的New Note和Clear All工具使用; 這是為什麼我們已經實現了一個Tool元件, 使用Image型別作為頂層型別, 處理滑鼠點選事件;

Image型別經經常使用作UI元素自身, 不論是靜態的或是動繪影象; 它會按畫素佈局, 能夠非常好地依照設計需求來定義;

// Tool.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
//
Use Image as the top level type
Image
{
    id:
root
    //
defining the clicked signal
    signal
clicked()
    //
using a MouseArea type to capture
    //
the mouse click of the user
    MouseArea
{
        anchors.fill:
parent
        //
emitting the clicked() signal of the root item
        onClicked:
root.clicked()
    }
}

如今用Tool元件來建立toolbar; 我們從原型階段改動程式碼, 用Tool item取代Rectangle元素;

//main.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
//...
    //
toolbar background
    Rectangle
{
        anchors.fill:
toolbar
        color: "white"
        opacity:
0.15
        radius:
16
        border
{ color: 
"#600";
width: 4 }
    }
 
    //
using a Column element to layout
    //
the Tool items vertically
    Column
//
sidebar toolbar
        id:
toolbar
        spacing:
16
        anchors
{
            top:
window.top
            left:
window.left
            bottom:
window.bottom
            topMargin:
50
            bottomMargin:
50
            leftMargin:
8
        }
        //
new note tool
        Tool
{
            id:
newNoteTool
            source: "images/add.png"
        }
        //
clear page tool
        Tool
{
            id:
clearAllTool
            source: "images/clear.png"
        }
    }

如今我們給我們的元件設定了全部的graphics, 程式應該有了更吸引人的外觀和很多其它定義好的UI了;

下一步

下一章具體介紹怎樣動態地建立和管理Note item以及怎樣在本地資料庫儲存它們;

---3End---

CHAPTER4 動態管理Note物件

我們眼下看到的QML是一個很強大的宣告性(declarative)語言, 和JavaScript組合使用讓它更強大; QML不僅提供了 inline JavaScript, 並且還能夠把整個JavaScript庫匯入到檔案裡;

NoteApp的核心功能是能夠讓使用者去建立, 改動和刪除note, 可是程式應該也要自己主動儲存note, 無需提示;

這一章會指導怎樣使用JavaScript來給QML程式碼加入邏輯, 實現本地儲存--Qt Quick Local Storage

本章主要主題:

- 使用JavaScript實現動態物件管理的功能;

- 怎樣使用 Qt Quick Database API 進行本地資料儲存;

4.1 建立和管理Note Item

使用者應該隨時能夠建立和刪除note, 這意味著我們的程式碼應該能夠動態地建立和刪除Note item; 有多種方式建立和管理QML物件; 其實, 我們已經使用一種--Repeater型別; 建立一個QML物件意味著在建立元件的例項之前, 元件必需要被建立和載入起來;

QML物件能夠通過 createObject(Item parent, object properties) JavaScript方法在元件上建立; 很多其它細節參考 Dynamic Object Management in QML http://qt-project.org/doc/qt-5/qtqml-javascript-dynamicobjectcreation.html  

4.1.1 Note物件的動態建立

我們知道一個Note item是屬於Page元件的, 它負責note物件的建立, 也會從資料庫中讀取筆記; 

如前面所說, 首先載入Note元件:

// Page.qml

1
2
3
4
5
6
//...
    //
loading the Note Component
    Component
{
        id:
noteComponent
        Note
{ }
    }

如今我們來定義一個Javascript方法, 建立QML Note物件; 建立QML物件的時候, 必須保證一個引數是這個物件的parent; 在Page元件中持有一個Note item容器(container)是個管理note物件的好主意, 這樣我們能夠在資料庫中儲存這些note;

// Page.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
//...
    //
creating an Item element that will be used as a note container
    Item
{ id: container }
 
    //
a Javascript helper function for creating QML Note objects
    function newNoteObject(args)
{
        //
calling the createObject() function on noteComponent item
        //
and the container item will be the parent of the new
        //
object and args as the set of arguments
        var note
= noteComponent.createObject(container, args)
        if(note
== 
null)
            console.log("note
object failed to be created!"
)
    }

在前面顯示的程式碼中, 我們看到一個新的 note item物件是如何在 newNoteObject()方法中建立的; 新建的 note物件隸屬於container item;

如今我們要在toolbar上的new note tool被按下的時候呼叫這種方法, toolbar在main.qml中; 因為PagePanel元件知道當前可見的page item, 我們能夠在PagePanel中建立一個新的屬性來儲存那個page;

// PagePanel.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
38
39
40
41
42
43
44
45
46
47
//...
    //
this property holds the current visible page
    property
Page currentPage: personalpage
 
    //
creating the list of states
    states:
[
        //
creating a state item with its corresponding name
        State
{
            name: "personal"
            PropertyChanges
{
                target:
personalpage
                opacity:1.0
                restoreEntryValues: true
            }
            PropertyChanges
{
                target:
root
                currentPage:
personalpage
                explicit: true
            }
        },
        State
{
            name: "fun"
            PropertyChanges
{
                target:
funpage
                opacity:1.0
                restoreEntryValues: true
            }
            PropertyChanges
{
                target:
root
                currentPage:
funpage
                explicit: true
            }
        },
        State
{
            name: "work"
            PropertyChanges
{
                target:
workpage
                opacity:1.0
                restoreEntryValues: true
            }
            PropertyChanges
{
                target:
root
                currentPage:
workpage
                explicit: true
            }
        }
    ]

我們改動了三個state來給currentPage屬性設定合適的值;

在main.qml中, 看看在new note tool被點選的時候如何呼叫方法來建立新的note物件;

// main.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
using a Column element to layout the Tool items vertically
Column
{
    id:
toolbar
    spacing:
16
    anchors
{
        top:
window.top; left: window.left; bottom: window.bottom
        topMargin:
50; bottomMargin: 50; leftMargin: 8
    }
    //
new note tool, also known as the plus icon
    Tool
{
        id:
newNoteTool
        source: "images/add.png"
        //
using the currentPage property of PagePanel and
        //
calling newNoteObject() function without any arguments.
        onClicked:
pagePanel.currentPage.newNoteObject()
    }

4.1.2 刪除Note物件

刪除Note物件是個更直接的過程, 由於QML item型別提供了一個JavaScipt方法--destroy() http://qt-project.org/doc/qt-5/qtqml-javascript-dynamicobjectcreation.html#deleting-objects-dynamically 
; 由於我們已經有一個container item的children是Note item, 我們能夠簡單地對children逐個地呼叫 destroy;

在Page元件上, 定義一個方法來為我們執行操作:

// Page.qml

1
2
3
4
5
6
7
//...
    //
a JavaScript helper function for iterating through the children elements of the
    //
container item and calls destroy() for deleting them
    function clear()
{
        for(var i=0;
i<container.children.length; ++i)
            container.children[i].destroy()
    }

在main.qml檔案裡, 我們在clear tool被按下時呼叫 clear()方法:

1
2
3
4
5
6
7
//...
        //
the clear tool
        Tool
{
            id:
clearAllTool
            source: "images/clear.png"
            onClicked:
pagePanel.currentPage.clear()
        }

為了讓使用者能夠獨立地刪除每個note, 我們在NoteToolbar元件中為Note元件加入了一個tool; 能夠使用簽名實現的Tool元件:

// Note.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//...
    //
creating a NoteToolbar that will be anchored to its parent
    NoteToolbar
{
        id:
toolbar
        height:
40
        anchors
{ top: root.top; left: root.left; right: root.right }
        //
using the drag property alias to set the drag.target to our Note item.
        drag.target:
root
        //
creating the delete  tool for deleting the note item
        Tool
{
            id:
deleteItem
            source: "images/delete.png"
            onClicked:
root.destroy()
        }
    }

下一步

關於怎樣在本地資料庫儲存note item的具體步驟;

4.2 從資料庫儲存和讀取資料

眼下我們實現了NoteApp的功能: 實時地建立和管理note item;

這裡我們會了解在本地資料庫儲存note的具體實現; QML提供了一個簡單的 Qt Quick Local Storage API, 使用SQLite資料庫來實現我們想要的功能;

首選的方式是在NoteApp程式啟動的時候從資料庫讀取note, 然後在程式關閉的是儲存它們; 使用者不會收到提示;

4.2.1 定義資料庫

NoteApp的資料庫非常easy; 它僅僅有一個table--note table, 包括我們所儲存的note的資訊;

看一下Table的定義, 讓我們瞭解下Note元件的哪些屬性或哪些新的資料應該被引入:

x和y是每一個QML item都有的幾何屬性; 從Note item獲得這些值非常easy; 這些值會用來粗糙你note在page中的位置; noteText是note的實際文字, 我們能夠從Note元件中的Text元素中獲取它們, 我們應該定義一個alias(別名_)--noteText; noteId和markerId是每一個note item都該有的識別符號; noteId是一個唯一的識別符號, 資料庫須要用到, markerId用來標識note item屬於哪一個page; 因此我們會在Note元件裡面加入兩個新的屬性;

// Note.qml

1
2
3
4
5
6
7
8
9
10
11
12
Item
{
    id:
root
    width:
200; height: 200
 
    property
string markerId;
    property
int  noteId;
    property
alias noteText: editArea.text
//...
    //
creating a TextEdit
    TextEdit
{
        id:
editArea
//...

考慮到Page元件負責建立Note item, 它應該也知道Note和哪個markerId相關聯; 當一個新的Note item創建出來(不是從資料庫讀取), Page應該設定好Note的markerId屬性;

使用一個JavaScript的 helper方法:

// Page.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Item
{
    id:
root
 
    //
this property is held for helping store
    //
the note items in the database
    property
string markerId
    //
this Javascript helper function is used to create,
    //
Note items not loaded from database so that it will set
    //
the markerId property of the note.
    function newNote()
{
        //
calling the newNoteObject and passing the a set of
        //
arguments where the markerId is set.
        newNoteObject(
"markerId":
root.markerId } )
    }
//...

先前在main.qml中, 我們使用了 newNoteObject()方法, 但就如前面解釋的, 我們須要用newNote()方法取代它來達到目的;

如今每一個Page元件有個markerId屬性, 能夠在Note item被建立的時候設定markerId; 我們必須保證page的markerId屬性在Page item在PagePanel元件中建立的時候就被設定了;

// PagePanel.qml

1
2
3
4
5
//...
//
creating three Page items that are anchored to fill the parent
Page
{ id: personalpage; anchors.fill: parent; markerId: 
"personal" }
Page
{ id: funpage; anchors.fill: parent; markerId: 
"fun" }
Page
{ id: workpage; anchors.fill: parent; markerId: 
"work" }

眼下我們保證的:

- 從相應的關係資料庫[relational database http://en.wikipedia.org/wiki/Relational_database ]得到的note和page之間的關係是正確的;

- 每一個Note item有一個唯一的ID, ID屬於page, 能夠識別marker ID;

- 這些屬性值要被正確設定;

以下來讀取和儲存筆記;

---TBC---