QtQuick桌面應用開發指導 3)實現UI和功能_B 4)動態管理Note對象_A

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讓圖形設計和開發之間的協作更容易, 設計可以立刻在基本的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的數據庫很簡單; 它只有一個table--note table, 包含我們所保存的note的信息;

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

x和y是每個QML item都有的幾何屬性; 從Note item獲得這些值很簡單; 這些值會用來粗糙你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---


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章