aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6/doc/tutorials/qmlsqlintegration
diff options
context:
space:
mode:
Diffstat (limited to 'sources/pyside6/doc/tutorials/qmlsqlintegration')
-rw-r--r--sources/pyside6/doc/tutorials/qmlsqlintegration/chat.qml98
-rw-r--r--sources/pyside6/doc/tutorials/qmlsqlintegration/example_list_view.pngbin0 -> 6954 bytes
-rw-r--r--sources/pyside6/doc/tutorials/qmlsqlintegration/main.py52
-rw-r--r--sources/pyside6/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst232
-rw-r--r--sources/pyside6/doc/tutorials/qmlsqlintegration/sqlDialog.py116
5 files changed, 498 insertions, 0 deletions
diff --git a/sources/pyside6/doc/tutorials/qmlsqlintegration/chat.qml b/sources/pyside6/doc/tutorials/qmlsqlintegration/chat.qml
new file mode 100644
index 000000000..da58ae9b2
--- /dev/null
+++ b/sources/pyside6/doc/tutorials/qmlsqlintegration/chat.qml
@@ -0,0 +1,98 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import ChatModel
+
+ApplicationWindow {
+ id: window
+ title: qsTr("Chat")
+ width: 640
+ height: 960
+ visible: true
+
+ SqlConversationModel {
+ id: chat_model
+ }
+
+ ColumnLayout {
+ anchors.fill: window
+
+ ListView {
+ id: listView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.margins: pane.leftPadding + messageField.leftPadding
+ displayMarginBeginning: 40
+ displayMarginEnd: 40
+ verticalLayoutDirection: ListView.BottomToTop
+ spacing: 12
+ model: chat_model
+ delegate: Column {
+ anchors.right: sentByMe ? listView.contentItem.right : undefined
+ spacing: 6
+
+ readonly property bool sentByMe: model.recipient !== "Me"
+ Row {
+ id: messageRow
+ spacing: 6
+ anchors.right: sentByMe ? parent.right : undefined
+
+ Rectangle {
+ width: Math.min(messageText.implicitWidth + 24,
+ listView.width - (!sentByMe ? messageRow.spacing : 0))
+ height: messageText.implicitHeight + 24
+ radius: 15
+ color: sentByMe ? "lightgrey" : "steelblue"
+
+ Label {
+ id: messageText
+ text: model.message
+ color: sentByMe ? "black" : "white"
+ anchors.fill: parent
+ anchors.margins: 12
+ wrapMode: Label.Wrap
+ }
+ }
+ }
+
+ Label {
+ id: timestampText
+ text: Qt.formatDateTime(model.timestamp, "d MMM hh:mm")
+ color: "lightgrey"
+ anchors.right: sentByMe ? parent.right : undefined
+ }
+ }
+
+ ScrollBar.vertical: ScrollBar {}
+ }
+
+ Pane {
+ id: pane
+ Layout.fillWidth: true
+
+ RowLayout {
+ width: parent.width
+
+ TextArea {
+ id: messageField
+ Layout.fillWidth: true
+ placeholderText: qsTr("Compose message")
+ wrapMode: TextArea.Wrap
+ }
+
+ Button {
+ id: sendButton
+ text: qsTr("Send")
+ enabled: messageField.length > 0
+ onClicked: {
+ listView.model.send_message("machine", messageField.text, "Me");
+ messageField.text = "";
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/sources/pyside6/doc/tutorials/qmlsqlintegration/example_list_view.png b/sources/pyside6/doc/tutorials/qmlsqlintegration/example_list_view.png
new file mode 100644
index 000000000..a0c189665
--- /dev/null
+++ b/sources/pyside6/doc/tutorials/qmlsqlintegration/example_list_view.png
Binary files differ
diff --git a/sources/pyside6/doc/tutorials/qmlsqlintegration/main.py b/sources/pyside6/doc/tutorials/qmlsqlintegration/main.py
new file mode 100644
index 000000000..314fd5aa5
--- /dev/null
+++ b/sources/pyside6/doc/tutorials/qmlsqlintegration/main.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import sys
+import logging
+
+from PySide6.QtCore import QDir, QFile, QUrl
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtQml import QQmlApplicationEngine
+from PySide6.QtSql import QSqlDatabase
+
+# We import the file just to trigger the QmlElement type registration.
+import sqlDialog
+
+logging.basicConfig(filename="chat.log", level=logging.DEBUG)
+logger = logging.getLogger("logger")
+
+
+def connectToDatabase():
+ database = QSqlDatabase.database()
+ if not database.isValid():
+ database = QSqlDatabase.addDatabase("QSQLITE")
+ if not database.isValid():
+ logger.error("Cannot add database")
+
+ write_dir = QDir("")
+ if not write_dir.mkpath("."):
+ logger.error("Failed to create writable directory")
+
+ # Ensure that we have a writable location on all devices.
+ abs_path = write_dir.absolutePath()
+ filename = f"{abs_path}/chat-database.sqlite3"
+
+ # When using the SQLite driver, open() will create the SQLite
+ # database if it doesn't exist.
+ database.setDatabaseName(filename)
+ if not database.open():
+ logger.error("Cannot open database")
+ QFile.remove(filename)
+
+
+if __name__ == "__main__":
+ app = QGuiApplication()
+ connectToDatabase()
+
+ engine = QQmlApplicationEngine()
+ engine.load(QUrl("chat.qml"))
+
+ if not engine.rootObjects():
+ sys.exit(-1)
+
+ app.exec()
diff --git a/sources/pyside6/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst b/sources/pyside6/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst
new file mode 100644
index 000000000..eee3f807e
--- /dev/null
+++ b/sources/pyside6/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst
@@ -0,0 +1,232 @@
+QML, SQL and PySide Integration Tutorial
+########################################
+
+This tutorial is very similar to the `Qt Chat Tutorial`_ one but it focuses on explaining how to
+integrate a SQL database into a PySide6 application using QML for its UI.
+
+.. _`Qt Chat Tutorial`: https://doc.qt.io/qt-6/qtquickcontrols-chattutorial-example.html
+
+sqlDialog.py
+------------
+
+We import the pertinent libraries to our program, define a global variable that hold the
+name of our table, and define the global function ``createTable()`` that creates a new table if it
+doesn't already exist.
+The database contains a single line to mock the beginning of a conversation.
+
+.. literalinclude:: sqlDialog.py
+ :linenos:
+ :lines: 4-43
+
+The ``SqlConversationModel`` class offers the read-only data model required for the non-editable
+contacts list. It derives from the :ref:`QSqlQueryModel` class, which is the logical choice for
+this use case.
+Then, we proceed to create the table, set its name to the one defined previously with the
+:meth:`~.QSqlTableModel.setTable` method.
+We add the necessary attributes to the table, to have a program that reflects the idea
+of a chat application.
+
+.. literalinclude:: sqlDialog.py
+ :linenos:
+ :lines: 47-59
+
+In ``setRecipient()``, you set a filter over the returned results from the database, and
+emit a signal every time the recipient of the message changes.
+
+.. literalinclude:: sqlDialog.py
+ :linenos:
+ :lines: 61-70
+
+The ``data()`` function falls back to ``QSqlTableModel``'s implementation if the role is not a
+custom user role.
+If you get a user role, we can subtract :meth:`~.QtCore.Qt.UserRole` from it to get the index of
+that field, and then use that index to find the value to be returned.
+
+.. literalinclude:: sqlDialog.py
+ :linenos:
+ :lines: 72-79
+
+
+In ``roleNames()``, we return a Python dictionary with our custom role and role names as key-values
+pairs, so we can use these roles in QML.
+Alternatively, it can be useful to declare an Enum to hold all of the role values.
+Note that ``names`` has to be a hash to be used as a dictionary key,
+and that's why we're using the ``hash`` function.
+
+.. literalinclude:: sqlDialog.py
+ :linenos:
+ :lines: 81-95
+
+The ``send_message()`` function uses the given recipient and message to insert a new record into
+the database.
+Using :meth:`~.QSqlTableModel.OnManualSubmit` requires you to also call ``submitAll()``,
+since all the changes will be cached in the model until you do so.
+
+.. literalinclude:: sqlDialog.py
+ :linenos:
+ :lines: 97-116
+
+chat.qml
+--------
+
+Let's look at the ``chat.qml`` file.
+
+.. literalinclude:: chat.qml
+ :linenos:
+ :lines: 4-6
+
+First, import the Qt Quick module.
+This gives us access to graphical primitives such as Item, Rectangle, Text, and so on.
+For a full list of types, see the `Qt Quick QML Types`_ documentation.
+We then add QtQuick.Layouts import, which we'll cover shortly.
+
+Next, import the Qt Quick Controls module.
+Among other things, this provides access to ``ApplicationWindow``, which replaces the existing
+root type, Window:
+
+Let's step through the ``chat.qml`` file.
+
+.. literalinclude:: chat.qml
+ :linenos:
+ :lines: 9-14
+
+``ApplicationWindow`` is a Window with some added convenience for creating a header and a footer.
+It also provides the foundation for popups and supports some basic styling, such as the background
+color.
+
+There are three properties that are almost always set when using ApplicationWindow: ``width``,
+``height``, and ``visible``.
+Once we've set these, we have a properly sized, empty window ready to be filled with content.
+
+Because we are exposing the :code:`SqlConversationModel` class to QML, we will
+declare a component to access it:
+
+.. literalinclude:: chat.qml
+ :linenos:
+ :lines: 16-18
+
+There are two ways of laying out items in QML: `Item Positioners`_ and `Qt Quick Layouts`_.
+
+- Item positioners (`Row`_, `Column`_, and so on) are useful for situations where the size of items
+ is known or fixed, and all that is required is to neatly position them in a certain formation.
+- The layouts in Qt Quick Layouts can both position and resize items, making them well suited for
+ resizable user interfaces.
+ Below, we use `ColumnLayout`_ to vertically lay out a `ListView`_ and a `Pane`_.
+
+ .. literalinclude:: chat.qml
+ :linenos:
+ :lines: 20-23
+
+ .. literalinclude:: chat.qml
+ :linenos:
+ :lines: 72-74
+
+Pane is basically a rectangle whose color comes from the application's style.
+It's similar to `Frame`_, but it has no stroke around its border.
+
+Items that are direct children of a layout have various `attached properties`_ available to them.
+We use `Layout.fillWidth`_ and `Layout.fillHeight`_ on the `ListView`_ to ensure that it takes as
+much space within the `ColumnLayout`_ as it can, and the same is done for the Pane.
+As `ColumnLayout`_ is a vertical layout, there aren't any items to the left or right of each child,
+so this results in each item consuming the entire width of the layout.
+
+On the other hand, the `Layout.fillHeight`_ statement in the `ListView`_ enables it to occupy the
+remaining space that is left after accommodating the Pane.
+
+.. _Item Positioners: https://doc.qt.io/qt-5/qtquick-positioning-layouts.html
+.. _Qt Quick Layouts: https://doc.qt.io/qt-5/qtquicklayouts-index.html
+.. _Row: https://doc.qt.io/qt-5/qml-qtquick-row.html
+.. _Column: https://doc.qt.io/qt-5/qml-qtquick-column.html
+.. _ColumnLayout: https://doc.qt.io/qt-5/qml-qtquick-layouts-columnlayout.html
+.. _ListView: https://doc.qt.io/qt-5/qml-qtquick-listview.html
+.. _Pane: https://doc.qt.io/qt-5/qml-qtquick-controls2-pane.html
+.. _Frame: https://doc.qt.io/qt-5/qml-qtquick-controls2-frame.html
+.. _attached properties: https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html
+.. _Layout.fillWidth: https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html#fillWidth-attached-prop
+.. _Layout.fillHeight: https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html#fillHeight-attached-prop
+.. _Qt Quick QML Types: https://doc.qt.io/qt-5/qtquick-qmlmodule.html
+
+Let's look at the ``Listview`` in detail:
+
+.. literalinclude:: chat.qml
+ :linenos:
+ :lines: 23-70
+
+After filling the ``width`` and ``height`` of its parent, we also set some margins on the view.
+
+Next, we set `displayMarginBeginning`_ and `displayMarginEnd`_.
+These properties ensure that the delegates outside the view don't disappear when you
+scroll at the edges of the view.
+To get a better understanding, consider commenting out the properties and then rerun your code.
+Now watch what happens when you scroll the view.
+
+We then flip the vertical direction of the view, so that first items are at the bottom.
+
+Additionally, messages sent by the contact should be distinguished from those sent by a contact.
+For now, when a message is sent by you, we set a ``sentByMe`` property, to alternate between
+different contacts.
+Using this property, we distinguish between different contacts in two ways:
+
+* Messages sent by the contact are aligned to the right side of the screen by setting
+ ``anchors.right`` to ``parent.right``.
+* We change the color of the rectangle depending on the contact.
+ Since we don't want to display dark text on a dark background, and vice versa, we also set the
+ text color depending on who the contact is.
+
+At the bottom of the screen, we place a `TextArea`_ item to allow multi-line text input, and a
+button to send the message.
+We use Pane to cover the area under these two items:
+
+.. literalinclude:: chat.qml
+ :linenos:
+ :lines: 72-96
+
+The `TextArea`_ should fill the available width of the screen.
+We assign some placeholder text to provide a visual cue to the contact as to where they should begin
+typing.
+The text within the input area is wrapped to ensure that it does not go outside of the screen.
+
+Lastly, we have a button that allows us to call the ``send_message`` method we defined on
+``sqlDialog.py``, since we're just having a mock up example here and there is only one possible
+recipient and one possible sender for this conversation we're just using strings here.
+
+.. _displayMarginBeginning: https://doc.qt.io/qt-5/qml-qtquick-listview.html#displayMarginBeginning-prop
+.. _displayMarginEnd: https://doc.qt.io/qt-5/qml-qtquick-listview.html#displayMarginEnd-prop
+.. _TextArea: https://doc.qt.io/qt-5/qml-qtquick-controls2-textarea.html
+
+
+main.py
+-------
+
+We use ``logging`` instead of Python's ``print()``, because it provides a better way to control the
+messages levels that our application will generate (errors, warnings, and information messages).
+
+.. literalinclude:: main.py
+ :linenos:
+ :lines: 4-16
+
+``connectToDatabase()`` creates a connection with the SQLite database, creating the actual file
+if it doesn't already exist.
+
+.. literalinclude:: main.py
+ :linenos:
+ :lines: 19-39
+
+A few interesting things happen in the ``main`` function:
+
+- Declaring a :ref:`QGuiApplication`.
+ You should use a :ref:`QGuiApplication` instead of :ref:`QApplication` because we're not
+ using the **QtWidgets** module.
+- Connecting to the database,
+- Declaring a :ref:`QQmlApplicationEngine`.
+ This allows you to access the QML Elements to connect Python
+ and QML from the conversation model we built on ``sqlDialog.py``.
+- Loading the ``.qml`` file that defines the UI.
+
+Finally, the Qt application runs, and your program starts.
+
+.. literalinclude:: main.py
+ :linenos:
+ :lines: 42-52
+
+.. image:: example_list_view.png
diff --git a/sources/pyside6/doc/tutorials/qmlsqlintegration/sqlDialog.py b/sources/pyside6/doc/tutorials/qmlsqlintegration/sqlDialog.py
new file mode 100644
index 000000000..d728aee59
--- /dev/null
+++ b/sources/pyside6/doc/tutorials/qmlsqlintegration/sqlDialog.py
@@ -0,0 +1,116 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import datetime
+import logging
+
+from PySide6.QtCore import Qt, Slot
+from PySide6.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord, QSqlTableModel
+from PySide6.QtQml import QmlElement
+
+table_name = "Conversations"
+QML_IMPORT_NAME = "ChatModel"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+def createTable():
+ if table_name in QSqlDatabase.database().tables():
+ return
+
+ query = QSqlQuery()
+ if not query.exec_(
+ """
+ CREATE TABLE IF NOT EXISTS 'Conversations' (
+ 'author' TEXT NOT NULL,
+ 'recipient' TEXT NOT NULL,
+ 'timestamp' TEXT NOT NULL,
+ 'message' TEXT NOT NULL,
+ FOREIGN KEY('author') REFERENCES Contacts ( name ),
+ FOREIGN KEY('recipient') REFERENCES Contacts ( name )
+ )
+ """
+ ):
+ logging.error("Failed to query database")
+
+ # This adds the first message from the Bot
+ # and further development is required to make it interactive.
+ query.exec_(
+ """
+ INSERT INTO Conversations VALUES(
+ 'machine', 'Me', '2019-01-07T14:36:06', 'Hello!'
+ )
+ """
+ )
+ logging.info(query)
+
+
+@QmlElement
+class SqlConversationModel(QSqlTableModel):
+ def __init__(self, parent=None):
+ super(SqlConversationModel, self).__init__(parent)
+
+ createTable()
+ self.setTable(table_name)
+ self.setSort(2, Qt.DescendingOrder)
+ self.setEditStrategy(QSqlTableModel.OnManualSubmit)
+ self.recipient = ""
+
+ self.select()
+ logging.debug("Table was loaded successfully.")
+
+ def setRecipient(self, recipient):
+ if recipient == self.recipient:
+ pass
+
+ self.recipient = recipient
+
+ filter_str = (f"(recipient = '{self.recipient}' AND author = 'Me') OR "
+ f"(recipient = 'Me' AND author='{self.recipient}')")
+ self.setFilter(filter_str)
+ self.select()
+
+ def data(self, index, role):
+ if role < Qt.UserRole:
+ return QSqlTableModel.data(self, index, role)
+
+ sql_record = QSqlRecord()
+ sql_record = self.record(index.row())
+
+ return sql_record.value(role - Qt.UserRole)
+
+ def roleNames(self):
+ """Converts dict to hash because that's the result expected
+ by QSqlTableModel"""
+ names = {}
+ author = "author".encode()
+ recipient = "recipient".encode()
+ timestamp = "timestamp".encode()
+ message = "message".encode()
+
+ names[hash(Qt.UserRole)] = author
+ names[hash(Qt.UserRole + 1)] = recipient
+ names[hash(Qt.UserRole + 2)] = timestamp
+ names[hash(Qt.UserRole + 3)] = message
+
+ return names
+
+ # This is a workaround because PySide doesn't provide Q_INVOKABLE
+ # So we declare this as a Slot to be able to call it from QML
+ @Slot(str, str, str)
+ def send_message(self, recipient, message, author):
+ timestamp = datetime.datetime.now()
+
+ new_record = self.record()
+ new_record.setValue("author", author)
+ new_record.setValue("recipient", recipient)
+ new_record.setValue("timestamp", str(timestamp))
+ new_record.setValue("message", message)
+
+ logging.debug(f'Message: "{message}" \n Received by: "{recipient}"')
+
+ if not self.insertRecord(self.rowCount(), new_record):
+ logging.error("Failed to send message: {self.lastError().text()}")
+ return
+
+ self.submitAll()
+ self.select()