Qt Quick Controls - Contact List

Tags: Android

A QML app using Qt Quick Controls and a Python class that implements a simple contact list. This example can also be deployed to Android using pyside6-android-deploy

A PySide6 application that demonstrates the analogous example in Qt ContactsList

ContactList Screenshot

Download this example

# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""
PySide6 port of Qt Quick Controls Contact List example from Qt v6.x
"""
import sys
from pathlib import Path
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine

from contactmodel import ContactModel  # noqa: F401

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    app.setOrganizationName("QtProject")
    app.setApplicationName("ContactsList")
    engine = QQmlApplicationEngine()

    engine.addImportPath(Path(__file__).parent)
    engine.loadFromModule("Contact", "ContactList")

    if not engine.rootObjects():
        sys.exit(-1)

    exit_code = app.exec()
    del engine
    sys.exit(exit_code)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import bisect
from dataclasses import dataclass
from enum import IntEnum

from PySide6.QtCore import (QAbstractListModel, QEnum, Qt, QModelIndex, Slot,
                            QByteArray)
from PySide6.QtQml import QmlElement

QML_IMPORT_NAME = "Backend"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class ContactModel(QAbstractListModel):

    @QEnum
    class ContactRole(IntEnum):
        FullNameRole = Qt.ItemDataRole.DisplayRole
        AddressRole = Qt.ItemDataRole.UserRole
        CityRole = Qt.ItemDataRole.UserRole + 1
        NumberRole = Qt.ItemDataRole.UserRole + 2

    @dataclass
    class Contact:
        fullName: str
        address: str
        city: str
        number: str

    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.m_contacts = []
        self.m_contacts.append(self.Contact("Angel Hogan", "Chapel St. 368 ", "Clearwater",
                                            "0311 1823993"))
        self.m_contacts.append(self.Contact("Felicia Patton", "Annadale Lane 2", "Knoxville",
                                            "0368 1244494"))
        self.m_contacts.append(self.Contact("Grant Crawford", "Windsor Drive 34", "Riverdale",
                                            "0351 7826892"))
        self.m_contacts.append(self.Contact("Gretchen Little", "Sunset Drive 348", "Virginia Beach",
                                            "0343 1234991"))
        self.m_contacts.append(self.Contact("Geoffrey Richards", "University Lane 54", "Trussville",
                                            "0423 2144944"))
        self.m_contacts.append(self.Contact("Henrietta Chavez", "Via Volto San Luca 3",
                                            "Piobesi Torinese", "0399 2826994"))
        self.m_contacts.append(self.Contact("Harvey Chandler", "North Squaw Creek 11",
                                            "Madisonville", "0343 1244492"))
        self.m_contacts.append(self.Contact("Miguel Gomez", "Wild Rose Street 13", "Trussville",
                                            "0343 9826996"))
        self.m_contacts.append(self.Contact("Norma Rodriguez", " Glen Eagles Street  53",
                                            "Buffalo", "0241 5826596"))
        self.m_contacts.append(self.Contact("Shelia Ramirez", "East Miller Ave 68", "Pickerington",
                                            "0346 4844556"))
        self.m_contacts.append(self.Contact("Stephanie Moss", "Piazza Trieste e Trento 77",
                                            "Roata Chiusani", "0363 0510490"))

    def rowCount(self, parent=QModelIndex()):
        return len(self.m_contacts)

    def data(self, index: QModelIndex, role: int):
        row = index.row()
        if row < self.rowCount():
            if role == ContactModel.ContactRole.FullNameRole:
                return self.m_contacts[row].fullName
            elif role == ContactModel.ContactRole.AddressRole:
                return self.m_contacts[row].address
            elif role == ContactModel.ContactRole.CityRole:
                return self.m_contacts[row].city
            elif role == ContactModel.ContactRole.NumberRole:
                return self.m_contacts[row].number

    def roleNames(self):
        default = super().roleNames()
        default[ContactModel.ContactRole.FullNameRole] = QByteArray(b"fullName")
        default[ContactModel.ContactRole.AddressRole] = QByteArray(b"address")
        default[ContactModel.ContactRole.CityRole] = QByteArray(b"city")
        default[ContactModel.ContactRole.NumberRole] = QByteArray(b"number")
        return default

    @Slot(int)
    def get(self, row: int):
        contact = self.m_contacts[row]
        return {"fullName": contact.fullName, "address": contact.address,
                "city": contact.city, "number": contact.number}

    @Slot(str, str, str, str)
    def append(self, full_name: str, address: str, city: str, number: str):
        contact = self.Contact(full_name, address, city, number)
        contact_names = [contact.fullName for contact in self.m_contacts]
        index = bisect.bisect(contact_names, contact.fullName)
        self.beginInsertRows(QModelIndex(), index, index)
        self.m_contacts.insert(index, contact)
        self.endInsertRows()

    @Slot(int, str, str, str, str)
    def set(self, row: int, full_name: str, address: str, city: str, number: str):
        if row < 0 or row >= len(self.m_contacts):
            return

        self.m_contacts[row] = self.Contact(full_name, address, city, number)
        self.dataChanged(self.index(row, 0), self.index(row, 0),
                         [ContactModel.ContactRole.FullNameRole,
                          ContactModel.ContactRole.AddressRole,
                          ContactModel.ContactRole.CityRole,
                          ContactModel.ContactRole.NumberRole])

    @Slot(int)
    def remove(self, row):
        if row < 0 or row >= len(self.m_contacts):
            return

        self.beginRemoveRows(QModelIndex(), row, row)
        del self.m_contacts[row]
        self.endRemoveRows()
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

Dialog {
    id: dialog

    signal finished(string fullName, string address, string city, string number)

    function createContact() {
        form.fullName.clear();
        form.address.clear();
        form.city.clear();
        form.number.clear();

        dialog.title = qsTr("Add Contact");
        dialog.open();
    }

    function editContact(contact) {
        form.fullName.text = contact.fullName;
        form.address.text = contact.address;
        form.city.text = contact.city;
        form.number.text = contact.number;

        dialog.title = qsTr("Edit Contact");
        dialog.open();
    }

    x: parent.width / 2 - width / 2
    y: parent.height / 2 - height / 2

    focus: true
    modal: true
    title: qsTr("Add Contact")
    standardButtons: Dialog.Ok | Dialog.Cancel

    contentItem: ContactForm {
        id: form
    }

    onAccepted: finished(form.fullName.text, form.address.text, form.city.text, form.number.text)
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

ItemDelegate {
    id: delegate

    checkable: true

    contentItem: ColumnLayout {
        spacing: 10

        Label {
            text: fullName
            font.bold: true
            elide: Text.ElideRight
            Layout.fillWidth: true
        }

        GridLayout {
            id: grid
            visible: false

            columns: 2
            rowSpacing: 10
            columnSpacing: 10

            Label {
                text: qsTr("Address:")
                Layout.leftMargin: 60
            }

            Label {
                text: address
                font.bold: true
                elide: Text.ElideRight
                Layout.fillWidth: true
            }

            Label {
                text: qsTr("City:")
                Layout.leftMargin: 60
            }

            Label {
                text: city
                font.bold: true
                elide: Text.ElideRight
                Layout.fillWidth: true
            }

            Label {
                text: qsTr("Number:")
                Layout.leftMargin: 60
            }

            Label {
                text: number
                font.bold: true
                elide: Text.ElideRight
                Layout.fillWidth: true
            }
        }
    }

    states: [
        State {
            name: "expanded"
            when: delegate.checked

            PropertyChanges {
                // TODO: When Qt Design Studio supports generalized grouped properties, change to:
                //       grid.visible: true
                target: grid
                visible: true
            }
        }
    ]
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

GridLayout {
    id: grid
    property alias fullName: fullName
    property alias address: address
    property alias city: city
    property alias number: number
    property int minimumInputSize: 120
    property string placeholderText: qsTr("<enter>")

    rows: 4
    columns: 2

    Label {
        text: qsTr("Full Name")
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
    }

    TextField {
        id: fullName
        focus: true
        Layout.fillWidth: true
        Layout.minimumWidth: grid.minimumInputSize
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
        placeholderText: grid.placeholderText
    }

    Label {
        text: qsTr("Address")
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
    }

    TextField {
        id: address
        Layout.fillWidth: true
        Layout.minimumWidth: grid.minimumInputSize
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
        placeholderText: grid.placeholderText
    }

    Label {
        text: qsTr("City")
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
    }

    TextField {
        id: city
        Layout.fillWidth: true
        Layout.minimumWidth: grid.minimumInputSize
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
        placeholderText: grid.placeholderText
    }

    Label {
        text: qsTr("Number")
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
    }

    TextField {
        id: number
        Layout.fillWidth: true
        Layout.minimumWidth: grid.minimumInputSize
        Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
        placeholderText: grid.placeholderText
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    id: window

    property int currentContact: -1

    width: 320
    height: 480
    visible: true
    title: qsTr("Contact List")

    ContactDialog {
        id: contactDialog
        onFinished: function(fullName, address, city, number) {
            if (currentContact == -1)
                contactView.model.append(fullName, address, city, number)
            else
                contactView.model.set(currentContact, fullName, address, city, number)
        }
    }

    Menu {
        id: contactMenu
        x: parent.width / 2 - width / 2
        y: parent.height / 2 - height / 2
        modal: true

        Label {
            padding: 10
            font.bold: true
            width: parent.width
            horizontalAlignment: Qt.AlignHCenter
            text: currentContact >= 0 ? contactView.model.get(currentContact).fullName : ""
        }
        MenuItem {
            text: qsTr("Edit...")
            onTriggered: contactDialog.editContact(contactView.model.get(currentContact))
        }
        MenuItem {
            text: qsTr("Remove")
            onTriggered: contactView.model.remove(currentContact)
        }
    }

    ContactView {
        id: contactView
        anchors.fill: parent
        onPressAndHold: {
            currentContact = index
            contactMenu.open()
        }
    }

    RoundButton {
        text: qsTr("+")
        highlighted: true
        anchors.margins: 10
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        onClicked: {
            currentContact = -1
            contactDialog.createContact()
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls
import Backend

ListView {
    id: listView

    signal pressAndHold(int index)

    width: 320
    height: 480

    focus: true
    boundsBehavior: Flickable.StopAtBounds

    section.property: "fullName"
    section.criteria: ViewSection.FirstCharacter
    section.delegate: SectionDelegate {
        width: listView.width
    }

    delegate: ContactDelegate {
        id: delegate
        width: listView.width
        onPressAndHold: listView.pressAndHold(index)
    }

    model: ContactModel {
        id: contactModel
    }

    ScrollBar.vertical: ScrollBar { }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

ToolBar {
    id: background

    Label {
        id: label
        text: section
        anchors.fill: parent
        horizontalAlignment: Qt.AlignHCenter
        verticalAlignment: Qt.AlignVCenter
    }
}