diff options
author | Mariana Meireles <mariana.meireles-gontijo@qt.io> | 2019-11-22 17:26:12 +0100 |
---|---|---|
committer | Cristián Maureira-Fredes <cristian.maureira-fredes@qt.io> | 2020-01-27 23:41:38 +0100 |
commit | 1273f72b013a875c5cf51d3da4fc9b081eee28c1 (patch) | |
tree | c066816f37a88d5e30cd471a2bfaa39a0176450e /sources/pyside2/doc | |
parent | 450086611ae9640a375b696f251c337e160cbfbf (diff) |
docs: QML and SQL integration tutorial
Change-Id: I6ba4ae9fe6a9bfe669b76a23f27556b1f3c59eb4
Reviewed-by: Venugopal Shivashankar <Venugopal.Shivashankar@qt.io>
Diffstat (limited to 'sources/pyside2/doc')
-rw-r--r-- | sources/pyside2/doc/tutorials/index.rst | 17 | ||||
-rw-r--r-- | sources/pyside2/doc/tutorials/qmlsqlintegration/chat.qml | 127 | ||||
-rw-r--r-- | sources/pyside2/doc/tutorials/qmlsqlintegration/example_list_view.png | bin | 0 -> 6954 bytes | |||
-rw-r--r-- | sources/pyside2/doc/tutorials/qmlsqlintegration/main.py | 85 | ||||
-rw-r--r-- | sources/pyside2/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst | 223 | ||||
-rw-r--r-- | sources/pyside2/doc/tutorials/qmlsqlintegration/sqlDialog.py | 146 |
6 files changed, 598 insertions, 0 deletions
diff --git a/sources/pyside2/doc/tutorials/index.rst b/sources/pyside2/doc/tutorials/index.rst index 7486554f9..73e6b6b26 100644 --- a/sources/pyside2/doc/tutorials/index.rst +++ b/sources/pyside2/doc/tutorials/index.rst @@ -7,6 +7,8 @@ documents were ported from C++ to Python and cover a range of topics, from basic use of widgets to step-by-step tutorials that show how an application is put together. +Basic tutorials +--------------- .. toctree:: :maxdepth: 2 @@ -16,8 +18,23 @@ application is put together. basictutorial/clickablebutton.rst basictutorial/dialog.rst basictutorial/uifiles.rst + +Real use-cases applications +--------------------------- + +.. toctree:: + :maxdepth: 2 + datavisualize/index.rst expenses/expenses.rst qmlapp/qmlapplication.rst qmlintegration/qmlintegration.rst + qmlsqlintegration/qmlsqlintegration.rst + +C++ and Python +-------------- + +.. toctree:: + :maxdepth: 2 + portingguide/index.rst diff --git a/sources/pyside2/doc/tutorials/qmlsqlintegration/chat.qml b/sources/pyside2/doc/tutorials/qmlsqlintegration/chat.qml new file mode 100644 index 000000000..487f5b36c --- /dev/null +++ b/sources/pyside2/doc/tutorials/qmlsqlintegration/chat.qml @@ -0,0 +1,127 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of Qt for Python. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +ApplicationWindow { + id: window + title: qsTr("Chat") + width: 640 + height: 960 + visible: true + ColumnLayout { + anchors.fill: parent + + 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 { + readonly property bool sentByMe: model.recipient !== "Me" + anchors.right: sentByMe ? parent.right : undefined + spacing: 6 + + Row { + id: messageRow + spacing: 6 + anchors.right: sentByMe ? parent.right : undefined + + Rectangle { + width: Math.min(messageText.implicitWidth + 24, listView.width - messageRow.spacing) + height: messageText.implicitHeight + 24 + radius: 15 + color: sentByMe ? "lightgrey" : "#ff627c" + + 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: { + chat_model.send_message("machine", messageField.text, "Me"); + messageField.text = ""; + } + } + } + } + } +} diff --git a/sources/pyside2/doc/tutorials/qmlsqlintegration/example_list_view.png b/sources/pyside2/doc/tutorials/qmlsqlintegration/example_list_view.png Binary files differnew file mode 100644 index 000000000..a0c189665 --- /dev/null +++ b/sources/pyside2/doc/tutorials/qmlsqlintegration/example_list_view.png diff --git a/sources/pyside2/doc/tutorials/qmlsqlintegration/main.py b/sources/pyside2/doc/tutorials/qmlsqlintegration/main.py new file mode 100644 index 000000000..c710e019a --- /dev/null +++ b/sources/pyside2/doc/tutorials/qmlsqlintegration/main.py @@ -0,0 +1,85 @@ +############################################################################# +## +## Copyright (C) 2019 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import logging + +from PySide2.QtCore import QDir, QFile, QUrl +from PySide2.QtGui import QGuiApplication +from PySide2.QtQml import QQmlApplicationEngine +from PySide2.QtSql import QSqlDatabase + +from sqlDialog import SqlConversationModel + +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. + filename = "{}/chat-database.sqlite3".format(write_dir.absolutePath()) + + # 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() + sql_conversation_model = SqlConversationModel() + + engine = QQmlApplicationEngine() + # Export pertinent objects to QML + engine.rootContext().setContextProperty("chat_model", sql_conversation_model) + engine.load(QUrl("chat.qml")) + + app.exec_() diff --git a/sources/pyside2/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst b/sources/pyside2/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst new file mode 100644 index 000000000..c26d154ac --- /dev/null +++ b/sources/pyside2/doc/tutorials/qmlsqlintegration/qmlsqlintegration.rst @@ -0,0 +1,223 @@ +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 PySide2 application using QML for its UI. + +.. _`Qt Chat Tutorial`: https://doc.qt.io/qt-5/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: 40-77 + +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: 80-91 + +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: 93-103 + +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: 105-112 + + +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: 114-128 + +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: 130-146 + +chat.qml +-------- + +Let's look at the ``chat.qml`` file. + + .. literalinclude:: chat.qml + :linenos: + :lines: 40-42 + +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: 44-49 + +``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. + +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: 50-53 + +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 +.. _ListView: https://doc.qt.io/qt-5/qml-qtquick-listview.html +.. _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: 53-99 + +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: 101-125 + +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: 40-50 + +``connectToDatabase()`` creates a connection with the SQLite database, creating the actual file +if it doesn't already exist. + + .. literalinclude:: main.py + :linenos: + :lines: 53-72 + + + +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 context property 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: 75-85 + +.. image:: example_list_view.png diff --git a/sources/pyside2/doc/tutorials/qmlsqlintegration/sqlDialog.py b/sources/pyside2/doc/tutorials/qmlsqlintegration/sqlDialog.py new file mode 100644 index 000000000..6a9ff8234 --- /dev/null +++ b/sources/pyside2/doc/tutorials/qmlsqlintegration/sqlDialog.py @@ -0,0 +1,146 @@ +############################################################################# +## +## Copyright (C) 2019 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import datetime +import logging + +from PySide2.QtCore import Qt, Slot +from PySide2.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord, QSqlTableModel + +table_name = "Conversations" + + +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) + + +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 = ( + "(recipient = '{}' AND author = 'Me') OR " "(recipient = 'Me' AND author='{}')" + ).format(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 + + 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('Message: "{}" \n Received by: "{}"'.format(message, recipient)) + + if not self.insertRecord(self.rowCount(), new_record): + logging.error("Failed to send message: {}".format(self.lastError().text())) + return + + self.submitAll() + self.select() |