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的操作性還是蠻強的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章