Qml組件化編程1-按鈕的定製與封裝

簡介

本文是《Qml組件化編程》系列文章的第一篇,濤哥將教大家,如何在Qml中實現各種功能的按鈕,

同時也會教大家一些組件化編程的思想,如何將做好的功能封裝成一個個組件,以便在工程中複用。

文章主要發佈在濤哥的博客知乎專欄-濤哥的Qt進階之路

寫作背景

作者“武威的濤哥”,從2015年開始參加工作,便入手了Qml,參與了很多大大小小的Qml項目,

至今已有四年多實戰經驗。

2019年,濤哥決定把自己掌握的很多知識都總結整理出來,以《Qml組件化編程》系列文章的形式分享給廣大Qml

愛好者和開發者。

濤哥會堅持高質量和深入淺出的原則,將文章寫好,讓支持濤哥的讀者能夠受益匪淺。

系列文章中涉及的源代碼,絕大部分濤哥都會在github上開源。

如果覺得濤哥寫的還不錯,還請爲濤哥打個賞,您的讚賞是濤哥持續創作的源泉。

有時也難免會犯一些錯誤,希望看到的讀者能夠熱心指出。有任何相關的問題,也歡迎與濤

哥交流,向濤哥提出建議和意見。

感謝大家!

文章定位

濤哥寫的是進階教程,教大家如何從新手成長爲高手,要理解文章中的內容,需要有一點兒Qml基礎。

關於基礎教程,網絡上有很多,濤哥推薦以下幾個質量比較高的:

豆子的文章前75章關於Widget部分也很不錯。

  • 還有一本安曉輝的書《QtQuick核心編程》

我眼中的QQuick

QQuick使用Qml來描述界面,Qml是可以與html5媲美的存在,其開發效率、舒適度、描述能力和定製化能力已經遠遠超

過了QWidget,配合各種屬性動畫、粒子系統和Effects特效可以輕易做出各種酷炫、現代化的UI,不規則的圖形可以通

過Painter的方式實現,對於渲染有性能要求的地方可以通過集成OpenGL、Vulkan等圖形API的方式來處理。

Qml的特性是自由和靈活,這也是它的缺點,上手Qml需要一小段時間的適應,之後就會大量的造輪子,造的多了就輕車

熟路了,常見的各種二維界面或效果基本上都能造出來。

(當然Qml中也有些bug,需要一定的經驗和技巧才能解決。話說回來,哪個框架沒點Bug呢?)

再久一點,可以考慮考慮Qml中的設計模式(或者叫慣用手法),如何抽象出通用Qml庫、如何最大程度地複用Qml

代碼、如何讓Qml代碼更容易維護等。

Qt版本的選擇

Qt發佈的版本有很多,一般穩妥的做法是使用LTS(長期支持版)版本的最後一個修正版本。

當前已知的LTS最新修正版本是5.9.8和5.12.3 , 濤哥會優先使用這兩個版本,後續有版本

要求的地方,濤哥都會進行額外的說明。

默認按鈕

開始進入正題了

Qml中已經有了現成的按鈕,在QtQuick.Controls模塊中, 目前有兩個版本:

  • QtQuick.Controls 1.x

預覽

1.x的版本是通過style的方式進行定製

預覽

  • QtQuick.Controls 2.x

預覽

2.x的版本則是通過修改Control屬性的方式進行定製

預覽

2.x版本的默認風格,還可以通過修改配置文件qtquickcontrols2.conf中的style來修改

可參考Qt在線文檔- style配置

Qml版本混用

這裏順便說一個經常有人問到的問題: 2.x版本中能否混用1.x版本的控件?

答案是可以。只需要使用別名機制即可。(如果你看過Qml的源代碼,就能輕易發現這個用法)

// MixControls.qml
import QtQuick 2.0                              
import QtQuick.Controls 2.0                    //導入Controls 2.0模塊
import QtQuick.Controls 1.4 as QC14            //導入Controls 1.4模塊,取別名QC14
import QtQuick.Controls.Styles 1.4 as QCS14    //導入Controls.Styles 1.4模塊,取別名QCS14

Rectange {
    Button {        //默認使用2.0的Button
    }

    QC14.Button {   //通過別名使用1.4的Button
        style: QCS14.ButtonStyle {  //通過別名使用1.4的style

        }
    }
}

按鈕的本質

默認的按鈕,很多時候並不能達到想要的效果,比如圓角、貼圖片、漸變色的按鈕等等,還需要做很多的定製化工作

其實按鈕的本質,就是一個可以點擊的區域(MouseArea),附帶顏色(Rectangle)、圖片(Image)或文字(Text)。

濤哥教大家造第一個輪子,一個帶文字的按鈕。

// Taobutton.qml
import QtQuick 2.0
import QtQuick.Controls 2.0
Item  {
    width: 800
    height: 600
    property color btnColor: "#009688"
    Rectangle {
        width: 140
        height: 40
        anchors.centerIn: parent

        color: btnArea.pressed ? Qt.darker(btnColor, 1.2) : (btnArea.containsMouse ? Qt.lighter(btnColor, 1.2) : btnColor)
        Text {
            id: btnText
            anchors.centerIn: parent
            text: qsTr("我是一個按鈕")
        }
        MouseArea {
            id: btnArea
            anchors.fill: parent
            hoverEnabled: true  //打開懸浮
            onClicked: {
                console.log(qsTr("按鈕被按了"))
            }
        }
    }
}

使用qmlscene預覽一下效果

預覽

(gif錄製工具不能鼠標懸浮,所以懸浮效果看不到,實際上是有懸浮效果的,可自行嘗試)

說明一下,qml中有個全局的對象Qt,它有一組調顏色的函數,Qt.lighter和Qt.darker,分別可以按係數來變淺和加深

一個顏色值,這樣只要有了一個顏色值,就能自動計算出深一點或者淺一點的顏色,可以減少很多配顏色的工作哦

再看一下這個表達式,當鼠標按下去的時候,使用深一點的顏色,鼠標懸浮的時候使用淺一點的顏色,其它情況就用設定的顏色

    color: btnArea.pressed ? Qt.darker(btnColor, 1.2) : (btnArea.containsMouse ? Qt.lighter(btnColor, 1.2) : btnColor)

這裏的1.2是試出來的值,也可以使用其它值。

按鈕的演變

如果要帶個邊框呢?只要給Rectange設置邊框的寬度和顏色就行了

    property color btnBorderColor: "orange"
    Rectangle {
        ...
        border.width: btnArea.containsMouse ? 2 : 0 //鼠標懸浮時有寬度,才能看到邊框
        border.color: btnBorderColor
        ...
    }

如果要圓角呢? 只要給Rectangle 設置radius就行了

    Rectangle{
        ...
        radius: 5
        ...
    }

如果要圓形呢?只要給Rectangle設置寬度和高度相等(正方形),radius是寬度的一半即可

    Rectangle{
        ...
        width: 120
        height: width
        radius: width / 2
        ...
    }

如果要背景色做成漸變的呢?

    Rectangle{
        id: gradientBtn
        ...
        property color btnColor: containsMouse ? Qt.darker("#009688") : "#009688"
        color: btnColor
        gradient: Gradient {
            GradientStop { position: 0 ; color: Qt.darker(gradientBtn.btnColor, 1.2) }
            GradientStop { position: 0.5 ; color:  Qt.darker(gradientBtn.btnColor, 1.4)  }
            GradientStop { position: 1 ; color:  Qt.darker(gradientBtn.btnColor, 1.6) }
        }
        ...
    }

順帶提一下,5.12的Rectangle,有了新的漸變色 漸變色網站,大約有180種

可以直接在Qml中使用。


    gradient: Gradient.NightFade //通過枚舉使用

    gradient: "SugarLollipop"       //通過字符串名字使用

預覽

(gif錄製工具不能鼠標懸浮,所以懸浮效果看不到,實際上是有懸浮效果的,可自行嘗試)

組件化按鈕

爲了能夠複用我們的按鈕,需要將它做成一個組件。

這裏以文本按鈕爲例:

// TTextBtn.qml

import QtQuick 2.9
import QtQuick.Controls 2.0
Rectangle {
    id: root
    property alias textItem: t      //導出Text實例,方便外部直接修改
    property alias text: t.text     //導出文本
    property alias textColor: t.color   //導出文本顏色
    property alias containsMouse: area.containsMouse    //導出鼠標懸浮
    property alias containsPress: area.containsPress    //導出鼠標按下
    signal clicked();               //自定義點擊信號
    color: "transparent"
    Text {
        id: t
        //默認座標居中
        anchors.centerIn: parent
        //默認文字對齊方式爲水平和垂直居中
        verticalAlignment: Text.AlignVCenter
        horizontalAlignment: Text.AlignHCenter
        //默認寬度爲parent的寬度,這樣字太長超出範圍時自動顯示省略號
        width: parent.width
    }

    MouseArea {
        id: area
        anchors.fill: parent;
        hoverEnabled: parent.enabled;
        onClicked: root.clicked();  //點擊時觸發自定義點擊信號
        cursorShape: Qt.PointingHandCursor  //懸浮或點擊時的鼠標樣式
    }
}

Qml組件,先以最簡單的方式理解,就是放在一個單獨的Qml文件中,聲明一些屬性導出,由使用者去實例化並設置屬性

比如這樣:

預覽

爲了和標準的組件區分開,濤哥寫的組件名字都以大寫的T開頭。

組件化的好處,包括容易複用(如上圖,多個實例都不一樣)、可以統一修改(後面會有動態換皮膚的方案,依賴於組件化)、

便於維護和擴展等。後續濤哥還會講如何做多層抽象的組件、單例組件、如何引用插件中的組件等。

組件化圖片按鈕

帶圖片的按鈕,只需要把Rectangle換成Image即可。

爲了應對各種按鈕狀態,濤哥做了以下的屬性擴展

// TImageBtn.qml

import QtQuick 2.9
import QtQuick.Controls 2.0
Item {
    id: root
    property url normalUrl      //常規狀態下的圖片路徑
    property url hoveredUrl     //懸浮
    property url pressedUrl     //按下
    property url disabledUrl    //禁用

    property alias imageItem: img           //直接別名導出Image實例,外面可以修改其任意屬性
    property alias imageUrl: img.source     //別名導出圖片路徑

    property alias imageWidth: img.width
    property alias imageHeight: img.height
    property alias imageAnchors: img.anchors
    property alias containsMouse: area.containsMouse
    property alias containsPress: area.containsPress
    //點擊信號
    signal clicked();
    Image {
        id: img
        anchors.fill: parent
        //默認按鼠標狀態選取不同的圖片
        source: root.enabled ? (containsPress ? pressedUrl : (containsMouse ? hoveredUrl : normalUrl)) : disabledUrl
    }

    MouseArea {
        id: area
        anchors.fill: parent;
        hoverEnabled: parent.enabled;
        onClicked: root.clicked();
        cursorShape: Qt.PointingHandCursor
        preventStealing: true
    }
}

組件化圖文按鈕

有了前面的文字按鈕和圖片按鈕,我們可以做一個圖片和文字都有的按鈕。

圖片和文字同時顯示,那麼就有了佈局問題。圖片在左?還是在右?在上?還是在下?

濤哥這裏用了點技巧,封裝了一個同時支持四種佈局的圖文按鈕

// TImgTextBtn.qml

import QtQuick 2.9
import QtQuick.Controls 2.0
Rectangle {
    id: root
    property url normalUrl
    property url hoveredUrl
    property url pressedUrl
    property url disabledUrl

    property alias imageItem: img
    property alias imageUrl: img.source
    property alias imageWidth: img.width
    property alias imageHeight: img.height

    property alias textItem: t
    property alias text: t.text
    property alias textColor: t.color
    property alias containsMouse: area.containsMouse
    property alias containsPress: area.containsPress
    signal clicked();

    //5.10以前的版本,Qml中沒有枚舉,用int屬性代替枚舉
    property int layoutType: layoutImageLeft    //佈局類型,默認圖片在左,外部可修改
    readonly property int layoutImageLeft: 0    //圖片在左 只讀屬性,代替枚舉
    readonly property int layoutImageRight: 1   //圖片在右 只讀屬性,代替枚舉
    readonly property int layoutImageUp: 2      //圖片在上 只讀屬性,代替枚舉
    readonly property int layoutImageDown: 3    //圖片在下 只讀屬性,代替枚舉
    color: "transparent"
    Image {
        id: img
        source: root.enabled ? (containsPress ? pressedUrl : (containsMouse ? hoveredUrl : normalUrl)) : disabledUrl
    }
    Text {
        id: t
        //默認文字對齊方式爲水平和垂直居中
        verticalAlignment: Text.AlignVCenter
        horizontalAlignment: Text.AlignHCenter
    }
    //按佈局類型 處理佈局
    Component.onCompleted: {
        switch (layoutType) {
        case layoutImageLeft:
            img.anchors.verticalCenter = root.verticalCenter
            t.anchors.verticalCenter = root.verticalCenter
            img.anchors.left = root.left
            t.anchors.left = img.right
            t.anchors.leftMargin = 6
            break;
        case layoutImageRight:
            img.anchors.verticalCenter = root.verticalCenter
            t.anchors.verticalCenter = root.verticalCenter
            t.anchors.left = root.left
            img.anchors.left = t.right
            img.anchors.leftMargin = 6
            break
        case layoutImageUp:
            img.anchors.horizontalCenter = root.horizontalCenter
            t.anchors.horizontalCenter = root.horizontalCenter
            img.anchors.top = root.top
            t.anchors.top = img.bottom
            t.anchors.topMargin = 6
            break
        case layoutImageDown:
            img.anchors.horizontalCenter = root.horizontalCenter
            t.anchors.horizontalCenter = root.horizontalCenter
            t.anchors.top = root.top
            img.anchors.top = t.bottom
            img.anchors.topMargin = 6
            break;
        }
    }
    MouseArea {
        id: area
        anchors.fill: parent;
        hoverEnabled: parent.enabled;
        onClicked: root.clicked();
        cursorShape: Qt.PointingHandCursor
    }
}

最後,我們來看看效果吧

預覽

轉載聲明

文章出自濤哥的博客
文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 © 濤哥

聯繫方式


作者 濤哥
開發理念 弘揚魯班文化,傳承工匠精神
博客 https://jaredtao.github.io
知乎 https://www.zhihu.com/people/wentao-jia
郵箱 [email protected]
微信 xsd2410421
QQ 759378563

請放心聯繫我,樂於提供諮詢服務,也可洽談商務合作相關事宜。

打賞


如果覺得濤哥寫的還不錯,還請爲濤哥打個賞,您的讚賞是濤哥持續創作的源泉。


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