首页 > 代码库 > QT Demo 之 imageelements

QT Demo 之 imageelements

在学习了MouseArea和Text之后,这一节开始学习image相关的知识。

和上一节QT Demo 之 text一样,imageelements的入口也是一个LauncherList,然后添加了5个子example,下面我就针对每一个子example进行详细分析。

borderimage.qml

首先看到的是borderimage.qml的主体结构是由一个BorderImageSelector和Flickable组成的:

Rectangle {
    id: page
    width: 320
    height: 480

    BorderImageSelector {...}

    Flickable {...}
}

BorderImageSelector

这里的BorderImageSelector是一个自定义的Component,由content/BorderImageSelector.qml文件定义。

从Demo的运行效果图上可以看到BorderImageSelector实际上是windows上部的左右导航按钮,如下图所示:

查看BorderImageSelector.qml的代码,也可以看到有一个有两个左右的按钮图片、一个按钮相应函数和一个使用Repeater来显示的文本:

Item {
    id: selector
    property int curIdx: 0
    property int maxIdx: 3
    property int gridWidth: 240
    property Flickable flickable
    width: parent.width
    height: 64
    function advance(steps) {...}
    Image {...}
    Image {...}
    Repeater {...}
}
两个左右的按钮,使用的是同一张图片,只不过一个做了镜像;

按钮的点击操作也是一样,一个向左一个向右(调用selector.advance()函数,传递不同的参数来实现);

至于两个按钮的透明度,则是根据当前的是否是最左(或最右)来判断后,进行设置,以便给用户进行操作提示;

    Image {
        source: "arrow.png"
        MouseArea{
            anchors.fill: parent
            onClicked: selector.advance(-1)
        }
        anchors.left: parent.left
        anchors.leftMargin: 8
        anchors.verticalCenter: parent.verticalCenter
        opacity: selector.curIdx == 0 ? 0.2 : 1.0
        Behavior on opacity {NumberAnimation{}}
    }
    Image {
        source: "arrow.png"
        mirror: true
        MouseArea{
            anchors.fill: parent
            onClicked: selector.advance(1)
        }
        opacity: selector.curIdx == selector.maxIdx ? 0.2 : 1.0
        Behavior on opacity {NumberAnimation{}}
        anchors.right: parent.right
        anchors.rightMargin: 8
        anchors.verticalCenter: parent.verticalCenter
    }
advance()函数,则是通过改变curIdx的值影响两个Image以及Repeater的显示效果,通过改变flickable.contentX的值,改变外面的Flickable显示效果:

    function advance(steps) {
         var nextIdx = curIdx + steps
         if (nextIdx < 0 || nextIdx > maxIdx)
            return;
         flickable.contentX += gridWidth * steps;
         curIdx += steps;
    }
最后的Repeater则是通过给定的数据([ "Scale", "Repeat", "Scale/Repeat", "Round" ]),按照指定的方式(delegate属性描述)进行多个item的显示,这里分别是设定了text的x座标以及透明度参数:
    Repeater {
        model: [ "Scale", "Repeat", "Scale/Repeat", "Round" ]
        delegate: Text {
            text: model.modelData
            anchors.verticalCenter: parent.verticalCenter

            x: (index - selector.curIdx) * 80 + 140
            Behavior on x { NumberAnimation{} }

            opacity: selector.curIdx == index ? 1.0 : 0.0
            Behavior on opacity { NumberAnimation{} }
        }
    }
注:在BorderImageSelector.qml中多次使用了类似下面的的动画效果:

        Behavior on opacity {NumberAnimation{}}

因为这里NumberAnimation{}动画是定义在Behavior中,所以其默认的from就是其属性变化的开始值(如上面的1.0),其默认的to就是其属性变化的结束值(如上面的0.0),duration的默认值是250ms。

Flickable子元素

该示例中除了上面的导航条,剩余的元素都放到了Flickable元素中,并且按照Grid的方式进行排列:

    Flickable {
        id: mainFlickable
        width: parent.width
        anchors.bottom: parent.bottom
        anchors.top: selector.bottom
        interactive: false //Animated through selector control
        contentX: -120
        Behavior on contentX { NumberAnimation {}}
        contentWidth: 1030
        contentHeight: 420
        Grid {...}
    }
注1:contentHeight的值是420,大小和整体的高度(480)减去导航条的高度(64)差不多,而contentWidth的值是1030,大概是整体宽度(320)的3倍多;

注2:在运行的程序中,我们发现无法使用鼠标进行水平方向的拖动,这与Flickable的特性冲突。这里的诀窍就是interactive: false属性,如注释中所说,该Flickable通过selector来控制

Flickable中共有8个MyBorderImage排列在一个Grid组中:

        Grid {
            anchors.centerIn: parent; spacing: 20

            MyBorderImage {}
            MyBorderImage {}
            MyBorderImage {}
            MyBorderImage {}
            MyBorderImage {}
            MyBorderImage {}
            MyBorderImage {}
            MyBorderImage {}
        }
Grid的宽度和父元素的宽度一致,各个MyBorderImage元素之间的间距是20

MyBorderImage组件

MyBorderImage定义在:/imageelements/content/MyBorderImage.qml文件中。

Item {
    id: container

    property alias horizontalMode: image.horizontalTileMode
    property alias verticalMode: image.verticalTileMode
    property alias source: image.source

    property int minWidth
    property int minHeight
    property int maxWidth
    property int maxHeight
    property int margin

    width: 240; height: 200

    BorderImage {...}
}
从MyBorderImage的代码中可以看出,一个MyBorderImage包含:最大/最小宽度、最大/最小高度、外边距,以及水平/垂直拉伸/平铺模式和图片的数据源,而调用出的代码也是分别设置不同的属性来演示不同的拉伸/平铺效果。

    BorderImage {
        id: image; anchors.centerIn: parent

        SequentialAnimation on width {}

        SequentialAnimation on height {}

        border.top: container.margin
        border.left: container.margin
        border.bottom: container.margin
        border.right: container.margin
    }

BorderImage的官方说明如下:

The BorderImage element provides an image that can be used as a border.
The BorderImage element is used to create borders out of images by scaling or tiling parts of each image.
A BorderImage element breaks a source image, specified using the url property, into 9 regions, as shown below:


When the image is scaled, regions of the source image are scaled or tiled to create the displayed border image in the following way:

    The corners (regions 1, 3, 7, and 9) are not scaled at all.
    Regions 2 and 8 are scaled according to horizontalTileMode.
    Regions 4 and 6 are scaled according to verticalTileMode.
    The middle (region 5) is scaled according to both horizontalTileMode and verticalTileMode.

使用一个BorderImage,只需要设置width、height、source,以及border属性和horizontalTileMode/verticalTileMode属性即可,但是为了演示BorderImage拉伸和平铺的具体动画效果,分别添加了在width和height上的动画效果。

        SequentialAnimation on width {
            loops: Animation.Infinite
            NumberAnimation {
                from: container.minWidth; to: container.maxWidth
                duration: 2000; easing.type: Easing.InOutQuad
            }
            NumberAnimation {
                from: container.maxWidth; to: container.minWidth
                duration: 2000; easing.type: Easing.InOutQuad
            }
        }
上述动画的意思是,在2s的时间内,BorderImage的width从minWidth变到maxWidth,然后再使用2s的时间变回来。即完整的演示BorderImage改变拉伸/平铺的具体过程,而在height上的动画效果和在width上的类似,此处不再展开。

BorderImage的horizontalTileMode/verticalTileMode属性共有三种取值,分别如下:

  • BorderImage.Stretch - Scales the image to fit to the available area.
  • BorderImage.Repeat - Tile the image until there is no more space. May crop the last image.
  • BorderImage.Round - Like Repeat, but scales the images down to ensure that the last image is not cropped.

分别的意思就是:拉伸、平铺和完整的(就是通过拉伸和平铺配合保证不出现裁边,且效果OK)这三种方式。

注意:其中拉伸的处理和android中的.9.png格式的图片渲染效果是一样的,也就是说BorderImage中的拉伸和平铺都是基于类似.9.png效果的、只针对于中间区域的拉伸/平铺、而保证四个边角不变的一种图像处理。

image.qml

image.qml中的例子比较简单,就是演示了图片的几种fillMode的不同效果:

Rectangle {
    width: 320
    height: 480
    Grid {
        property int cellWidth: (width - (spacing * (columns - 1))) / columns
        property int cellHeight: (height - (spacing * (rows - 1))) / rows

        anchors.fill: parent
        anchors.margins: 30

        columns: 2
        rows: 3
        spacing: 30

        ImageCell { mode: Image.Stretch; caption: "Stretch" }
        ImageCell { mode: Image.PreserveAspectFit; caption: "PreserveAspectFit" }
        ImageCell { mode: Image.PreserveAspectCrop; caption: "PreserveAspectCrop" }

        ImageCell { mode: Image.Tile; caption: "Tile" }
        ImageCell { mode: Image.TileHorizontally; caption: "TileHorizontally" }
        ImageCell { mode: Image.TileVertically; caption: "TileVertically" }
    }
}
上面的cellWidth和cellHeight是通过设置的columns、rows和spacing来计算出来的,即Grid中每一个Item的大小,其值用于ImageCell的width和height中;而columns、rows和spacing则是Grid的属性变量。

这里的ImageCell也是一个自定义的Component,由:/imageelements/content/ImageCell.qml文件描述。

Item {
    property alias mode: image.fillMode
    property alias caption: captionItem.text

    width: parent.cellWidth; height: parent.cellHeight

    Image {
        id: image
        width: parent.width; height: parent.height - captionItem.height
        source: "qt-logo.png"
        clip: true      // only makes a difference if mode is PreserveAspectCrop
    }

    Text {
        id: captionItem
        anchors.horizontalCenter: parent.horizontalCenter; anchors.bottom: parent.bottom
    }
}
从代码中可以看出,该ImageCell是由一个Image和一个Text组成,Image的fileMode和Text的text都是由外面的调用处指定,如:

        ImageCell { mode: Image.Stretch; caption: "Stretch" }
        ImageCell { mode: Image.PreserveAspectFit; caption: "PreserveAspectFit" }
        ImageCell { mode: Image.PreserveAspectCrop; caption: "PreserveAspectCrop" }
Image的fillMode属性是一个枚举变量,支持以下数值:

  • Image.Stretch - the image is scaled to fit(水平和垂直都放大缩小的外边框)
  • Image.PreserveAspectFit - the image is scaled uniformly to fit without cropping(保持比例进行缩放)
  • Image.PreserveAspectCrop - the image is scaled uniformly to fill, cropping if necessary(保持比例进行缩放,但是为了填满空间,会对多出来的部分进行裁剪)
  • Image.Tile - the image is duplicated horizontally and vertically(水平和垂直方向都会平铺)
  • Image.TileVertically - the image is stretched horizontally and tiled vertically(垂直方向平铺)
  • Image.TileHorizontally - the image is stretched vertically and tiled horizontally(水平方向平铺)
  • Image.Pad - the image is not transformed(保持不变)

上面的几种填充模式,默认的是Image.Stretch,如果要保持图片不变,需要自己设置为Image.Pad。

shadows.qml

shadows示例中主要也是演示了BorderImage的特性,只不过通过设置anchors的Margin参数为负值,来实现立体投影的效果:

//! [shadow]
    BorderImage {
        anchors.fill: rectangle
        anchors { leftMargin: -6; topMargin: -6; rightMargin: -8; bottomMargin: -8 }
        border { left: 10; top: 10; right: 10; bottom: 10 }
        source: "shadow.png"
    }
//! [shadow]
因为这个示例中没有什么新鲜知识,不再详述。

animatedsprite.qml

这一个示例重点演示了如何使用AnimatedSprite来进行动画演示,其中MouseArea里面的操作也是针对AnimatedSprite的功能进行演示:

Item {
    width: 320
    height: 480
    Rectangle {
        anchors.fill: parent
        color: "white"
    }

//! [sprite]
    AnimatedSprite {...}
//! [sprite]

    MouseArea {...}
}

AnimatedSprite简述

AnimatedSprite provides rendering and control over animations which are provided as multiple frames in the same image file. You can play it at a fixed speed, at the frame rate of your display, or manually advance and control the progress..

从官方说明中可以了解到,这是一种通过显示图片中不同部分(反过来看,即把一个动画的所有帧都保存在一张图片上)来显示一个动画效果的组件。如本例中的图片(原图片太大,此处只显示了第一行的5帧):


示例中的AnimatedSprite代码如下:

//! [sprite]
    AnimatedSprite {
        id: sprite
        width: 170
        height: 170
        anchors.centerIn: parent
        source: "content/speaker.png"
        frameCount: 60
        frameSync: true
        frameWidth: 170
        frameHeight: 170
        loops: 3
    }
//! [sprite]
通过查看sourece指定的content/speaker.png图片,我们可以看到其长宽分别为850*2040,对应与上面的frameWidth和frameHeight都是170,正好是5*12即frameCount的值。

frameSync的说明是这样的:

If true, then the animation will have no duration. Instead, the animation will advance one frame each time a frame is rendered to the screen. This synchronizes it with the painting rate as opposed to elapsed time.

对于the animation will advance one frame each time a frame is rendered to the screen比较好理解,就是一帧渲染后紧接这渲染下一帧;但是对于This synchronizes it with the painting rate as opposed to elapsed time这句话就完全高度不懂了,正所谓,每个单词的意思都懂,但是连在一起就不知道要表达什么了。大概的意思,我理解的是绘画的速度是相对于渲染速度,而不是按照时间,也就是说如果CPU计算能力强,那么就显示的时间短,如果CPU云算能力太烂,那就慢慢渲染吧。

鼠标操作部分则是演示了AnimatedSprite的几个属性(running、paused)和几个函数操作(start()、pause()、advance()):

    MouseArea {
        anchors.fill: parent
        acceptedButtons: Qt.LeftButton | Qt.RightButton
        onClicked: {
            if (!sprite.running)
                sprite.start();
            if (!sprite.paused)
                sprite.pause();
            if ( mouse.button == Qt.LeftButton ) {
                sprite.advance(1);
            } else {
                sprite.advance(-1);
            }
        }
    }

这里的鼠标左右键操作分别是显示向前和向后的一帧,如果我们需要完成左键正转、右键反转的效果,则可以修改代码如下所示:

        onClicked: {
            if ( mouse.button == Qt.LeftButton ) {
                sprite.reverse = false;
                sprite.restart();
            } else {
                sprite.reverse = true;
                sprite.restart();
            }
        }

spritesequence.qml

这个示例也是展示了一个动画效果,不过采用的方法和上一个示例有所不同,先看一下源文件的主体结构:

Item {
    width: 320
    height: 480
    MouseArea {...}
//! [animation]
    SequentialAnimation {...}
//! [animation]
    SpriteSequence {...}
}
其中SpriteSequence和上一个示例中学到的AnimatedSprite都是属于QML Types中的Visual Types,但是它和AnimatedSprite的使用上有非常大的差异。

SpriteSequence简述

SpriteSequence renders and controls a list of animations defined by Sprite types.

从官方说明上来看,一个SpriteSequence是有多个Sprite来组成的并通过一定的顺序渲染到UI上,这里的例子也可以看的出:

    SpriteSequence {
        id: image
        width: 256
        height: 256
        anchors.horizontalCenter: parent.horizontalCenter
        interpolate: false
        goalSprite: ""
//! [still]
        Sprite{}
//! [still]
        Sprite{}
        Sprite{}
        Sprite{}
        Sprite{}
    }
其中,interpolate参数指定了在进行动画渲染时不需要进行插值处理(如果设置为true,实际效果并不好,此处是希望完全按照Sprite的方式进行动画演示),goalSprite属性设置为空,但是在SequentialAnimation中会改变其值。

剩下的五个Sprite,则依次描述了几个Sprite(可以翻译成为调皮鬼),然后再通过MouseArea的操作,启动SequentialAnimation动画,进一步通过修改SpriteSequence的属性,完成整个动画效果。分开来讲就是:

第一个Sprite,frameCount是1,frameX和frameY属性没有设置,默认都是0,那么取到的就是图片中的第一帧:

        Sprite{
            name: "still"
            source: "content/BearSheet.png"
            frameCount: 1
            frameWidth: 256
            frameHeight: 256
            frameDuration: 100
            to: {"still":1, "blink":0.1, "floating":0}
        }
第二个Sprite,frameCount是3,frameX和frameY分别是256和1536,那么就是图片中的最后三帧:


        Sprite{
            name: "blink"
            source: "content/BearSheet.png"
            frameCount: 3
            frameX: 256
            frameY: 1536
            frameWidth: 256
            frameHeight: 256
            frameDuration: 100
            to: {"still":1}
        }
除了frameCount、frameX和frameY这三个参数,还有两个参数需要重点关心:

frameDuration:still的Sprite只有1帧,而blink的Sprite却有3帧,但是他们的frameDuration都是100,这样动画的效果就会是小熊眨眼的动作很快;

to:这个字段表示了当前Sprite动画完成后,下一个进行的Sprite。blink的to字段只有still,则blink动画完成后肯定会显示still动画,但是still的to字段是{"still":1, "blink":0.1, "floating":0},则表明有10/11的概率会继续显示still,而只有1/11的概率才会显示blink,这两个Sprite配合起来的效果就是小熊半天才会眨一次眼,但是眨眼的动画会很快,而且因为这两个的Sprite的to字段没有跳出他们两个,那么如果没有外界触发,则会一直显示这两个动画。

第三个Sprite的名字是floating,意思是飘动的,结合frameX和frameY都是0,frameCount是9,则是下述的几帧动画:


        Sprite{
            name: "floating"
            source: "content/BearSheet.png"
            frameCount: 9
            frameX: 0
            frameY: 0
            frameWidth: 256
            frameHeight: 256
            frameDuration: 160
            to: {"still":0, "flailing":1}
        }
第四个Sprite,名称是flailing,意思是挥舞,结合frameX和frameY分别是0和768,frameCount是8,则是下述的几帧动画:

        Sprite{
            name: "flailing"
            source: "content/BearSheet.png"
            frameCount: 8
            frameX: 0
            frameY: 768
            frameWidth: 256
            frameHeight: 256
            frameDuration: 160
            to: {"falling":1}
        }
最后一个Sprite,其名称是falling,意思是下落,结合frameX和frameY分别是0和1280,frameCount是5,则是下述的几帧动画:

        Sprite{
            name: "falling"
            source: "content/BearSheet.png"
            frameCount: 5
            frameY: 1280
            frameWidth: 256
            frameHeight: 256
            frameDuration: 160
            to: {"falling":1}
        }
在了解了每一个Sprite的具体图像和意义后,我们需要仔细分析一下他们的to字段,它们之间的关系可以用下图描述:

其中虚线表示,不会自动进行动画跳转,但是如果通过设置goalSprite属性,则可以通过虚线路径进行动画跳转,而本示例中也是利用这一点,在鼠标事件中进行了处理。

下面就看看鼠标事件和触发的动画的代码:

    MouseArea {
        onClicked: anim.start();
        anchors.fill: parent
    }
//! [animation]
    SequentialAnimation {
        id: anim
        ScriptAction { script: image.goalSprite = "falling"; }
        NumberAnimation { target: image; property: "y"; to: 480; duration: 12000; }
        ScriptAction { script: {image.goalSprite = ""; image.jumpTo("still");} }
        PropertyAction { target: image; property: "y"; value: 0 }
    }
//! [animation]
从代码中可以看到,鼠标的点击事件触发了anim动画的开始,而anim动画是一个SequentialAnimation的动画,从名字上也可以看出在这个动画中的的子动画一个个的顺序执行,如官方说明的一般“Animations defined in a SequentialAnimation are run one after the other”。

anim动画中有3个Action和一个Animation,按照顺序执行,则是:

  • 第一步,通过设置SpriteSequence的goalSprite属性,让整个SpriteSequence按照上面我绘制的Sprite关系图进行动画显示,即(blink->)still->floating->flailing->failing
  • 第二步,在整个动画整体演示的过程中,通过改变y坐标让整个动画不断下降,即做到小熊下降的效果
  • 第三步,当上一个NumberAnimation执行完毕后(需要12s的时间,此时y坐标变到480),将SpriteSequence的goalSprite属性置空,并且通过jumpTo接口跳转到still的Sprite动画部分,继续演示(不过这个时候就仍然是开始的眨眼效果)
  • 最后一步,将y坐标调整为0字段,整体恢复到开始状态

总结

学到的知识:

  1. 学到了Behavior和Animation的配合使用
  2. 了解了Repeater的使用
  3. 学习Grid的排版方式
  4. 学习了通过contentX来控制Flickable显示区域的方法
  5. 学习使用BorderImage组件
  6. 了解Image的fillMode字段的作用方式
  7. 学习使用AnimatedSprite组件
  8. 学习了使用SpriteSequence和Sprite组件

这一章示例的学习也耗费了好几天的时间,尤其是在最后一个SpriteSequence组件的使用让我刚开始简直是无处下手的感觉。虽然和AnimatedSprite组件一样,都是展示图片中的动画帧,但是AnimatedSprite组件的使用方式比较单一,只能完全按照图片中定义的动画帧顺序进行显示,而SpriteSequence组件则非常的灵活,通过自定义的Sprite列表(尤其是to字段),结合Animation动画则会完成丰富多彩的视觉效果。

QT Demo 之 imageelements