首页 > 代码库 > QtQuick桌面应用开发指导 1)关于教程 2)原型和设计 3)实现UI和功能_A

QtQuick桌面应用开发指导 1)关于教程 2)原型和设计 3)实现UI和功能_A

Release1.0 http://qt-project.org/wiki/developer-guides 

Qt Quick Application Developer Guide for Desktop

这个教程的目的是让你熟悉使用QtQuick构建QML程序的最佳编程实践方法; 先决条件: 对QML有相当的理解, 相关阅读: <qtquick/qtquick-applicationdevelopers.html>; 本教程会涉及QML开发最佳实践的各个方面, 以及在典型的桌面环境下部署应用的情况; 参阅更多相关的信息源可以让你对QML编程的理解更深刻;


CHAPTER1 关于教程

1.1 为何阅读

这份教程对于开发丰富特性的应用以及在多种桌面平台部署应用的情况, 提供了一个QML和QtQuick技术总览;

重点在于QtQuick和如何有效地编写整个应用而不使用C++; 教程一步步指导你如何初始化开发环境, 配置一个可以部署的新项目; 这里有一个简单应用(NoteApp);

这里有许多章节, 每一步都会特定描述应用的特性, 开发方式和QML代码细节; 应用覆盖了各个方面, 例如高级UI概念, 包括animation, database storage和Javascript应用程序逻辑;

这个应用看起来不会像典型或经典的桌面程序, 普通的UI元素比如 toolbar, menu, dialog之类没有被使用; 这个应用是受到现代的流UI(fluid UIs)启发, 目标是桌面环境;

为了方便描述, NoteApp* 对应每个章节有自己的版本; 建议阅读教程的时候参考代码;

教程结束时, 你应该有对使用QML语言, 利用QML/QtQuick技术开发程序有一个深入理解;

NoteApp*程序的截图;


1.2 源代码

1.3 License

---1End---


CHAPTER2 设计原型和起始设计

QtQuick和QML的一大优点是, 它使得你可以快速开发原型; 考虑Prototyping阶段作为开发NoteApp程序的第一步有两个原因: 1) 前面提到过, QML让我们可以快速开发原型, UI设计师可以不费劲地草绘出一些初始化一些UI屏幕; 2) 原型可以让你和设计者紧密合作, UI概念的应用将在几个很短的迭代过程中完成;

接下去, 这个原型将作为继续开发的基础; 这章中, 我们将引导你实施开发阶段, 包括UI概念, 特性集合, UI交互流程, 一些初始化的QML屏幕作为原型的一部分; 其中会有一些主要QML概念的介绍, 比如创建QML组件以及QML item布局;

这章主要讨论点的简要列表:

- UI概念和特性集合

- 使用QtCreator创建QML组件

- 使用Anchor和Repeater类型来给UI元素布局


2.1 NoteApp程序概览

NoteApp程序是一个便利贴(Post-it note) [http://en.wikipedia.org/wiki/Post-it_note]程序, 帮助用户创建note并且在本地存储; 如果note有一个类别(category)的话, 就更易管理, 因此考虑有三种不同的类别; 根据视觉的角度来看, 一个类别可以被一个区域表示; 我们来介绍一下Page的概念; 一个Page是一个区域, 在这个区域中可以创建note并且放置进去;

用户应该可以一个一个地删除note, 也可以一次性全部删除; note可以自由地在Page区域中移动; 为了简化, 我们定义三个Page然后使用 Marker来识别每张Page; 另外, 每个marker可以有不同的颜色;

一个有趣的特性是本地存储note, 而且可能是自动完成, 不用询问用户是否存储;

总结下特性:

- 创建/删除 Note item;

- 编辑Note和在page中任意位置放置note

- 本地存储note

- 三个不同page由一个page marker表示

2.1.1 UI元素

基于前面讨论的需求, 我们从一个线框(wire-frame)设计开始; 由于NoteApp可能有很多种的设计, 让我们考虑采用其中一种;

从UI的角度看, 上面的图片给出了用户想要的样子, 而且它也能帮助你找到可能的UI元素, 以及可以应用的交互;

2.1.2 UI流程(Flows)

如前面所提到的, 有三个Page可以包含Note item; 我们也可以在右边看到Marker, 左边看到toolbar; toolbar包含: New Note工具--创建新的note; Clear All工具--清除整个page; Note item有一个toolbar可以用来拖拽(drag)note, 按下鼠标左键, 移动鼠标可以在page中拖动note; 另外, note toolbar上还有一个 delete工具可以删除note;

下一步

?要确认特性中需要实现的QML组件, 以及如何创建它们;


2.2 为UI元素创建一个QML组件

一旦我们恰当地定义了特性集合和UI概念, 确认了基本的UI元素, 就可以安全地开始实现NoteApp的原型;

原型可以是非常基础的UI, 没有任何功能, 但是它提供了这个程序在经过迭代实现后, 在完成时看上去的大概样子;

这一步中, 你会找到使用QtCreator创建QtQuick UI的细节, 但最重要的是, 如何确定和创建一个QML组件;

2.2.1 QtCreator中创建一个QtQuick UI项目

在原型阶段, 很重要的一点是, 创建一个Qt Quick UI项目是推荐的方式, 是一个有效的方法; 这种方式下, prototyping, 特别是开发和测试每个独立的QML组件更简单; 独立地测试每个新建的组件是很重要的, 这样你才能立刻定位错误, 使用QtQuick UI项目让这一切更简单;

更多细节参考 Creating a Qt Quick UI http://qt-project.org/doc/qtcreator-2.6/quick-projects.html#creating-qt-quick-ui-projects;

Note: 总是要有一个QML文件, 定义为主文件来加载和运行程序; 对于NoteApp, 我们有main.qml, 它是由QtCreator产生的文件;

2.2.2 识别作为UI元素的QML组件

如果想要和面向对象编程做一个类比, QML组件可以看作类, 用来定义和实例化对象; 你可能会用一个大的QML文件来写一整个简单程序, 但是那样可能会增加复杂性, 使得代码重用性和维护十分困难--有些甚至是不可能;

QML组件可以被看成是一个普通UI元素的小组; 更多情况下, 它代表一个UI元素以及预定义的action和property;

基于我们的UI概念和设计, 这里有一个, 这里有一个自定义QML组件列表, 在接下去的迭代中会用到;

Note: 每个QML组件使用自己的QML文件(.qml), qml文件和组件的名字一样; 例如, Note组件会命名为 Note.qml;

- Note 代表note item;

- Page 这个组件包含item;

- Marker 代表一个page marker, 让户可以使用marker在page之间切换;

- NoteToolbar  在note item上的toolbar用来拖拽和布局

更多使用QtCreator创建组件的细节参考: Creating QML Components with QtCreator3 http://qt-project.org/doc/qtcreator-2.6/quick-components.html 

下一步

接下去看如何进一步强化定义的组件以及开始实现原型UI;


2.3 Anchoring QML Item和实现QML组件

Rectangle QML类型是构建UI块的一个自然的选择, 也可作为在prototype阶段初始化QML的组件; 它是一个拥有属性的visual类型, 你可以随意调整它, 使得prototype和test更为简单;

Note: 总是使用一样的默认几何数据来定义组件是一个好习惯, 有助于测试;

让我们来看看QML组件的代码; 首先, 我们开始实现Note组件;

2.3.1 Note和 NoteToolbar组件

首先, 就像前面步骤中所见, 我们已经创建了一个新的QML文件用来实现所需的组件;

要符合线框设计, 代码看起来会是下面这样:

// NoteToolbar.qml

1
2
3
4
5
6
7
8
import QtQuick 2.0
// A Rectangle element with defined geometries and color.
Rectangle {
    id: root
    width: 100
    height: 62
    color: "#9e964a"
}

Note组件有一个toolbar UI元素 -- NoteToolBar组件; 另外, 有一个 text input元素来获取用户输入文字; 我们使用 TextEdit QML类型; 为了把这些UI元素放入Note组件, 要使用anchor属性; 这个属性继承自 Item类型--是最基本的类, 每个QML组件默认继承自它;

更多布局细节参考 Anchor-based Layout in QML http://qt-project.org/doc/qt-5/qtquick-positioning-anchors.html  ;

Note Anchor-based Layout不能和绝对位置混用;

// Note.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
import QtQuick 2.0
Rectangle {
    id: root
    width: 200
    height: 200
    color: "#cabf1b"
    // creating a NoteToolbar that will
    // be anchored to its parent on the top, left and right
    NoteToolbar {
        id: toolbar
        // height is required to be specified
        // since there is no bottom anchoring.
        height: 40
        // anchoring it to the parent
        // using just three anchors
        anchors {
            top: root.top
            left: root.left
            right: root.right
        }
    }
    // creating a TextEdit used for the text input from user.
    TextEdit {
        anchors {
            top: toolbar.bottom
            bottom: root.bottom
            right: root.right
            left: root.left
        }
        wrapMode: TextEdit.WrapAnywhere
    }
}

Warning 由于性能原因, anchor应该只用在兄弟(sibling)或直接的父类(parent)上;

2.3.2 Page

一旦Note组件好了, 就可以开始Page组件的工作, 放两个Note在里面;

// Page.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Rectangle {
    id: root
    width: 600
    height: 400
    color: "#222525"
    // creating a Note item
    Note {
        id: note1
        // use x and y properties to specify
        // the absolute position relative to the parent
        x: 105; y: 144
    }
    Note {
        id: note2
        x: 344
        y: 83
    }
}

在QtCreator中, 你可以简单地运行上面的文件, 实际上使用 qmlscense加载Page.qml更简单; 

2.3.3 Marker

和其余的组件一样, Marker组件也使用Rectange类型和预定义的几何形状; 后面会看到怎样使用Marker组件;

// Marker.qml

1
2
3
4
5
6
Rectangle {
    id: root
    width: 50
    height: 90
    color: "#0a7bfb"
}

下一步

下一章我们会看到如何使用 Repeater QML类型和 使用Column来管理一个静态的marker列表;


2.4 使用Repeater和Delegate来创建Marker的List

在前面, 我们看到了如何创建QML组件: Note, NoteToolbar, Page, Marker, 以及如何使用 anchors放置QML组件;

回顾之前的设计概念, 我们注意到三个 Marker元素是垂直并排的; 使用 anchors可以固定UI元素做到这个要求, 但是那样会增加代码复杂度; QML有方便的方式--layout和 positioning类型; Column类型是其中一个, 使得UI元素可以一个接一个排成一列; 

因为我们想要放三个Marker组件在 Column中, 可以使用方便的QML类型--Repeater;

现在看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
Column {
    id: layout
    // spacing property can set to let the item have space between them
    spacing: 10
    // a Repeater item that uses a simple model with 3 items
    Repeater {
        model: 3
        delegate:
        // using the Marker component as our delegate
        Marker { id: marker }
    }
}

上面代码中, Repeater产生三个QML组件, 基于model和delegate; 由于我们想要三个Marker item, 就简单地使用Marker组件作为 delegate;

更多关于 positioning的信息参阅 Important Concepts In Qt Quick - Positioning http://qt-project.org/doc/qt-5.0/qtquick/qtquick-positioning-topic.html 

一个问题很自然地出现:"上面代码要放在哪个qml文件的哪个位置?"; 我们需要一个独立的QML组件--MarkerPanel; MarkerPanel是一个简单列表, 包含三个Marker item可以用作UI元素;

// MarkerPanel.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Rectangle {
    id: root
    width: 50
    height: 300
    // column type that anchors to the entire parent
    Column {
        id: layout
        anchors.fill: parent
        spacing: 10
        Repeater {
            // just three Marker items
            model: 3
            delegate:
                Marker { id: marker }
        }
    }
}

Note: 建议总是在prototype阶段对组件进行独立地运行和测试; 这样你可以立刻定位错误;

运行 MarkerPanel组件:

下一步

我们将会看到如何使用创建出来的组件来完成原型;


2.5 完成原型

现在QML组件都就绪了, 可以用来构建我们的原型; 

- Note    - NoteToolbar    -Marker    -MarkerPanel    -Page

在接下去的阶段中可能会有更多QML组件出现;

前面提到过, QtCreator产生一个 main.qml可以当作main文件来加载和运行NoteApp; 因此, 我们开始在 main.qml中安排组件, 组合出一个原型;

2.5.1 组合出原型

回到UI概念, 再看一下设计, 然后开始布局; 我们有Marker的面板(panel)--MarkerPanel组件放在右边, Page组件在中间; 目前为止还没有提到 toolbar; 

toolbar包含两个工具, 一个用来创建新的note, 一个是清除Page; 为了简化, 我们不为它创建一个组件了, 直接在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
// using a Rectangle element to represent our toolbar
// it helps to align the column better with the rest of the components
Rectangle {
    id: toolbar
    // setting a width because there is no right anchoring
    width: 50
    color: "#444a4b"
    anchors {
        left: window.left
        top: window.top; bottom: window.bottom
        topMargin: 100; bottomMargin: 100
    }
    // using a Column type to place the tools
    Column {
        anchors { fill: parent; topMargin: 30 }
        spacing: 20
        // For the purpose of this prototype we simply use
        //a Repeater to generate two Rectangle items.
        Repeater {
            model: 2
            // using a Rectangle item to represent
            // our tools, just for prototype only.
            Rectangle { width: 50; height: 50; color: "red" }
        }
    }
}

现在, 可以实现我们的原型了; 

// 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Rectangle {
    // using window as the identifier for this item as
    // it will the only window of the NoteApp
    id: window
    width: 800
    height: 600
    // creating a MarkerPanel item
    MarkerPanel {
        id: markerPanel
        width: 50
        anchors.topMargin: 20
        anchors {
            right: window.right
            top: window.top
            bottom: window.bottom
        }
    }
    // the toolbar
    Rectangle {
        id: toolbar
        width: 50
        color: "#444a4b"
        anchors {
            left: window.left
            top: window.top
            bottom: window.bottom
            topMargin: 100
            bottomMargin: 100
        }
        Column {
            anchors { fill: parent; topMargin: 30 }
            spacing: 20
            Repeater {
                model: 2
                Rectangle {
                    width: 50;
                    height: 50;
                    color: "red"
                }
            }
        }
    }
    // creating a Page item
    Page {
        id: page1
        anchors {
            top: window.top
            bottom: window.bottom
            right: markerPanel.left
            left: toolbar.right
        }
    }
}

运行起来是这样的:

2.5.2 让Note组件可以拖拽

目前我们有了一个基础的原型, 可以作为NoteApp UI的架子; 有一个酷炫的UI功能在原型阶段就可以完成--让用户在page里拖拽note item; 要做到这点, MouseArea QML Type有个属性群叫做drag; 我们将使用 drag.target 属性, 将note组件的 id设置给它; 

考虑到用户会使用 NoteToolbar来拖拽一个note, MouseArea类型应该放在 NoteToolbar组件里面; NoteToolbar会处理用户的拖拽操作, 我们应该把 drag.target设置成Note组件; 

想做到这点, 我们要允许NoteToolbar在Note之中将 MouseArea的drag.target属性绑定到Note的id; QML提供了 Property Aliases http://qt-project.org/doc/qt-5/qtqml-syntax-objectattributes.html#property-aliases  来实现这一点; 

我们来为NoteToolbar中MouseArea的drap属性组创建一个property alias(属性别名);

// NoteToolbar.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Rectangle {
    id: root
    width: 100
    height: 62
    color: "#9e964a"
    // declaring a property alias to the drag
    // property of MouseArea type
    property alias drag: mousearea.drag
    // creating a MouseArea item
    MouseArea {
        id: mousearea
        anchors.fill: parent
    }
}

从上面的code可见, NoteToolbar的drag属性别名绑定到了MouseArea的drag属性, 现在就可以在Note组件中使用它了;

// Note.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
Rectangle {
    id: root
    width: 200
    height: 200
    color: "#cabf1b"
    // 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 a TextEdit
    TextEdit {
        anchors {
            top: toolbar.bottom
            bottom: root.bottom
            right: root.right
            left: root.left
        }
        wrapMode: TextEdit.WrapAnywhere
    }
}

更多细节参考 Property Binding http://qt-project.org/doc/qt-5/qtqml-syntax-propertybinding.html

下一步

基于原型开始实现UI和基本功能;

---2End---


CHAPTER3 实现UI和添加功能

初始的原型引入的是基础的UI和NoteApp功能概念, 可以帮助我们找出所需的QML组件;

基于原型, 我们朝着更完整的UI和功能, 试着构建一个可以工作的程序; 现在, 使用目前实现的组件开始组合出这个程序的UI;

这一章更多关于QML图形, 图像, 背景, 强化UI, 同时也使用Javascript添加方法; 你可以看到一些QML类型更深层的内容, 通过一点点增加代码复杂度, 添加功能来了解更多;

要点:

- 引入PagePanel组件, 使用Repeater元素管理Page元素;

- 使用QML图形;

- 更深入使用QML类型;

3.1 创建PagePanel组件

方便起见, 我们用Page组件只创建一个page item; 我们已经把它放置到其他item一起, anchor到它的parent; 无论如何, NoteApp概念和需求是要求三个不同page, 然后用户可以使用marker导航(navigate)到任何一个; 我们可以看到MarkerPanel组件如何帮助我们创建和布局三个Marker item, 接下去可以使用相同方法处理Page item, 这样实现PagePanel组件;

3.1.1 使用一个Item类型

在我们更进一步之前, 理解为什么Rectangle类型在组件中作为顶层这样的设计, 现在开始要防止出现; 原因是我们使用Rectangle元素是因为它可以帮助我们迅速得到可视化的结果, 这也是原型阶段的需求;

但一旦原型完成, 用 Item类型替换 Rectangle类型会更合理, 特别当考虑到拿图形作为背景的UI元素;

// Page.qml

1
2
Item {
    id: root

Warning: 从现在开始, 考虑用 Item QML类型来作为组件的顶层元素; 参考每一章的源代码;

3.1.2 使用States

继续PagePanel组件的开发, 可见PagePanel和MarkerPanel之间主要的区别是三个Marker item应该总是可见的, 但是Page item同时只能有一个是可见的; 这依赖于用户点选了哪个Marker item;

实现这个行为可以有多种方式; 其中一个是inline(内联) Javascript方法, 根据当前用户点选的Marker切换(toggle) Page item的可见性;

在NoteApp里面, 我们使用了 State类型实现所需要的行为; PagePanel组件有三个state, 每个都绑定到一张Page的 visible属性; 因此在page之间切换的事件时候就转换成了在PagePanel里面设置独立的状态;

首先, 在Page组件里, 我们要设置 opacity(透明度)属性为0.0作为默认值; 这是为了让page在初始的时候不可见, 然后基于各自的state改变让它们变得可见; 

// Page.qml

1
2
3
4
5
Item {
    id: root
    opacity: 0.0
...
}

一旦创建了PagePanel.qml文件, 就可以开始创建state和Page item; 我们需要三个state:

- personal fun work 

这会改变下面每个Page对应各自的opacity属性: personalpage     funpage    workpage

下面的图示展示了state关联到各自的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
Item {
    id: root
    // creating the list of states
    states: [
        // creating a state item with its corresponding name
        State {
            name: "personal"
            //the properties that are about to change for a defined target
            PropertyChanges {
                target: personalpage
                opacity:1.0
                restoreEntryValues: true
            }
        },
        State {
            name: "fun"
            PropertyChanges {
                target: funpage
                opacity:1.0
                restoreEntryValues: true
            }
        },
        State {
            name: "work"
            PropertyChanges {
                target: workpage
                opacity:1.0
                restoreEntryValues: true
            }
        }
    ]
    // creating three page items that are anchored to fill the parent
    Page { id: personalpage; anchors.fill: parent }
    Page { id: funpage; anchors.fill: parent }
    Page { id: workpage; anchors.fill: parent }
}

Note: 设置 restoreEntryValues 属性为true可以让target变化后的属性重设成默认值, 这样当state变化的时候, page的opacity属性可以被重设为false;

观察上面的代码, 可以看到三个Page item创建出来了, 不同的state会改变这些item的opacity属性; 这一步, 我们设法创建了一个新的组件--PagePanel, 可以帮助我们使用三个state切换页面;

下一步

使用Marker item变换PagePanel item的state;

---TBC---



QtQuick桌面应用开发指导 1)关于教程 2)原型和设计 3)实现UI和功能_A