第七章:QtQuick控件


第七章:QtQuick控件

UI 控件

本章介紹如何使用 Qt Quick Controls 模塊。 Qt Quick Controls 用於創建由標准組件(如按鈕、標簽、滑塊等)構建的高級用戶界面。
Qt Quick Controls 可以使用 布局模塊 進行排列,並且易於設置樣式。在深入定制樣式之前,我們還將研究不同平台的各種樣式。

控件簡介

Qt Quick 可以提供原始的圖形和交互元素讓你從頭開始構建用戶界面。使用 Qt Quick Controls,可以從一組稍微結構化的控件開始構建。
控件種類豐富,從簡單的文本標簽、按鈕到復雜的滑塊和軟鍵盤。如果你想創建基於經典用戶交互模式下的用戶界面,這些控件非常好用,它們提供了很豐富的基礎功能。
Qt Quick控件自帶開箱即用的豐富的樣式,展示如下。Basic 是基礎的平面樣式,Universal(通用)樣式基於微軟通用設計規范,而 Material (材質)是基於谷歌設計規范,Fusion (混合)是桌面導向的樣式。
有些樣式可以通過修改調色板來調整。Imagine風格是基於圖像文件,這允許圖形設計師創建一個新的風格,而不需要編寫任何代碼,甚至不需要調整調色板顏色代碼。

  • Basic
  • Fusion
  • macOS
  • Material
  • Imagine
  • Windows
  • Universal

    導入QtQuick.Controls就可以使用Qt Quick Controls 2 控件了。下面模塊也很有意思:
  • QtQuick.Controls - 基礎控件
  • QtQuick.Templates - 影響控件行為的非可視化基礎類型
  • QtQuick.Controls.Imagine - 支持Imagine樣式
  • QtQuick.Controls.Material - 支持Material樣式
  • QtQuick.Controls.Universal - 支持Universal樣式
  • Qt.labs.platform - 支持平台原生的常見對話框,如文件選擇框,顏色選擇框等,以及系統托盤圖標和標准路徑。

Qt.Labs
注意Qt.labs模塊是實驗性的,這意味着模塊內的API在各版本間可能會有比較大的差異。

圖片查看器

一起來看下稍大點的例子里,Qt Quick控件是如何使用的。我們將創建一個簡單的圖片查看器。
首先,我們使用Fusion樣式創建一個桌面程序。之后在最終代碼完成前,將對其進行重構使家具有移動設備的操作體驗。

桌面版

桌面版基於經典的Windows桌面應用的樣式,有一個菜單欄、一個工具欄以及一個文檔區域。程序運行界面如下:

先使用Qt Creator工程模板創建一個空的Qt Quick應用,但需要在模板中將默認的Window元素替換為QtQuick.Controls模塊里的ApplicationWindow元素。下面的main.qml中的代碼將以默認的尺寸和標題生成一個Windows窗體。

  1. import QtQuick 
  2. import QtQuick.Controls 
  3. import Qt.labs.platform 
  4.  
  5. ApplicationWindow { 
  6. visible: true 
  7. width: 640 
  8. height: 480 
  9.  
  10. // ... 
  11.  
  12. } 

ApplicationWindow由以下4個主要區域組成。由MenuBarToolBarTabBar控件生成的菜單欄、工具欄、狀態欄,而內容區域則由子窗體承擔。注意圖片查看器一般沒有狀態欄,所以下面的代碼里也沒有狀態欄,如上圖所示。

編寫桌面程序,我們使用Fusion 樣式。通過配置文件、環境變量、命令行參數或程序中的C++代碼,都可以配置樣式。這里使用代碼的方法,如下所示:

QQuickStyle::setStyle("Fusion");

接下來就在main.qml中添加Image元素來展示內容,以及其它用戶界面。當用戶點擊這個元素時,它被用來承載圖片,而目前它還只是占位符。background屬性用來替換窗體內容后面的背景的一個元素,當沒有圖片加載,或者圖片邊框寬高比例不足以填充滿窗體內容區域時,背景元素將被顯示。

  1. ApplicationWindow { 
  2.  
  3. // ... 
  4.  
  5. background: Rectangle { 
  6. color: "darkGray" 
  7. } 
  8.  
  9. Image { 
  10. id: image 
  11. anchors.fill: parent 
  12. fillMode: Image.PreserveAspectFit 
  13. asynchronous: true 
  14. } 
  15.  
  16. // ... 
  17.  
  18. } 

接下來增加工具欄ToolBar。這要用到Window的toolBar屬性。在工具欄里要增加一個Flow元素,以確保工具按鈕的適當寬度,當工具按鈕足夠多時會在適當的位置換到下一行。在這個flow元素中,放一個ToolButton工具按鈕。
ToolButton有一些有意思的屬性。Text屬性是字符串型的,而icon.name的值取自freedesktop.org Icon Naming Specification (opens new window)。在本文檔中,按名字列出了標准圖標的列表。通過引用圖標名,Qt將為當前桌面樣式選擇恰當的圖標。
ToolButtononClicked信號處理函數中編寫一段代碼,它調用了fileOpenDialog元素的open函數。

  1. ApplicationWindow { 
  2.  
  3. // ... 
  4.  
  5. header: ToolBar { 
  6. Flow { 
  7. anchors.fill: parent 
  8. ToolButton { 
  9. text: qsTr("Open") 
  10. icon.name: "document-open" 
  11. onClicked: fileOpenDialog.open() 
  12. } 
  13. } 
  14. } 
  15.  
  16. // ... 
  17.  
  18. } 

fileOpenDialog 元素是來自 Qt.labs.platform 模塊的 FileDialog 控件。文件對話框可用於打開或保存文件。
首先在代碼指定一個標題title。然后我們使用 StandardsPaths 類設置起始文件夾。 StandardsPaths 類包含常用文件夾的指向鏈接,例如用戶的主頁、文檔等。之后,我們設置一個文件類型過濾器來控制用戶可以使用對話框查看和選擇哪些文件。
最后,輪到 onAccepted 信號處理函數了,其中的 Image 元素被設置為承載並顯示所選文件。還有一個 onRejected 信號,但這里不需要處理它。

ApplicationWindow {
    
    // ...
    
    FileDialog {
        id: fileOpenDialog
        title: "Select an image file"
        folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
        nameFilters: [
            "Image files (*.png *.jpeg *.jpg)",
        ]
        onAccepted: {
            image.source = fileOpenDialog.fileUrl
        }
    }

    // ...

}

接下來處理菜單欄MenuBar。創建菜單,要將Menu元素放到菜單欄中,然后在每個菜單Menu中彈出菜單項MenuItem元素。
以下代碼創建了兩個菜單:FileHelp。在File下放一個Open菜單項,將圖標和動作設置得與工具欄上的 打開 按鈕一致。在Help下會看到一個About菜單,它會調用 aboutDialogOpen 方法。
請注意,Menutitle 屬性和 MenuItemtext 屬性中的邏輯與符號 (“&”) 將其后字符轉換為鍵盤快捷鍵;例如按 Alt+F 進入文件菜單,然后按 Alt+O 觸發打開項目。

ApplicationWindow {
    
    // ...
    
    menuBar: MenuBar {
        Menu {
            title: qsTr("&File")
            MenuItem {
                text: qsTr("&Open...")
                icon.name: "document-open"
                onTriggered: fileOpenDialog.open()
            }
        }

        Menu {
            title: qsTr("&Help")
            MenuItem {
                text: qsTr("&About...")
                onTriggered: aboutDialog.open()
            }
        }
    }

    // ...

}

aboutDialog 元素基於 QtQuick.Controls 模塊中的 Dialog 控件,而它(Dialog)是自定義對話框的基礎。我們即將創建的對話框如下圖所示。

aboutDialog 的代碼可以分為三個部分。首先,我們設置帶有標題的對話窗口。然后,我們為對話框提供一些內容——在本例中是一個標簽控件。最后,我們選擇使用標准的 Ok 按鈕來關閉對話框。

ApplicationWindow {
    
    // ...
    
    Dialog {
        id: aboutDialog
        title: qsTr("About")
        Label {
            anchors.fill: parent
            text: qsTr("QML Image Viewer\nA part of the QmlBook\nhttp://qmlbook.org")
            horizontalAlignment: Text.AlignHCenter
        }

        standardButtons: StandardButton.Ok
    }

    // ...

}

以上便完成一個用於查看圖像的簡單可用的桌面應用程序。

遷移到移動端

用戶對於應用在移動終端上和桌面上的運行方式及界面樣式有很多不同的期待。最大的不同在於功能如何被觸達到。不同於菜單欄和工具欄,這里將使用抽屜式功能訪問方式。抽屜可以推進側邊隱藏,同時在標題欄上提供了關閉按扭。下圖是抽屜菜單被打開的樣子。

首先,需要在main.cpp里將樣式從Fution改為Material

QQuickStyle::setStyle("Material");

然后開始適配用戶界面。先把菜單替換為抽屜。下面的代碼,把Drawer控件添加到ApplicationWindow下作為子元素。在抽屜元素中,添加包含ItemDelegateListView。也包含一個ScrollIndicator滾動條,以便當內容過長時方便拖動顯示。我們的列表中只有兩個項目,所在本例中見不到這個滾動條。
抽屜菜單的ListView是由ListModel填充的,每個ListItem對應一個菜單項。每當一個項目被點擊,會調用onClicked方法,進而會調用相應的ListItemtriggered方法。這樣,我們就可以用一個委托來觸發不同的動作,具體代碼如下:

ApplicationWindow {
    
    // ...
    
    id: window

    Drawer {
        id: drawer

        width: Math.min(window.width, window.height) / 3 * 2
        height: window.height

        ListView {
            focus: true
            currentIndex: -1
            anchors.fill: parent

            delegate: ItemDelegate {
                width: parent.width
                text: model.text
                highlighted: ListView.isCurrentItem
                onClicked: {
                    drawer.close()
                    model.triggered()
                }
            }

            model: ListModel {
                ListElement {
                    text: qsTr("Open...")
                    triggered: function() { fileOpenDialog.open(); }
                }
                ListElement {
                    text: qsTr("About...")
                    triggered: function() { aboutDialog.open(); }
                }
            }

            ScrollIndicator.vertical: ScrollIndicator { }
        }
    }

    // ...

}

下一個更改是在 ApplicationWindow 的標題header中。我們添加了一個用於打開抽屜的按鈕和一個用於應用程序標題的標簽,而不是桌面樣式的工具欄。

ToolBar包含兩個子項:ToolButtonLabel
ToolButton用於打開抽屜,相應的關閉close函數,可以在ListView的代理函數中找到。當菜單項被選中,抽屜就關掉了。ToolButton的圖標來自材質設計圖標頁

ApplicationWindow {
    
    // ...
    
    header: ToolBar {
        ToolButton {
            id: menuButton
            anchors.left: parent.left
            anchors.verticalCenter: parent.verticalCenter
            icon.source: "images/baseline-menu-24px.svg"
            onClicked: drawer.open()
        }
        Label {
            anchors.centerIn: parent
            text: "Image Viewer"
            font.pixelSize: 20
            elide: Label.ElideRight
        }
    }

    // ...

}

最后,我們使工具欄的背景漂亮些—至少換成橙色的。為此,我們更改 Material.background 附加屬性。這是 QtQuick.Controls.Material里的模塊,僅影響 Material 樣式。

import QtQuick.Controls.Material

ApplicationWindow {
    
    // ...
    
    header: ToolBar {
        Material.background: Material.Orange

    // ...

}

通過這些代碼更改,我們將桌面版的圖片查看器轉化成了適合移動設備的版本。

共享代碼

在以上兩部分代碼中,我們看到一個桌面版的圖片查看器改造與移動版的過程。
看下代碼,大部分代碼仍然是共享的。相同的部分多是跟文檔區域相關的,如,圖片。不同這處主要在於桌面與移動端各自不同的操作方式。我們當然想將這些代碼統一起來。QML通過文件選擇器***file selectors***可以實現。
文件選擇器允許替換被標記為活動的個性化文件。Qt文檔中維護了一個選擇器QFileSelector類列表。本例中,我們將桌面版文件設為默認,當遇到Android選擇器時,再替換成別的。開發時,可以把環境變量QT_FILE_SELECTORS設置為android來模擬適配。

文件選擇器
通過selector,文件選擇器可以將文件替換為備選文件。
通過在你想替換的文件的同一目錄下創建一個名為+selector的目錄(其中selector代表一個選擇器的名稱),然后你可以在該目錄內放置與你想替換的文件同名的文件。當選擇器出現時,該目錄中的文件將被選中替換掉原始文件。
選擇器是基於平台的:如安卓、ios、osx、linux、qnx等。它們還可以包括所使用的Linux發行版的名稱(如果能確定的話),例如:Debian、ubuntu、Fedora。最后,它們還包括地區設置,如en_US、sv_SE,等等。
也可以添加你自定義選擇器。

第一步是分離出共享代碼。創建ImageViewerWindow元素來代替ApplicationWindow用於我們的兩個版本中。這將包括對話框、Image元素和背景。為了在特定於平台都可以正常打開對話框,我們需要使用函數 openFileDialogopenAboutDialog

import QtQuick
import QtQuick.Controls
import Qt.labs.platform

ApplicationWindow {
    function openFileDialog() { fileOpenDialog.open(); }
    function openAboutDialog() { aboutDialog.open(); }

    visible: true
    title: qsTr("Image Viewer")

    background: Rectangle {
        color: "darkGray"
    }

    Image {
        id: image
        anchors.fill: parent
        fillMode: Image.PreserveAspectFit
        asynchronous: true
    }

    FileDialog {
        id: fileOpenDialog

        // ...

    }

    Dialog {
        id: aboutDialog

        // ...

    }
}

接下來,我們為我們的默認樣式 Fusion 創建一個新的 main.qml,即用戶界面的桌面版本。
這里,我們圍繞 ImageViewerWindow 而不是 ApplicationWindow 建立用戶界面。然后我們將平台特定的部分添加進來,例如菜單欄MenuBar和工具欄ToolBar。對這些的唯一更改是打開相應對話框的調用是針對新功能而不是直接針對對話框控件進行的。唯一變化的是,打開各自的對話框的函數調用是由新的函數來完成的,而不是直接調用對話框控件。

import QtQuick
import QtQuick.Controls

ImageViewerWindow {
    id: window
    
    width: 640
    height: 480
    
    menuBar: MenuBar {
        Menu {
            title: qsTr("&File")
            MenuItem {
                text: qsTr("&Open...")
                icon.name: "document-open"
                onTriggered: window.openFileDialog()
            }
        }

        Menu {
            title: qsTr("&Help")
            MenuItem {
                text: qsTr("&About...")
                onTriggered: window.openAboutDialog()
            }
        }
    }

    header: ToolBar {
        Flow {
            anchors.fill: parent
            ToolButton {
                text: qsTr("Open")
                icon.name: "document-open"
                onClicked: window.openFileDialog()
            }
        }
    }
}

接下來,我們必須創建一個適用於移動設備的main.qml。這將基於 Material 主題。在這里,我們保留了 Drawer 和適配於移動設備的工具欄。同樣,唯一的變化是對話框的打開方式。

import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material

ImageViewerWindow {
    id: window

    width: 360
    height: 520

    Drawer {
        id: drawer

        // ...

        ListView {

            // ...

            model: ListModel {
                ListElement {
                    text: qsTr("Open...")
                    triggered: function(){ window.openFileDialog(); }
                }
                ListElement {
                    text: qsTr("About...")
                    triggered: function(){ window.openAboutDialog(); }
                }
            }

            // ...

        }
    }

    header: ToolBar {

        // ...

    }
}

兩個 main.qml 文件放在文件系統中,如下所示。這讓 QML 引擎自動創建的文件選擇器可以選擇正確的文件。默認情況下,會加載 Fusion main.qml。如果存在 android 選擇器,則改為加載 Material main.qml

目前樣式存在main.cpp。我們可以繼續在main.cpp中使用條件表達式#ifdef來為不同的平台設置不同的樣式。但我們要使用文件選擇器通過選擇配置文件來設置樣式。下面你可以看到Material樣式文件,而Fusion樣式文件也同樣簡單。

[Controls]
Style=Material

通過這些變化,我們把所有可共享的代碼整合起來,僅把用戶交互有差異的代碼單獨處理。有多種實現方式,比如,將文檔保存在包含特定平台接口的特定組件中,或者象本例那樣,從不同平台中抽象出共同的代碼。當你知道特定平台的樣式且能夠從特性中分離出共性時,就會做出最佳的路徑來決定如何處理代碼。

原生對話框

當使用圖片查看器程序時,你會發現它使用了非標准文件選擇窗口,看起來挺別扭。
Qt.labs.platform模塊有助於解決這個問題。它將QML綁定到原生窗口,如文件選擇框、顏色選擇框、字體選擇框等。同時也提供API創建系統托盤圖標,還能提供頂部的系統全局菜單(如OSX那樣)。這樣做的代價是對QtWidgets模塊的依賴,因為萬一在缺少原生支持的情況時,會備份啟用基於widget的對話框。
為了在圖片查看器里集成原生對話框,我們需要引入Qt.labs.platform模塊。因為跟QtQuick.Dialogs有命名空間的沖突,所以要刪除舊的引入聲明。
在實際的文件對話框元素中,我們必須更改文件夾folder屬性的設置方式,並確保 onAccepted 處理程序使用文件file屬性而不是fileUrl 屬性。除了這些細節之外,用法與 QtQuick.Dialogs 中的 FileDialog 相同。

import QtQuick
import QtQuick.Controls
import Qt.labs.platform

ApplicationWindow {
    
    // ...
    
    FileDialog {
        id: fileOpenDialog
        title: "Select an image file"
        folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
        nameFilters: [
            "Image files (*.png *.jpeg *.jpg)",
        ]
        onAccepted: {
            image.source = fileOpenDialog.file
        }
    }

    // ...

}

除了 QML 更改之外,我們還需要更改圖像查看器的項目文件以包含widgets模塊。

QT += quick quickcontrols2 widgets

還需要更新main.qml來實例化QApplication對象,用於取代QGuiApplication對象。因為QGuiApplication對於圖形應用有最小化的環境依賴,而QApplication繼承自QGuiApplication,且支持QtWidgets的特征。

include <QApplication>

// ...

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // ...

}

通過這些更改,圖像查看器現在將在大多數平台上使用原生對話框。支持的平台是 iOS、Linux(帶有 GTK+ 平台主題)、macOS、Windows 和 WinRT。對於 Android,它將使用 QtWidgets 模塊提供的默認 Qt 對話框。

常見樣式

使用Qt Quick Controls可以實現很多常見的用戶界面樣式。本部分,我們將演示一些常見樣式如何創建。

嵌套界面

這個例子里,將創建一個層級界面,每頁都可以從上級界面訪問。結構如下:

這個用戶界面的關鍵元素是StackView。它可以在棧中放置一個界面,當用戶想返回時,可以從棧中彈出。本例將展示如何實現。
程序的起始界面如下:

我們從mail.qml開始,這里定義了一個ApplicationWindow,它包含了一個ToolBar,一個Drawer,一個StackView還有一個Home元素。下面挨個組件看一下。

import QtQuick
import QtQuick.Controls

ApplicationWindow {

    // ...

    header: ToolBar {

        // ...

    }

    Drawer {

        // ...

    }

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: Home {}
    }
}

首頁Home.qmlpage組成,page是一個支持頁眉和頁腳的控件元素。本例,我們只居中放置一個寫有Home Screen 的標簽。因為StackView的內容自動填充界面,所以頁面page會有恰當的尺寸。

import QtQuick
import QtQuick.Controls

Page {
    title: qsTr("Home")

    Label {
        anchors.centerIn: parent
        text: qsTr("Home Screen")
    }
}

回到main.qml,看一下抽屜菜單的部分。這是導航欄可以導到起始頁面。當前用戶界面是ItemDelegate項。在onClicked函數中,下一頁被壓入棧stackView中。
正如下面的代碼所示,它可以推送一個Component或一個特定QML文件的引用。無論哪種方式都會導致一個新的實例被創建並推送到堆棧。

ApplicationWindow {

    // ...

    Drawer {
        id: drawer
        width: window.width * 0.66
        height: window.height

        Column {
            anchors.fill: parent

            ItemDelegate {
                text: qsTr("Profile")
                width: parent.width
                onClicked: {
                    stackView.push("Profile.qml")
                    drawer.close()
                }
            }
            ItemDelegate {
                text: qsTr("About")
                width: parent.width
                onClicked: {
                    stackView.push(aboutPage)
                    drawer.close()
                }
            }
        }
    }

    // ...

    Component {
        id: aboutPage

        About {}
    }

    // ...

}

另一個難題是工具欄。思路是,當stackView中包含多於一個頁面時,工具欄上顯示一個后退按鈕,否則顯示一個菜單按鈕。這個邏輯可以在text屬性中看到,其中\\u...字符串表示一個unicode圖標。
在(工具欄上的按鈕)onClicked處理函數中,可以看到當棧中超過一個頁面時,棧中最頂層的頁面會被彈出棧。如果棧中僅有一個項,比如只有首頁,抽屜菜單就會彈出顯示。
ToolBar下面,有一個標簽Label。這個元素在頁眉中間顯示一個中心界面頁的標題。

ApplicationWindow {

    // ...

    header: ToolBar {
        contentHeight: toolButton.implicitHeight

        ToolButton {
            id: toolButton
            text: stackView.depth > 1 ? "\u25C0" : "\u2630"
            font.pixelSize: Qt.application.font.pixelSize * 1.6
            onClicked: {
                if (stackView.depth > 1) {
                    stackView.pop()
                } else {
                    drawer.open()
                }
            }
        }

        Label {
            text: stackView.currentItem.title
            anchors.centerIn: parent
        }
    }

    // ...

}

現在,一起來看看如何操作 AboutProfile頁面,同時也要能夠從 Profile 頁面訪問 Edit Profile 頁面。這可通過在 Profile 頁面上的 Button 來實現。點擊按鈕時,EditProfile.qml 被推入棧StackView

import QtQuick
import QtQuick.Controls

Page {
    title: qsTr("Profile")

    Column {
        anchors.centerIn: parent
        spacing: 10
        Label {
            anchors.horizontalCenter: parent.horizontalCenter
            text: qsTr("Profile")
        }
        Button {
            anchors.horizontalCenter: parent.horizontalCenter
            text: qsTr("Edit");
            onClicked: stackView.push("EditProfile.qml")
        }
    }
}

並行屏幕

本例中將創建一個界面由用戶可切換的三個頁面組成。頁面關系如下圖所示。這可以作為健康追蹤應用的界面,追蹤用戶當前的狀態、指標統計、統計總覽。

以下證明當前頁Current在程序中如何顯示。屏幕的主要部分由SwipeView負責管理,就是它實現了並行屏幕的交互模式。圖中的標題與文本來自於SwipeView中的頁面。而PageIndactor(頁面指示器,屏幕下的三個小點)來自於main.qml,位於SwipeView下面。頁面指示器向用戶指明當前的活動頁,以幫助用戶選擇頁面。

來看main.qml,它是由內部嵌套SwipeViewApplicationWindow構成。

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 640
    height: 480

    title: qsTr("Side-by-side")

    SwipeView {

        // ...

    }

    // ...

}

SwipeView里,子頁面將按照聲明的順序進行初始化顯示。子頁面分別是CurrentUserStatsTotalStats

ApplicationWindow {

    // ...

    SwipeView {
        id: swipeView
        anchors.fill: parent

        Current {
        }

        UserStats {
        }

        TotalStats {
        }
    }

    // ...

}

最后,把SwipeViewcountcurrentIndex屬性綁到PageIndactor元素。這樣就完成了組織這些頁面的架構。

ApplicationWindow {

    // ...

    SwipeView {
        id: swipeView

        // ...

    }

    PageIndicator {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter

        currentIndex: swipeView.currentIndex
        count: swipeView.count
    }
}

每個界面都有一個page頁面,頁面上有header標題,標題上有Label標簽,頁面還有其它內容。對於CurrentUser Stats頁面,其內容僅有一個Label標簽,但對於Community Stats頁面,還包括一個退回按鈕。

import QtQuick
import QtQuick.Controls

Page {
    header: Label {
        text: qsTr("Community Stats")
        font.pixelSize: Qt.application.font.pixelSize * 2
        padding: 10
    }

    // ...

}



退回按鈕單獨調用了 SwipeViewsetCurrentIndex方法,來將當前頁設置為第0頁,將用戶直接導航到 Current頁。每次頁面切換時, SwipeView都提供了過渡效果,所以,單獨切換頁面索引時,會有切換的方向感。

提示
當編程實現SwipeView的頁面切換時,一定不要使用JavaScript腳本賦值來指定currentIndex。因為這樣會破壞QML的綁定關系。正確的做法是使用setCurrentIndexincrementCurrentIndexdecrementCurrentIndex方法。這樣就保留了QML的綁定關系。關於綁定與賦值的關系與區別,前面的第五章快速入門中的***Binding綁定***有詳細介紹,點這里查看

Page {

    // ...

    Column {
        anchors.centerIn: parent
        spacing: 10
        Label {
            anchors.horizontalCenter: parent.horizontalCenter
            text: qsTr("Community statistics")
        }
        Button {
            anchors.horizontalCenter: parent.horizontalCenter
            text: qsTr("Back")
            onClicked: swipeView.setCurrentIndex(0);
        }
    }
}

文檔視圖

本例展示如何實現一個面向桌面應用的,以文檔為中心的用戶界面。思路是:每個文檔都有一個窗口,每打開一個新文檔,同時開啟一個新窗口。對用戶來說,每個窗口都是包含一個文檔。
/media/sammy/工作盤/forstudy/qt6book/docs/ch06-controls/assets/interface-document-window.pngenter description here
代碼從ApplicationWindow開始,包含一個文件菜單File,菜單中有一些常見操作:New,Open,Save,Save As。我們將這些放在DocumentWindow.qml里。
使用原生對話框,要導入Qt.labs.platform,還要針對原生對話框,把工程文件和main.cpp做一系列的更改。

import QtQuick
import QtQuick.Controls
import Qt.labs.platform as NativeDialogs

ApplicationWindow {
    id: root

    // ...

    menuBar: MenuBar {
        Menu {
            title: qsTr("&File")
            MenuItem {
                text: qsTr("&New")
                icon.name: "document-new"
                onTriggered: root.newDocument()
            }
            MenuSeparator {}
            MenuItem {
                text: qsTr("&Open")
                icon.name: "document-open"
                onTriggered: openDocument()
            }
            MenuItem {
                text: qsTr("&Save")
                icon.name: "document-save"
                onTriggered: saveDocument()
            }
            MenuItem {
                text: qsTr("Save &As...")
                icon.name: "document-save-as"
                onTriggered: saveAsDocument()
            }
        }
    }

    // ...

}

要啟動程序,先從main.qml創建一個DocumentWindow實例,這也是程序的入口。

import QtQuick

DocumentWindow {
    visible: true
}

在本章開頭的例子中,每個MenuItem被點擊時,會調用一個相應的函數。先處理New菜單項,它調用了newDocument函數。
這個函數接着又調用了createNewDocument,從而從DocumentWindow.qml中動態創建一個新的實例,也就是新的DocumentWindow實例。單獨新拆分成一個函數的原因是,每當打開文檔時,都要用到它。
注意,在使用createObject創建新實例時,我們並沒有指定父元素。這樣,就創建了一個頂層的元素。如果在創新文檔時,將當前元素指定為父元素,那么當父窗口銷毀時,也會將其子窗口銷毀。

ApplicationWindow {

    // ...

    function createNewDocument() {
        var component = Qt.createComponent("DocumentWindow.qml");
        var window = component.createObject();
        return window;
    }

    function newDocument() {
        var window = createNewDocument();
        window.show();
    }

    // ...

}

看下Open菜單,發現它調用了openDocument函數。它只是調用了openDialog,讓用戶選擇要打開的文件。如果沒有指定文檔類型、文件擴展名等,對話框會將大多數的屬性設置為默認值。在實際應用中,應當設置一下這些參數(文件類型、擴展名,以對要打開的文件進行過濾)。
onAccepted函數中,調用createdNewDocument函數創建了一個新的文檔窗口實例,在窗口顯示前已經設置好了文件名。本例中,沒有真的加載文件。

提示
我們將模塊Qt.labs.platforms引入為NativeDialogs,因為其中的MenuItemQtQuick.Controls模塊中的MenuItem有命名沖突。

ApplicationWindow {

    // ...

    function openDocument(fileName) {
        openDialog.open();
    }

    NativeDialogs.FileDialog {
        id: openDialog
        title: "Open"
        folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)
        onAccepted: {
            var window = root.createNewDocument();
            window.fileName = openDialog.file;
            window.show();
        }
    }

    // ...

}

文件名包含一對描述文檔的屬性:fileNameisDirtyfileName屬性表示文檔的名稱,而isDirty表示文檔是否有未保存的變更。這個邏輯用於保存另存為邏輯,下面會提到。
當未指定文件名做保存操作時,saveAsDocument被激活。這導至saveAsDialog窗體調用,這會指定一個文件名並嘗試在onAccepted函數中再次保存。
注意 ,saveAsDocumentsaveDocument對應着Save as 和 Save菜單。
文檔保存后,在saveDocument函數中,tryingToClose屬性被選中。如果用戶想要在關閉窗口時保存文檔,這個標志就會被設置。相應的,對文檔進行保存操作后,窗體也會被關掉。再次強調,本例中並沒有實際地保存。

ApplicationWindow {

    // ...

    property bool isDirty: true        // Has the document got unsaved changes?
    property string fileName // The filename of the document
    property bool tryingToClose: false // Is the window trying to close (but needs a file name first)?

    // ...

    function saveAsDocument() {
        saveAsDialog.open();
    }

    function saveDocument() {
        if (fileName.length === 0)
        {
            root.saveAsDocument();
        }
        else
        {
            // Save document here
            console.log("Saving document")
            root.isDirty = false;

            if (root.tryingToClose)
                root.close();
        }
    }

    NativeDialogs.FileDialog {
        id: saveAsDialog
        title: "Save As"
        folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)
        onAccepted: {
            root.fileName = saveAsDialog.file
            saveDocument();
        }
        onRejected: {
            root.tryingToClose = false;
        }
    }

    // ...

}

這指導我們如何關閉窗口。當窗口正被關閉時,onClosing函數被調用。這里,代碼選擇可以不接受關閉請求。如果文檔存在未保存的變動,就打開closeWaringDialog並拒絕關閉請求。
closingWaringDialog詢問用戶是否將變動保存,但用戶也有取消關閉的選項。取消邏輯在onRejected處理是最簡的處理方式,不關閉文檔窗口。
當用戶不想保存變動,在onNoClicked里,isDirty標志被置為false,且窗口被關閉。這次,onClosing將允許關閉請求,因為isDirty為false。
最后,當用戶想要保存變動,我們在取消關閉前將tryingToClose標志置為true。保存/另存為的邏輯如下:

ApplicationWindow {

    // ...

    onClosing: {
        if (root.isDirty) {
            closeWarningDialog.open();
            close.accepted = false;
        }
    }

    NativeDialogs.MessageDialog {
        id: closeWarningDialog
        title: "Closing document"
        text: "You have unsaved changed. Do you want to save your changes?"
        buttons: NativeDialogs.MessageDialog.Yes | NativeDialogs.MessageDialog.No | NativeDialogs.MessageDialog.Cancel
        onYesClicked: {
            // Attempt to save the document
            root.tryingToClose = true;
            root.saveDocument();
        }
        onNoClicked: {
            // Close the window
            root.isDirty = false;
            root.close()
        }
        onRejected: {
            // Do nothing, aborting the closing of the window
        }
    }
}

整個關閉流程及保存/另存為的邏輯如下圖。系統在關閉狀態時進入,而關閉不關閉是結果。
相比於使用Qt Widgets和 C++ 來實現,這種實現方式看起來更復雜。這是因為對話框對QML沒有阻斷作用。這意味着我們不能在一個switch表達式中等待對話框結果。相應地,我們要記住狀態然后繼續在各自相應的函數onYesClickedonNoClickedonAcceptedonRejected中處理。

最后就是窗體標題。它是由兩個屬性組成的:fileNameisDirty

ApplicationWindow {

    // ...

    title: (fileName.length===0?qsTr("Document"):fileName) + (isDirty?"*":"")

    // ...

}

這個例子離實用還很遠。比如,文檔未加載或保存。另一塊缺失的內容是處理一次性關閉所有窗口的邏輯,比如,當程序退出時。實現這個功能,需要一個保存所有當前DocumentWindow實例列表的單例。但這屬於另一種去觸發窗口關閉的方式,所以這里展示的邏輯圖仍然是有意義的。

想象風格

Qt Quick Controls 的一個設計目標就是將控件的界面與邏輯分離。對於大多數的樣式來說,界面樣式的實現是由QML代碼和圖形附件混合組成的。然而,使用Imagine風格,可以僅使用圖形附件來定制基於Qt Quick Controls 的應用程序。
想象風格是基於9-patch 圖象。這允許圖像攜帶有關它們如何被拉伸以及哪些部分被視為元素的一部分以及外部哪些部分的信息;比如,影子。對於每個控件,樣式里支持幾個元素,每個元素中都有大量的狀態可用。通過向這些元素和狀態提供一定的素材,你可以控制控件的樣式細節。
Imagine 樣式文檔中詳細介紹了 9-path 圖像的詳細信息,以及如何設置每個控件的樣式。這里,我們將為一個假想的設備界面自定義一個樣式,來展示風格如何使用。
應用程序的風格決定了ApplicationWindowButton控件。對這些按鈕來說,其正常狀態,以及按下選中狀態都已經被處理了。演示程序效果如下:

代碼為可點擊按鈕創建了一個Column,並為可選按鈕創建了Grid。可點擊按鈕為適應窗體寬度做了拉伸。

import QtQuick
import QtQuick.Controls

ApplicationWindow {

    // ...

    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Column {
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.margins: 10

        width: parent.width/2

        spacing: 10

        // ...

        Repeater {
            model: 5
            delegate: Button {
                width: parent.width
                height: 70
                text: qsTr("Click me!")
            }
        }
    }

    Grid {
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.margins: 10

        columns: 2

        spacing: 10

        // ...

        Repeater {
            model: 10

            delegate: Button {
                height: 70
                text: qsTr("Check me!")
                checkable: true
            }
        }
    }
}

當我們使用Imagine風格時,所有被用到的控件都需要使用附件格式化。最簡單的是ApplicationWindow的背景,這是一個定義背景顏色的單像素紋理。通過命名文件applicationwindow-background.png然后使用 qtquickcontrols2.conf配置將樣式指向它,該文件就被拾取了。
在下面展示的qtquickcontrols2.conf文件,可以看到如何將Style設置為Imagine,然后為風格設置Path以便能找到素材附件。最后還需要設置一些調色板屬性。可用的調色析屬性值可以在QML調色析基礎樣式找到。

[Controls]
Style=Imagine

[Imagine]
Path=:images/imagine

[Imagine\Palette]
Text=#ffffff
ButtonText=#ffffff
BrightText=#ffffff

Button控件的素材附件是button-background.9.png, button-background-pressed.9.pngbutton-background-checked.9.png。遵循control-element-state(控件-元素-狀態)的規范模式。無狀態的文件,象button-background.9.png用於沒有素材附件的所有狀態。根據想象風格元素引用表,按鈕可以有如下狀態:

  • disabled
  • pressed
  • checked
  • checkable
  • focused
  • highlighted
  • flat
  • mirrored
  • hovered
    是否需要這些狀態有賴於你的用戶界面。比如,懸空(hovered)樣式在觸摸交互型的用戶界面里,永遠不會用到。

    看下上面放大版的button-background-checked.9.png,可以看到兩側的指導線。出於視覺效果,添加了紫色背景。而本例所用到的素材附件實際上是透明的。
    圖片邊上的象素也可以是白的/透明的、黑的或紅的,有着不同的意義,以下逐一說明:
  • 黑色 線在左邊和上邊標記圖象的可拉伸部分。這意味着當按鈕被拉伸時,示例中的圓角和白色標記不受影響。
  • 黑色 線在右邊和下邊,標記了控件的內容區域。這意味着在示例中用於文本的按鈕部分。
  • 紅色 線在右邊和下邊,標記了嵌入區域。這些區域是圖像的一部分,但不被認為是控件的一部分。對於上面的可選圖像,這是用在延伸到按鈕外面的柔和光暈。

內嵌(inset)區域的使用演示如下button-background.9.png,而上面的button-background-checked.9.png:看起來象點亮,但沒移動。

小結

本章介紹了Qt Quick Controls 2,涉及到比基礎QML元素更高級的概念的一系列元素。多數場景下,會用到Qt Quick Controls 2以節省內存消耗,提高性能,因為它們是基於優化的 C++ 邏輯實現的,而非Javascript和QML。
我們已經演示了不同風格如何應用,以及一段可共用的代碼如何通過文件選擇器使用。這種方式,一段代碼可以在不同的平台,以不同的交互方式和界面風格部署。
最后,我們一起學習了想象風格,它允許你使用圖形素材來定制化一個基於QML的應用程序外觀。這種方法,可以讓一個應用在不改動任何代碼的條件下更換皮膚。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM