diff options
Diffstat (limited to 'sources/pyside6/doc/tutorials/portingguide')
28 files changed, 1817 insertions, 0 deletions
diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter1/chapter1.rst b/sources/pyside6/doc/tutorials/portingguide/chapter1/chapter1.rst new file mode 100644 index 000000000..87fb97660 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter1/chapter1.rst @@ -0,0 +1,89 @@ +Chapter 1: ``initDb.h`` to ``createDb.py`` +******************************************* + +To begin with, port the C++ code that creates an SQLite +database and tables, and adds data to them. In this case, +all C++ code related to this lives in ``initdb.h``. The +code in this header file is divided into following parts: + +* ``initDb`` - Creates a db and the necessary tables +* ``addBooks`` - Adds data to the **books** table. +* ``addAuthor`` - Adds data to the **authors** table. +* ``addGenre`` - Adds data to the **genres** table. + +To start with, add these following ``import`` statements at +the beginning of ``createdb.py``: + +.. literalinclude:: createdb.py + :language: python + :linenos: + :lines: 3-5 + +The ``initDb`` function does most of the work needed to +set up the database, but it depends on the ``addAuthor``, +``addGenre``, and ``addBook`` helper functions to populate +the tables. Port these helper functions first. Here is how +the C++ and Python versions of these functions look like: + +C++ version +----------- + +.. literalinclude:: initdb.h + :language: c++ + :linenos: + :lines: 9-33 + +Python version +-------------- + +.. literalinclude:: createdb.py + :language: python + :linenos: + :lines: 7-28 + +Now that the helper functions are in place, port ``initDb``. +Here is how the C++ and Python versions of this function +looks like: + +C++ version (initDb) +-------------------- + +.. literalinclude:: initdb.h + :language: c++ + :linenos: + :lines: 35-112 + +Python version (init_db) +------------------------ + +.. literalinclude:: createdb.py + :language: python + :linenos: + :lines: 28- + +.. note:: The Python version uses the ``check`` function to + execute the SQL statements instead of the ``if...else`` + block like in the C++ version. Although both are valid + approaches, the earlier one produces code that looks + cleaner and shorter. + +Your Python code to set up the database is ready now. To +test it, add the following code to ``main.py`` and run it: + +.. literalinclude:: main.py + :language: python + :linenos: + :lines: 3- + +Use the following command from the prompt to run: + +.. code-block:: bash + + python main.py + +Your table will look like this: + +.. image:: images/chapter1_books.png + +Try modifying the SQL statment in ``main.py`` to get data +from the ``genres`` or ``authors`` table. diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter1/createdb.py b/sources/pyside6/doc/tutorials/portingguide/chapter1/createdb.py new file mode 100644 index 000000000..da7d201a8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter1/createdb.py @@ -0,0 +1,94 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtSql import QSqlDatabase, QSqlError, QSqlQuery +from datetime import date + + +def add_book(q, title, year, authorId, genreId, rating): + q.addBindValue(title) + q.addBindValue(year) + q.addBindValue(authorId) + q.addBindValue(genreId) + q.addBindValue(rating) + q.exec_() + + +def add_genre(q, name): + q.addBindValue(name) + q.exec_() + return q.lastInsertId() + + +def add_author(q, name, birthdate): + q.addBindValue(name) + q.addBindValue(str(birthdate)) + q.exec_() + return q.lastInsertId() + +BOOKS_SQL = """ + create table books(id integer primary key, title varchar, author integer, + genre integer, year integer, rating integer) + """ +AUTHORS_SQL = """ + create table authors(id integer primary key, name varchar, birthdate text) + """ +GENRES_SQL = """ + create table genres(id integer primary key, name varchar) + """ +INSERT_AUTHOR_SQL = """ + insert into authors(name, birthdate) values(?, ?) + """ +INSERT_GENRE_SQL = """ + insert into genres(name) values(?) + """ +INSERT_BOOK_SQL = """ + insert into books(title, year, author, genre, rating) + values(?, ?, ?, ?, ?) + """ + +def init_db(): + """ + init_db() + Initializes the database. + If tables "books" and "authors" are already in the database, do nothing. + Return value: None or raises ValueError + The error value is the QtSql error instance. + """ + def check(func, *args): + if not func(*args): + raise ValueError(func.__self__.lastError()) + db = QSqlDatabase.addDatabase("QSQLITE") + db.setDatabaseName(":memory:") + + check(db.open) + + q = QSqlQuery() + check(q.exec_, BOOKS_SQL) + check(q.exec_, AUTHORS_SQL) + check(q.exec_, GENRES_SQL) + check(q.prepare, INSERT_AUTHOR_SQL) + + asimovId = add_author(q, "Isaac Asimov", date(1920, 2, 1)) + greeneId = add_author(q, "Graham Greene", date(1904, 10, 2)) + pratchettId = add_author(q, "Terry Pratchett", date(1948, 4, 28)) + + check(q.prepare,INSERT_GENRE_SQL) + sfiction = add_genre(q, "Science Fiction") + fiction = add_genre(q, "Fiction") + fantasy = add_genre(q, "Fantasy") + + check(q.prepare,INSERT_BOOK_SQL) + add_book(q, "Foundation", 1951, asimovId, sfiction, 3) + add_book(q, "Foundation and Empire", 1952, asimovId, sfiction, 4) + add_book(q, "Second Foundation", 1953, asimovId, sfiction, 3) + add_book(q, "Foundation's Edge", 1982, asimovId, sfiction, 3) + add_book(q, "Foundation and Earth", 1986, asimovId, sfiction, 4) + add_book(q, "Prelude to Foundation", 1988, asimovId, sfiction, 3) + add_book(q, "Forward the Foundation", 1993, asimovId, sfiction, 3) + add_book(q, "The Power and the Glory", 1940, greeneId, fiction, 4) + add_book(q, "The Third Man", 1950, greeneId, fiction, 5) + add_book(q, "Our Man in Havana", 1958, greeneId, fiction, 4) + add_book(q, "Guards! Guards!", 1989, pratchettId, fantasy, 3) + add_book(q, "Night Watch", 2002, pratchettId, fantasy, 3) + add_book(q, "Going Postal", 2004, pratchettId, fantasy, 3) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter1/images/chapter1_books.png b/sources/pyside6/doc/tutorials/portingguide/chapter1/images/chapter1_books.png Binary files differnew file mode 100644 index 000000000..164674220 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter1/images/chapter1_books.png diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter1/initdb.h b/sources/pyside6/doc/tutorials/portingguide/chapter1/initdb.h new file mode 100644 index 000000000..5596defc0 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter1/initdb.h @@ -0,0 +1,113 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef INITDB_H +#define INITDB_H + +#include <QtSql> + +void addBook(QSqlQuery &q, const QString &title, int year, const QVariant &authorId, + const QVariant &genreId, int rating) +{ + q.addBindValue(title); + q.addBindValue(year); + q.addBindValue(authorId); + q.addBindValue(genreId); + q.addBindValue(rating); + q.exec(); +} + +QVariant addGenre(QSqlQuery &q, const QString &name) +{ + q.addBindValue(name); + q.exec(); + return q.lastInsertId(); +} + +QVariant addAuthor(QSqlQuery &q, const QString &name, const QDate &birthdate) +{ + q.addBindValue(name); + q.addBindValue(birthdate); + q.exec(); + return q.lastInsertId(); +} + +const auto BOOKS_SQL = QLatin1String(R"( + create table books(id integer primary key, title varchar, author integer, + genre integer, year integer, rating integer) + )"); + +const auto AUTHORS_SQL = QLatin1String(R"( + create table authors(id integer primary key, name varchar, birthdate date) + )"); + +const auto GENRES_SQL = QLatin1String(R"( + create table genres(id integer primary key, name varchar) + )"); + +const auto INSERT_AUTHOR_SQL = QLatin1String(R"( + insert into authors(name, birthdate) values(?, ?) + )"); + +const auto INSERT_BOOK_SQL = QLatin1String(R"( + insert into books(title, year, author, genre, rating) + values(?, ?, ?, ?, ?) + )"); + +const auto INSERT_GENRE_SQL = QLatin1String(R"( + insert into genres(name) values(?) + )"); + +QSqlError initDb() +{ + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName(":memory:"); + + if (!db.open()) + return db.lastError(); + + QStringList tables = db.tables(); + if (tables.contains("books", Qt::CaseInsensitive) + && tables.contains("authors", Qt::CaseInsensitive)) + return QSqlError(); + + QSqlQuery q; + if (!q.exec(BOOKS_SQL)) + return q.lastError(); + if (!q.exec(AUTHORS_SQL)) + return q.lastError(); + if (!q.exec(GENRES_SQL)) + return q.lastError(); + + if (!q.prepare(INSERT_AUTHOR_SQL)) + return q.lastError(); + QVariant asimovId = addAuthor(q, QLatin1String("Isaac Asimov"), QDate(1920, 2, 1)); + QVariant greeneId = addAuthor(q, QLatin1String("Graham Greene"), QDate(1904, 10, 2)); + QVariant pratchettId = addAuthor(q, QLatin1String("Terry Pratchett"), QDate(1948, 4, 28)); + + if (!q.prepare(INSERT_GENRE_SQL)) + return q.lastError(); + QVariant sfiction = addGenre(q, QLatin1String("Science Fiction")); + QVariant fiction = addGenre(q, QLatin1String("Fiction")); + QVariant fantasy = addGenre(q, QLatin1String("Fantasy")); + + if (!q.prepare(INSERT_BOOK_SQL)) + return q.lastError(); + addBook(q, QLatin1String("Foundation"), 1951, asimovId, sfiction, 3); + addBook(q, QLatin1String("Foundation and Empire"), 1952, asimovId, sfiction, 4); + addBook(q, QLatin1String("Second Foundation"), 1953, asimovId, sfiction, 3); + addBook(q, QLatin1String("Foundation's Edge"), 1982, asimovId, sfiction, 3); + addBook(q, QLatin1String("Foundation and Earth"), 1986, asimovId, sfiction, 4); + addBook(q, QLatin1String("Prelude to Foundation"), 1988, asimovId, sfiction, 3); + addBook(q, QLatin1String("Forward the Foundation"), 1993, asimovId, sfiction, 3); + addBook(q, QLatin1String("The Power and the Glory"), 1940, greeneId, fiction, 4); + addBook(q, QLatin1String("The Third Man"), 1950, greeneId, fiction, 5); + addBook(q, QLatin1String("Our Man in Havana"), 1958, greeneId, fiction, 4); + addBook(q, QLatin1String("Guards! Guards!"), 1989, pratchettId, fantasy, 3); + addBook(q, QLatin1String("Night Watch"), 2002, pratchettId, fantasy, 3); + addBook(q, QLatin1String("Going Postal"), 2004, pratchettId, fantasy, 3); + + return QSqlError(); +} + +#endif diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter1/main.py b/sources/pyside6/doc/tutorials/portingguide/chapter1/main.py new file mode 100644 index 000000000..c1293bcbd --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter1/main.py @@ -0,0 +1,22 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtSql import QSqlQueryModel +from PySide6.QtWidgets import QTableView, QApplication + +import createdb + +if __name__ == "__main__": + app = QApplication() + createdb.init_db() + + model = QSqlQueryModel() + model.setQuery("select * from books") + + table_view = QTableView() + table_view.setModel(model) + table_view.resize(800, 600) + table_view.show() + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.cpp b/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.cpp new file mode 100644 index 000000000..3d246667b --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "bookdelegate.h" + +#include <QtWidgets> + +BookDelegate::BookDelegate(QObject *parent) + : QSqlRelationalDelegate(parent), star(QPixmap(":images/star.png")) +{ +} + +void BookDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (index.column() != 5) { + QStyleOptionViewItem opt = option; + // Since we draw the grid ourselves: + opt.rect.adjust(0, 0, -1, -1); + QSqlRelationalDelegate::paint(painter, opt, index); + } else { + const QAbstractItemModel *model = index.model(); + QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? + (option.state & QStyle::State_Active) ? + QPalette::Normal : + QPalette::Inactive : + QPalette::Disabled; + + if (option.state & QStyle::State_Selected) + painter->fillRect( + option.rect, + option.palette.color(cg, QPalette::Highlight)); + + int rating = model->data(index, Qt::DisplayRole).toInt(); + int width = star.width(); + int height = star.height(); + int x = option.rect.x(); + int y = option.rect.y() + (option.rect.height() / 2) - (height / 2); + for (int i = 0; i < rating; ++i) { + painter->drawPixmap(x, y, star); + x += width; + } + // Since we draw the grid ourselves: + drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1)); + } + + QPen pen = painter->pen(); + painter->setPen(option.palette.color(QPalette::Mid)); + painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); + painter->drawLine(option.rect.topRight(), option.rect.bottomRight()); + painter->setPen(pen); +} + +QSize BookDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (index.column() == 5) + return QSize(5 * star.width(), star.height()) + QSize(1, 1); + // Since we draw the grid ourselves: + return QSqlRelationalDelegate::sizeHint(option, index) + QSize(1, 1); +} + +bool BookDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &option, + const QModelIndex &index) +{ + if (index.column() != 5) + return QSqlRelationalDelegate::editorEvent(event, model, option, index); + + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event); + int stars = qBound(0, int(0.7 + qreal(mouseEvent->pos().x() + - option.rect.x()) / star.width()), 5); + model->setData(index, QVariant(stars)); + // So that the selection can change: + return false; + } + + return true; +} + +QWidget *BookDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (index.column() != 4) + return QSqlRelationalDelegate::createEditor(parent, option, index); + + // For editing the year, return a spinbox with a range from -1000 to 2100. + QSpinBox *sb = new QSpinBox(parent); + sb->setFrame(false); + sb->setMaximum(2100); + sb->setMinimum(-1000); + + return sb; +} diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.h b/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.h new file mode 100644 index 000000000..d0b157b39 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.h @@ -0,0 +1,36 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef BOOKDELEGATE_H +#define BOOKDELEGATE_H + +#include <QModelIndex> +#include <QPixmap> +#include <QSize> +#include <QSqlRelationalDelegate> + +QT_FORWARD_DECLARE_CLASS(QPainter) + +class BookDelegate : public QSqlRelationalDelegate +{ +public: + BookDelegate(QObject *parent); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + bool editorEvent(QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &option, + const QModelIndex &index) override; + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + QPixmap star; +}; + +#endif diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.py b/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.py new file mode 100644 index 000000000..2c2b80157 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/bookdelegate.py @@ -0,0 +1,101 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import copy +import os +from pathlib import Path + +from PySide6.QtSql import QSqlRelationalDelegate +from PySide6.QtWidgets import (QItemDelegate, QSpinBox, QStyledItemDelegate, + QStyle, QStyleOptionViewItem) +from PySide6.QtGui import QMouseEvent, QPixmap, QPalette, QImage +from PySide6.QtCore import QEvent, QSize, Qt, QUrl + + +class BookDelegate(QSqlRelationalDelegate): + """Books delegate to rate the books""" + + def __init__(self, parent=None): + QSqlRelationalDelegate.__init__(self, parent) + star_png = Path(__file__).parent / "images" / "star.png" + self.star = QPixmap(star_png) + + def paint(self, painter, option, index): + """ Paint the items in the table. + + If the item referred to by <index> is a StarRating, we + handle the painting ourselves. For the other items, we + let the base class handle the painting as usual. + + In a polished application, we'd use a better check than + the column number to find out if we needed to paint the + stars, but it works for the purposes of this example. + """ + if index.column() != 5: + # Since we draw the grid ourselves: + opt = copy.copy(option) + opt.rect = option.rect.adjusted(0, 0, -1, -1) + QSqlRelationalDelegate.paint(self, painter, opt, index) + else: + model = index.model() + if option.state & QStyle.State_Enabled: + if option.state & QStyle.State_Active: + color_group = QPalette.Normal + else: + color_group = QPalette.Inactive + else: + color_group = QPalette.Disabled + + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, + option.palette.color(color_group, QPalette.Highlight)) + rating = model.data(index, Qt.DisplayRole) + width = self.star.width() + height = self.star.height() + x = option.rect.x() + y = option.rect.y() + (option.rect.height() / 2) - (height / 2) + for i in range(rating): + painter.drawPixmap(x, y, self.star) + x += width + + # Since we draw the grid ourselves: + self.drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1)) + + pen = painter.pen() + painter.setPen(option.palette.color(QPalette.Mid)) + painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight()) + painter.drawLine(option.rect.topRight(), option.rect.bottomRight()) + painter.setPen(pen) + + def sizeHint(self, option, index): + """ Returns the size needed to display the item in a QSize object. """ + if index.column() == 5: + size_hint = QSize(5 * self.star.width(), self.star.height()) + QSize(1, 1) + return size_hint + # Since we draw the grid ourselves: + return QSqlRelationalDelegate.sizeHint(self, option, index) + QSize(1, 1) + + def editorEvent(self, event, model, option, index): + if index.column() != 5: + return False + + if event.type() == QEvent.MouseButtonPress: + mouse_pos = event.pos() + new_stars = int(0.7 + (mouse_pos.x() - option.rect.x()) / self.star.width()) + stars = max(0, min(new_stars, 5)) + model.setData(index, stars) + # So that the selection can change + return False + + return True + + def createEditor(self, parent, option, index): + if index.column() != 4: + return QSqlRelationalDelegate.createEditor(self, parent, option, index) + + # For editing the year, return a spinbox with a range from -1000 to 2100. + spinbox = QSpinBox(parent) + spinbox.setFrame(False) + spinbox.setMaximum(2100) + spinbox.setMinimum(-1000) + return spinbox diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/chapter2.rst b/sources/pyside6/doc/tutorials/portingguide/chapter2/chapter2.rst new file mode 100644 index 000000000..83ba3357b --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/chapter2.rst @@ -0,0 +1,93 @@ +Chapter 2: ``bookdelegate.cpp`` to ``bookdelegate.py`` +******************************************************* + +Now that your database is in place, port the C++ code for the +``BookDelegate`` class. This class offers a delegate to present +and edit the data in a ``QTableView``. It inherits +``QSqlRelationalDelegate`` interface, which offers features +specific for handling relational databases, such as a combobox +editor for foreign key fields. To begin with, create +``bookdelegate.py`` and add the following imports to it: + +.. literalinclude:: bookdelegate.py + :language: python + :linenos: + :lines: 3-10 + +After the necessary ``import`` statements, port the +constructor code for the ``BookDelegate`` class. Both +the C++ and Python versions of this code initialize a +``QSqlRelationalDelegate`` and ``QPixmap`` instance. +Here is how they look: + +C++ version +------------- + +.. literalinclude:: bookdelegate.cpp + :language: c++ + :linenos: + :lines: 17-22 + +Python version +--------------- + +.. literalinclude:: bookdelegate.py + :language: python + :linenos: + :lines: 10-17 + +.. note:: The Python version loads the ``QPixmap`` using + the absolute path of ``star.png`` in the local + filesystem. + +As the default functionality offered by the +``QSqlRelationalDelegate`` is not enough to present +the books data, you must reimplement a few functions. +For example, painting stars to represent the rating for +each book in the table. Here is how the reimplemented +code looks like: + +C++ version (bookdelegate) +-------------------------- + +.. literalinclude:: bookdelegate.cpp + :language: c++ + :linenos: + :lines: 22- + +Python version (bookdelegate) +----------------------------- + +.. literalinclude:: bookdelegate.py + :language: python + :linenos: + :lines: 18- + +Now that the delegate is in place, run the following +``main.py`` to see how the data is presented: + +.. literalinclude:: main.py + :language: python + :linenos: + :lines: 3- + +Here is how the application will look when you run it: + +.. image:: images/chapter2_books.png + :alt: Books table data + +The only difference you'll notice now in comparison to +:doc:`chapter 1 <../chapter1/chapter1>` is that the +``rating`` column looks different. + +Try improving the table even further by adding these +features: + +* Title for each column +* SQL relation for the ``author_id`` and ``genre_id`` columns +* Set a title to the window + +With these features, this is how your table will look like: + +.. image:: images/chapter2_books_with_relation.png + :alt: Books table with SQL relation diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/createdb.py b/sources/pyside6/doc/tutorials/portingguide/chapter2/createdb.py new file mode 100644 index 000000000..da7d201a8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/createdb.py @@ -0,0 +1,94 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtSql import QSqlDatabase, QSqlError, QSqlQuery +from datetime import date + + +def add_book(q, title, year, authorId, genreId, rating): + q.addBindValue(title) + q.addBindValue(year) + q.addBindValue(authorId) + q.addBindValue(genreId) + q.addBindValue(rating) + q.exec_() + + +def add_genre(q, name): + q.addBindValue(name) + q.exec_() + return q.lastInsertId() + + +def add_author(q, name, birthdate): + q.addBindValue(name) + q.addBindValue(str(birthdate)) + q.exec_() + return q.lastInsertId() + +BOOKS_SQL = """ + create table books(id integer primary key, title varchar, author integer, + genre integer, year integer, rating integer) + """ +AUTHORS_SQL = """ + create table authors(id integer primary key, name varchar, birthdate text) + """ +GENRES_SQL = """ + create table genres(id integer primary key, name varchar) + """ +INSERT_AUTHOR_SQL = """ + insert into authors(name, birthdate) values(?, ?) + """ +INSERT_GENRE_SQL = """ + insert into genres(name) values(?) + """ +INSERT_BOOK_SQL = """ + insert into books(title, year, author, genre, rating) + values(?, ?, ?, ?, ?) + """ + +def init_db(): + """ + init_db() + Initializes the database. + If tables "books" and "authors" are already in the database, do nothing. + Return value: None or raises ValueError + The error value is the QtSql error instance. + """ + def check(func, *args): + if not func(*args): + raise ValueError(func.__self__.lastError()) + db = QSqlDatabase.addDatabase("QSQLITE") + db.setDatabaseName(":memory:") + + check(db.open) + + q = QSqlQuery() + check(q.exec_, BOOKS_SQL) + check(q.exec_, AUTHORS_SQL) + check(q.exec_, GENRES_SQL) + check(q.prepare, INSERT_AUTHOR_SQL) + + asimovId = add_author(q, "Isaac Asimov", date(1920, 2, 1)) + greeneId = add_author(q, "Graham Greene", date(1904, 10, 2)) + pratchettId = add_author(q, "Terry Pratchett", date(1948, 4, 28)) + + check(q.prepare,INSERT_GENRE_SQL) + sfiction = add_genre(q, "Science Fiction") + fiction = add_genre(q, "Fiction") + fantasy = add_genre(q, "Fantasy") + + check(q.prepare,INSERT_BOOK_SQL) + add_book(q, "Foundation", 1951, asimovId, sfiction, 3) + add_book(q, "Foundation and Empire", 1952, asimovId, sfiction, 4) + add_book(q, "Second Foundation", 1953, asimovId, sfiction, 3) + add_book(q, "Foundation's Edge", 1982, asimovId, sfiction, 3) + add_book(q, "Foundation and Earth", 1986, asimovId, sfiction, 4) + add_book(q, "Prelude to Foundation", 1988, asimovId, sfiction, 3) + add_book(q, "Forward the Foundation", 1993, asimovId, sfiction, 3) + add_book(q, "The Power and the Glory", 1940, greeneId, fiction, 4) + add_book(q, "The Third Man", 1950, greeneId, fiction, 5) + add_book(q, "Our Man in Havana", 1958, greeneId, fiction, 4) + add_book(q, "Guards! Guards!", 1989, pratchettId, fantasy, 3) + add_book(q, "Night Watch", 2002, pratchettId, fantasy, 3) + add_book(q, "Going Postal", 2004, pratchettId, fantasy, 3) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/images/chapter2_books.png b/sources/pyside6/doc/tutorials/portingguide/chapter2/images/chapter2_books.png Binary files differnew file mode 100644 index 000000000..e456b7d8f --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/images/chapter2_books.png diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/images/chapter2_books_with_relation.png b/sources/pyside6/doc/tutorials/portingguide/chapter2/images/chapter2_books_with_relation.png Binary files differnew file mode 100644 index 000000000..82a5f449c --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/images/chapter2_books_with_relation.png diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/images/star.png b/sources/pyside6/doc/tutorials/portingguide/chapter2/images/star.png Binary files differnew file mode 100644 index 000000000..87f4464bd --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/images/star.png diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter2/main.py b/sources/pyside6/doc/tutorials/portingguide/chapter2/main.py new file mode 100644 index 000000000..3cc55fa46 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter2/main.py @@ -0,0 +1,26 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtCore import Qt +from PySide6.QtSql import QSqlQueryModel +from PySide6.QtWidgets import QTableView, QApplication + +import createdb +from bookdelegate import BookDelegate + +if __name__ == "__main__": + app = QApplication() + createdb.init_db() + + model = QSqlQueryModel() + model.setQuery("select title, author, genre, year, rating from books") + + table = QTableView() + table.setModel(model) + table.setItemDelegate(BookDelegate()) + table.resize(800, 600) + table.show() + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/bookdelegate-old.py b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookdelegate-old.py new file mode 100644 index 000000000..b3187e054 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookdelegate-old.py @@ -0,0 +1,101 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import copy +import os +from pathlib import Path + +from PySide6.QtSql import QSqlRelationalDelegate +from PySide6.QtWidgets import (QItemDelegate, QSpinBox, QStyledItemDelegate, + QStyle, QStyleOptionViewItem) +from PySide6.QtGui import QMouseEvent, QPixmap, QPalette, QImage +from PySide6.QtCore import QEvent, QSize, Qt, QUrl + + +class BookDelegate(QSqlRelationalDelegate): + """Books delegate to rate the books""" + + def __init__(self, star_png, parent=None): + QSqlRelationalDelegate.__init__(self, parent) + star_png = Path(__file__).parent / "images" / "star.png" + self.star = QPixmap(star_png) + + def paint(self, painter, option, index): + """ Paint the items in the table. + + If the item referred to by <index> is a StarRating, we + handle the painting ourselves. For the other items, we + let the base class handle the painting as usual. + + In a polished application, we'd use a better check than + the column number to find out if we needed to paint the + stars, but it works for the purposes of this example. + """ + if index.column() != 5: + # Since we draw the grid ourselves: + opt = copy.copy(option) + opt.rect = option.rect.adjusted(0, 0, -1, -1) + QSqlRelationalDelegate.paint(self, painter, opt, index) + else: + model = index.model() + if option.state & QStyle.State_Enabled: + if option.state & QStyle.State_Active: + color_group = QPalette.Normal + else: + color_group = QPalette.Inactive + else: + color_group = QPalette.Disabled + + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, + option.palette.color(color_group, QPalette.Highlight)) + rating = model.data(index, Qt.DisplayRole) + width = self.star.width() + height = self.star.height() + x = option.rect.x() + y = option.rect.y() + (option.rect.height() / 2) - (height / 2) + for i in range(rating): + painter.drawPixmap(x, y, self.star) + x += width + + # Since we draw the grid ourselves: + self.drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1)) + + pen = painter.pen() + painter.setPen(option.palette.color(QPalette.Mid)) + painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight()) + painter.drawLine(option.rect.topRight(), option.rect.bottomRight()) + painter.setPen(pen) + + def sizeHint(self, option, index): + """ Returns the size needed to display the item in a QSize object. """ + if index.column() == 5: + size_hint = QSize(5 * self.star.width(), self.star.height()) + QSize(1, 1) + return size_hint + # Since we draw the grid ourselves: + return QSqlRelationalDelegate.sizeHint(self, option, index) + QSize(1, 1) + + def editorEvent(self, event, model, option, index): + if index.column() != 5: + return False + + if event.type() == QEvent.MouseButtonPress: + mouse_pos = event.pos() + new_stars = int(0.7 + (mouse_pos.x() - option.rect.x()) / self.star.width()) + stars = max(0, min(new_stars, 5)) + model.setData(index, stars) + # So that the selection can change + return False + + return True + + def createEditor(self, parent, option, index): + if index.column() != 4: + return QSqlRelationalDelegate.createEditor(self, parent, option, index) + + # For editing the year, return a spinbox with a range from -1000 to 2100. + spinbox = QSpinBox(parent) + spinbox.setFrame(False) + spinbox.setMaximum(2100) + spinbox.setMinimum(-1000) + return spinbox diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/bookdelegate.py b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookdelegate.py new file mode 100644 index 000000000..145d6b73e --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookdelegate.py @@ -0,0 +1,96 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import copy, os +from PySide6.QtSql import QSqlRelationalDelegate +from PySide6.QtWidgets import (QItemDelegate, QSpinBox, QStyledItemDelegate, + QStyle, QStyleOptionViewItem) +from PySide6.QtGui import QMouseEvent, QPixmap, QPalette, QImage +from PySide6.QtCore import QEvent, QSize, Qt, QUrl + +class BookDelegate(QSqlRelationalDelegate): + """Books delegate to rate the books""" + + def __init__(self, star_png, parent=None): + QSqlRelationalDelegate.__init__(self, parent) + self.star = QPixmap(":/images/star.png") + + def paint(self, painter, option, index): + """ Paint the items in the table. + + If the item referred to by <index> is a StarRating, we + handle the painting ourselves. For the other items, we + let the base class handle the painting as usual. + + In a polished application, we'd use a better check than + the column number to find out if we needed to paint the + stars, but it works for the purposes of this example. + """ + if index.column() != 5: + # Since we draw the grid ourselves: + opt = copy.copy(option) + opt.rect = option.rect.adjusted(0, 0, -1, -1) + QSqlRelationalDelegate.paint(self, painter, opt, index) + else: + model = index.model() + if option.state & QStyle.State_Enabled: + if option.state & QStyle.State_Active: + color_group = QPalette.Normal + else: + color_group = QPalette.Inactive + else: + color_group = QPalette.Disabled + + if option.state & QStyle.State_Selected: + painter.fillRect(option.rect, + option.palette.color(color_group, QPalette.Highlight)) + rating = model.data(index, Qt.DisplayRole) + width = self.star.width() + height = self.star.height() + x = option.rect.x() + y = option.rect.y() + (option.rect.height() / 2) - (height / 2) + for i in range(rating): + painter.drawPixmap(x, y, self.star) + x += width + + # Since we draw the grid ourselves: + self.drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1)) + + pen = painter.pen() + painter.setPen(option.palette.color(QPalette.Mid)) + painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight()) + painter.drawLine(option.rect.topRight(), option.rect.bottomRight()) + painter.setPen(pen) + + def sizeHint(self, option, index): + """ Returns the size needed to display the item in a QSize object. """ + if index.column() == 5: + size_hint = QSize(5 * self.star.width(), self.star.height()) + QSize(1, 1) + return size_hint + # Since we draw the grid ourselves: + return QSqlRelationalDelegate.sizeHint(self, option, index) + QSize(1, 1) + + def editorEvent(self, event, model, option, index): + if index.column() != 5: + return False + + if event.type() == QEvent.MouseButtonPress: + mouse_pos = event.pos() + new_stars = int(0.7 + (mouse_pos.x() - option.rect.x()) / self.star.width()) + stars = max(0, min(new_stars, 5)) + model.setData(index, stars) + # So that the selection can change + return False + + return True + + def createEditor(self, parent, option, index): + if index.column() != 4: + return QSqlRelationalDelegate.createEditor(self, parent, option, index) + + # For editing the year, return a spinbox with a range from -1000 to 2100. + spinbox = QSpinBox(parent) + spinbox.setFrame(False) + spinbox.setMaximum(2100) + spinbox.setMinimum(-1000) + return spinbox diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/books.qrc b/sources/pyside6/doc/tutorials/portingguide/chapter3/books.qrc new file mode 100644 index 000000000..d6ad21337 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/books.qrc @@ -0,0 +1,5 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>images/star.png</file> +</qresource> +</RCC> diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.cpp b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.cpp new file mode 100644 index 000000000..6ec1b9e19 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.cpp @@ -0,0 +1,124 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "bookwindow.h" +#include "bookdelegate.h" +#include "initdb.h" + +#include <QtSql> + +BookWindow::BookWindow() +{ + ui.setupUi(this); + + if (!QSqlDatabase::drivers().contains("QSQLITE")) + QMessageBox::critical( + this, + "Unable to load database", + "This demo needs the SQLITE driver" + ); + + // Initialize the database: + QSqlError err = initDb(); + if (err.type() != QSqlError::NoError) { + showError(err); + return; + } + + // Create the data model: + model = new QSqlRelationalTableModel(ui.bookTable); + model->setEditStrategy(QSqlTableModel::OnManualSubmit); + model->setTable("books"); + + // Remember the indexes of the columns: + authorIdx = model->fieldIndex("author"); + genreIdx = model->fieldIndex("genre"); + + // Set the relations to the other database tables: + model->setRelation(authorIdx, QSqlRelation("authors", "id", "name")); + model->setRelation(genreIdx, QSqlRelation("genres", "id", "name")); + + // Set the localized header captions: + model->setHeaderData(authorIdx, Qt::Horizontal, tr("Author Name")); + model->setHeaderData(genreIdx, Qt::Horizontal, tr("Genre")); + model->setHeaderData(model->fieldIndex("title"), + Qt::Horizontal, tr("Title")); + model->setHeaderData(model->fieldIndex("year"), Qt::Horizontal, tr("Year")); + model->setHeaderData(model->fieldIndex("rating"), + Qt::Horizontal, tr("Rating")); + + // Populate the model: + if (!model->select()) { + showError(model->lastError()); + return; + } + + // Set the model and hide the ID column: + ui.bookTable->setModel(model); + ui.bookTable->setItemDelegate(new BookDelegate(ui.bookTable)); + ui.bookTable->setColumnHidden(model->fieldIndex("id"), true); + ui.bookTable->setSelectionMode(QAbstractItemView::SingleSelection); + + // Initialize the Author combo box: + ui.authorEdit->setModel(model->relationModel(authorIdx)); + ui.authorEdit->setModelColumn( + model->relationModel(authorIdx)->fieldIndex("name")); + + ui.genreEdit->setModel(model->relationModel(genreIdx)); + ui.genreEdit->setModelColumn( + model->relationModel(genreIdx)->fieldIndex("name")); + + // Lock and prohibit resizing of the width of the rating column: + ui.bookTable->horizontalHeader()->setSectionResizeMode( + model->fieldIndex("rating"), + QHeaderView::ResizeToContents); + + QDataWidgetMapper *mapper = new QDataWidgetMapper(this); + mapper->setModel(model); + mapper->setItemDelegate(new BookDelegate(this)); + mapper->addMapping(ui.titleEdit, model->fieldIndex("title")); + mapper->addMapping(ui.yearEdit, model->fieldIndex("year")); + mapper->addMapping(ui.authorEdit, authorIdx); + mapper->addMapping(ui.genreEdit, genreIdx); + mapper->addMapping(ui.ratingEdit, model->fieldIndex("rating")); + + connect(ui.bookTable->selectionModel(), + &QItemSelectionModel::currentRowChanged, + mapper, + &QDataWidgetMapper::setCurrentModelIndex + ); + + ui.bookTable->setCurrentIndex(model->index(0, 0)); + createMenuBar(); +} + +void BookWindow::showError(const QSqlError &err) +{ + QMessageBox::critical(this, "Unable to initialize Database", + "Error initializing database: " + err.text()); +} + +void BookWindow::createMenuBar() +{ + QAction *quitAction = new QAction(tr("&Quit"), this); + QAction *aboutAction = new QAction(tr("&About"), this); + QAction *aboutQtAction = new QAction(tr("&About Qt"), this); + + QMenu *fileMenu = menuBar()->addMenu(tr("&File")); + fileMenu->addAction(quitAction); + + QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); + helpMenu->addAction(aboutAction); + helpMenu->addAction(aboutQtAction); + + connect(quitAction, &QAction::triggered, this, &BookWindow::close); + connect(aboutAction, &QAction::triggered, this, &BookWindow::about); + connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt); +} + +void BookWindow::about() +{ + QMessageBox::about(this, tr("About Books"), + tr("<p>The <b>Books</b> example shows how to use Qt SQL classes " + "with a model/view framework.")); +} diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.py b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.py new file mode 100644 index 000000000..bb033c6d2 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.py @@ -0,0 +1,99 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtGui import QAction +from PySide6.QtWidgets import (QAbstractItemView, QDataWidgetMapper, + QHeaderView, QMainWindow, QMessageBox) +from PySide6.QtGui import QKeySequence +from PySide6.QtSql import (QSqlRelation, QSqlRelationalTableModel, QSqlTableModel, + QSqlError) +from PySide6.QtCore import QAbstractItemModel, QObject, QSize, Qt, Slot +import createdb +from ui_bookwindow import Ui_BookWindow +from bookdelegate import BookDelegate + + +class BookWindow(QMainWindow, Ui_BookWindow): + # """A window to show the books available""" + + def __init__(self): + QMainWindow.__init__(self) + self.setupUi(self) + + #Initialize db + createdb.init_db() + + model = QSqlRelationalTableModel(self.bookTable) + model.setEditStrategy(QSqlTableModel.OnManualSubmit) + model.setTable("books") + + # Remember the indexes of the columns: + author_idx = model.fieldIndex("author") + genre_idx = model.fieldIndex("genre") + + # Set the relations to the other database tables: + model.setRelation(author_idx, QSqlRelation("authors", "id", "name")) + model.setRelation(genre_idx, QSqlRelation("genres", "id", "name")) + + # Set the localized header captions: + model.setHeaderData(author_idx, Qt.Horizontal, self.tr("Author Name")) + model.setHeaderData(genre_idx, Qt.Horizontal, self.tr("Genre")) + model.setHeaderData(model.fieldIndex("title"), Qt.Horizontal, self.tr("Title")) + model.setHeaderData(model.fieldIndex("year"), Qt.Horizontal, self.tr("Year")) + model.setHeaderData(model.fieldIndex("rating"), Qt.Horizontal, self.tr("Rating")) + + if not model.select(): + print(model.lastError()) + + # Set the model and hide the ID column: + self.bookTable.setModel(model) + self.bookTable.setItemDelegate(BookDelegate(self.bookTable)) + self.bookTable.setColumnHidden(model.fieldIndex("id"), True) + self.bookTable.setSelectionMode(QAbstractItemView.SingleSelection) + + # Initialize the Author combo box: + self.authorEdit.setModel(model.relationModel(author_idx)) + self.authorEdit.setModelColumn(model.relationModel(author_idx).fieldIndex("name")) + + self.genreEdit.setModel(model.relationModel(genre_idx)) + self.genreEdit.setModelColumn(model.relationModel(genre_idx).fieldIndex("name")) + + # Lock and prohibit resizing of the width of the rating column: + self.bookTable.horizontalHeader().setSectionResizeMode(model.fieldIndex("rating"), + QHeaderView.ResizeToContents) + + mapper = QDataWidgetMapper(self) + mapper.setModel(model) + mapper.setItemDelegate(BookDelegate(self)) + mapper.addMapping(self.titleEdit, model.fieldIndex("title")) + mapper.addMapping(self.yearEdit, model.fieldIndex("year")) + mapper.addMapping(self.authorEdit, author_idx) + mapper.addMapping(self.genreEdit, genre_idx) + mapper.addMapping(self.ratingEdit, model.fieldIndex("rating")) + + selection_model = self.bookTable.selectionModel() + selection_model.currentRowChanged.connect(mapper.setCurrentModelIndex) + + self.bookTable.setCurrentIndex(model.index(0, 0)) + self.create_menubar() + + def showError(err): + QMessageBox.critical(self, "Unable to initialize Database", + "Error initializing database: " + err.text()) + + def create_menubar(self): + file_menu = self.menuBar().addMenu(self.tr("&File")) + quit_action = file_menu.addAction(self.tr("&Quit")) + quit_action.triggered.connect(qApp.quit) + + help_menu = self.menuBar().addMenu(self.tr("&Help")) + about_action = help_menu.addAction(self.tr("&About")) + about_action.setShortcut(QKeySequence.HelpContents) + about_action.triggered.connect(self.about) + aboutQt_action = help_menu.addAction("&About Qt") + aboutQt_action.triggered.connect(qApp.aboutQt) + + def about(self): + QMessageBox.about(self, self.tr("About Books"), + self.tr("<p>The <b>Books</b> example shows how to use Qt SQL classes " + "with a model/view framework.")) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.ui b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.ui new file mode 100644 index 000000000..e1668288f --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/bookwindow.ui @@ -0,0 +1,149 @@ +<ui version="4.0" > + <author></author> + <comment></comment> + <exportmacro></exportmacro> + <class>BookWindow</class> + <widget class="QMainWindow" name="BookWindow" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>601</width> + <height>420</height> + </rect> + </property> + <property name="windowTitle" > + <string>Books</string> + </property> + <widget class="QWidget" name="centralWidget" > + <layout class="QVBoxLayout" > + <property name="margin" > + <number>9</number> + </property> + <property name="spacing" > + <number>6</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox" > + <property name="title" > + <string>Books</string> + </property> + <layout class="QVBoxLayout" > + <property name="margin" > + <number>9</number> + </property> + <property name="spacing" > + <number>6</number> + </property> + <item> + <widget class="QTableView" name="bookTable" > + <property name="selectionBehavior" > + <enum>QAbstractItemView::SelectRows</enum> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2" > + <property name="title" > + <string>Details</string> + </property> + <layout class="QFormLayout" > + <item row="0" column="0" > + <widget class="QLabel" name="label_5" > + <property name="text" > + <string><b>Title:</b></string> + </property> + </widget> + </item> + <item row="0" column="1" > + <widget class="QLineEdit" name="titleEdit" > + <property name="enabled" > + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" > + <widget class="QLabel" name="label_2_2_2_2" > + <property name="text" > + <string><b>Author: </b></string> + </property> + </widget> + </item> + <item row="1" column="1" > + <widget class="QComboBox" name="authorEdit" > + <property name="enabled" > + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0" > + <widget class="QLabel" name="label_3" > + <property name="text" > + <string><b>Genre:</b></string> + </property> + </widget> + </item> + <item row="2" column="1" > + <widget class="QComboBox" name="genreEdit" > + <property name="enabled" > + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0" > + <widget class="QLabel" name="label_4" > + <property name="text" > + <string><b>Year:</b></string> + </property> + </widget> + </item> + <item row="3" column="1" > + <widget class="QSpinBox" name="yearEdit" > + <property name="enabled" > + <bool>true</bool> + </property> + <property name="prefix" > + <string/> + </property> + <property name="maximum" > + <number>2100</number> + </property> + <property name="minimum" > + <number>-1000</number> + </property> + </widget> + </item> + <item row="4" column="0" > + <widget class="QLabel" name="label" > + <property name="text" > + <string><b>Rating:</b></string> + </property> + </widget> + </item> + <item row="4" column="1" > + <widget class="QSpinBox" name="ratingEdit" > + <property name="maximum" > + <number>5</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + <pixmapfunction></pixmapfunction> + <tabstops> + <tabstop>bookTable</tabstop> + <tabstop>titleEdit</tabstop> + <tabstop>authorEdit</tabstop> + <tabstop>genreEdit</tabstop> + <tabstop>yearEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/chapter3.rst b/sources/pyside6/doc/tutorials/portingguide/chapter3/chapter3.rst new file mode 100644 index 000000000..98d4d3982 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/chapter3.rst @@ -0,0 +1,121 @@ +Chapter 3: Port ``bookdwindow.cpp`` to ``bookwindow.py`` +********************************************************* + +After the bookdelegate, port the C++ code for the +``BookWindow`` class. It offers a QMainWindow, containing a +``QTableView`` to present the books data, and a **Details** +section with a set of input fields to edit the selected row +in the table. To begin with, create the ``bookwindow.py`` +and add the following imports to it: + +.. literalinclude:: bookwindow.py + :language: python + :linenos: + :lines: 3-16 + +.. note:: The imports include the ``BookDelegate`` you + ported earlier and the ``Ui_BookWindow``. The pyside-uic + tool generates the ``ui_bookwindow`` Python code based + on the ``bookwindow.ui`` XML file. + +To generate this Python code, run the following command on the +prompt: + +.. code-block:: bash + + pyside6-uic bookwindow.ui -o ui_bookwindow.py + +Try porting the remaining code now. To begin with, here is +how both the versions of the constructor code looks: + +C++ version +------------ + +.. literalinclude:: bookwindow.cpp + :language: c++ + :linenos: + :lines: 20-103 + +Python version +--------------- + +.. literalinclude:: bookwindow.py + :language: python + :linenos: + :lines: 16-79 + +.. note:: The Python version of the ``BookWindow`` class + definition inherits from both ``QMainWindow`` and + ``Ui_BookWindow``, which is defined in the + ``ui_bookwindow.py`` file that you generated earlier. + +Here is how the rest of the code looks like: + +C++ version +------------ + +.. literalinclude:: bookwindow.cpp + :language: c++ + :linenos: + :lines: 78- + +Python version +--------------- + +.. literalinclude:: bookwindow.py + :language: python + :linenos: + :lines: 80- + +Now that all the necessary pieces are in place, try to put +them together in ``main.py``. + +.. literalinclude:: main.py + :language: python + :linenos: + :lines: 3- + +Try running this to see if you get the following output: + +.. image:: images/chapter3-books.png + :alt: BookWindow with a QTableView and a few input fields + +Now, if you look back at :doc:`chapter2 <../chapter2/chapter2>`, +you'll notice that the ``bookdelegate.py`` loads the +``star.png`` from the filesytem. Instead, you could add it +to a ``qrc`` file, and load from it. The later approach is +rececommended if your application is targeted for +different platforms, as most of the popular platforms +employ stricter file access policy these days. + +To add the ``star.png`` to a ``.qrc``, create a file called +``books.qrc`` and the following XML content to it: + +.. literalinclude:: books.qrc + :linenos: + +This is a simple XML file defining a list all resources that +your application needs. In this case, it is the ``star.png`` +image only. + +Now, run the ``pyside6-rcc`` tool on the ``books.qrc`` file +to generate ``rc_books.py``. + +.. code-block:: bash + + pyside6-rcc books.qrc -o rc_books.py + +Once you have the Python script generated, make the +following changes to ``bookdelegate.py`` and ``main.py``: + +.. literalinclude:: bookdelegate.py + :diff: ../chapter2/bookdelegate.py + +.. literalinclude:: main.py + :diff: main-old.py + +Although there will be no noticeable difference in the UI +after these changes, using a ``.qrc`` is a better approach. + +Now that you have successfully ported the SQL Books example, +you know how easy it is. Try porting another C++ application. diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/createdb.py b/sources/pyside6/doc/tutorials/portingguide/chapter3/createdb.py new file mode 100644 index 000000000..da7d201a8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/createdb.py @@ -0,0 +1,94 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtSql import QSqlDatabase, QSqlError, QSqlQuery +from datetime import date + + +def add_book(q, title, year, authorId, genreId, rating): + q.addBindValue(title) + q.addBindValue(year) + q.addBindValue(authorId) + q.addBindValue(genreId) + q.addBindValue(rating) + q.exec_() + + +def add_genre(q, name): + q.addBindValue(name) + q.exec_() + return q.lastInsertId() + + +def add_author(q, name, birthdate): + q.addBindValue(name) + q.addBindValue(str(birthdate)) + q.exec_() + return q.lastInsertId() + +BOOKS_SQL = """ + create table books(id integer primary key, title varchar, author integer, + genre integer, year integer, rating integer) + """ +AUTHORS_SQL = """ + create table authors(id integer primary key, name varchar, birthdate text) + """ +GENRES_SQL = """ + create table genres(id integer primary key, name varchar) + """ +INSERT_AUTHOR_SQL = """ + insert into authors(name, birthdate) values(?, ?) + """ +INSERT_GENRE_SQL = """ + insert into genres(name) values(?) + """ +INSERT_BOOK_SQL = """ + insert into books(title, year, author, genre, rating) + values(?, ?, ?, ?, ?) + """ + +def init_db(): + """ + init_db() + Initializes the database. + If tables "books" and "authors" are already in the database, do nothing. + Return value: None or raises ValueError + The error value is the QtSql error instance. + """ + def check(func, *args): + if not func(*args): + raise ValueError(func.__self__.lastError()) + db = QSqlDatabase.addDatabase("QSQLITE") + db.setDatabaseName(":memory:") + + check(db.open) + + q = QSqlQuery() + check(q.exec_, BOOKS_SQL) + check(q.exec_, AUTHORS_SQL) + check(q.exec_, GENRES_SQL) + check(q.prepare, INSERT_AUTHOR_SQL) + + asimovId = add_author(q, "Isaac Asimov", date(1920, 2, 1)) + greeneId = add_author(q, "Graham Greene", date(1904, 10, 2)) + pratchettId = add_author(q, "Terry Pratchett", date(1948, 4, 28)) + + check(q.prepare,INSERT_GENRE_SQL) + sfiction = add_genre(q, "Science Fiction") + fiction = add_genre(q, "Fiction") + fantasy = add_genre(q, "Fantasy") + + check(q.prepare,INSERT_BOOK_SQL) + add_book(q, "Foundation", 1951, asimovId, sfiction, 3) + add_book(q, "Foundation and Empire", 1952, asimovId, sfiction, 4) + add_book(q, "Second Foundation", 1953, asimovId, sfiction, 3) + add_book(q, "Foundation's Edge", 1982, asimovId, sfiction, 3) + add_book(q, "Foundation and Earth", 1986, asimovId, sfiction, 4) + add_book(q, "Prelude to Foundation", 1988, asimovId, sfiction, 3) + add_book(q, "Forward the Foundation", 1993, asimovId, sfiction, 3) + add_book(q, "The Power and the Glory", 1940, greeneId, fiction, 4) + add_book(q, "The Third Man", 1950, greeneId, fiction, 5) + add_book(q, "Our Man in Havana", 1958, greeneId, fiction, 4) + add_book(q, "Guards! Guards!", 1989, pratchettId, fantasy, 3) + add_book(q, "Night Watch", 2002, pratchettId, fantasy, 3) + add_book(q, "Going Postal", 2004, pratchettId, fantasy, 3) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/images/chapter3-books.png b/sources/pyside6/doc/tutorials/portingguide/chapter3/images/chapter3-books.png Binary files differnew file mode 100644 index 000000000..952cb14e8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/images/chapter3-books.png diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/images/star.png b/sources/pyside6/doc/tutorials/portingguide/chapter3/images/star.png Binary files differnew file mode 100644 index 000000000..87f4464bd --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/images/star.png diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/main-old.py b/sources/pyside6/doc/tutorials/portingguide/chapter3/main-old.py new file mode 100644 index 000000000..164fc589a --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/main-old.py @@ -0,0 +1,15 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import QApplication +from bookwindow import BookWindow + +if __name__ == "__main__": + app = QApplication([]) + + window = BookWindow() + window.resize(800, 600) + window.show() + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/portingguide/chapter3/main.py b/sources/pyside6/doc/tutorials/portingguide/chapter3/main.py new file mode 100644 index 000000000..9a6575dc2 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/chapter3/main.py @@ -0,0 +1,16 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import QApplication +from bookwindow import BookWindow +import rc_books + +if __name__ == "__main__": + app = QApplication([]) + + window = BookWindow() + window.resize(800, 600) + window.show() + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/portingguide/hello_world_ex.py b/sources/pyside6/doc/tutorials/portingguide/hello_world_ex.py new file mode 100644 index 000000000..d67906fa7 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/hello_world_ex.py @@ -0,0 +1,39 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import random + +from PySide6.QtWidgets import (QApplication, QLabel, + QPushButton, QVBoxLayout, QWidget) +from PySide6.QtCore import Qt, Slot + +class MyWidget(QWidget): + def __init__(self): + super().__init__() + + self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир"] + + self.button = QPushButton("Click me!") + self.text = QLabel("Hello World") + self.text.setAlignment(Qt.AlignCenter) + + self.layout = QVBoxLayout() + self.layout.addWidget(self.text) + self.layout.addWidget(self.button) + self.setLayout(self.layout) + + self.button.clicked.connect(self.magic) + + @Slot() + def magic(self): + self.text.setText(random.choice(self.hello)) + +if __name__ == "__main__": + app = QApplication(sys.argv) + + widget = MyWidget() + widget.resize(800, 600) + widget.show() + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/portingguide/index.rst b/sources/pyside6/doc/tutorials/portingguide/index.rst new file mode 100644 index 000000000..0b89c2ff8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/portingguide/index.rst @@ -0,0 +1,194 @@ +Porting a C++ Application to Python +************************************* + +Qt for Python lets you use Qt APIs in a Python application. +So the next question is: What does it take to port an +existing C++ application? Try porting a Qt C++ application +to Python to understand this. + +Before you start, ensure that all the prerequisites for +Qt for Python are met. See +:doc:`Getting Started <../../gettingstarted/index>` for more +information. In addition, familiarize yourself with the +basic differences between Qt in C++ and in Python. + +Basic differences +================== + +This section highlights some of the basic differences +between C++ and Python, and how Qt differs between these +two contexts. + +C++ vs Python +-------------- + +* In the interest of code reuse, both C++ and Python + provide ways for one file of code to use facilities + provided by another. In C++, this is done using the + ``#include`` directive to access the API definition of + the reused code. The Python equivalent is an ``import`` + statement. +* The constructor of a C++ class shares the name of its + class and automatically calls the constructor of any + base-classes (in a predefined order) before it runs. + In Python, the ``__init__()`` method is the constructor + of the class, and it can explicitly call base-class + constructors in any order. +* C++ uses the keyword, ``this``, to implicitly refer to + the current object. In python, you need to explicitly + mention the current object as the first parameter + to each instance method of the class; it is conventionally + named ``self``. +* And more importantly, forget about curly braces, {}, and + semi-colon, ;. +* Precede variable definitions with the ``global`` keyword, + only if they need global scope. + +.. code:: python + + var = None + def func(key, value = None): + """Does stuff with a key and an optional value. + + If value is omitted or None, the value from func()'s + last call is reused. + """ + global var + if value is None: + if var is None: + raise ValueError("Must pass a value on first call", key, value) + value = var + else: + var = value + doStuff(key, value) + +In this example, ``func()`` would treat ``var`` as a local +name without the ``global`` statement. This would lead to +a ``NameError`` in the ``value is None`` handling, on +accessing ``var``. For more information about this, see +`Python refernce documentation <python refdoc>`_. + +.. _python refdoc: https://docs.python.org/3/reference/simple_stmts.html#the-global-statement + +.. tip:: Python being an interpreted language, most often + the easiest way is to try your idea in the interperter. + You could call the ``help()`` function in the + interpreter on any built-in function or keyword in + Python. For example, a call to ``help(import)`` should + provide documentation about the ``import`` statment + +Last but not the least, try out a few examples to +familiarize yourself with the Python coding style and +follow the guidelines outlined in the +`PEP8 - Style Guide <pep8>`_. + +.. _pep8: <https://www.python.org/dev/peps/pep-0008/#naming-conventions> + +.. code-block:: python + + import sys + + from PySide6.QtWidgets import QApplication, QLabel + + app = QApplication(sys.argv) + label = QLabel("Hello World") + label.show() + sys.exit(app.exec()) + +.. note:: Qt provides classes that are meant to manage + the application-specific requirements depending on + whether the application is console-only + (QCoreApplication), GUI with QtWidgets (QApplication), + or GUI without QtWidgets (QGuiApplication). These + classes load necessary plugins, such as the GUI + libraries required by an application. In this case, it is + QApplication that is initialized first as the application + has a GUI with QtWidgets. + +Qt in the C++ and Python context +--------------------------------- + +Qt behaves the same irrespective of whether it is used +in a C++ or a Python application. Considering that C++ +and Python use different language semantics, some +differences between the two variants of Qt are inevitable. +Here are a few important ones that you must be aware of: + +* **Qt Properties**: ``Q_PROPERTY`` macros are used in C++ to add a + public member variable with getter and setter functions. Python's + alternative for this is the ``@property`` decorator before the + getter and setter function definitions. +* **Qt Signals and Slots**: Qt offers a unique callback mechanism, + where a signal is emitted to notify the occurrence of an event, so + that slots connected to this signal can react to it. In C++, + the class definition must define the slots under the + ``public Q_SLOTS:`` and signals under ``Q_SIGNALS:`` + access specifier. You connect these two using one of the + several variants of the QObject::connect() function. Python's + equivalent for this is the `@Slot`` decorator just before the + function definition. This is necessary to register the slots + with the QtMetaObject. +* **QString, QVariant, and other types** + + - Qt for Python does not provide access to QString and + QVariant. You must use Python's native types instead. + - QChar and QStringRef are represented as Python strings, + and QStringList is converted to a list of strings. + - QDate, QDateTime, QTime, and QUrl's __hash__() methods + return a string representation so that identical dates + (and identical date/times or times or URLs) have + identical hash values. + - QTextStream's bin(), hex(), and oct() functions are + renamed to bin_(), hex_(), and oct_() respectively. This + should avoid name conflicts with Python's built-in + functions. + +* **QByteArray**: A QByteArray is treated as a list of + bytes without encoding. Python 3 uses + "bytes". QString is represented as an encoded human readable string, + which means it is a "str". + +Here is the improved version of the Hello World example, +demonstrating some of these differences: + +.. literalinclude:: hello_world_ex.py + :linenos: + :lines: 3- + +.. note:: The ``if`` block is just a good practice when + developing a Python application. It lets the Python file + behave differently depending on whether it is imported + as a module in another file or run directly. The + ``__name__`` variable will have different values in + these two scenarios. It is ``__main__`` when the file is + run directly, and the module's file name + (``hello_world_ex`` in this case) when imported as a + module. In the later case, everything defined in the + module except the ``if`` block is available to the + importing file. + +Notice that the QPushButton's ``clicked`` signal is +connected to the ``magic`` function to randomly change the +QLabel's ``text`` property. The `@Slot`` decorator marks +the methods that are slots and informs the QtMetaObject about +them. + +Porting a Qt C++ example +========================= + +Qt offers several C++ examples to showcase its features and help +beginners learn. You can try porting one of these C++ examples to +Python. The +`books SQL example <https://code.qt.io/cgit/qt/qtbase.git/tree/examples/sql/books>`_ +is a good starting point as it does not require you to write UI-specific code in +Python, but can use its ``.ui`` file instead. + +The following chapters guides you through the porting process: + +.. toctree:: + :glob: + :titlesonly: + + chapter1/chapter1 + chapter2/chapter2 + chapter3/chapter3 |