首页 > 代码库 > QT Demo 之 calqlatr(2) calqlatr.qml

QT Demo 之 calqlatr(2) calqlatr.qml

import QtQuick 2.0
import "content"
import "content/calculator.js" as CalcEngine

同样,这次我们针对qml代码开始的最常见的import部分也不放过了,也要至少做到基本了解和使用。

在Qml中如果需要使用系统组件,必须在开始进行声明。对于自定义的组件也需要在开始的时候import进来,并且需要注意的是,系统组件直接通过名称即可,而对于自定义组件,需要使用""包起来。

QML支持三种的import,分别是:

  • import组件(命名空间):The most common type of import is a module import. Clients can import QML modules which register QML object types and JavaScript resources into a given namespace.
  • import目录:      A directory which contains QML documents may also be imported directly in a QML document. This provides a simple way for QML types to be segmented into reusable groupings: directories on the filesystem.
  • import js文件:JavaScript resources may be imported directly in a QML document. Every JavaScript resource must have an identifier by which it is accessed.

注:三种import中只有import js时,必须使用as指定出唯一的一个Identifier,其它两种可选。

calqlatr.qml的代码主结构

Rectangle {
    id: window
    width: 320
    height: 480
    focus: true
    color: "#272822"

    onWidthChanged: controller.reload()
    onHeightChanged: controller.reload()

    function operatorPressed(operator) { CalcEngine.operatorPressed(operator) }
    function digitPressed(digit) { CalcEngine.digitPressed(digit) }

    Item {}

    AnimationController {}

    Display {}

}

其中id/width/height和color这几个基本properties无需多讲,在之前的示例代码中已经多次使用。这次新用到的一个property是focus,但是我们发现,就算我们把focus的值改为false或者去掉focus属性(使用default值)后,程序的运行并没有任何异常和不同。这里只能说明在这个示例中目前没有使用到这个property,TODO:后面我们会学习到focus这个property具体的作用

onWidthChanged和onHeightChanged

在代码中有下述两行:

    onWidthChanged: controller.reload()
    onHeightChanged: controller.reload()

其意思非常好理解,就是当width或height改变的时候,调用controller.reload()函数来完成UI的重绘。但是让我纠结的是,onWidthChanged和onHeightChanged这两个事件响应函数是在那里定义的???

当我尝试通过帮助文档寻找这两个函数或者关于width和height的signal时,结果也是没有找到。这个时候,我就猜测了,估计又是Qt做了一些内置处理但是又没有任何文档说明的事情。

是不是,针对每一个property,都会有对应的onXXXChanged事件响应函数?答案,是的。因此,除了上面的onWidthChanged和onHeightChanged函数,还有下面的一系列函数:

    onParentChanged: ;
    onOpacityChanged: ;
    onColorChanged: ;
    onXChanged: ;
    onYChanged: ;
    onVisibleChanged: ;
    onFocusChanged: ;

当然,对于id这个特殊的property,就没有对应的onIdChanged函数了。

digitPressed(digit)和operatorPressed(operator)函数

紧接着下面就又定义了两个函数:

    function operatorPressed(operator) { CalcEngine.operatorPressed(operator) }
    function digitPressed(digit) { CalcEngine.digitPressed(digit) }

在之前的文章《如何在QML中定义和使用函数》是我们有了解到如何在Qml中定义和调用一个函数,这里的函数主体也非常简单,是直接透传调用content/calculator.js中对应的函数。但是这两个函数是在哪里有调用到呢???

我找找找,当前文件中没有,无奈使用grep进行查找,发现在Button.qml中有下述的函数调用:

    MouseArea {
        onClicked: {
            if (operator)
                window.operatorPressed(parent.text)
            else
                window.digitPressed(parent.text)
        }
    }

无力吐槽,又是跨文件的使用Object Id,这种设计和使用简直是要系统大了会莫明其妙崩溃的节奏啊。

其他几个子元素

在整个Rectangle下有以下三个子元素:

  • Item,数字和运算符部分
  • AnimationController:UI的初始化,以及当width/height变化时和拖到下面的一个控制条时的UI处理以及动画部分(有点绕,其实就是当UI需要改变时的处理)
  • Display:运算的输出结果部分以及底部的grip控制条,具体实现是在Display.qml文件中

下面就针对每一个子元素详细展开分析。

Item部分

    Item {
        id: pad
        width: 180
        NumberPad { y: 10; anchors.horizontalCenter: parent.horizontalCenter }
    }

代码中指定了Item的width为180,其中数字和运算符部分封装到一个独立的NumberPad的qml文档中。并且指定了NumberPad在Item中的位置是一坐标为相对值10,水平方向相对于Item居中(PS:读者可以自己改变这里的数值和对齐方式,看一下分别的运行效果)。

NumberPad

Grid {
    columns: 3
    columnSpacing: 32
    rowSpacing: 16

    Button { text: "7" }
    Button { text: "8" }
    Button { text: "9" }
    Button { text: "4" }
    Button { text: "5" }
    Button { text: "6" }
    Button { text: "1" }
    Button { text: "2" }
    Button { text: "3" }
    Button { text: "0" }
    Button { text: "." }
    Button { text: " " }
    Button { text: "±"; color: "#6da43d"; operator: true }
    Button { text: "?"; color: "#6da43d"; operator: true }
    Button { text: "+"; color: "#6da43d"; operator: true }
    Button { text: "√"; color: "#6da43d"; operator: true }
    Button { text: "÷"; color: "#6da43d"; operator: true }
    Button { text: "×"; color: "#6da43d"; operator: true }
    Button { text: "C"; color: "#6da43d"; operator: true }
    Button { text: " "; color: "#6da43d"; operator: true }
    Button { text: "="; color: "#6da43d"; operator: true }
}

打开NumberPad.qml文件,看到源码,我才知道原来布局这么简单,直接一个Grid搞定了(其中columns: 3 指定了每行排列三个元素)。但是需要注意的是,在"."之后以及"C"之后因为有一个空白,所以在上面使用Grid进行布局的时候,也需要添加对应的" "的控件:

    Button { text: "." }
    Button { text: " " }
...
    Button { text: "C"; color: "#6da43d"; operator: true }
    Button { text: " "; color: "#6da43d"; operator: true }

在上面的操作符部分,其中"±"、"?"、"√"、"÷"、"×"这几个操作符都不是标准的ASCII字符,其十六进制值分别是"C2B1"、"E2889"2、"E2889A"、"C3B7"、"C397",采用的是”UTF-8无BOM格式"编码。

请注意,这里的Button显示的text和在calculator.js脚本文件中的判断是一一相关的,也就是说,如果在这里修改操作符的text,就会出现在点击该按钮时不能完成原来的功能了(语言有点绕,下面讲解到calculator.js时会讲到这一部分)。

AnimationController部分

    AnimationController {
        id: controller
        animation: ParallelAnimation {
            id: anim
            NumberAnimation { target: display; property: "x"; duration: 400; from: -16; to: window.width - display.width; easing.type: Easing.InOutQuad }
            NumberAnimation { target: pad; property: "x"; duration: 400; from: window.width - pad.width; to: 0; easing.type: Easing.InOutQuad }
            SequentialAnimation {
                NumberAnimation { target: pad; property: "scale"; duration: 200; from: 1; to: 0.97; easing.type: Easing.InOutQuad }
                NumberAnimation { target: pad; property: "scale"; duration: 200; from: 0.97; to: 1; easing.type: Easing.InOutQuad }
            }
        }
    }

这里使用到的AnimationController是我们之前没有遇到的,先看一下官方文档中说明:

Normally animations are driven by an internal timer, but the AnimationController allows the given animation to be driven by a progress value explicitly.

从文档中我们了解到,AnimationController是一种手动来控制其中的动画运行方式的控件。

AnimationController 有两个属性和三个函数,分别是:

Properties

  • animation : Animation
  • progress : real

Methods

  • completeToBeginning()
  • completeToEnd()
  • reload()

本示例中使用到了上面的所有属性和函数,下面就一个个逐步展开。

animation属性以及ParallelAnimation控件

This property holds the animation to be controlled by the AnimationController.

该字段的意义很好理解,就是指的AnimationController中的具体animation。这里具体使用到的是ParallelAnimation控件,这个也是我们之前没有遇到的。其实ParallelAnimation非常好理解,下面是官方说明:

The SequentialAnimation and ParallelAnimation types allow multiple animations to be run together. Animations defined in a SequentialAnimation are run one after the other, while animations defined in a ParallelAnimation are run at the same time.

从说明上可以看出,SequentialAnimation和ParallelAnimation都是封装多个子animation来执行动画,只不过SequentialAnimation中的子animation是按顺序一个个执行,而ParallelAnimation中的子animation是同时执行。官方有一个例子很好的演示了什么情况下需要多个animation同时执行:

Rectangle {
    id: rect
    width: 100; height: 100
    color: "red"

    ParallelAnimation {
        running: true
        NumberAnimation { target: rect; property: "x"; to: 50; duration: 1000 }
        NumberAnimation { target: rect; property: "y"; to: 50; duration: 1000 }
    }
}

上面的示例演示了如何让一个Rectangle按照斜线方向进行移动。

我们看一下在calqlatr示例中,使用ParallelAnimation控件来表现什么动画效果呢?

    AnimationController {
        id: controller
        animation: ParallelAnimation {
            id: anim
            NumberAnimation { target: display; property: "x"; duration: 400; from: -16; to: window.width - display.width; easing.type: Easing.InOutQuad }
            NumberAnimation { target: pad; property: "x"; duration: 400; from: window.width - pad.width; to: 0; easing.type: Easing.InOutQuad }
            SequentialAnimation {
                NumberAnimation { target: pad; property: "scale"; duration: 200; from: 1; to: 0.97; easing.type: Easing.InOutQuad }
                NumberAnimation { target: pad; property: "scale"; duration: 200; from: 0.97; to: 1; easing.type: Easing.InOutQuad }
            }
        }
    }

其中:

  • 第一个NumberAnimation描述了输出结果部分从-16的位置移动到window.width - display.width处;为什么是从-16部分,是因为开始的时候隐藏了左侧的分隔条图案;而且移动到window.width - display.width处的时候会隐藏右边的分隔条图案;具体见Display部分的分析。
  • 第二个NumberAnimation描述了数字和运算符部分从window.width - pad.width移动到0处
  • 第三个是一个SequentialAnimation动画组合,前200ms,pad部分从100%缩小到97%;后200ms,pad部分又从97%恢复到100%大小。(如果不明白,将代码中的0.97改成0.5则效果非常明显)

这样,上面三个动画因为集成在一个ParallelAnimation控件中,那么同时进行动画,就完成了最终的效果(具体见运行效果)。

progress属性

先看一下progress属性的说明:

This property holds the animation progress value.
The valid progress value is 0.0 to 1.0, setting values less than 0 will be converted to 0, setting values great than 1 will be converted to 1.

因为在介绍ParallelAnimation控件时讲到,“AnimationController是一种手动来控制其中的动画运行方式的控件”,那么如何让动画停在具体的那一帧呢?这里就需要使用到progress属性。

在本示例中使用到progress属性的时候略微复杂,那么我们就可以做一个专门的测试,在一个Button的onClicked函数中使用下述代码:

    onClicked: controller.progress = 0.3
    (or onClicked: controller.progress = 0.7)

经过测试,在单击该Button的时候,UI则会按照AnimationController控件中的动画描述停留在30%(或70%)的位置处。

该示例中使用progress是完成了当拖动Display下部的按钮时,整个UI跟随着鼠标的位置在慢慢的变化。

completeToBeginning()函数

Finishes running the controlled animation in a backwards direction.
After calling this method, the animation runs normally from the current progress point in a backwards direction to the beginning state.
The animation controller‘s progress value will be automatically updated while the animation is running.

从上面的说明中,我们了解到completeToBeginning()函数实际上是从动画的当前位置倒序执行,并变化到初识状态。

该示例中使用completeToBeginning()函数是完成了当拖动Display下部的按钮从右侧向左侧移动的过程中松开鼠标时,整个UI回到初识位置,而不是停留在当前鼠标松开的位置(对于计算器的UI只有左右两种UI效果,不存在切换到一半的效果)。

completeToEnd()函数

Finishes running the controlled animation in a forwards direction.
After calling this method, the animation runs normally from the current progress point in a forwards direction to the end state.
The animation controller‘s progress value will be automatically updated while the animation is running.

其意义和使用效果均和completeToBeginning()函数是相对称的,不在赘述。

reload()函数

Reloads the animation properties
If the animation properties changed, calling this method to reload the animation definations.

从说明上可以看出,当ParallelAnimation控件中的animation属性发生变化时,需要调用该函数来进行UI重绘以及整个ParallelAnimation控件状态的创建。

具体到calqlatr示例中,就是当使用鼠标改变应用的宽和高时,需要重新对UI布局,并刷新ParallelAnimation控件的状态(比如completeToEnd()的时候,需要移动到更远的位置)。

    onWidthChanged: controller.reload()
    onHeightChanged: controller.reload()

Display部分

    Display {
        id: display
        x: -16
        width: window.width - pad.width
        height: parent.height

        MouseArea {...}
    }

Display控件是一个自定义的qml控件,具体实现是在Display.qml文件中。这里调用时,只是设置了x坐标、width和height以及MouseArea的具体动作。

这里为什么设置x坐标为-16,是因为Display左右两侧各有一个宽度为16的条边的图片。设置为-16,这样在左侧的时候,只会显示右边的条边图案;当移动到右侧时,只会显示左侧的条边图案。

Display控件的具体实现

Item {
    id: display
    property bool enteringDigits: false

    function displayOperator(operator){}
    function newLine(operator, operand){}
    function appendDigit(digit){}
    function clear(){}

    Item {
        id: theItem
        width: parent.width + 32
        height: parent.height

        Rectangle {}
        Image {}
        Image {}

        Image {}

        ListView {}
    }
}

从代码上可以看出,Display控件有以下元素:

  • 一个Rectangle来将Display部分的底色修改为白色,以便显示运算过程和结果
  • 两个Image分别显示左右侧的条边
  • 第三个Image显示底部的拖动按钮
  • 最后的ListView用来按行显示运算结果
  • 定义了四个运算符,分别是显示运算符、添加新行、添加数字以及清空处理

这几个函数以及ListView中的内容变化均留在分析calculator.js的时候详细展开,本章中重点是针对qml控件的变化。

Display底部的按钮动作

        MouseArea {
            property real startX: 0
            property real oldP: 0
            property bool rewind: false

            anchors {}
            height: 50
            onPositionChanged: {}
            onPressed: startX = mapToItem(window, mouse.x).x
            onReleased: {}
        }

该MouseArea定义了在操作Display底部的按钮时,整个UI的变化。

先看一下anchors和height部分:

            anchors {
                bottom: parent.bottom
                left: parent.left
                right: parent.right
            }
<pre name="code" class="plain">            height: 50

通过设置anchors的bottom、left和right均为parent的对应值,而高度是50是因为该按钮和底部的距离是20再加上自身按钮的高度是30。那么最终的效果就是,按钮的点击范围扩大为Display底部的整个区域,便于用户的操作。

我们继续分析MouseArea的onPositionChanged部分:

            onPositionChanged: {
                var reverse = startX > window.width / 2
                var mx = mapToItem(window, mouse.x).x
                var p = Math.abs((mx - startX) / (window.width - display.width))
                if (p < oldP)
                    rewind = reverse ? false : true
                else
                    rewind = reverse ? true : false
                controller.progress = reverse ? 1 - p : p
                oldP = p
            }

注意,这里使用到了startX变量,该变量是在单击鼠标时获取的鼠标位置:

            onPressed: startX = mapToItem(window, mouse.x).x

其中mapToItem函数我们在之前《QT Demo 之 MouseArea》中有学习到,其作用是把在当前空间中的坐标映射到window空间中的位置,简单的讲就是获取当前鼠标基于整个窗口的位置。

在得到startX之后,通过和窗口的水平中心进行比较,即可得到是在左侧还是在右侧进行操作。

当鼠标移动时,再次通过 mapToItem(window, mouse.x).x获取到鼠标的当前位置,并赋值给mx临时变量。因为鼠标移动是一个过程,中间会触发多次onPositionChanged回调函数,那么不断的通过比较当前坐标和初始坐标之间的间隔,以及配合初始鼠标的位置,即可得到鼠标最后释放时的操作方向。不过在整个过程中,都是通过设置controller.progress的属性来是的UI动画和鼠标同步。

上面的解释比较复杂,而且描述的不是很清晰,下面就使用场景来描述一下:

  1. 按钮在左侧,使用鼠标向右移动:那么reverse为false,p值始终大于oldP,则rewind和reverse保持一致都是false,即鼠标的移动方向最终是向右
  2. 按钮在左侧,使用坐标向右移动的过程中再折返回来:那么reverse为false,开始p值大于oldP,折返后p值开始小雨oldP,则rewind和reverse相反为true,即鼠标的移动方向最终是向左
  3. 按钮在右侧,使用鼠标向左移动:那么reverse为true,p值始终大于oldP,则rewind和reverse保持一致都是true,即鼠标的移动方向最终是向左

  4. 按钮在右侧,使用坐标向左移动的过程中再折返回来:那么reverse为true,开始p值大于oldP,折返后p值开始小雨oldP,则rewind和reverse相反为false,即鼠标的移动方向最终是向右

但是有两种场景上面没有考虑到,那就是:

  • 按钮在左侧,使用鼠标向左移动
  • 按钮在右侧,使用鼠标向右移动

其实也是可以添加条件进行限制的,只是注意这里的p使用的绝对值,在处理上面四种场景时方便但不能覆盖所有场景。至于如何优化此处的代码使得也能覆盖上面两种场景,此处不再详述。如有读者读到这里不清楚,则可以在进行交流沟通。


按照上面的场景分析,当确定鼠标的最终移动方向后,则当释放鼠标按钮时,就需要UI动画切换到开始或者结束的状态,而不能停留在中间的某个状态上。

            onReleased: {
                if (rewind)//鼠标的移动方向最终是向左
                    controller.completeToBeginning()
                else//鼠标的移动方向最终是向右
                    controller.completeToEnd()
            }

总结

本节学到的新知识:

  1. onPropertyChanged系列函数
  2. AnimationController控件的学习和使用

在最刚开始接触Qml时,乍看calqlatr的qml代码完全是一头雾水,在经过前面几篇分析的基础上,现在在来学习calqlatr的代码就不是那么复杂和难于理解了。

这一篇的博客篇幅比较长,也花费了我将近一周的业余时间完成。和之前的示例代码相比,虽然都是使用各种qml控件,但是这个示例中尤其是Mousearea的处理部分则是添加了很多的业务逻辑部分,而且理解起来还不是那儿直接。

不知道具体的效率如何,但是从代码的编写上,感觉Qt的Qml对于UI的操作性还是蛮强的。

QT Demo 之 calqlatr(2) calqlatr.qml