Common Patterns

    For this example we will create a tree of pages that can be reached from the previous level of screens. The structure is pictured below.

    The key component in this type of user interface is the . It allows us to place pages on a stack which then can be popped when the user wants to go back. In the example here, we will show how this can be implemented.

    The initial home screen of the application is shown in the figure below.

    Common Patterns - 图2

    The application starts in main.qml, where we have an ApplicationWindow containing a ToolBar, a Drawer, a StackView and a home page element, Home. We will look into each of the components below.

    The home page, Home.qml consists of a Page, which is n control element that support headers and footers. In this example we simply center a Label with the text Home Screen on the page. This works because the contents of a StackView automatically fill the stack view, so the page will have the right size for this to work.

    1. import QtQuick
    2. import QtQuick.Controls
    3. Page {
    4. title: qsTr("Home")
    5. Label {
    6. anchors.centerIn: parent
    7. text: qsTr("Home Screen")
    8. }
    9. }

    Returning back to main.qml, we now look at the drawer part. This is where the navigation to the pages begin. The active parts of the user interface are the ÌtemDelegate items. In the onClicked handler, the next page is pushed onto the stackView.

    As shown in the code below, it possible to push either a Component or a reference to a specific QML file. Either way results in a new instance being created and pushed onto the stack.

    1. ApplicationWindow {
    2. // ...
    3. Drawer {
    4. id: drawer
    5. width: window.width * 0.66
    6. height: window.height
    7. Column {
    8. anchors.fill: parent
    9. ItemDelegate {
    10. text: qsTr("Profile")
    11. width: parent.width
    12. onClicked: {
    13. stackView.push("Profile.qml")
    14. drawer.close()
    15. }
    16. }
    17. ItemDelegate {
    18. text: qsTr("About")
    19. width: parent.width
    20. onClicked: {
    21. stackView.push(aboutPage)
    22. drawer.close()
    23. }
    24. }
    25. }
    26. }
    27. // ...
    28. Component {
    29. id: aboutPage
    30. About {}
    31. }
    32. // ...
    33. }

    The other half of the puzzle is the toolbar. The idea is that a back button is shown when the stackView contains more than one page, otherwise a menu button is shown. The logic for this can be seen in the text property where the "\\u..." strings represents the unicode symbols that we need.

    In the onClicked handler, we can see that when there is more than one page on the stack, the stack is popped, i.e. the top page is removed. If the stack contains only one item, i.e. the home screen, the drawer is opened.

    Below the ToolBar, there is a Label. This element shows the title of each page in the center of the header.

    1. ApplicationWindow {
    2. // ...
    3. header: ToolBar {
    4. contentHeight: toolButton.implicitHeight
    5. ToolButton {
    6. id: toolButton
    7. text: stackView.depth > 1 ? "\u25C0" : "\u2630"
    8. font.pixelSize: Qt.application.font.pixelSize * 1.6
    9. onClicked: {
    10. if (stackView.depth > 1) {
    11. stackView.pop()
    12. } else {
    13. drawer.open()
    14. }
    15. }
    16. }
    17. Label {
    18. text: stackView.currentItem.title
    19. anchors.centerIn: parent
    20. }
    21. }
    22. // ...
    23. }

    Now we’ve seen how to reach the About and Profile pages, but we also want to make it possible to reach the Edit Profile page from the Profile page. This is done via the Button on the Profile page. When the button is clicked, the EditProfile.qml file is pushed onto the StackView.

    1. import QtQuick
    2. import QtQuick.Controls
    3. Page {
    4. title: qsTr("Profile")
    5. Column {
    6. anchors.centerIn: parent
    7. spacing: 10
    8. Label {
    9. anchors.horizontalCenter: parent.horizontalCenter
    10. text: qsTr("Profile")
    11. }
    12. Button {
    13. anchors.horizontalCenter: parent.horizontalCenter
    14. text: qsTr("Edit");
    15. onClicked: stackView.push("EditProfile.qml")
    16. }
    17. }
    18. }
    1. import QtQuick
    2. import QtQuick.Controls
    3. Page {
    4. title: qsTr("Profile")
    5. Column {
    6. anchors.centerIn: parent
    7. spacing: 10
    8. Label {
    9. anchors.horizontalCenter: parent.horizontalCenter
    10. text: qsTr("Profile")
    11. }
    12. Button {
    13. anchors.horizontalCenter: parent.horizontalCenter
    14. onClicked: stackView.push("EditProfile.qml")
    15. }
    16. }
    17. }

    For this example we create a user interface consisting of three pages that the user can shift through. The pages are shown in the diagram below. This could be the interface of a health tracking app, tracking the current state, the user’s statistics and the overall statistics.

    Common Patterns - 图4

    Diving into main.qml, it consists of an ApplicationWindow with the .

    Inside the SwipeView each of the child pages are instantiated in the order they are to appear. They are Current, UserStats and TotalStats.

    1. ApplicationWindow {
    2. // ...
    3. SwipeView {
    4. id: swipeView
    5. anchors.fill: parent
    6. Current {
    7. }
    8. UserStats {
    9. }
    10. TotalStats {
    11. }
    12. }
    13. // ...
    14. }

    Finally, the count and currentIndex properties of the SwipeView are bound to the PageIndicator element. This completes the structure around the pages.

    1. ApplicationWindow {
    2. // ...
    3. SwipeView {
    4. id: swipeView
    5. // ...
    6. }
    7. PageIndicator {
    8. anchors.bottom: parent.bottom
    9. anchors.horizontalCenter: parent.horizontalCenter
    10. currentIndex: swipeView.currentIndex
    11. count: swipeView.count
    12. }
    13. }

    Each page consists of a Page with a header consisting of a Label and some contents. For the Current and User Stats pages the contents consist of a simple Label, but for the Community Stats page, a back button is included.

    1. import QtQuick
    2. import QtQuick.Controls
    3. Page {
    4. header: Label {
    5. text: qsTr("Community Stats")
    6. font.pixelSize: Qt.application.font.pixelSize * 2
    7. padding: 10
    8. }
    9. // ...
    10. }

    Common Patterns - 图6

    The back button explicitly calls the setCurrentIndex of the SwipeView to set the index to zero, returning the user directly to the Current page. During each transition between pages the SwipeView provides a transition, so even when explicitly changing the index the user is given a sense of direction.

    TIP

    When navigating in a SwipeView programatically it is important not to set the currentIndex by assignment in JavaScript. This is because doing so will break any QML bindings it overrides. Instead use the methods setCurrentIndex, incrementCurrentIndex, and decrementCurrentIndex. This preserves the QML bindings.

    1. Page {
    2. // ...
    3. Column {
    4. anchors.centerIn: parent
    5. spacing: 10
    6. Label {
    7. anchors.horizontalCenter: parent.horizontalCenter
    8. text: qsTr("Community statistics")
    9. }
    10. Button {
    11. anchors.horizontalCenter: parent.horizontalCenter
    12. text: qsTr("Back")
    13. onClicked: swipeView.setCurrentIndex(0);
    14. }
    15. }
    16. }

    This example shows how to implement a desktop-oriented, document-centric user interface. The idea is to have one window per document. When opening a new document, a new window is opened. To the user, each window is a self contained world with a single document.

    %20}})

    The code starts from an ApplicationWindow with a File menu with the standard operations: New, Open, Save and Save As. We put this in the DocumentWindow.qml.

    We import Qt.labs.platform for native dialogs, and have made the subsequent changes to the project file and main.cpp as described in the section on native dialogs above.

    1. import QtQuick
    2. import QtQuick.Controls
    3. import Qt.labs.platform as NativeDialogs
    4. ApplicationWindow {
    5. id: root
    6. // ...
    7. menuBar: MenuBar {
    8. Menu {
    9. title: qsTr("&File")
    10. MenuItem {
    11. text: qsTr("&New")
    12. icon.name: "document-new"
    13. onTriggered: root.newDocument()
    14. }
    15. MenuSeparator {}
    16. MenuItem {
    17. text: qsTr("&Open")
    18. icon.name: "document-open"
    19. onTriggered: openDocument()
    20. }
    21. MenuItem {
    22. text: qsTr("&Save")
    23. icon.name: "document-save"
    24. onTriggered: saveDocument()
    25. }
    26. MenuItem {
    27. text: qsTr("Save &As...")
    28. icon.name: "document-save-as"
    29. onTriggered: saveAsDocument()
    30. }
    31. }
    32. }
    33. // ...
    34. }

    To bootstrap the program, we create the first DocumentWindow instance from main.qml, which is the entry point of the application.

    1. import QtQuick
    2. DocumentWindow {
    3. visible: true
    4. }

    In the example at the beginning of this chapter, each MenuItem calls a corresponding function when triggered. Let’s start with the New item, which calls the newDocument function.

    The function, in turn, relies on the createNewDocument function, which dynamically creates a new element instance from the DocumentWindow.qml file, i.e. a new DocumentWindow instance. The reason for breaking out this part of the new function is that we use it when opening documents as well.

    1. ApplicationWindow {
    2. // ...
    3. function createNewDocument()
    4. {
    5. var window = component.createObject();
    6. return window;
    7. }
    8. function newDocument()
    9. {
    10. var window = createNewDocument();
    11. window.show();
    12. }
    13. // ...
    14. }

    Looking at the Open item, we see that it calls the openDocument function. The function simply opens the openDialog, which lets the user pick a file to open. As we don’t have a document format, file extension or anything like that, the dialog has most properties set to their default value. In a real world application, this would be better configured.

    In the onAccepted handler a new document window is instantiated using the createNewDocument method, and a file name is set before the window is shown. In this case, no real loading takes place.

    TIP

    We imported the Qt.labs.platform module as NativeDialogs. This is because it provides a MenuItem that clashes with the MenuItem provided by the QtQuick.Controls module.

    1. ApplicationWindow {
    2. // ...
    3. function openDocument(fileName)
    4. {
    5. openDialog.open();
    6. }
    7. NativeDialogs.FileDialog {
    8. id: openDialog
    9. title: "Open"
    10. folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)
    11. onAccepted: {
    12. var window = root.createNewDocument();
    13. window.fileName = openDialog.file;
    14. window.show();
    15. }
    16. }
    17. // ...
    18. }

    The file name belongs to a pair of properties describing the document: fileName and isDirty. The fileName holds the file name of the document name and isDirty is set when the document has unsaved changes. This is used by the save and save as logic, which is shown below.

    When trying to save a document without a name, the saveAsDocument is invoked. This results in a round-trip over the saveAsDialog, which sets a file name and then tries to save again in the onAccepted handler.

    Notice that the saveAsDocument and saveDocument functions correspond to the Save As and Save menu items.

    After having saved the document, in the saveDocument function, the tryingToClose property is checked. This flag is set if the save is the result of the user wanting to save a document when the window is being closed. As a consequence, the window is closed after the save operation has been performed. Again, no actual saving takes place in this example.

    1. ApplicationWindow {
    2. // ...
    3. property bool isDirty: true // Has the document got unsaved changes?
    4. property string fileName // The filename of the document
    5. property bool tryingToClose: false // Is the window trying to close (but needs a file name first)?
    6. // ...
    7. function saveAsDocument()
    8. {
    9. saveAsDialog.open();
    10. }
    11. function saveDocument()
    12. {
    13. if (fileName.length === 0)
    14. {
    15. root.saveAsDocument();
    16. }
    17. else
    18. {
    19. // Save document here
    20. console.log("Saving document")
    21. root.isDirty = false;
    22. if (root.tryingToClose)
    23. root.close();
    24. }
    25. }
    26. NativeDialogs.FileDialog {
    27. id: saveAsDialog
    28. title: "Save As"
    29. folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)
    30. onAccepted: {
    31. root.fileName = saveAsDialog.file
    32. saveDocument();
    33. }
    34. onRejected: {
    35. root.tryingToClose = false;
    36. }
    37. }
    38. // ...
    39. }

    This leads us to the closing of windows. When a window is being closed, the onClosing handler is invoked. Here, the code can choose not to accept the request to close. If the document has unsaved changes, we open the closeWarningDialog and reject the request to close.

    The closeWarningDialog asks the user if the changes should be saved, but the user also has the option to cancel the close operation. The cancelling, handled in onRejected, is the easiest case, as we rejected the closing when the dialog was opened.

    When the user does not want to save the changes, i.e. in onNoClicked, the isDirty flag is set to false and the window is closed again. This time around, the onClosing will accept the closure, as isDirty is false.

    Finally, when the user wants to save the changes, we set the tryingToClose flag to true before calling save. This leads us to the save/save as logic.

    1. ApplicationWindow {
    2. // ...
    3. onClosing: {
    4. if (root.isDirty) {
    5. closeWarningDialog.open();
    6. close.accepted = false;
    7. }
    8. }
    9. NativeDialogs.MessageDialog {
    10. id: closeWarningDialog
    11. title: "Closing document"
    12. text: "You have unsaved changed. Do you want to save your changes?"
    13. buttons: NativeDialogs.MessageDialog.Yes | NativeDialogs.MessageDialog.No | NativeDialogs.MessageDialog.Cancel
    14. onYesClicked: {
    15. // Attempt to save the document
    16. root.tryingToClose = true;
    17. root.saveDocument();
    18. }
    19. onNoClicked: {
    20. // Close the window
    21. root.isDirty = false;
    22. root.close()
    23. }
    24. onRejected: {
    25. // Do nothing, aborting the closing of the window
    26. }
    27. }
    28. }

    The entire flow for the close and save/save as logic is shown below. The system is entered at the close state, while the closed and not closed states are outcomes.

    This looks complicated compared to implementing this using Qt Widgets and C++. This is because the dialogs are not blocking to QML. This means that we cannot wait for the outcome of a dialog in a switch statement. Instead we need to remember the state and continue the operation in the respective onYesClicked, onNoClicked, onAccepted, and onRejected handlers.

    Common Patterns - 图8

    The final piece of the puzzle is the window title. It is composed from the fileName and isDirty properties.