diff options
Diffstat (limited to 'sources/pyside6/doc/tutorials/qmlsqlintegration')
-rw-r--r-- | sources/pyside6/doc/tutorials/qmlsqlintegration/chat.qml | 98 | ||||
-rw-r--r-- | sources/pyside6/doc/tutorials/qmlsqlintegration/example_list_view.png | bin | 0 -> 6954 bytes | |||
-rw-r--r-- | sources/pyside6/doc/tutorials/qmlsqlintegration/main.py | 52 | ||||
-rw-r--r-- | sources/pyside6/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst | 232 | ||||
-rw-r--r-- | sources/pyside6/doc/tutorials/qmlsqlintegration/sqlDialog.py | 116 |
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 Binary files differnew file mode 100644 index 000000000..a0c189665 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlsqlintegration/example_list_view.png 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() |