与 Qt model/view 架构类似,在自定义用户界面中,代理扮演着重要的角色。模型中的每一个数据项都要通过一个代理向用户展示,事实上,用户看到的可视部分就是代理。
每一个代理都可以访问一系列属性和附加属性。这些属性及附加属性中,有些来自于数据模型,有些则来自于视图。前者为代理提供了每一个数据项的数据信息;后者则是有关视图的状态信息。
代理中最常用到的是来自于视图的附加属性ListView.isCurrentItem
和ListView.view
。前者是一个布尔值,用于表示代理所代表的数据项是不是视图所展示的当前数据项;后者则是一个只读属性,表示该代理所属于的视图。通过访问视图的相关数据,我们就可以创建通用的可复用的代理,用于适配视图的大小和展示特性。下面的例子展示了每一个代理的宽度都绑定到视图的宽度,而代理的背景色则根据附加属性ListView.isCurrentItem
的不同而有所不同。
import QtQuick 2.0 Rectangle { width: 120 height: 300 gradient: Gradient { GradientStop { position: 0.0; color: "#f6f6f6" } GradientStop { position: 1.0; color: "#d7d7d7" } } ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 delegate: numberDelegate spacing: 5 focus: true } Component { id: numberDelegate Rectangle { width: ListView.view.width height: 40 color: ListView.isCurrentItem?"#157efb":"#53d769" border.color: Qt.lighter(color, 1.1) Text { anchors.centerIn: parent font.pixelSize: 10 text: index } } } }
代码运行结果如下图所示:
如果该模型的每一个数据项都关联一个动作,例如,响应对该数据项的点击操作,那么,这种功能就应该是每一个代理的一部分。这将事件管理从视图分离出来。视图主要处理的是各个子视图之间的导航、切换,而代理则是对一个特定的数据项的事件进行处理。完成这一功能最常用的方法是,为每一个视图创建一个MouseArea
,然后响应其onClicked
信号。我们会在后面看到这种实现的示例。
为增加、移除项添加动画
很多情况下,一个视图中的数据项并不是固定不变的,而是需要动态地增加、移除。数据项的增加、移除,其实是底层模型的修改的相关反应。此时,添加动画效果往往是个不错的选择,可以让用户清晰地明白究竟是哪些数据发生了改变。
为了达到这一目的,QML 为每个代理提供了两个信号,onAdd
和onRemove
,只要将这些信号与动画效果关联起来即可。
下面的例子演示了为动态修改ListModel
增加动画效果。在屏幕下方有一个用于新增数据项的按钮。点击该按钮,会通过调用append
函数向模型增加一个数据项。这将触发视图创建一个新的代理,并且发出GridView.onAdd
信号。该信号关联了一个SequentialAnimation
类型的动画,利用scale
属性的变化,将代理缩放到视图。当视图中的一个数据项被点击时,该项会通过调用视图的remove
函数被移除。这会发出GridView.onRemove
信号,触发另一个SequentialAnimation
类型的动画。不过,这一次代理需要在动画结束之后才能被销毁(相比之下,在添加代理时,代理必须在动画开始之前就被创建)。为了达到这一目的,我们使用PropertyAction
元素,在动画开始之前将GridView.delayRemove
属性设置为true
,动画完成之后,再将其设置为false
。这保证了在代理被销毁之前,动画能够顺利完成。
import QtQuick 2.0 Rectangle { width: 480 height: 300 gradient: Gradient { GradientStop { position: 0.0; color: "#dbddde" } GradientStop { position: 1.0; color: "#5fc9f8" } } ListModel { id: theModel ListElement { number: 0 } ListElement { number: 1 } ListElement { number: 2 } ListElement { number: 3 } ListElement { number: 4 } ListElement { number: 5 } ListElement { number: 6 } ListElement { number: 7 } ListElement { number: 8 } ListElement { number: 9 } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 20 height: 40 color: "#53d769" border.color: Qt.lighter(color, 1.1) Text { anchors.centerIn: parent text: "Add item!" } MouseArea { anchors.fill: parent onClicked: { theModel.append({"number": ++parent.count}); } } property int count: 9 } GridView { anchors.fill: parent anchors.margins: 20 anchors.bottomMargin: 80 clip: true model: theModel cellWidth: 45 cellHeight: 45 delegate: numberDelegate } Component { id: numberDelegate Rectangle { id: wrapper width: 40 height: 40 gradient: Gradient { GradientStop { position: 0.0; color: "#f8306a" } GradientStop { position: 1.0; color: "#fb5b40" } } Text { anchors.centerIn: parent font.pixelSize: 10 text: number } MouseArea { anchors.fill: parent onClicked: { if (!wrapper.GridView.delayRemove) theModel.remove(index); } } GridView.onRemove: SequentialAnimation { PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: true } NumberAnimation { target: wrapper; property: "scale"; to: 0; duration: 250; easing.type: Easing.InOutQuad } PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: false } } GridView.onAdd: SequentialAnimation { NumberAnimation { target: wrapper; property: "scale"; from: 0; to: 1; duration: 250; easing.type: Easing.InOutQuad } } } } }
下图是运行初始效果。
改变代理的形状
在表现列表时,常常会有这么一种机制:当数据项被选中时,该项会变大以充满屏幕。这种行为可以将被激活的数据项放置在屏幕中央,或者为用户显示更详细的信息。在下面的例子中,ListView
的每一个数据项在点击时都会充满整个列表视图,多出来的额外空间用于显示更多信息。我们使用状态实现这种机制。在这个过程中,列表的很多属性都会发生改变。
首先,wrapper
的高度会被设置为ListView
的高度;缩略图会变大,从先前的位置移动到一个更大的位置。除此以外,两个隐藏的组件,factsView
和closeButton
会显示在恰当的位置。最后,ListView
的contentsY
属性会被重新设置为代理的y
值。contentsY
属性其实就是视图的可见部分的顶部距离。视图的interactive
属性会被设置为false
,这可以避免用户通过拖动滚动条使视图移动。当数据项第一次被点击时,它会进入expanded
状态,是其代理充满整个ListView
,并且重新布局内容。当点击关闭按钮时,expanded
状态被清除,代理重新回到原始的状态,ListView
的交互也重新被允许。
import QtQuick 2.0 Item { width: 300 height: 480 Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: "#4a4a4a" } GradientStop { position: 1.0; color: "#2b2b2b" } } } ListView { id: listView anchors.fill: parent delegate: detailsDelegate model: planets } ListModel { id: planets ListElement { name: "Mercury"; imageSource: "images/mercury.jpeg"; facts: "Mercury is the smallest planet in the Solar System. It is the closest planet to the sun. It makes one trip around the Sun once every 87.969 days." } ListElement { name: "Venus"; imageSource: "images/venus.jpeg"; facts: "Venus is the second planet from the Sun. It is a terrestrial planet because it has a solid, rocky surface. The other terrestrial planets are Mercury, Earth and Mars. Astronomers have known Venus for thousands of years." } ListElement { name: "Earth"; imageSource: "images/earth.jpeg"; facts: "The Earth is the third planet from the Sun. It is one of the four terrestrial planets in our Solar System. This means most of its mass is solid. The other three are Mercury, Venus and Mars. The Earth is also called the Blue Planet, 'Planet Earth', and 'Terra'." } ListElement { name: "Mars"; imageSource: "images/mars.jpeg"; facts: "Mars is the fourth planet from the Sun in the Solar System. Mars is dry, rocky and cold. It is home to the largest volcano in the Solar System. Mars is named after the mythological Roman god of war because it is a red planet, which signifies the colour of blood." } } Component { id: detailsDelegate Item { id: wrapper width: listView.width height: 30 Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: 30 color: "#333" border.color: Qt.lighter(color, 1.2) Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 4 font.pixelSize: parent.height-4 color: '#fff' text: name } } Rectangle { id: image width: 26 height: 26 anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: 2 anchors.topMargin: 2 color: "black" Image { anchors.fill: parent fillMode: Image.PreserveAspectFit source: imageSource } } MouseArea { anchors.fill: parent onClicked: parent.state = "expanded" } Item { id: factsView anchors.top: image.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom opacity: 0 Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: "#fed958" } GradientStop { position: 1.0; color: "#fecc2f" } } border.color: '#000000' border.width: 2 Text { anchors.fill: parent anchors.margins: 5 clip: true wrapMode: Text.WordWrap color: '#1f1f21' font.pixelSize: 12 text: facts } } } Rectangle { id: closeButton anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: 2 anchors.topMargin: 2 width: 26 height: 26 color: "#157efb" border.color: Qt.lighter(color, 1.1) opacity: 0 MouseArea { anchors.fill: parent onClicked: wrapper.state = "" } } states: [ State { name: "expanded" PropertyChanges { target: wrapper; height: listView.height } PropertyChanges { target: image; width: listView.width; height: listView.width; anchors.rightMargin: 0; anchors.topMargin: 30 } PropertyChanges { target: factsView; opacity: 1 } PropertyChanges { target: closeButton; opacity: 1 } PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false } } ] transitions: [ Transition { NumberAnimation { duration: 200; properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY" } } ] } } }
运行结果如下所示:
点击每一项可以开始一个动画:
这里展示的技术在某些方面非常实用,比如一些歌曲播放器允许用户在点击某首歌曲后,会将该歌曲的信息放大显示等。
8 评论
一年半以前我在MFC和Qt的选择中选择了后者,网上好难找到系统入门级的东西,最后跟着您的文章一路学习,并在一年前找到了一份C++/Qt的开发工作,今天,我经过了一年多的锻炼,对于公司的开发需求,大部分我已经可以独自完成,昨天买了您的书,我想今后可以把QML的加入到我的开发中,因为我觉得之前开发的东西虽然功能实现了,但是样子太丑了,我知道自己还有很远的路要走,再次在此对您说声感谢!将来我有了能力,一定要像您一样造福后来者。
顺便借此请教个问题,最近要做个功能,定时发送一封带附件的邮件到制定邮箱,找了很多资料好多都过实了,请问您有没有什么办法可以告之于我。谢谢。
定时发送可以利用 QTimer 实现,在 timeout() 信号槽里面发送邮件。这样子可以吗?
额,不好意思,我没有说清楚,其实我是想问的时如何实现发送邮件。
Qt 本身没有发送邮件的 API,可以选择 QxtSmtp 库,或者自己按照 SMTP 协议,使用 QTcpSocket 连接服务器发送邮件,可以参考 http://www.qtcentre.org/threads/2221-Sending-email-using-Qt 这里的代码示例。
感谢提点,我去试试
QxtSmtp这库真是不错,果然好用,谢谢,关于Qt Creator 引入lib 有没有资料可以参考下?我昨天弄到12点,也不能用,以前实在没怎么用过Creator , 今早在VS上弄好的。。。
豆子老师你好,请教个问题:在视频层上面添加个控件,要讲该控件透明,又不盖住该区域的视频,怎么处理。试了好多办法都不行,网上也没找到可行的办法。不胜感激!!!
豆子你好,我在tableViewColum里定义里delegate,发现delegate里面定义的id,属性什么的都不能在delegate以外调用,会出现not defined的报错,这要怎么解决呢?