Qt Quick应用开发介绍 9(交互式UI)

Chapter9 Interactive UI with Multiple Top-Level Windows 多个顶层窗口下的交互式UI

现在我们的程序需要添加一些方法来变得适合日常工作中的重用; 首先要有个button来退出; 其次, 要有top-level窗口来管理配置; 用户修改配置时, 程序应该检查变动, 让用户知道改动是否正确; 

9.1 A Button

button用来退出程序, 打开窗口, 关闭窗口等; button应该有基本的可视化参数, 并且在点击时发送信号; button在接收到用户的输入后应该提供可视化的反馈; 一个button一般可以有多个特性; 有许多方式来实现一个button, 这里就描述一个适合我们需求的方式;

我们的button可以是一个简单的点击相应的有圆角的rectange; 前文中看到的element可以接受鼠标事件, 使得鼠标事件填充到整个元素的表面; 另外, 我们的button必须emit发送一个信号通知相关的程序部件告知点击事件; 我们需要使用QtQuick信号来实现; 

之前已经看到过QtQuick的方法, 来实现一个handler处理属性变化; 

1
2
3
4
5
6
7
8
9
Image {
     id: background
     source:  "../content/resources/background.png"
     fillMode:  "Tile"
     anchors.fill: parent
     onStatusChanged:  if  (background.status == Image.Error)
                          console.log( "Background image \""  +
                          source +  "\" cannot be loaded" )                                   
}

信号和属性变化通知很像; Signal handler工作起来也是一样, item显式地emit一个信号, 代替porperty change; Signal handler也可以接受信号参数, 和property change的handler不同, emit一个信号是一个方法的调用;

e.g. utils/Button.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
Rectangle {
     id: root
 
     property string text:  "Button"
 
     color:  "transparent"
 
     width: label.width + 15
     height: label.height + 10
 
     border.width: Style.borderWidth
     border.color: pressedColor(Style.penColor)
     radius: Style.borderRadius
 
     signal clicked (variant mouse)
     signal pressedAtXY (string coordinates)
 
     function  pressedColor (color) {
         return  mouseArea.pressed ? Qt.darker(color, 5.0) : color
     }
 
     function  logPresses (mouse) {
         pressedAtXY (mouse.x +  ","  + mouse.y)
     }
 
     Component.onCompleted: {
         mouseArea.clicked.connect(root.clicked)
     }
 
     Text {
         id: label
         anchors.centerIn: parent
         color: pressedColor(Style.penColor)
         text: parent.text
         font.pixelSize: Style.textPixelSize
     }
 
     MouseArea {
         id: mouseArea
         anchors.fill: parent
         Connections {
             onPressed: logPresses(mouse)
         }
         // this works as well instead of using Connections
         // onPressed: logPresses(mouse)
     }
}

Button定义了两个信号: clicked和pressedAtXY; 两个信号使用不同方式来emit; pressedAtXY从JavaScript方法调用, 作为onPressed的handler; clicked直接连接到了MouseArea的clicked信号上; 两种方式都有各自的使用案例; 一个直接的signal-to-signal理解允许简单的信号转发; 这也是我们需要在Button中实现的: 在鼠标事件发生时像一个MouseArea一样反应; 一些其他案例中, 你可以在emit信号前添加一些方法, 比如logPresses;      

Note 有个重点是信号的参数名称; 看看MouseArea的代码块, 你可能会好奇mouse参数是哪来的; 我们没有声明它, 它实际上属于MouseArea元素中clicked信号的定义; pressedAtXY信号也是一样, 定义了一个坐标coordinate参数; 所有使用Button和调用pressedAtXY信号的item必须使用准确的名字"coordinates"来获得这个参数: e.g.

1
2
3
4
5
6
7
Button {
     id: toggleStatesButton
//...
     onPressedAtXY: {
         console.log ( "pressed at: "  + coordinates)
     }
}

Note 注意我们定义的clicked信号是这样的:

1
signal clicked (variant mouse)

mouse是MouseEvent类型, 现在的QtQuick版本中, 信号参数只能是basic type http://qt-project.org/doc/qt-4.8/qdeclarativebasictypes.html 但是不用担心, 因为收到信号时参数类型可以转换;

关于QtQuick中使用signal的更多细节: QML Signal and Handler Event System: http://qt-project.org/doc/qt-4.8/qmlevents.html MouseArea http://qt-project.org/doc/qt-4.8/qml-mousearea.html MouseEvents http://qt-project.org/doc/qt-4.8/mouseevents.html 发现更多的可能性: 如得到其他mouse event, 追踪hovering事件, 实现drap-drop;

作为Button, 也要提供一些按键反馈; 我们可以稍稍变化下颜色, 使用JavaScript方法来修改颜色值:

1
2
3
function  pressedColor (color) {
     return  mouseArea.pressed ? Qt.darker(color, 5.0) : color
}

可以在button的border上和label text上使用这个toggle颜色, 把方法的发挥值绑定到颜色属性上:

1
2
border.color: pressedColor(Style.penColor)
color: pressedColor(Style.penColor)

9.2 A Simple Dialog

Dialog是另一个我们需要的utility component; 我们使用它来通知用户; 我们的Dialog很简单, 它会在弹出所有元素的顶层, 显示text信息, 带有OKbutton;  

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
Rectangle {
     id: root
 
     property string message:  "Error! This is a long message with details"
 
     width: 100
     height: 40
 
     color: Style.backgroundColor
     border.color: Style.penColor
     border.width: Style.borderWidth
     radius: Style.borderRadius
 
     visible:  true
 
     function  show(text) {
         root.message = text;
         root.visible =  true ;
     }
 
     function  hide() {
         root.visible =  false ;
     }
 
     Text {
         id: messageText
         anchors.top: parent.top
         anchors.topMargin: Style.baseMargin
         anchors.left: parent.left
         anchors.right: parent.right
         horizontalAlignment: Text.AlignHCenter
         wrapMode:  "WordWrap"
         text: root.message
         font.pixelSize: Style.textPixelSize
         color: Style.penColor
         onPaintedHeightChanged: {
             root.height = messageText.paintedHeight + okButton.height + 3*Style.baseMargin
         }
     }
 
     Button {
         id: okButton
         text: qsTr( "OK" )
         anchors.top: messageText.bottom
         anchors.topMargin: Style.baseMargin
         anchors.horizontalCenter: parent.horizontalCenter
         onClicked: root.hide()
     }
}

Dialog作为一个child item, 从另一个元素中pop-up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Item {
id: root
...
Dialog {
     id: errorDialog
     width: root.width
     anchors.centerIn: parent
     z: root.z+1
     visible:  false
}
...
Button {
     id: exitButton
...
     onClicked: {
...
         errorDialog.show (qsTr( "The location cannot be empty" ));
...
     }
}
...
}

Dialog在加载的时候就初始化为可见的; 它出现在parent的顶部(前面代码中的root); z: root.z+1 完成了这个小把戏; 我们把z属性绑定到一个总是比root.z更大的值; 接下去可以调用show(), 带上信息来显示; show()使得Dialog可见, 然后把信息文字显示出来; 当用户点击OK, dialog就把自己隐藏起来;

Note TabWidget Example--Qt Document, 展示了以一种方式, 在其他element顶部动态地显示和隐藏element;

我们的Dialog还有几个有用的特性需要知道; 为了有效利用屏幕空间, 它copy了parent的宽度; 当Dialog打开一个长的text信息, 信息会根据Dialog的宽度换行wraps; 当高度由于wrapping而改变的时候, messageText元素会改变root Dialog的高度;

1
2
3
4
5
6
7
8
9
10
11
12
13
Rectangle {
id: root
...
Text {
     id: messageText
...
     onPaintedHeightChanged: {
         root.height = messageText.paintedHeight +
             okButton.height +
             3 Style.baseMargin
     }
...
}

9.3 Checkbox

可以使用Text Input元素, 基于用户的输入来获取text或digit, 但我们需要一些其他的东西来处理on-off的设置类型; 通常使用的是check box UI元素; 在QtQuick(version1)里没有checkbox元素, 我们要拟写一个; 通过QtQuick我们可以方便地创建一个; 

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
import QtQuick 1.1
 
Item {
     id: root
     property bool checked:  true
     // we should pre-set the size to get it working perperly in a positioner
     width: checkBox.width
     height: checkBox.height
 
     Image {
         id: checkBox
         source: root.checked ?
                     "../content/resources/checkbox.png"  :
                     "../content/resources/draw-rectangle.png"
         Keys.onPressed: {
             if  (event.key == Qt.Key_Return ||
                     event.key == Qt.Key_Enter ||
                     event.key == Qt.Key_Space)
                 root.checked = !root.checked;
         }
         MouseArea {
             anchors.fill: parent
             onClicked: {
                 root.checked = !root.checked;
            }
         }
     }
     //onValueChanged: console.log ("value: " + root.value)
}

CheckBox基于Item, 它扩展了Item, 有一个boolean属性称为checked; 如果box被checked, checked是true, 否则为false; 整个visual实现由两个可以前后翻转的图片组成; 实现的方法是将source property(checkbox的图片)绑定到checkbox image或normal rectangle, source的值根据checked的值来变化;

下面是CheckBox在屏幕上看起来的样子, checked和unchecked:

接下去还有keyboard navigation处理的代码; 

9.4 Handling Keyboard Input and Navigation 处理键盘输入和导航

用户交互的另一个重要方面是键盘输入和导航; 我们将通过了解基于新UI component的Configure模块来探索这块; 

同时, 我们有多个hard-coded属性值, 但实际上应该是让用户来改变:

-天气预报的Location name 

-多久的时间间歇Time interval来更新天气数据

-关闭秒和日期显示, 让时钟更紧凑

name和interval属性需要一个text input区域, 而最后一个开关可以用Checkbox实现;

Text input很直接: QtQuick提供Text Input元素; 可以用它获取天气预报位置的值, 和新的更新间隔的值; TextInput元素将键盘捕捉的输入和text属性绑定; 加载这个元素时, 根据locationTextInput和forecastUpdateInterval值预设到text属性, 我们把当前的设置显示给用户; 用户可以编辑这些, 我们也不用担心字符输入处理的细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
TextInput {
     id: locationTextInput
...
     width: controlElements.width - locationTextInput.x - controlElements.spacing
     text: locationText
     focus:  true
}
...
TextInput {
     id: updateTextInput
...
     text: forecastUpdateInterval
     maximumLength: 3
     // we use IntValidator just to filter the input. onAccepted is not used here
     validator: IntValidator{bottom: 1; top: 999;}
}
...

updateTextInput使用了一个validator来限制字符长度, 保证拿到的数字是正常范围内的;

Location的名字不需要validator, 但是还需要处理text input, 它会比updateTextInput里的数字长一点; 这个可以通过限制width达到目的, 保证长文字不会超出顶层item的边界; 如果没有设置width, TextInput会跟着输入的文字一起扩展, 超出可视化的边界;

Note 如果有一个multi-line的text给用户编辑, 你可以使用TextEdit元素;

locationTextInput会显式地接收到键盘焦点, 我们把focus设置为了true; 当Configure被加载的时候, 用户可以直接开始改变location的名字:

TextInput元素和新的CheckBox会对鼠标点击进行反应; 如果想要在鼠标之外加上键盘支持的navigation, 用户从一个input元素导航到另一个该怎么做? 如果想要在CheckBox上启动键盘输入又该怎么做?

QtQuick提供了按键navigation和raw key处理过程; 先看一眼key navigation;

为了支持key navigation, 这里对TextInput有些改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TextInput {
     id: locationTextInput
...
     focus:  true
     KeyNavigation.up: offlineCheckBox
     KeyNavigation.down: updateTextInput
}
...
TextInput {
     id: updateTextInput
...
     KeyNavigation.up: locationTextInput
     KeyNavigation.down: secondsCheckBox
}

locationTextInput通过设置focus属性为true来显式地拉取focus; KeyNavigation项提供了附加的属性, 监视按键和输入focus从一个element到另一个element的移动; 对我们的情况来说, KeyNavigation帮了大忙, 我们有很多元素, 需要组织好输入focus的移动;

上面的code sample中, 输入focus从一个locationTextInput移动到updateTextInput, 使用了down arrow键; 如果用户按下up键, focus从updateTextInput到locationTextInput移动回去; 我们把这些语句加到Configure组件中所有相关的元素中; 

处理用户输入的时候, 有时会你需要捕获特定的键; 我们的例子中是Checkbox; 使用桌面程序的时候, 用户学到了使用space键可以toggle切换check box状态; 我们应该把这个功能做到程序中:

这里就要使用Key了; 基本上它是一种信号发射器, 对应几乎键盘上的键值; 信号有个KeyEvent参数, 包含keypress的详细信息; 在checkbox中使用Keys; 上面的代码块中使用了附加的 Keys.onPressed属性, 可以用Return, Enter和Space键切换checkbox的状态; 

更多关于键盘输入处理的细节在Qt Doc: Keyboard Focus in QML

现在有了一个输入元素可以处理用户输入; 要完成Configure组件, 有个步骤还需完成-- 对要存储的新值的verification验证;

当用户点击exitButton, 我们需要检查新设置的值, 没问题的话把它们传递给应用; 这里也是放置Dialog来通知用户新值不正确的地方; 这种情况下. Configure不会关闭, 保持打开状态直到用户提供了正确的值; 查看exitButton的onClick处理代码: 

components/Configure.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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
Rectangle {
     id: root
     property bool showSeconds:  true
     property bool showDate:  true
     property int forecastUpdateInterval: 5  // minutes
     property string locationText:  "Munich"
     property bool forceOffline:  false
 
     width: 320
     height: 480
 
     Image {
         ...
     }
 
     Grid {
         id: controlElements
         spacing: 10
         ...
 
         Text {
             id: locationLabel
             ...   
         }
 
         TextInput {
             id: locationTextInput
             ...
             focus:  true
             KeyNavigation.up: offlineCheckBox
             KeyNavigation.down: updateTextInput
         }
 
         Text {
             id: updateLabel
             height: 90
             text: qsTr( "update interval: <br>(in min)" )
             color: updateTextInput.focus?
                        Qt.lighter(Style.penColor) : Style.penColor
             font.pixelSize: Style.textPixelSize
         }
 
         TextInput {
             id: updateTextInput
             ...
         }
 
         Text {
             id: secondsLabel
             ...
         }
 
         CheckBox {
             id: secondsCheckBox
             checked: showSeconds
             ...
         }
 
         Text {
             id: dateLabel
             ...
         }
 
         CheckBox {
             id: dateCheckBox
             checked: showDate
             ...
         }
 
         Text {
             id: offlineLabel
             ...
         }
 
         CheckBox {
             id: offlineCheckBox
             ...
         }
     }
 
     Dialog {
         id: errorDialog
         ...
         z: root.z+1
         visible:  false
     }
 
     Button {
         id: exitButton
         text: qsTr( "OK" )
         ...
         onClicked: {
             // update interval and location cannot be empty
             // update interval cannot be zero
             if  (updateTextInput.text ==  ""  || updateTextInput.text == 0)
                 errorDialog.show (qsTr( "The update interval cannot be empty" ))
             else  if  (locationTextInput.text ==  "" )
                 errorDialog.show (qsTr( "The location cannot be empty" ))
             else  {
                 forecastUpdateInterval = updateTextInput.text
                 root.locationText = locationTextInput.text
                 root.visible =  false
             }
             // update check box relevant settings
             root.showSeconds = secondsCheckBox.checked
             root.showDate = dateCheckBox.checked
             root.forceOffline = offlineCheckBox.checked
         }
     }
}

下一步

注意offlineCheckBox和相关的magical forceOffline设置; 这个新的设置是用来toggle应用的状态; 下一个版本的应用中我们也会看一下QtQuick的animation动画, 用它们来实现一些漂亮效果;

---9---

---YCR---


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值