diff options
Diffstat (limited to 'sources/pyside6/doc/tutorials')
175 files changed, 10118 insertions, 0 deletions
diff --git a/sources/pyside6/doc/tutorials/basictutorial/clickablebutton.png b/sources/pyside6/doc/tutorials/basictutorial/clickablebutton.png Binary files differnew file mode 100644 index 000000000..bfdc23fe0 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/clickablebutton.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/clickablebutton.rst b/sources/pyside6/doc/tutorials/basictutorial/clickablebutton.rst new file mode 100644 index 000000000..c5464640b --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/clickablebutton.rst @@ -0,0 +1,95 @@ +Using a Simple Button +===================== + +In this tutorial, we'll show you how to handle **signals and slots** +using Qt for Python. **Signals and slots** is a Qt feature that lets +your graphical widgets communicate with other graphical widgets or +your python code. Our application creates a button that logs the +`Button clicked, Hello!` message to the python console each time you +click it. + +Let's start by importing the necessary PySide6 classes and python +`sys` module: +:: + + import sys + from PySide6.QtWidgets import QApplication, QPushButton + from PySide6.QtCore import Slot + +Let's also create a python function that logs the message to the +console: +:: + + # Greetings + @Slot() + def say_hello(): + print("Button clicked, Hello!") + +.. note:: The `@Slot()` is a decorator that identifies a function as + a slot. It is not important to understand why for now, + but use it always to avoid unexpected behavior. + +Now, as mentioned in previous examples you must create the +`QApplication` to run your PySide6 code: +:: + + # Create the Qt Application + app = QApplication(sys.argv) + +Let's create the clickable button, which is a `QPushButton` instance. +To label the button, we pass a python string to the constructor: +:: + + # Create a button + button = QPushButton("Click me") + +Before we show the button, we must connect it to the `say_hello()` +function that we defined earlier. There are two ways of doing this; +using the old style or the new style, which is more pythonic. Let's +use the new style in this case. You can find more information about +both these styles in the +`Signals and Slots in PySide6 <https://wiki.qt.io/Qt_for_Python_Signals_and_Slots>`_ +wiki page. + +The `QPushButton` has a predefined signal called **clicked**, which +is triggered every time the button is clicked. We'll connect this +signal to the `say_hello()` function: +:: + + # Connect the button to the function + button.clicked.connect(say_hello) + +Finally, we show the button and start the Qt main loop: + +.. code-block:: python + + # Show the button + button.show() + # Run the main Qt loop + app.exec() + +Here is the complete code for this example: + +.. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication, QPushButton + from PySide6.QtCore import Slot + + @Slot() + def say_hello(): + print("Button clicked, Hello!") + + # Create the Qt Application + app = QApplication(sys.argv) + # Create a button, connect it and show it + button = QPushButton("Click me") + button.clicked.connect(say_hello) + button.show() + # Run the main Qt loop + app.exec() + +After a few clicks, you will get something like this on your terminal: + +.. image:: clickablebutton.png + :alt: Clickable Button Example diff --git a/sources/pyside6/doc/tutorials/basictutorial/dialog.png b/sources/pyside6/doc/tutorials/basictutorial/dialog.png Binary files differnew file mode 100644 index 000000000..ad5690927 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/dialog.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/dialog.rst b/sources/pyside6/doc/tutorials/basictutorial/dialog.rst new file mode 100644 index 000000000..b7712672b --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/dialog.rst @@ -0,0 +1,149 @@ +Creating a Dialog Application +============================= + +This tutorial shows how to build a simple dialog with some +basic widgets. The idea is to let users provide their name +in a ``QLineEdit``, and the dialog greets them on click of a +``QPushButton``. + +Let us just start with a simple stub that creates and shows +a dialog. This stub is updated during the course of this +tutorial, but you can use this stub as is if you need to: +:: + + import sys + from PySide6.QtWidgets import QApplication, QDialog, QLineEdit, QPushButton + + class Form(QDialog): + + def __init__(self, parent=None): + super(Form, self).__init__(parent) + self.setWindowTitle("My Form") + + + if __name__ == '__main__': + # Create the Qt Application + app = QApplication(sys.argv) + # Create and show the form + form = Form() + form.show() + # Run the main Qt loop + sys.exit(app.exec()) + +The imports aren't new to you, the same for the creation of the +``QApplication`` and the execution of the Qt main loop. +The only novelty here is the **class definition**. + +You can create any class that subclasses PySide6 widgets. +In this case, we are subclassing ``QDialog`` to define a custom +dialog, which we name as **Form**. We have also implemented the +``init()`` method that calls the ``QDialog``'s init method with the +parent widget, if any. Also, the new ``setWindowTitle()`` method +just sets the title of the dialog window. In ``main()``, you can see +that we are creating a *Form object* and showing it to the world. + +Create the Widgets +------------------ + +We are going to create two widgets: a ``QLineEdit`` where users can +enter their name, and a ``QPushButton`` that prints the contents of +the ``QLineEdit``. +So, let's add the following code to the ``init()`` method of our Form: +:: + + # Create widgets + self.edit = QLineEdit("Write my name here..") + self.button = QPushButton("Show Greetings") + +It's obvious from the code that both widgets will show the corresponding +texts. + +Create a layout to organize the Widgets +--------------------------------------- + +Qt comes with layout-support that helps you organize the widgets +in your application. In this case, let's use ``QVBoxLayout`` to lay out +the widgets vertically. Add the following code to the ``init()`` method, +after creating the widgets: +:: + + # Create layout and add widgets + layout = QVBoxLayout(self) + layout.addWidget(self.edit) + layout.addWidget(self.button) + +So, we create the layout, add the widgets with ``addWidget()``. + +Create the function to greet and connect the Button +--------------------------------------------------- + +Finally, we just have to add a function to our custom **Form** +and *connect* our button to it. Our function will be a part of +the Form, so you have to add it after the ``init()`` function: +:: + + # Greets the user + def greetings(self): + print(f"Hello {self.edit.text()}") + +Our function just prints the contents of the ``QLineEdit`` to the +python console. We have access to the text by means of the +``QLineEdit.text()`` method. + +Now that we have everything, we just need to *connect* the +``QPushButton`` to the ``Form.greetings()`` method. To do so, add the +following line to the ``init()`` method: +:: + + # Add button signal to greetings slot + self.button.clicked.connect(self.greetings) + +Once executed, you can enter your name in the ``QLineEdit`` and watch +the console for greetings. + +Complete code +------------- + +Here is the complete code for this tutorial: + +.. code-block:: python + + import sys + from PySide6.QtWidgets import (QLineEdit, QPushButton, QApplication, + QVBoxLayout, QDialog) + + class Form(QDialog): + + def __init__(self, parent=None): + super(Form, self).__init__(parent) + # Create widgets + self.edit = QLineEdit("Write my name here") + self.button = QPushButton("Show Greetings") + # Create layout and add widgets + layout = QVBoxLayout() + layout.addWidget(self.edit) + layout.addWidget(self.button) + # Set dialog layout + self.setLayout(layout) + # Add button signal to greetings slot + self.button.clicked.connect(self.greetings) + + # Greets the user + def greetings(self): + print(f"Hello {self.edit.text()}") + + if __name__ == '__main__': + # Create the Qt Application + app = QApplication(sys.argv) + # Create and show the form + form = Form() + form.show() + # Run the main Qt loop + sys.exit(app.exec()) + + +When you execute the code, and write down your name, +the button will display messages on the terminal: + +.. image:: dialog.png + :alt: Simple Dialog Example diff --git a/sources/pyside6/doc/tutorials/basictutorial/greenapplication.png b/sources/pyside6/doc/tutorials/basictutorial/greenapplication.png Binary files differnew file mode 100644 index 000000000..29ea0a701 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/greenapplication.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons.png b/sources/pyside6/doc/tutorials/basictutorial/icons.png Binary files differnew file mode 100644 index 000000000..a5a554eba --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons.zip b/sources/pyside6/doc/tutorials/basictutorial/icons.zip Binary files differnew file mode 100644 index 000000000..e279e37b8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons.zip diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons/forward.png b/sources/pyside6/doc/tutorials/basictutorial/icons/forward.png Binary files differnew file mode 100644 index 000000000..c7a532dfe --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons/forward.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons/pause.png b/sources/pyside6/doc/tutorials/basictutorial/icons/pause.png Binary files differnew file mode 100644 index 000000000..d0beadb43 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons/pause.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons/play.png b/sources/pyside6/doc/tutorials/basictutorial/icons/play.png Binary files differnew file mode 100644 index 000000000..345685337 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons/play.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons/previous.png b/sources/pyside6/doc/tutorials/basictutorial/icons/previous.png Binary files differnew file mode 100644 index 000000000..979f18565 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons/previous.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/icons/stop.png b/sources/pyside6/doc/tutorials/basictutorial/icons/stop.png Binary files differnew file mode 100644 index 000000000..1e88ded3a --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/icons/stop.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/player-new.png b/sources/pyside6/doc/tutorials/basictutorial/player-new.png Binary files differnew file mode 100644 index 000000000..8e45c757d --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/player-new.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/player.png b/sources/pyside6/doc/tutorials/basictutorial/player.png Binary files differnew file mode 100644 index 000000000..0563d3223 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/player.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/qml.rst b/sources/pyside6/doc/tutorials/basictutorial/qml.rst new file mode 100644 index 000000000..49cd3d94a --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/qml.rst @@ -0,0 +1,79 @@ +Your First QtQuick/QML Application +********************************** + +QML_ is a declarative language that lets you develop applications +faster than with traditional languages. It is ideal for designing the +UI of your application because of its declarative nature. In QML, a +user interface is specified as a tree of objects with properties. In +this tutorial, we will show how to make a simple "Hello World" +application with PySide6 and QML. + +A PySide6/QML application consists, at least, of two different files - +a file with the QML description of the user interface, and a python file +that loads the QML file. To make things easier, let's save both files in +the same directory. + +Here is a simple QML file called :code:`view.qml`: + +.. code-block:: javascript + + import QtQuick + + Rectangle { + id: main + width: 200 + height: 200 + color: "green" + + Text { + text: "Hello World" + anchors.centerIn: main + } + } + +We start by importing :code:`QtQuick`, which is a QML module. + +The rest of the QML code is pretty straightforward for those who +have previously used HTML or XML files. Basically, we are creating +a green rectangle with the size `200*200`, and adding a Text element +that reads "Hello World". The code :code:`anchors.centerIn: main` makes +the text appear centered within the object with :code:`id: main`, +which is the Rectangle in this case. + +Now, let's see how the code looks on the PySide6. +Let's call it :code:`main.py`: + +.. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication + from PySide6.QtQuick import QQuickView + + if __name__ == "__main__": + app = QApplication() + view = QQuickView() + + view.setSource("view.qml") + view.show() + sys.exit(app.exec()) + +If you are already familiar with PySide6 and have followed our +tutorials, you have already seen much of this code. +The only novelties are that you must :code:`import QtQuick` and set the +source of the :code:`QQuickView` object to the URL of your QML file. +Then, similar to what you do with any Qt widget, you call +:code:`QQuickView.show()`. + +.. note:: If you are programming for desktop, you should consider + adding `view.setResizeMode(QQuickView.SizeRootObjectToView)` + before showing the view. + +When you execute the :code:`main.py` script, you will see the following +application: + + +.. image:: greenapplication.png + :alt: Simple QML and Python example + :align: center + +.. _QML: https://doc.qt.io/qt-6/qmlapplications.html diff --git a/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst b/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst new file mode 100644 index 000000000..858293beb --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/qrcfiles.rst @@ -0,0 +1,173 @@ +.. _using_qrc_files: + +Using ``.qrc`` Files (``pyside6-rcc``) +************************************** + +The `Qt Resource System`_ is a mechanism for storing binary files +in an application. + +The files will be embedded into the application and be acessible for the +``QFile`` class and the constructors of the ``QIcon`` and ``QPixmap`` +classes taking a file name by using a special file name starting with ``:/``. + +The most common uses are for custom images, icons, fonts, among others. + +In this tutorial you will learn how to load custom images as button icons. + +For inspiration, we will try to adapt the multimedia player example +from Qt. + +As you can see on the following image, the ``QPushButton`` that are used +for the media actions (play, pause, stop, and so on) are using the +default icons meant for such actions. + +.. image:: player.png + :alt: Multimedia Player Qt Example + +You could make the application more attractive by designing the icons, +but in case you don't want to design them, you can download and use them. + +:download:`Download icons <icons.zip>` + +.. image:: icons.png + :alt: New Multimedia icons + +You can find more information about the ``rcc`` command, and ``.qrc`` file +format, and the resource system in general in the `Qt Resource System`_ +site. + + +The ``.qrc`` file +================= + +Before running any command, add information about the resources to a ``.qrc`` +file. +In the following example, notice how the resources are listed in ``icons.qrc`` + +:: + + <!DOCTYPE RCC><RCC version="1.0"> + <qresource> + <file>icons/play.png</file> + <file>icons/pause.png</file> + <file>icons/stop.png</file> + <file>icons/previous.png</file> + <file>icons/forward.png</file> + </qresource> + </RCC> + + +Generating a Python file +========================= + +Now that the ``icons.qrc`` file is ready, use the ``pyside6-rcc`` tool to generate +a Python class containing the binary information about the resources + +To do this, we need to run:: + + pyside6-rcc icons.qrc -o rc_icons.py + +The ``-o`` option lets you specify the output filename, +which is ``rc_icons.py`` in this case. + +To use the generated file, add the following import at the top of your main Python file:: + + import rc_icons + + +Changes in the code +=================== + +As you are modifying an existing example, you need to modify the following +lines: + +.. code-block:: python + + from PySide6.QtGui import QIcon, QKeySequence + playIcon = self.style().standardIcon(QStyle.SP_MediaPlay) + previousIcon = self.style().standardIcon(QStyle.SP_MediaSkipBackward) + pauseIcon = self.style().standardIcon(QStyle.SP_MediaPause) + nextIcon = self.style().standardIcon(QStyle.SP_MediaSkipForward) + stopIcon = self.style().standardIcon(QStyle.SP_MediaStop) + +and replace them with the following: + +.. code-block:: python + + from PySide6.QtGui import QIcon, QKeySequence, QPixmap + playIcon = QIcon(QPixmap(":/icons/play.png")) + previousIcon = QIcon(QPixmap(":/icons/previous.png")) + pauseIcon = QIcon(QPixmap(":/icons/pause.png")) + nextIcon = QIcon(QPixmap(":/icons/forward.png")) + stopIcon = QIcon(QPixmap(":/icons/stop.png")) + +This ensures that the new icons are used instead of the default ones provided +by the application theme. +Notice that the lines are not consecutive, but are in different parts +of the file. + +After all your imports, add the following + +.. code-block:: python + + import rc_icons + +Now, the constructor of your class should look like this: + +.. code-block:: python + + def __init__(self): + super(MainWindow, self).__init__() + + self.playlist = QMediaPlaylist() + self.player = QMediaPlayer() + + toolBar = QToolBar() + self.addToolBar(toolBar) + + fileMenu = self.menuBar().addMenu("&File") + openAction = QAction(QIcon.fromTheme("document-open"), + "&Open...", self, shortcut=QKeySequence.Open, + triggered=self.open) + fileMenu.addAction(openAction) + exitAction = QAction(QIcon.fromTheme("application-exit"), "E&xit", + self, shortcut="Ctrl+Q", triggered=self.close) + fileMenu.addAction(exitAction) + + playMenu = self.menuBar().addMenu("&Play") + playIcon = QIcon(QPixmap(":/icons/play.png")) + self.playAction = toolBar.addAction(playIcon, "Play") + self.playAction.triggered.connect(self.player.play) + playMenu.addAction(self.playAction) + + previousIcon = QIcon(QPixmap(":/icons/previous.png")) + self.previousAction = toolBar.addAction(previousIcon, "Previous") + self.previousAction.triggered.connect(self.previousClicked) + playMenu.addAction(self.previousAction) + + pauseIcon = QIcon(QPixmap(":/icons/pause.png")) + self.pauseAction = toolBar.addAction(pauseIcon, "Pause") + self.pauseAction.triggered.connect(self.player.pause) + playMenu.addAction(self.pauseAction) + + nextIcon = QIcon(QPixmap(":/icons/forward.png")) + self.nextAction = toolBar.addAction(nextIcon, "Next") + self.nextAction.triggered.connect(self.playlist.next) + playMenu.addAction(self.nextAction) + + stopIcon = QIcon(QPixmap(":/icons/stop.png")) + self.stopAction = toolBar.addAction(stopIcon, "Stop") + self.stopAction.triggered.connect(self.player.stop) + playMenu.addAction(self.stopAction) + + # many lines were omitted + +Executing the example +===================== + +Run the application by calling ``python main.py`` to checkout the new icon-set: + +.. image:: player-new.png + :alt: New Multimedia Player Qt Example + +.. _`Qt Resource System`: https://doc.qt.io/qt-5/resources.html diff --git a/sources/pyside6/doc/tutorials/basictutorial/signals_and_slots.rst b/sources/pyside6/doc/tutorials/basictutorial/signals_and_slots.rst new file mode 100644 index 000000000..0bfd9e276 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/signals_and_slots.rst @@ -0,0 +1,263 @@ +.. _signals-and-slots: + +Signals and Slots +================= + +Due to the nature of Qt, ``QObject``\s require a way to communicate, and that's +the reason for this mechanism to be a **central feature of Qt**. + +In simple terms, you can understand **Signal and Slots** in the same way you +interact with the lights in your house. When you move the light switch +(signal) you get a result which may be that your light bulbs are switched +on/off (slot). + +While developing interfaces, you can get a real example by the effect of +clicking a button: the 'click' will be the signal, and the slot will be what +happens when that button is clicked, like closing a window, saving a document, +etc. + +.. note:: + If you have experience with other frameworks or toolkits, it's likely + that you read a concept called 'callback'. Leaving the implementation + details aside, a callback will be related to a notification function, + passing a pointer to a function in case it's required due to the events + that happen in your program. This approach might sound similar, but + there are essential differences that make it an unintuitive approach, + like ensuring the type correctness of callback arguments, and some others. + +All classes that inherit from ``QObject`` or one of its subclasses, like +``QWidget`` can contain signals and slots. **Signals are emitted by objects** +when they change their state in a way that may be interesting to other objects. +This is all the object does to communicate. It does not know or care whether +anything is receiving the signals it emits. This is true information +encapsulation, and ensures that the object can be used as a software component. + +**Slots can be used for receiving signals**, but they are also normal member +functions. Just as an object does not know if anything receives its signals, +a slot does not know if it has any signals connected to it. This ensures that +truly independent components can be created with Qt. + +You can connect as many signals as you want to a single slot, and a signal can +be connected to as many slots as you need. It is even possible to connect +a signal directly to another signal. (This will emit the second signal +immediately whenever the first is emitted.) + +Qt's widgets have many predefined signals and slots. For example, +``QAbstractButton`` (base class of buttons in Qt) has a ``clicked()`` +signal and ``QLineEdit`` (single line input field) has a slot named +``clear()``. So, a text input field with a button to clear the text +could be implemented by placing a ``QToolButton`` to the right of the +``QLineEdit`` and connecting its ``clicked()`` signal to the slot +``clear()``. This is done using the ``connect()`` method of the signal: + +.. code-block:: python + + button = QToolButton() + line_edit = QLineEdit() + button.clicked.connect(line_edit.clear) + +``connect()`` returns a ``QMetaObject.Connection`` object, which can be +used with the ``disconnect()`` method to sever the connection. + +Signals can also be connected to free functions: + +.. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication, QPushButton + + + def function(): + print("The 'function' has been called!") + + app = QApplication() + button = QPushButton("Call function") + button.clicked.connect(function) + button.show() + sys.exit(app.exec()) + +Connections can be spelled out in code or, for widget forms, +designed in the +`Signal-Slot Editor <https://doc.qt.io/qt-6/designer-connection-mode.html>`_ +of *Qt Widgets Designer*. + +The Signal Class +---------------- + +When writing classes in Python, signals are declared as class level +variables of the class ``QtCore.Signal()``. A QWidget-based button +that emits a ``clicked()`` signal could look as +follows: + +.. code-block:: python + + from PySide6.QtCore import Qt, Signal + from PySide6.QtWidgets import QWidget + + class Button(QWidget): + + clicked = Signal(Qt.MouseButton) + + ... + + def mousePressEvent(self, event): + self.clicked.emit(event.button()) + +The constructor of ``Signal`` takes a tuple or a list of Python types +and C types: + +.. code-block:: python + + signal1 = Signal(int) # Python types + signal2 = Signal(QUrl) # Qt Types + signal3 = Signal(int, str, int) # more than one type + signal4 = Signal((float,), (QDate,)) # optional types + +In addition to that, it can receive also a named argument ``name`` that defines +the signal name. If nothing is passed, the new signal will have the same name +as the variable that it is being assigned to. + +.. code-block:: python + + # TODO + signal5 = Signal(int, name='rangeChanged') + # ... + rangeChanged.emit(...) + +Another useful option of ``Signal`` is the arguments name, +useful for QML applications to refer to the emitted values by name: + +.. code-block:: python + + sumResult = Signal(int, arguments=['sum']) + +.. code-block:: javascript + + Connections { + target: ... + function onSumResult(sum) { + // do something with 'sum' + } + + +.. _slot-decorator: + +The Slot Class +-------------- + +Slots in QObject-derived classes should be indicated by the decorator +``@QtCore.Slot()``. Again, to define a signature just pass the types +similar to the ``QtCore.Signal()`` class. + +.. code-block:: python + + @Slot(str) + def slot_function(self, s): + ... + + +``Slot()`` also accepts a ``name`` and a ``result`` keyword. +The ``result`` keyword defines the type that will be returned and can be a C or +Python type. The ``name`` keyword behaves the same way as in ``Signal()``. If +nothing is passed as name then the new slot will have the same name as the +function that is being decorated. + +We recommend marking all methods used by signal connections with a +``@QtCore.Slot()`` decorator. Not doing causes run-time overhead due to the +method being added to the ``QMetaObject`` when creating the connection. This is +particularly important for ``QObject`` classes registered with QML, where +missing decorators can introduce bugs. + +Missing decorators can be diagnosed by setting activating warnings of the +logging category ``qt.pyside.libpyside``; for example by setting the +environment variable: + +.. code-block:: bash + + export QT_LOGGING_RULES="qt.pyside.libpyside.warning=true" + +.. _overloading-signals-and-slots: + +Overloading Signals and Slots with Different Types +-------------------------------------------------- + +It is actually possible to use signals and slots of the same name with different +parameter type lists. This is legacy from Qt 5 and not recommended for new code. +In Qt 6, signals have distinct names for different types. + +The following example uses two handlers for a Signal and a Slot to showcase +the different functionality. + +.. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication, QPushButton + from PySide6.QtCore import QObject, Signal, Slot + + + class Communicate(QObject): + # create two new signals on the fly: one will handle + # int type, the other will handle strings + speak = Signal((int,), (str,)) + + def __init__(self, parent=None): + super().__init__(parent) + + self.speak[int].connect(self.say_something) + self.speak[str].connect(self.say_something) + + # define a new slot that receives a C 'int' or a 'str' + # and has 'say_something' as its name + @Slot(int) + @Slot(str) + def say_something(self, arg): + if isinstance(arg, int): + print("This is a number:", arg) + elif isinstance(arg, str): + print("This is a string:", arg) + + if __name__ == "__main__": + app = QApplication(sys.argv) + someone = Communicate() + + # emit 'speak' signal with different arguments. + # we have to specify the str as int is the default + someone.speak.emit(10) + someone.speak[str].emit("Hello everybody!") + + +.. _signals-and-slots-strings: + +Specifying Signals and Slots by Method Signature Strings +-------------------------------------------------------- + + +Signals and slots can also be specified as C++ method signature +strings passed through the ``SIGNAL()`` and/or ``SLOT()`` functions: + +.. code-block:: python + + from PySide6.QtCore import SIGNAL, SLOT + + button.connect(SIGNAL("clicked(Qt::MouseButton)"), + action_handler, SLOT("action1(Qt::MouseButton)")) + +This is not normally recommended; it is only needed +for a few cases where signals are only accessible via ``QMetaObject`` +(``QAxObject``, ``QAxWidget``, ``QDBusInterface`` or ``QWizardPage::registerField()``): + +.. code-block:: python + + wizard.registerField("text", line_edit, "text", + SIGNAL("textChanged(QString)")) + +The signature strings can be found by querying ``QMetaMethod.methodSignature()`` +when introspecting ``QMetaObject``: + +.. code-block:: python + + mo = widget.metaObject() + for m in range(mo.methodOffset(), mo.methodCount()): + print(mo.method(m).methodSignature()) + +Slots should be decorated using :ref:`@Slot <slot-decorator>`. diff --git a/sources/pyside6/doc/tutorials/basictutorial/signals_slots.png b/sources/pyside6/doc/tutorials/basictutorial/signals_slots.png Binary files differnew file mode 100644 index 000000000..0801cf16e --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/signals_slots.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/style.qss b/sources/pyside6/doc/tutorials/basictutorial/style.qss new file mode 100644 index 000000000..b84b98f05 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/style.qss @@ -0,0 +1,23 @@ +QListWidget { + color: #FFFFFF; + background-color: #33373B; +} + +QListWidget::item { + height: 50px; +} + +QListWidget::item:selected { + background-color: #2ABf9E; +} + +QLabel { + background-color: #FFFFFF; + qproperty-alignment: AlignCenter; +} + +QPushButton { + background-color: #2ABf9E; + padding: 20px; + font-size: 18px; +} diff --git a/sources/pyside6/doc/tutorials/basictutorial/tablewidget.png b/sources/pyside6/doc/tutorials/basictutorial/tablewidget.png Binary files differnew file mode 100644 index 000000000..e2549f7d0 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/tablewidget.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/tablewidget.rst b/sources/pyside6/doc/tutorials/basictutorial/tablewidget.rst new file mode 100644 index 000000000..5c04529fd --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/tablewidget.rst @@ -0,0 +1,97 @@ +Displaying Data Using a Table Widget +==================================== + +If you want to display data arranged in a table, use a ``QTableWidget`` to do +so, without dealing with much configuration. + +Notice that using a ``QTableWidget`` is not the only path to display +information in tables. You can also create a data model and display it using +a ``QTableView``, but that is not in the scope of this tutorial. + +.. note:: This Widget is a ready-to-use version of something you can customize + further on. To know more about the Model/View architecture in Qt, refer to + its `official documentation <https://doc.qt.io/qt-6/model-view-programming.html>`_. + +1. Import ``QTableWidget``, ``QTableWidgetItem``, and ``QColor`` to display + background colors: + + .. code-block:: python + + import sys + from PySide6.QtGui import QColor + from PySide6.QtWidgets import (QApplication, QTableWidget, + QTableWidgetItem) + +2. Create a simple data model containing the list of names and hex codes for + different colors: + + .. code-block:: python + + colors = [("Red", "#FF0000"), + ("Green", "#00FF00"), + ("Blue", "#0000FF"), + ("Black", "#000000"), + ("White", "#FFFFFF"), + ("Electric Green", "#41CD52"), + ("Dark Blue", "#222840"), + ("Yellow", "#F9E56d")] + +3. Define a function to translate the hex code into an RGB equivalent: + + .. code-block:: python + + def get_rgb_from_hex(code): + code_hex = code.replace("#", "") + rgb = tuple(int(code_hex[i:i+2], 16) for i in (0, 2, 4)) + return QColor.fromRgb(rgb[0], rgb[1], rgb[2]) + +4. Initialize the ``QApplication`` singleton: + + .. code-block:: python + + app = QApplication() + +5. Configure the ``QTableWidget`` to have a number of rows equivalent + to the amount of items from the ``colors`` structure, and a number of + columns with the members of one color entry, plus one. + You can set the column name using the ``setHorizontalHeaderLabels`` as + described below: + + .. code-block:: python + + table = QTableWidget() + table.setRowCount(len(colors)) + table.setColumnCount(len(colors[0]) + 1) + table.setHorizontalHeaderLabels(["Name", "Hex Code", "Color"]) + + .. note:: the reason of using ``+ 1`` is to include a new column where + we can display the color. + +6. Iterate the data structure, create the ``QTableWidgetItems`` instances, and + add them into the table using a ``x, y`` coordinate. Here the data is being + assigned row-per-row: + + .. code-block:: python + + for i, (name, code) in enumerate(colors): + item_name = QTableWidgetItem(name) + item_code = QTableWidgetItem(code) + item_color = QTableWidgetItem() + item_color.setBackground(get_rgb_from_hex(code)) + table.setItem(i, 0, item_name) + table.setItem(i, 1, item_code) + table.setItem(i, 2, item_color) + +7. Show the table and execute the ``QApplication``. + + .. code-block:: python + + table.show() + sys.exit(app.exec()) + + +The final application will look like this: + +.. image:: tablewidget.png + :alt: QTableWidget example + :align: center diff --git a/sources/pyside6/doc/tutorials/basictutorial/translations.png b/sources/pyside6/doc/tutorials/basictutorial/translations.png Binary files differnew file mode 100644 index 000000000..b9fc1ba17 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/translations.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/translations.rst b/sources/pyside6/doc/tutorials/basictutorial/translations.rst new file mode 100644 index 000000000..21c16cdcd --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/translations.rst @@ -0,0 +1,232 @@ +.. _translations: + +Translating Applications +======================== + +.. image:: translations.png + :alt: Translation Image + +Qt Linguist +----------- + +`Qt Linguist`_ and +its related tools can be used to provide translations for applications. + +The :ref:`qt-linguist-example` example illustrates this. The example is +very simple, it has a menu and shows a list of programming languages with +multiselection. + +Translation works by passing the message strings through function calls that +look up the translation. Each ``QObject`` instance provides a ``tr()`` +function for that purpose. There is also ``QCoreApplication.translate()`` +for adding translated texts to non-QObject classes. + +Qt ships its own translations containing the error messages and standard +dialog captions. + +The linguist example has a number of messages enclosed in ``self.tr()``. +The status bar message shown in response to a selection change uses +a plural form depending on a count: + +.. code-block:: python + + count = len(self._list_widget.selectionModel().selectedRows()) + message = self.tr("%n language(s) selected", "", count) + +The translation workflow for the example is as follows: +The translated messages are extracted using the ``lupdate`` tool, +producing XML-based ``.ts`` files: + +.. code-block:: bash + + pyside6-lupdate main.py -ts example_de.ts + +If ``example_de.ts`` already exists, it will be updated with the new +messages added to the code in-between. + +If there are form files (``.ui``) and/or QML files (``.qml``) in the project, +they should be passed to the ``pyside6-lupdate`` tool as well: + +.. code-block:: bash + + pyside6-lupdate main.py main.qml form.ui -ts example_de.ts + +The source files generated by ``pyside6-uic`` from the form files +should **not** be passed. + +The ``lupdate`` mode of ``pyside6-project`` can also be used for this. It +collects all source files and runs ``pyside6-lupdate`` when ``.ts`` file(s) +are given in the ``.pyproject`` file: + +.. code-block:: bash + + pyside6-project lupdate . + +``.ts`` files are translated using *Qt Linguist*. Once this is complete, +the files are converted to a binary form (``.qm`` files): + +.. code-block:: bash + + pyside6-lrelease example_de.ts -qm example_de.qm + +``pyside6-project`` will build the ``.qm`` file automatically when +``.ts`` file(s) are given in the ``.pyproject`` file: + +.. code-block:: bash + + pyside6-project build . + +To avoid having to ship the ``.qm`` files, it is recommend +to put them into a Qt resource file along with icons and other +applications resources (see :ref:`using_qrc_files`). +The resource file ``linguist.qrc`` provides the ``example_de.qm`` +under ``:/translations``: + +.. code-block:: xml + + <!DOCTYPE RCC><RCC version="1.0"> + <qresource prefix="translations"> + <file>example_de.qm</file> + </qresource> + </RCC> + +At runtime, the translations need to be loaded using the ``QTranslator`` class: + +.. code-block:: python + + path = QLibraryInfo.location(QLibraryInfo.TranslationsPath) + translator = QTranslator(app) + if translator.load(QLocale.system(), 'qtbase', '_', path): + app.installTranslator(translator) + translator = QTranslator(app) + path = ':/translations' + if translator.load(QLocale.system(), 'example', '_', path): + app.installTranslator(translator) + +The code first loads the translations shipped for Qt and then +the translations of the applications loaded from resources. + +The example can then be run in German: + +.. code-block:: bash + + LANG=de python main.py + +.. _Qt Linguist: https://doc.qt.io/qt-6/qtlinguist-index.html + +GNU gettext +----------- + +The `GNU gettext`_ module +can be used to provide translations for applications. + +The :ref:`gettext-example` example illustrates this. The example is +very simple, it has a menu and shows a list of programming languages with +multiselection. + +Translation works by passing the message strings through function calls that +look up the translation. It is common to alias the main translation function +to ``_``. There is a special translation function for sentences that contain +a plural form depending on a count ("{0} items(s) selected"). It is commonly +aliased to ``ngettext``. + +Those functions are defined at the top: + +.. code-block:: python + + import gettext + # ... + _ = None + ngettext = None + +and later assigned as follows: + +.. code-block:: python + + src_dir = Path(__file__).resolve().parent + try: + translation = gettext.translation('example', localedir=src_dir / 'locales') + if translation: + translation.install() + _ = translation.gettext + ngettext = translation.ngettext + except FileNotFoundError: + pass + if not _: + _ = gettext.gettext + ngettext = gettext.ngettext + +This specifies that our translation file has the base name ``example`` and +will be found in the source tree under ``locales``. The code will try +to load a translation matching the current language. + +Messages to be translated look like: + +.. code-block:: python + + file_menu = self.menuBar().addMenu(_("&File")) + +The status bar message shown in response to a selection change uses +a plural form depending on a count: + +.. code-block:: python + + count = len(self._list_widget.selectionModel().selectedRows()) + message = ngettext("{0} language selected", + "{0} languages selected", count).format(count) + +The ``ngettext()`` function takes the singular form, plural form and the count. +The returned string still contains the formatting placeholder, so it needs +to be passed through ``format()``. + +In order to translate the messages to say German, a template file (``.pot``) +is first created: + +.. code-block:: bash + + mkdir -p locales/de_DE/LC_MESSAGES + xgettext -L Python -o locales/example.pot main.py + +This file has a few generic placeholders which can be replaced by the +appropriate values. It is then copied to the ``de_DE/LC_MESSAGES`` directory. + +.. code-block:: bash + + cd locales/de_DE/LC_MESSAGES/ + cp ../../example.pot . + +Further adaptions need to be made to account for the German plural +form and encoding: + +.. code-block:: + + "Project-Id-Version: PySide6 gettext example\n" + "POT-Creation-Date: 2021-07-05 14:16+0200\n" + "Language: de_DE\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "Plural-Forms: nplurals=2; plural=n != 1;\n" + +Below, the translated messages can be given: + +.. code-block:: + + #: main.py:57 + msgid "&File" + msgstr "&Datei" + +Finally, the ``.pot`` is converted to its binary form (machine object file, +``.mo``), which needs to be deployed: + +.. code-block:: bash + + msgfmt -o example.mo example.pot + +The example can then be run in German: + +.. code-block:: bash + + LANG=de python main.py + +.. _GNU gettext: https://docs.python.org/3/library/gettext.html diff --git a/sources/pyside6/doc/tutorials/basictutorial/treewidget.png b/sources/pyside6/doc/tutorials/basictutorial/treewidget.png Binary files differnew file mode 100644 index 000000000..990fe977b --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/treewidget.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/treewidget.rst b/sources/pyside6/doc/tutorials/basictutorial/treewidget.rst new file mode 100644 index 000000000..f431cb5c4 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/treewidget.rst @@ -0,0 +1,79 @@ +Displaying Data Using a Tree Widget +=================================== + +If you want to display data arranged in a tree, use a ``QTreeWidget`` to do so. + +Notice that using a ``QTreeWidget`` is not the only path to display +information in trees. You can also create a data model and display it using a +``QTreeView``, but that is not in the scope of this tutorial. + +.. note:: This Widget is a ready-to-use version of something you can customize + further on. To know more about the Model/View architecture in Qt, refer to + its `official documentation <https://doc.qt.io/qt-6/model-view-programming.html>`_. + +#. Import ``QTreeWidget`` and ``QTreeWidgetItem`` for this application: + + .. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem + +#. Define a dictionary with project structures to display the information as a + tree, with files belonging to each project: + + .. code-block:: python + + data = {"Project A": ["file_a.py", "file_a.txt", "something.xls"], + "Project B": ["file_b.csv", "photo.jpg"], + "Project C": []} + +#. Initialize the ``QApplication`` singleton: + + .. code-block:: python + + app = QApplication() + +#. Configure the ``QTreeWidget`` to have two columns, one for the item name, + and the other for item type information of the files in the project + directories. + You can set the column name with the ``setHeaderLabels`` as described below: + + .. code-block:: python + + tree = QTreeWidget() + tree.setColumnCount(2) + tree.setHeaderLabels(["Name", "Type"]) + +#. Iterate the data structure, create the ``QTreeWidgetItem`` elements, and add + the corresponding children to each parent. + We also extract the extension name for only the files and add them + into the second column. + In the constructor, you can see that each element (``QTreeWidgetItem``) is + added to different columns of the tree (``QTreeWidget``). + + .. code-block:: python + + items = [] + for key, values in data.items(): + item = QTreeWidgetItem([key]) + for value in values: + ext = value.split(".")[-1].upper() + child = QTreeWidgetItem([value, ext]) + item.addChild(child) + items.append(item) + + tree.insertTopLevelItems(0, items) + +#. Show the tree and execute the ``QApplication``. + + .. code-block:: python + + tree.show() + sys.exit(app.exec()) + + +The final application will look like this: + +.. image:: treewidget.png + :alt: QTreeWidget example + :align: center diff --git a/sources/pyside6/doc/tutorials/basictutorial/uifiles.png b/sources/pyside6/doc/tutorials/basictutorial/uifiles.png Binary files differnew file mode 100644 index 000000000..918efec6d --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/uifiles.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst b/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst new file mode 100644 index 000000000..cb945908d --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/uifiles.rst @@ -0,0 +1,304 @@ +.. _using_ui_files: + +Using ``.ui`` files from Designer or QtCreator with ``QUiLoader`` and ``pyside6-uic`` +************************************************************************************* + +This page describes the use of +`Qt Widgets Designer <https://doc.qt.io/qt-6/qtdesigner-manual.html>`_ to create +graphical interfaces based on Qt Widgets for your Qt for Python project. +*Qt Widgets Designer* is a graphical UI design tool which is available as a +standalone binary (``pyside6-designer``) or embedded into the +`Qt Creator IDE <https://doc.qt.io/qtcreator>`_. Its use within *Qt Creator* +is described at +`Using Qt Widgets Designer <https://doc.qt.io/qtcreator/creator-using-qt-designer.html>`_. + +.. image:: uifiles.png + :alt: Designer and the equivalent code + +The designs are stored in ``.ui`` files, which is an XML-based format. It will +be converted to Python or C++ code populating a widget instance at project build +time by the `pyside6-uic <https://doc.qt.io/qt-6/uic.html>`_ tool. + +To create a new Qt Design Form in *Qt Creator*, choose +``File/New File Or Project`` and "Main Window" for template. Save it as +``mainwindow.ui``. Add a ``QPushButton`` to the center of the centralwidget. + +Your file ``mainwindow.ui`` should look something like this: + +.. code-block:: xml + + <?xml version="1.0" encoding="UTF-8"?> + <ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralWidget"> + <widget class="QPushButton" name="pushButton"> + <property name="geometry"> + <rect> + <x>110</x> + <y>80</y> + <width>201</width> + <height>81</height> + </rect> + </property> + <property name="text"> + <string>PushButton</string> + </property> + </widget> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>20</height> + </rect> + </property> + </widget> + <widget class="QToolBar" name="mainToolBar"> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + </widget> + <widget class="QStatusBar" name="statusBar"/> + </widget> + <layoutdefault spacing="6" margin="11"/> + <resources/> + <connections/> + </ui> + +Now we are ready to decide how to use the **UI file** from Python. + +Option A: Generating a Python class +=================================== + +The standard way to interact with a **UI file** is to generate a Python +class from it. This is possible thanks to the ``pyside6-uic`` tool. +To use this tool, you need to run the following command on a console:: + + pyside6-uic mainwindow.ui -o ui_mainwindow.py + +We redirect all the output of the command to a file called ``ui_mainwindow.py``, +which will be imported directly:: + + from ui_mainwindow import Ui_MainWindow + +Now to use it, we should create a personalized class for our widget +to **setup** this generated design. + +To understand the idea, let's take a look at the whole code: + +.. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication, QMainWindow + from PySide6.QtCore import QFile + from ui_mainwindow import Ui_MainWindow + + class MainWindow(QMainWindow): + def __init__(self): + super(MainWindow, self).__init__() + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + if __name__ == "__main__": + app = QApplication(sys.argv) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + +What is inside the *if* statement is already known from the previous +examples, and our new basic class contains only two new lines +that are in charge of loading the generated python class from the UI +file: + +.. code-block:: python + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + +.. note:: + + You must run ``pyside6-uic`` again every time you make changes + to the **UI file**. + +Option B: Loading it directly +============================= + +To load the UI file directly, we will need a class from the **QtUiTools** +module: + +.. code-block:: python + + from PySide6.QtUiTools import QUiLoader + +The ``QUiLoader`` lets us load the **ui file** dynamically +and use it right away: + +.. code-block:: python + + ui_file = QFile("mainwindow.ui") + ui_file.open(QFile.ReadOnly) + + loader = QUiLoader() + window = loader.load(ui_file) + window.show() + +The complete code of this example looks like this: + +.. code-block:: python + + # File: main.py + import sys + from PySide6.QtUiTools import QUiLoader + from PySide6.QtWidgets import QApplication + from PySide6.QtCore import QFile, QIODevice + + if __name__ == "__main__": + app = QApplication(sys.argv) + + ui_file_name = "mainwindow.ui" + ui_file = QFile(ui_file_name) + if not ui_file.open(QIODevice.ReadOnly): + print(f"Cannot open {ui_file_name}: {ui_file.errorString()}") + sys.exit(-1) + loader = QUiLoader() + window = loader.load(ui_file) + ui_file.close() + if not window: + print(loader.errorString()) + sys.exit(-1) + window.show() + + sys.exit(app.exec()) + +Then to execute it we just need to run the following on a +command prompt: + +.. code-block:: bash + + python main.py + +.. note:: + + ``QUiLoader`` uses ``connect()`` calls taking the function signatures as string + arguments for signal/slot connections. + It is thus unable to handle Python types like ``str`` or ``list`` from + custom widgets written in Python since these types are internally mapped + to different C++ types. + +.. _designer_custom_widgets: + +Custom Widgets in Qt Widgets Designer +===================================== + +*Qt Widgets Designer* is able to use user-provided (custom) widgets. +They are shown in the widget box and can be dragged onto the form just like +Qt's widgets (see +`Using Custom Widgets with Qt Widgets Designer <https://doc.qt.io/qt-6/designer-using-custom-widgets.html>`_ +). Normally, this requires implementing the widget as a plugin to +*Qt Widgets Designer* written in C++ implementing its +`QDesignerCustomWidgetInterface`_ . + +Qt for Python provides a simple interface for this which is similar to +:meth:`registerCustomWidget()<PySide6.QtUiTools.QUiLoader.registerCustomWidget>`. + +The widget needs to be provided as a Python module, as shown by +the :ref:`widgetbinding-example` (file ``wigglywidget.py``) or +the :ref:`task-menu-extension-example` (file ``tictactoe.py``). + +Registering this with *Qt Widgets Designer* is done by providing +a registration script named ``register*.py`` and pointing +the path-type environment variable ``PYSIDE_DESIGNER_PLUGINS`` +to the directory. + +The code of the registration script looks as follows: + +.. code-block:: python + + # File: registerwigglywidget.py + from wigglywidget import WigglyWidget + + import QtDesigner + + + TOOLTIP = "A cool wiggly widget (Python)" + DOM_XML = """ + <ui language='c++'> + <widget class='WigglyWidget' name='wigglyWidget'> + <property name='geometry'> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>200</height> + </rect> + </property> + <property name='text'> + <string>Hello, world</string> + </property> + </widget> + </ui> + """ + + QPyDesignerCustomWidgetCollection.registerCustomWidget(WigglyWidget, module="wigglywidget", + tool_tip=TOOLTIP, xml=DOM_XML) + + +QPyDesignerCustomWidgetCollection provides an implementation of +`QDesignerCustomWidgetCollectionInterface`_ +exposing custom widgets to *Qt Widgets Designer* with static convenience +functions for registering types or adding instances of +`QDesignerCustomWidgetInterface`_ . + +The function +:meth:`registerCustomWidget()<PySide6.QtDesigner.QPyDesignerCustomWidgetCollection.registerCustomWidget>` +is used to register a widget type with *Qt Widgets Designer*. In the simple case, it +can be used like ``QUiLoader.registerCustomWidget()``. It takes the custom widget +type and some optional keyword arguments passing values that correspond to the +getters of +`QDesignerCustomWidgetInterface`_ : + +When launching *Qt Widgets Designer* via its launcher ``pyside6-designer``, +the custom widget should be visible in the widget box. + +For advanced usage, it is also possible to pass the function an implementation +of the class QDesignerCustomWidgetInterface instead of the type to +:meth:`addCustomWidget()<PySide6.QtDesigner.QPyDesignerCustomWidgetCollection.addCustomWidget>`. +This is shown in taskmenuextension example, where a custom context menu +is registered for the custom widget. The example is a port of the +corresponding C++ +`Task Menu Extension Example <https://doc.qt.io/qt-6/qtdesigner-taskmenuextension-example.html>`_ . + +.. _QDesignerCustomWidgetCollectionInterface: https://doc.qt.io/qt-6/qdesignercustomwidgetcollectioninterface.html +.. _QDesignerCustomWidgetInterface: https://doc.qt.io/qt-6/qdesignercustomwidgetinterface.html + +Troubleshooting the Qt Widgets Designer Plugin +++++++++++++++++++++++++++++++++++++++++++++++ + +- The launcher ``pyside6-designer`` must be used. The standalone + *Qt Widgets Designer* will not load the plugin. +- The menu item **Help/About Plugin** brings up a dialog showing the plugins + found and potential load error messages. +- Check the console or Windows Debug view for further error messages. +- Due to the buffering of output by Python, error messages may appear + only after *Qt Widgets Designer* has terminated. +- When building Qt for Python, be sure to set the ``--standalone`` option + for the plugin to be properly installed. diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgets.png b/sources/pyside6/doc/tutorials/basictutorial/widgets.png Binary files differnew file mode 100644 index 000000000..de7a969f9 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgets.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgets.rst b/sources/pyside6/doc/tutorials/basictutorial/widgets.rst new file mode 100644 index 000000000..ef14c7e99 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgets.rst @@ -0,0 +1,52 @@ +Your First QtWidgets Application +********************************* + +As with any other programming framework, +you start with the traditional "Hello World" program. + +Here is a simple example of a Hello World application in PySide6: + +.. code-block:: python + + import sys + from PySide6.QtWidgets import QApplication, QLabel + + app = QApplication(sys.argv) + label = QLabel("Hello World!") + label.show() + app.exec() + + +When you execute it the code, the application will look like: + +.. image:: widgets.png + :alt: Simple Widget + + +For a widget application using PySide6, you must always start by +importing the appropriate class from the `PySide6.QtWidgets` module. + +After the imports, you create a `QApplication` instance. As Qt can +receive arguments from command line, you may pass any argument to +the QApplication object. Usually, you don't need to pass any +arguments so you can leave it as is, or use the following approach: + +.. code-block:: python + + app = QApplication([]) + +After the creation of the application object, we have created a +`QLabel` object. A `QLabel` is a widget that can present text +(simple or rich, like html), and images: + +.. code-block:: python + + # This HTML approach will be valid too! + label = QLabel("<font color=red size=40>Hello World!</font>") + +.. note:: After creating the label, we call `show()` on it. + +Finally, we call `app.exec()` to enter the Qt main loop and start +to execute the Qt code. In reality, it is only here where the label +is shown, but this can be ignored for now. + diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-no.png b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-no.png Binary files differnew file mode 100644 index 000000000..f8346533f --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-no.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-simple-no.png b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-simple-no.png Binary files differnew file mode 100644 index 000000000..d510a80cd --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-simple-no.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-simple-yes.png b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-simple-yes.png Binary files differnew file mode 100644 index 000000000..e7a0c0ef7 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-simple-yes.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-yes.png b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-yes.png Binary files differnew file mode 100644 index 000000000..9b83b8267 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling-yes.png diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgetstyling.py b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling.py new file mode 100644 index 000000000..106483b7b --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling.py @@ -0,0 +1,58 @@ +# 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.QtWidgets import (QApplication, QHBoxLayout, QLabel, QListWidget, + QListWidgetItem, QPushButton, QVBoxLayout, + QWidget) + +_placeholder = """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate +velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint +occaecat cupidatat non proident, sunt in culpa qui officia deserunt +mollit anim id est laborum +""" + + +class Widget(QWidget): + def __init__(self, parent=None): + super(Widget, self).__init__(parent) + + menu_widget = QListWidget() + for i in range(10): + item = QListWidgetItem(f"Item {i}") + item.setTextAlignment(Qt.AlignCenter) + menu_widget.addItem(item) + + text_widget = QLabel(_placeholder) + button = QPushButton("Something") + + content_layout = QVBoxLayout() + content_layout.addWidget(text_widget) + content_layout.addWidget(button) + main_widget = QWidget() + main_widget.setLayout(content_layout) + + layout = QHBoxLayout() + layout.addWidget(menu_widget, 1) + layout.addWidget(main_widget, 4) + self.setLayout(layout) + + +if __name__ == "__main__": + app = QApplication() + + w = Widget() + w.show() + + _style = None + with open("style.qss", "r") as f: + _style = f.read() + app.setStyleSheet(_style) + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/basictutorial/widgetstyling.rst b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling.rst new file mode 100644 index 000000000..2fa51c0a8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/basictutorial/widgetstyling.rst @@ -0,0 +1,171 @@ +.. _widgetstyling: + +Styling the Widgets Application +=============================== + +Qt Widgets application use a default theme depending on the platform. +In some cases, there are system-wide configurations that modify the Qt theme, +and applications are displayed differently. + +However, you can take care of your own widgets and provide a custom style +to each component. As an example, look at the following simple snippet: + +.. code-block:: python + + import sys + from PySide6.QtCore import Qt + from PySide6.QtWidgets import QApplication, QLabel + + if __name__ == "__main__": + app = QApplication() + w = QLabel("This is a placeholder text") + w.setAlignment(Qt.AlignCenter) + w.show() + sys.exit(app.exec()) + +When you execute this code, you will see a simple ``QLabel`` aligned at the +center, and with a placeholder text. + +.. image:: widgetstyling-simple-no.png + :alt: Simple Widget with no style + +You can style your application using the CSS-like syntax. +For more information, see `Qt Style Sheets Reference`_. + +A ``QLabel`` can be styled differently by setting some of its CSS +properties, such as ``background-color`` and ``font-family``, +so let's see how does the code look like with these changes: + +.. code-block:: python + + import sys + from PySide6.QtCore import Qt + from PySide6.QtWidgets import QApplication, QLabel + + if __name__ == "__main__": + app = QApplication() + w = QLabel("This is a placeholder text") + w.setAlignment(Qt.AlignCenter) + w.setStyleSheet(""" + background-color: #262626; + color: #FFFFFF; + font-family: Titillium; + font-size: 18px; + """) + w.show() + sys.exit(app.exec()) + +Now when you run the code, notice that the ``QLabel`` looks different with your +custom style: + +.. image:: widgetstyling-simple-yes.png + :alt: Simple Widget with Style + + +.. note:: + + If you don't have the font ``Titillium`` installed, you can try with any + other you prefer. + Remember you can list your installed fonts using ``QFontDatabase``, + specifically the ``families()`` method. + + +Styling each UI element separately like you did in the previous snippet is a +lot of work. The easier alternative for this is to use Qt Style Sheets, +which is one or more ``.qss`` files defining the style for the UI elements in +your application. + +More examples can be found in the `Qt Style Sheet Examples`_ documentation +page. + + +.. _`Qt Style Sheets Reference`: https://doc.qt.io/qt-5/stylesheet-reference.html +.. _`Qt Style Sheet Examples`: https://doc.qt.io/qt-5/stylesheet-examples.html + +Qt Style Sheets +--------------- + +.. warning:: + + Before starting modifying your application, keep in mind that you will be + responsible for all the graphical details of the application. + Altering margins, and sizes might end up looking strange or incorrect, so you + need to be careful when altering the style. + It's recommended to create a full new Qt style to cover all the possible + corner cases. + +A ``qss`` file is quite similar to a CSS file, but you need to specify the Widget +component and optionally the name of the object:: + + QLabel { + background-color: red; + } + + QLabel#title { + font-size: 20px; + } + +The first style defines a ``background-color`` for all ``QLabel`` objects in your +application, whereas the later one styles the ``title`` object only. + +.. note:: + + You can set object names with the `setObjectName(str)` function to any Qt + object, for example: for a `label = QLabel("Test")`, you can write + `label.setObjectName("title")` + + +Once you have a ``qss`` file for your application, you can apply it by reading +the file and using the ``QApplication.setStyleSheet(str)`` function: + +.. code-block:: python + + if __name__ == "__main__": + app = QApplication() + + w = Widget() + w.show() + + with open("style.qss", "r") as f: + _style = f.read() + app.setStyleSheet(_style) + + sys.exit(app.exec()) + +Having a general ``qss`` file allows you to decouple the styling aspects of +the code, without mixing it in the middle of the general functionality, and you +can simply enable it or disable it. + +Look at this new example, with more widgets components: + +.. literalinclude:: widgetstyling.py + :linenos: + :lines: 22-44 + +This displays a two column widget, with a ``QListWidget`` on the left and a +``QLabel`` and a ``QPushButton`` on the right. It looks like this when you run the +code: + +.. image:: widgetstyling-no.png + :alt: Widget with no style + +If you add content to the previously described ``style.qss`` file, you can modify +the look-n-feel of the previous example: + +.. literalinclude:: style.qss + :linenos: + +The style changes mainly the color of the different widgets, alter the +alignment, and includes some spacing. +You can also use state-based styling on the QListWidget *items* for example, to +style them differently depending on whether they are *selected* or not. + +After applying all the styling alternatives you explored in this topic, notice +that the ``QLabel`` example looks a lot different now. +Try running the code to check its new look: + +.. image:: widgetstyling-yes.png + :alt: Widget with style + +You have the freedom to tune your style sheets and provide a really nice +look-n-feel to all your applications. diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst b/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst new file mode 100644 index 000000000..59a9d9ee0 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst @@ -0,0 +1,20 @@ +Chapter 5 - Add a chart view +============================= + +A table is nice to present data, but a chart is even better. For this, you +need the QtCharts module that provides many types of plots and options to +graphically represent data. + +The placeholder for a plot is a QChartView, and inside that Widget you can +place a QChart. As a first step, try including only this without any data to +plot. + +Make the following highlighted changes to :code:`main_widget.py` from the +previous chapter to add a QChartView: + +.. literalinclude:: datavisualize5/main_widget.py + :linenos: + :lines: 3- + :emphasize-lines: 2-3,6,22-36,47-49 + + diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst b/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst new file mode 100644 index 000000000..623372a07 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst @@ -0,0 +1,32 @@ +Chapter 3 - Create an empty QMainWindow +========================================== + +You can now think of presenting your data in a UI. A QMainWindow provides a +convenient structure for GUI applications, such as a menu bar and status bar. +The following image shows the layout that QMainWindow offers out-of-the box: + +.. image:: images/QMainWindow-layout.png + :alt: QMainWindow layout + :align: right + +In this case, let your application inherit from QMainWindow, and add the +following UI elements: + +* A "File" menu to open a File dialog. +* An "Exit" menu close the window. +* A status message on the status bar when the application starts. + +In addition, you can define a fixed size for the window or adjust it based on +the resolution you currently have. In the following snippet, you will see how +window size is defined based on available screen width (80%) and height (70%). + +.. note:: You can achieve a similar structure using other Qt elements like + QMenuBar, QWidget, and QStatusBar. Refer the QMainWindow layout for + guidance. + +.. literalinclude:: datavisualize3/main_window.py + :language: python + :linenos: + :lines: 4- + +Try running the script to see what output you get with it. diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst b/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst new file mode 100644 index 000000000..f658640bf --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst @@ -0,0 +1,70 @@ +Chapter 4 - Add a QTableView +============================= + +Now that you have a QMainWindow, you can include a centralWidget to your +interface. Usually, a QWidget is used to display data in most data-driven +applications. Use a table view to display your data. + +The first step is to add a horizontal layout with just a QTableView. You +can create a QTableView object and place it inside a QHBoxLayout. Once the +QWidget is properly built, pass the object to the QMainWindow as its central +widget. + +Remember that a QTableView needs a model to display information. In this case, +you can use a QAbstractTableModel instance. + +.. note:: You could also use the default item model that comes with a + QTableWidget instead. QTableWidget is a convenience class that reduces + your codebase considerably as you don't need to implement a data model. + However, it's less flexible than a QTableView, as QTableWidget cannot be + used with just any data. For more insight about Qt's model-view framework, + refer to the + `Model View Programming <https://doc.qt.io/qt-5/model-view-programming.html>` + documentation. + +Implementing the model for your QTableView, allows you to: +- set the headers, +- manipulate the formats of the cell values (remember we have UTC time and float +numbers), +- set style properties like text alignment, +- and even set color properties for the cell or its content. + +To subclass the QAbstractTable, you must reimplement its virtual methods, +rowCount(), columnCount(), and data(). This way, you can ensure that the data +is handled properly. In addition, reimplement the headerData() method to +provide the header information to the view. + +Here is a script that implements the CustomTableModel: + +.. literalinclude:: datavisualize4/table_model.py + :language: python + :linenos: + :lines: 3- + +Now, create a QWidget that has a QTableView, and connect it to your +CustomTableModel. + +.. literalinclude:: datavisualize4/main_widget.py + :language: python + :linenos: + :emphasize-lines: 12-17 + :lines: 3- + +You also need minor changes to the :code:`main_window.py` and +:code:`main.py` from chapter 3 to include the Widget inside the +MainWindow. + +In the following snippets you'll see those changes highlighted: + +.. literalinclude:: datavisualize4/main_window.py + :language: python + :linenos: + :lines: 3- + :emphasize-lines: 8,11 + +.. literalinclude:: datavisualize4/main.py + :language: python + :linenos: + :lines: 3- + :emphasize-lines: 46-47 + diff --git a/sources/pyside6/doc/tutorials/datavisualize/all_hour.csv b/sources/pyside6/doc/tutorials/datavisualize/all_hour.csv new file mode 100644 index 000000000..400947c3c --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/all_hour.csv @@ -0,0 +1,8 @@ +time,latitude,longitude,depth,mag,magType,nst,gap,dmin,rms,net,id,updated,place,type,horizontalError,depthError,magError,magNst,status,locationSource,magSource +2019-01-10T12:11:24.810Z,34.1281662,-117.7754974,4.46,1.18,ml,22,69,0.04475,0.13,ci,ci38421072,2019-01-10T12:13:30.138Z,"3km NNW of La Verne, CA",earthquake,0.3,0.55,0.246,6,automatic,ci,ci +2019-01-10T12:04:26.320Z,19.4433327,-155.6159973,0.72,1.79,md,22,99,0.04026,0.3,hv,hv70763571,2019-01-10T12:07:28.690Z,"26km E of Honaunau-Napoopoo, Hawaii",earthquake,0.6,1.79,0.28,6,automatic,hv,hv +2019-01-10T11:57:48.980Z,33.3225,-116.3931667,4.84,0.62,ml,15,211,0.05776,0.16,ci,ci38421064,2019-01-10T12:01:29.166Z,"8km NNW of Borrego Springs, CA",earthquake,0.71,0.68,0.111,11,automatic,ci,ci +2019-01-10T11:52:09.490Z,38.8356667,-122.8366699,1.28,2.74,md,25,77,0.003061,0.04,nc,nc73131566,2019-01-10T12:14:02.757Z,"10km NW of The Geysers, CA",earthquake,0.19,0.29,0.06,7,automatic,nc,nc +2019-01-10T11:25:44.854Z,65.1082,-149.3701,20.6,2.1,ml,,,,1.02,ak,ak019gq2oer,2019-01-10T11:37:07.060Z,"60km NNW of North Nenana, Alaska",earthquake,,0.3,,,automatic,ak,ak +2019-01-10T11:25:23.786Z,69.1518,-144.4977,10.4,3.7,ml,,,,0.74,ak,ak019gq2ndz,2019-01-10T11:47:11.284Z,"114km SSW of Kaktovik, Alaska",earthquake,,1.6,,,reviewed,ak,ak +2019-01-10T11:16:11.761Z,61.3318,-150.0708,20.1,2.7,ml,,,,0.83,ak,ak019gq0ozj,2019-01-10T11:29:24.610Z,"15km NW of Anchorage, Alaska",earthquake,,0.4,,,automatic,ak,ak diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize1/main.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize1/main.py new file mode 100644 index 000000000..995b9906a --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize1/main.py @@ -0,0 +1,18 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import argparse +import pandas as pd + + +def read_data(fname): + return pd.read_csv(fname) + + +if __name__ == "__main__": + options = argparse.ArgumentParser() + options.add_argument("-f", "--file", type=str, required=True) + args = options.parse_args() + data = read_data(args.file) + print(data) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize2/main.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize2/main.py new file mode 100644 index 000000000..9ea96097c --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize2/main.py @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import argparse +import pandas as pd + +from PySide6.QtCore import QDateTime, QTimeZone + + +def transform_date(utc, timezone=None): + utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ" + new_date = QDateTime().fromString(utc, utc_fmt) + if timezone: + new_date.setTimeZone(timezone) + return new_date + + +def read_data(fname): + # Read the CSV content + df = pd.read_csv(fname) + + # Remove wrong magnitudes + df = df.drop(df[df.mag < 0].index) + magnitudes = df["mag"] + + # My local timezone + timezone = QTimeZone(b"Europe/Berlin") + + # Get timestamp transformed to our timezone + times = df["time"].apply(lambda x: transform_date(x, timezone)) + + return times, magnitudes + + +if __name__ == "__main__": + options = argparse.ArgumentParser() + options.add_argument("-f", "--file", type=str, required=True) + args = options.parse_args() + data = read_data(args.file) + print(data) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main.py new file mode 100644 index 000000000..bbf85e17d --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import argparse +import pandas as pd + +from PySide6.QtCore import QDateTime, QTimeZone +from PySide6.QtWidgets import QApplication +from main_window import MainWindow + + +def transform_date(utc, timezone=None): + utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ" + new_date = QDateTime().fromString(utc, utc_fmt) + if timezone: + new_date.setTimeZone(timezone) + return new_date + + +def read_data(fname): + # Read the CSV content + df = pd.read_csv(fname) + + # Remove wrong magnitudes + df = df.drop(df[df.mag < 0].index) + magnitudes = df["mag"] + + # My local timezone + timezone = QTimeZone(b"Europe/Berlin") + + # Get timestamp transformed to our timezone + times = df["time"].apply(lambda x: transform_date(x, timezone)) + + return times, magnitudes + + +if __name__ == "__main__": + options = argparse.ArgumentParser() + options.add_argument("-f", "--file", type=str, required=True) + args = options.parse_args() + data = read_data(args.file) + + # Qt Application + app = QApplication(sys.argv) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py new file mode 100644 index 000000000..745f2fefe --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py @@ -0,0 +1,31 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Slot +from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtWidgets import QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self): + QMainWindow.__init__(self) + self.setWindowTitle("Eartquakes information") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = QAction("Exit", self) + exit_action.setShortcut(QKeySequence.Quit) + exit_action.triggered.connect(self.close) + + self.file_menu.addAction(exit_action) + + # Status Bar + self.status = self.statusBar() + self.status.showMessage("Data loaded and plotted") + + # Window dimensions + geometry = self.screen().availableGeometry() + self.setFixedSize(geometry.width() * 0.8, geometry.height() * 0.7) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main.py new file mode 100644 index 000000000..87b962bd3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main.py @@ -0,0 +1,53 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import argparse +import pandas as pd + +from PySide6.QtCore import QDateTime, QTimeZone +from PySide6.QtWidgets import QApplication +from main_window import MainWindow +from main_widget import Widget + + +def transform_date(utc, timezone=None): + utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ" + new_date = QDateTime().fromString(utc, utc_fmt) + if timezone: + new_date.setTimeZone(timezone) + return new_date + + +def read_data(fname): + # Read the CSV content + df = pd.read_csv(fname) + + # Remove wrong magnitudes + df = df.drop(df[df.mag < 0].index) + magnitudes = df["mag"] + + # My local timezone + timezone = QTimeZone(b"Europe/Berlin") + + # Get timestamp transformed to our timezone + times = df["time"].apply(lambda x: transform_date(x, timezone)) + + return times, magnitudes + + +if __name__ == "__main__": + options = argparse.ArgumentParser() + options.add_argument("-f", "--file", type=str, required=True) + args = options.parse_args() + data = read_data(args.file) + + # Qt Application + app = QApplication(sys.argv) + + widget = Widget(data) + window = MainWindow(widget) + window.show() + + sys.exit(app.exec()) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py new file mode 100644 index 000000000..2a2bfeb09 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py @@ -0,0 +1,43 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtWidgets import (QHBoxLayout, QHeaderView, QSizePolicy, + QTableView, QWidget) + +from table_model import CustomTableModel + + +class Widget(QWidget): + def __init__(self, data): + QWidget.__init__(self) + + # Getting the Model + self.model = CustomTableModel(data) + + # Creating a QTableView + self.table_view = QTableView() + self.table_view.setModel(self.model) + + # QTableView Headers + self.horizontal_header = self.table_view.horizontalHeader() + self.vertical_header = self.table_view.verticalHeader() + self.horizontal_header.setSectionResizeMode( + QHeaderView.ResizeToContents + ) + self.vertical_header.setSectionResizeMode( + QHeaderView.ResizeToContents + ) + self.horizontal_header.setStretchLastSection(True) + + # QWidget Layout + self.main_layout = QHBoxLayout() + size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + + ## Left layout + size.setHorizontalStretch(1) + self.table_view.setSizePolicy(size) + self.main_layout.addWidget(self.table_view) + + # Set the layout to the QWidget + self.setLayout(self.main_layout) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py new file mode 100644 index 000000000..ebe2997a1 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py @@ -0,0 +1,32 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Slot +from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtWidgets import QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self, widget): + QMainWindow.__init__(self) + self.setWindowTitle("Eartquakes information") + self.setCentralWidget(widget) + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + ## Exit QAction + exit_action = QAction("Exit", self) + exit_action.setShortcut(QKeySequence.Quit) + exit_action.triggered.connect(self.close) + + self.file_menu.addAction(exit_action) + + # Status Bar + self.status = self.statusBar() + self.status.showMessage("Data loaded and plotted") + + # Window dimensions + geometry = self.screen().availableGeometry() + self.setFixedSize(geometry.width() * 0.8, geometry.height() * 0.7) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py new file mode 100644 index 000000000..08eeeeed6 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex +from PySide6.QtGui import QColor + + +class CustomTableModel(QAbstractTableModel): + def __init__(self, data=None): + QAbstractTableModel.__init__(self) + self.load_data(data) + + def load_data(self, data): + self.input_dates = data[0].values + self.input_magnitudes = data[1].values + + self.column_count = 2 + self.row_count = len(self.input_magnitudes) + + def rowCount(self, parent=QModelIndex()): + return self.row_count + + def columnCount(self, parent=QModelIndex()): + return self.column_count + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return None + if orientation == Qt.Horizontal: + return ("Date", "Magnitude")[section] + else: + return f"{section}" + + def data(self, index, role=Qt.DisplayRole): + column = index.column() + row = index.row() + + if role == Qt.DisplayRole: + if column == 0: + date = self.input_dates[row].toPython() + return str(date)[:-3] + elif column == 1: + magnitude = self.input_magnitudes[row] + return f"{magnitude:.2f}" + elif role == Qt.BackgroundRole: + return QColor(Qt.white) + elif role == Qt.TextAlignmentRole: + return Qt.AlignRight + + return None + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main.py new file mode 100644 index 000000000..87b962bd3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main.py @@ -0,0 +1,53 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import argparse +import pandas as pd + +from PySide6.QtCore import QDateTime, QTimeZone +from PySide6.QtWidgets import QApplication +from main_window import MainWindow +from main_widget import Widget + + +def transform_date(utc, timezone=None): + utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ" + new_date = QDateTime().fromString(utc, utc_fmt) + if timezone: + new_date.setTimeZone(timezone) + return new_date + + +def read_data(fname): + # Read the CSV content + df = pd.read_csv(fname) + + # Remove wrong magnitudes + df = df.drop(df[df.mag < 0].index) + magnitudes = df["mag"] + + # My local timezone + timezone = QTimeZone(b"Europe/Berlin") + + # Get timestamp transformed to our timezone + times = df["time"].apply(lambda x: transform_date(x, timezone)) + + return times, magnitudes + + +if __name__ == "__main__": + options = argparse.ArgumentParser() + options.add_argument("-f", "--file", type=str, required=True) + args = options.parse_args() + data = read_data(args.file) + + # Qt Application + app = QApplication(sys.argv) + + widget = Widget(data) + window = MainWindow(widget) + window.show() + + sys.exit(app.exec()) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py new file mode 100644 index 000000000..15b7e97e5 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QDateTime, Qt +from PySide6.QtGui import QPainter +from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView, + QSizePolicy) +from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis + +from table_model import CustomTableModel + + +class Widget(QWidget): + def __init__(self, data): + QWidget.__init__(self) + + # Getting the Model + self.model = CustomTableModel(data) + + # Creating a QTableView + self.table_view = QTableView() + self.table_view.setModel(self.model) + + # QTableView Headers + self.horizontal_header = self.table_view.horizontalHeader() + self.vertical_header = self.table_view.verticalHeader() + self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents) + self.vertical_header.setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontal_header.setStretchLastSection(True) + + # Creating QChart + self.chart = QChart() + self.chart.setAnimationOptions(QChart.AllAnimations) + + # Creating QChartView + self.chart_view = QChartView(self.chart) + self.chart_view.setRenderHint(QPainter.Antialiasing) + + # QWidget Layout + self.main_layout = QHBoxLayout() + size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + + ## Left layout + size.setHorizontalStretch(1) + self.table_view.setSizePolicy(size) + self.main_layout.addWidget(self.table_view) + + ## Right Layout + size.setHorizontalStretch(4) + self.chart_view.setSizePolicy(size) + self.main_layout.addWidget(self.chart_view) + + # Set the layout to the QWidget + self.setLayout(self.main_layout) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py new file mode 100644 index 000000000..ed12c1f8f --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py @@ -0,0 +1,32 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Slot +from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtWidgets import QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self, widget): + QMainWindow.__init__(self) + self.setWindowTitle("Eartquakes information") + self.setCentralWidget(widget) + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = QAction("Exit", self) + exit_action.setShortcut(QKeySequence.Quit) + exit_action.triggered.connect(self.close) + + self.file_menu.addAction(exit_action) + + # Status Bar + self.status = self.statusBar() + self.status.showMessage("Data loaded and plotted") + + # Window dimensions + geometry = self.screen().availableGeometry() + self.setFixedSize(geometry.width() * 0.8, geometry.height() * 0.7) + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py new file mode 100644 index 000000000..08eeeeed6 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex +from PySide6.QtGui import QColor + + +class CustomTableModel(QAbstractTableModel): + def __init__(self, data=None): + QAbstractTableModel.__init__(self) + self.load_data(data) + + def load_data(self, data): + self.input_dates = data[0].values + self.input_magnitudes = data[1].values + + self.column_count = 2 + self.row_count = len(self.input_magnitudes) + + def rowCount(self, parent=QModelIndex()): + return self.row_count + + def columnCount(self, parent=QModelIndex()): + return self.column_count + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return None + if orientation == Qt.Horizontal: + return ("Date", "Magnitude")[section] + else: + return f"{section}" + + def data(self, index, role=Qt.DisplayRole): + column = index.column() + row = index.row() + + if role == Qt.DisplayRole: + if column == 0: + date = self.input_dates[row].toPython() + return str(date)[:-3] + elif column == 1: + magnitude = self.input_magnitudes[row] + return f"{magnitude:.2f}" + elif role == Qt.BackgroundRole: + return QColor(Qt.white) + elif role == Qt.TextAlignmentRole: + return Qt.AlignRight + + return None + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main.py new file mode 100644 index 000000000..2d851cc14 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main.py @@ -0,0 +1,55 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import argparse +import pandas as pd + +from PySide6.QtCore import QDateTime, QTimeZone +from PySide6.QtWidgets import QApplication + +from main_window import MainWindow +from main_widget import Widget + + +def transform_date(utc, timezone=None): + utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ" + new_date = QDateTime().fromString(utc, utc_fmt) + if timezone: + new_date.setTimeZone(timezone) + return new_date + + +def read_data(fname): + # Read the CSV content + df = pd.read_csv(fname) + + # Remove wrong magnitudes + df = df.drop(df[df.mag < 0].index) + magnitudes = df["mag"] + + # My local timezone + timezone = QTimeZone(b"Europe/Berlin") + + # Get timestamp transformed to our timezone + times = df["time"].apply(lambda x: transform_date(x, timezone)) + + return times, magnitudes + + +if __name__ == "__main__": + options = argparse.ArgumentParser() + options.add_argument("-f", "--file", type=str, required=True) + args = options.parse_args() + data = read_data(args.file) + + # Qt Application + app = QApplication(sys.argv) + + # QWidget + widget = Widget(data) + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + + window.show() + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py new file mode 100644 index 000000000..cbcc126a1 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py @@ -0,0 +1,95 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QDateTime, Qt +from PySide6.QtGui import QPainter +from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView, + QSizePolicy) +from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis + +from table_model import CustomTableModel + + +class Widget(QWidget): + def __init__(self, data): + QWidget.__init__(self) + + # Getting the Model + self.model = CustomTableModel(data) + + # Creating a QTableView + self.table_view = QTableView() + self.table_view.setModel(self.model) + + # QTableView Headers + resize = QHeaderView.ResizeToContents + self.horizontal_header = self.table_view.horizontalHeader() + self.vertical_header = self.table_view.verticalHeader() + self.horizontal_header.setSectionResizeMode(resize) + self.vertical_header.setSectionResizeMode(resize) + self.horizontal_header.setStretchLastSection(True) + + # Creating QChart + self.chart = QChart() + self.chart.setAnimationOptions(QChart.AllAnimations) + self.add_series("Magnitude (Column 1)", [0, 1]) + + # Creating QChartView + self.chart_view = QChartView(self.chart) + self.chart_view.setRenderHint(QPainter.Antialiasing) + + # QWidget Layout + self.main_layout = QHBoxLayout() + size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + + # Left layout + size.setHorizontalStretch(1) + self.table_view.setSizePolicy(size) + self.main_layout.addWidget(self.table_view) + + # Right Layout + size.setHorizontalStretch(4) + self.chart_view.setSizePolicy(size) + self.main_layout.addWidget(self.chart_view) + + # Set the layout to the QWidget + self.setLayout(self.main_layout) + + def add_series(self, name, columns): + # Create QLineSeries + self.series = QLineSeries() + self.series.setName(name) + + # Filling QLineSeries + for i in range(self.model.rowCount()): + # Getting the data + t = self.model.index(i, 0).data() + date_fmt = "yyyy-MM-dd HH:mm:ss.zzz" + + x = QDateTime().fromString(t, date_fmt).toSecsSinceEpoch() + y = float(self.model.index(i, 1).data()) + + if x > 0 and y > 0: + self.series.append(x, y) + + self.chart.addSeries(self.series) + + # Setting X-axis + self.axis_x = QDateTimeAxis() + self.axis_x.setTickCount(10) + self.axis_x.setFormat("dd.MM (h:mm)") + self.axis_x.setTitleText("Date") + self.chart.addAxis(self.axis_x, Qt.AlignBottom) + self.series.attachAxis(self.axis_x) + # Setting Y-axis + self.axis_y = QValueAxis() + self.axis_y.setTickCount(10) + self.axis_y.setLabelFormat("%.2f") + self.axis_y.setTitleText("Magnitude") + self.chart.addAxis(self.axis_y, Qt.AlignLeft) + self.series.attachAxis(self.axis_y) + + # Getting the color from the QChart to use it on the QTableView + color_name = self.series.pen().color().name() + self.model.color = f"{color_name}" + diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py new file mode 100644 index 000000000..116e53dc3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py @@ -0,0 +1,32 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Slot +from PySide6.QtGui import QAction, QKeySequence, QScreen +from PySide6.QtWidgets import QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self, widget): + QMainWindow.__init__(self) + self.setWindowTitle("Eartquakes information") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = QAction("Exit", self) + exit_action.setShortcut(QKeySequence.Quit) + exit_action.triggered.connect(self.close) + + self.file_menu.addAction(exit_action) + + # Status Bar + self.status = self.statusBar() + self.status.showMessage("Data loaded and plotted") + + # Window dimensions + geometry = self.screen().availableGeometry() + self.setFixedSize(geometry.width() * 0.8, geometry.height() * 0.7) + self.setCentralWidget(widget) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py new file mode 100644 index 000000000..4952c0c38 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex +from PySide6.QtGui import QColor + + +class CustomTableModel(QAbstractTableModel): + def __init__(self, data=None): + QAbstractTableModel.__init__(self) + self.color = None + self.load_data(data) + + def load_data(self, data): + self.input_dates = data[0].values + self.input_magnitudes = data[1].values + + self.column_count = 2 + self.row_count = len(self.input_magnitudes) + + def rowCount(self, parent=QModelIndex()): + return self.row_count + + def columnCount(self, parent=QModelIndex()): + return self.column_count + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return None + if orientation == Qt.Horizontal: + return ("Date", "Magnitude")[section] + else: + return f"{section}" + + def data(self, index, role=Qt.DisplayRole): + column = index.column() + row = index.row() + + if role == Qt.DisplayRole: + if column == 0: + date = self.input_dates[row].toPython() + return str(date)[:-3] + elif column == 1: + magnitude = self.input_magnitudes[row] + return f"{magnitude:.2f}" + elif role == Qt.BackgroundRole: + return (QColor(Qt.white), QColor(self.color))[column] + elif role == Qt.TextAlignmentRole: + return Qt.AlignRight + + return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst b/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst new file mode 100644 index 000000000..edfac3862 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst @@ -0,0 +1,29 @@ +Chapter 2 - Filtering data +=========================== + +In the previous chapter, you learned how to read and print data that is a +bit raw. Now, try to select a few columns and handle them properly. + +Start with these two columns: Time (time) and Magnitude (mag). After getting +the information from these columns, filter and adapt the data. Try formatting +the date to Qt types. + +There is not much to do for the Magnitude column, as it's just a floating point +number. You could take special care to check if the data is correct. This could +be done by filtering the data that follows the condition, "magnitude > 0", to +avoid faulty data or unexpected behavior. + +The Date column provides data in UTC format (for example, +2018-12-11T21:14:44.682Z), so you could easily map it to a QDateTime object +defining the structure of the string. Additionally, you can adapt the time +based on the timezone you are in, using QTimeZone. + +The following script filters and formats the CSV data as described earlier: + +.. literalinclude:: datavisualize2/main.py + :language: python + :linenos: + :lines: 3- + +Now that you have a tuple of QDateTime and float data, try improving the +output further. That's what you'll learn in the following chapters. diff --git a/sources/pyside6/doc/tutorials/datavisualize/images/QMainWindow-layout.png b/sources/pyside6/doc/tutorials/datavisualize/images/QMainWindow-layout.png Binary files differnew file mode 100644 index 000000000..075d796b8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/images/QMainWindow-layout.png diff --git a/sources/pyside6/doc/tutorials/datavisualize/images/datavisualization_app.png b/sources/pyside6/doc/tutorials/datavisualize/images/datavisualization_app.png Binary files differnew file mode 100644 index 000000000..8d7e4ba13 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/images/datavisualization_app.png diff --git a/sources/pyside6/doc/tutorials/datavisualize/index.rst b/sources/pyside6/doc/tutorials/datavisualize/index.rst new file mode 100644 index 000000000..35b56706a --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/index.rst @@ -0,0 +1,31 @@ +Data Visualization Tool Tutorial +********************************* + +In this tutorial, you'll learn about the data visualization capabilities +of |project|. To start with, find some open data to visualize. For example, +data about the magnitude of earthquakes during the last hour published on the +US Geological Survey website. You could download the +`All earthquakes <https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv>`_ +open data in a CSV format for this tutorial. + +.. image:: images/datavisualization_app.png + :width: 800 + :alt: Data Visualization Screenshot + +In the following chapters of this tutorial you'll learn how to +visualize data from a CSV in a line chart. + +.. toctree:: + :glob: + :titlesonly: + + read* + filter* + add_main* + add_tab* + add_chart* + plot* + +You can download the sources from `here`_. + +.. _here: https://code.qt.io/cgit/pyside/pyside-setup.git/tree/sources/pyside6/doc/tutorials/datavisualize diff --git a/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst b/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst new file mode 100644 index 000000000..f71fca4c8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst @@ -0,0 +1,27 @@ +Chapter 6 - Plot the data in the ChartView +=========================================== + +The last step of this tutorial is to plot the CSV data inside our QChart. For +this, you need to go over our data and include the data on a QLineSeries. + +After adding the data to the series, you can modify the axis to properly +display the QDateTime on the X-axis, and the magnitude values on the Y-axis. + +Here is the updated :code:`main_widget.py` that includes an additional +function to plot data using a QLineSeries: + +.. literalinclude:: datavisualize6/main_widget.py + :language: python + :linenos: + :lines: 3- + :emphasize-lines: 33,56-91 + +Now, run the application to visualize the earthquake magnitudes +data at different times. + +.. image:: images/datavisualization_app.png + :width: 600 + :alt: Data Visualization Screenshot + +Try modifying the sources to get different output. For example, you could try +to plot more data from the CSV. diff --git a/sources/pyside6/doc/tutorials/datavisualize/read_data.rst b/sources/pyside6/doc/tutorials/datavisualize/read_data.rst new file mode 100644 index 000000000..53b0d1cb0 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/read_data.rst @@ -0,0 +1,41 @@ +Chapter 1 - Reading data from a CSV +=================================== + +There are several ways to read data from a CSV file. The following are the most +common ways: + +- Native reading +- the `CSV module <https://docs.python.org/3/library/csv.html>`_ +- the `numpy module <https://www.numpy.org>`_ +- the `pandas module <https://pandas.pydata.org/>`_ + +In this chapter, you will learn to use pandas to read and filter CSV data. +In addition, you could pass the data file through a command-line option to your +script. + +The following python script, :code:`main.py`, demonstrates how to do it: + +.. literalinclude:: datavisualize1/main.py + :language: python + :linenos: + :lines: 3- + +The Python script uses the :code:`argparse` module to accept and parse input +from the command line. It then uses the input, which in this case is the filename, +to read and print data to the prompt. + +Try running the script in the following way to check if you get desired output: + +:: + + $python datavisualize1/main.py -f all_hour.csv + time latitude longitude depth ... magNst status locationSource magSource + 0 2019-01-10T12:11:24.810Z 34.128166 -117.775497 4.46 ... 6.0 automatic ci ci + 1 2019-01-10T12:04:26.320Z 19.443333 -155.615997 0.72 ... 6.0 automatic hv hv + 2 2019-01-10T11:57:48.980Z 33.322500 -116.393167 4.84 ... 11.0 automatic ci ci + 3 2019-01-10T11:52:09.490Z 38.835667 -122.836670 1.28 ... 7.0 automatic nc nc + 4 2019-01-10T11:25:44.854Z 65.108200 -149.370100 20.60 ... NaN automatic ak ak + 5 2019-01-10T11:25:23.786Z 69.151800 -144.497700 10.40 ... NaN reviewed ak ak + 6 2019-01-10T11:16:11.761Z 61.331800 -150.070800 20.10 ... NaN automatic ak ak + + [7 rows x 22 columns] diff --git a/sources/pyside6/doc/tutorials/debugging/mixed_debugging.rst b/sources/pyside6/doc/tutorials/debugging/mixed_debugging.rst new file mode 100644 index 000000000..85c326347 --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/mixed_debugging.rst @@ -0,0 +1,126 @@ +How To Debug a C++ Extension of a PySide6 Application? +****************************************************** + +When debugging PySide code, very often you would also like to debug the +corresponding C++ extension of the PySide module. This is done by attaching your +debugger to the Python interpreter. In this tutorial, we are going to take you +through a comprehensive guide in building Qt 6, using the built Qt 6 to build +PySide6, and then starting a debugging process in either *Qt Creator* or VSCode. + +With VSCode, you should be able to see the combined call stacks for both C++ and +Python together. With *Qt Creator*, unfortunately you would only be able to +debug the native C++ code of the PySide module; that is you won't be able to set +breakpoints inside the Python code. + +.. note:: This tutorial is created on Ubuntu 20.04 LTS with the debugger as GDB. + As such, this tutorial is mainly focused on Linux users. Nevertheless, links to + setup everything in other platforms are mentioned along with each + sub-section. + +Let's get started. + +Install All The Library Dependencies Based on Your Platform +----------------------------------------------------------- + +.. code-block:: bash + + sudo apt install libfontconfig1-dev libfreetype6-dev \ + libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev \ + libxi-dev libxrender-dev libxcb1-dev libxcb-glx0-dev \ + libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev \ + libxcb-icccm4-dev libxcb-sync-dev libxcb-xfixes0-dev \ + libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0-dev \ + libxcb-util-dev libxcb-xinerama0-dev libxcb-xkb-dev \ + libxkbcommon-dev libxkbcommon-x11-dev libatspi2.0-dev \ + libopengl0 -y + +If you have to use the Qt Multimedia module, you have to install gstreamer also. + +.. code-block:: bash + + sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc \ + gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl \ + gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio + +Generally, any information on missing packages can be found by inspecting the +config.summary in you CMake build folder. + +.. note:: For other platforms install the same packages using the instructions + mentioned here `Qt Install on Linux <https://doc.qt.io/qt-6/linux-requirements.html>`_ + +Build Qt +-------- + +This is an optional step in case you only want to debug the CPython bindings or if you have DEBUG_SYMBOLS for Qt 6 already. + +There are multiple ways to build Qt - configure script or manually with CMake. +Find the build system information `Qt 6 Build System +<https://www.qt.io/blog/qt-6-build-system>`_. + +1. Get the source code. + + .. code-block:: bash + + git clone git://code.qt.io/qt/qt5.git + # Get submodules + cd qt5 + perl init-repository + +2. Once you have the source code, the next step is to generate the build using + CMake, then building and installing it. + + .. code-block:: bash + + cmake -GNinja -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX=/path/to/install/Qt -DBUILD_qtwebengine=OFF .. + cmake --build . --parallel + cmake --install . + + As you notice here, we are skipping the Qt WebEngine module because this greatly + decreases the build time. However, PySide6 supports Qt WebEngine + features. So feel free to add it, if you need it. + +More instructions on building Qt 6 can be found in the following links: + +* https://wiki.qt.io/Building_Qt_6_from_Git +* https://code.qt.io/cgit/qt/qtbase.git/tree/cmake/README.md +* https://code.qt.io/cgit/qt/qtbase.git/tree/cmake/configure-cmake-mapping.md + +Add the **bin** and **lib** path to the environment variables +-------------------------------------------------------------- + +.. code-block:: bash + + export PATH="/path/to/custom/qt/bin:$PATH" + export LD_LIBRARY_PATH="/path/to/custom/qt/lib:$LD_LIBRARY_PATH" + +Build PySide6 using the Qt 6 that you built earlier +---------------------------------------------------- + +Follow the steps mentioned `Getting Started - Qt for Python +<https://doc.qt.io/qtforpython/gettingstarted/index.html>`_ + +You may manually select the modules to install using the ``--module-subset`` cli +argument for `setup.py`. This was my installation script + +.. code-block:: bash + + python setup.py install --qpaths=/path/to/qpaths --debug \ + --ignore-git --reuse-build + +It is recommended to use a Python virtual environment rather than installing in the global Python. + +Debugging the process using your preferred IDE +---------------------------------------------- + +The following sections guide you through the setup for *Qt Creator* or VSCode. + +.. toctree:: + :glob: + :titlesonly: + + qtcreator/qtcreator + vscode/vscode diff --git a/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst b/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst new file mode 100644 index 000000000..223e608fc --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/qml_debugging.rst @@ -0,0 +1,32 @@ +Using Qt Creator's QML Debugger for a PySide6 QML Application +************************************************************* + +Besides the C++ debugger, *Qt Creator* provides a `QML debugger`_ which lets you +inspect JavaScript code. It works by connecting to a socket server run by the +``QmlEngine`` instance. The port is passed on the command line. To enable it, +add the below code to your QML application: + +.. code-block:: python + + from argparse import ArgumentParser, RawTextHelpFormatter + + ... + + if __name__ == "__main__": + argument_parser = ArgumentParser(...) + argument_parser.add_argument("-qmljsdebugger", action="store", + help="Enable QML debugging") + options = argument_parser.parse_args() + if options.qmljsdebugger: + QQmlDebuggingEnabler.enableDebugging(True) + app = QApplication(sys.argv) + + +For instructions on how to use the QML debugger, see +`Debugging a Qt Quick Example Application`_. + +.. note:: The code should be removed or disabled when shipping the application + as it poses a security risk. + +.. _`QML debugger`: https://doc.qt.io/qtcreator/creator-debugging-qml.html +.. _`Debugging a Qt Quick Example Application`: https://doc.qt.io/qtcreator/creator-qml-debugging-example.html diff --git a/sources/pyside6/doc/tutorials/debugging/qtcreator/breakpoint_cpp.png b/sources/pyside6/doc/tutorials/debugging/qtcreator/breakpoint_cpp.png Binary files differnew file mode 100644 index 000000000..c8197118e --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/qtcreator/breakpoint_cpp.png diff --git a/sources/pyside6/doc/tutorials/debugging/qtcreator/custom_executable_create.png b/sources/pyside6/doc/tutorials/debugging/qtcreator/custom_executable_create.png Binary files differnew file mode 100644 index 000000000..8362c3310 --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/qtcreator/custom_executable_create.png diff --git a/sources/pyside6/doc/tutorials/debugging/qtcreator/custom_executable_run_config.png b/sources/pyside6/doc/tutorials/debugging/qtcreator/custom_executable_run_config.png Binary files differnew file mode 100644 index 000000000..4f0bbe3d4 --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/qtcreator/custom_executable_run_config.png diff --git a/sources/pyside6/doc/tutorials/debugging/qtcreator/qtcreator.rst b/sources/pyside6/doc/tutorials/debugging/qtcreator/qtcreator.rst new file mode 100644 index 000000000..a78a67fad --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/qtcreator/qtcreator.rst @@ -0,0 +1,39 @@ +Debugging PySide with Qt Creator (Linux) +**************************************** + +As opposed to VSCode, presently *Qt Creator* does not support mixed mode debugging. +However, we can debug the C++ implementation of the corresponding Python PySide +code. Unlike VSCode, *Qt Creator* provides a very easy interface to attach GDB to +the Python interpreter. It saves you from doing all the extra configuration +steps, that have to be done with VSCode. + +Here are the steps: + +1. Set a breakpoint on the C++ code. + +2. Go to Projects -> Run -> Run Configuration -> Add. This is going to open a + new window shown below. + + .. image:: custom_executable_create.png + :alt: creation of custom executable + :align: center + +3. Click on Custom Executable and `Create` a new configuration. Feed in the + details like shown below. + + .. image:: custom_executable_run_config.png + :alt: run configuration of custom executable + :align: center + +4. Debug -> Start Debugging -> Start Debugging Without Deployment. + + .. image:: start_debugging_without_deployment.png + :alt: start debugging without deployment + :align: center + +You will now hit you breakpoint and can start debugging your code. + +.. image:: breakpoint_cpp.png + :alt: breakpoint cpp + :align: center + diff --git a/sources/pyside6/doc/tutorials/debugging/qtcreator/start_debugging_without_deployment.png b/sources/pyside6/doc/tutorials/debugging/qtcreator/start_debugging_without_deployment.png Binary files differnew file mode 100644 index 000000000..9ce0688d3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/qtcreator/start_debugging_without_deployment.png diff --git a/sources/pyside6/doc/tutorials/debugging/vscode/audioformat_cpp.png b/sources/pyside6/doc/tutorials/debugging/vscode/audioformat_cpp.png Binary files differnew file mode 100644 index 000000000..57577eea4 --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/vscode/audioformat_cpp.png diff --git a/sources/pyside6/doc/tutorials/debugging/vscode/audioformat_wrapper.png b/sources/pyside6/doc/tutorials/debugging/vscode/audioformat_wrapper.png Binary files differnew file mode 100644 index 000000000..319577fea --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/vscode/audioformat_wrapper.png diff --git a/sources/pyside6/doc/tutorials/debugging/vscode/breakpoint_gdb.png b/sources/pyside6/doc/tutorials/debugging/vscode/breakpoint_gdb.png Binary files differnew file mode 100644 index 000000000..b9bc4acde --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/vscode/breakpoint_gdb.png diff --git a/sources/pyside6/doc/tutorials/debugging/vscode/find_process_gdb.png b/sources/pyside6/doc/tutorials/debugging/vscode/find_process_gdb.png Binary files differnew file mode 100644 index 000000000..65176d9bb --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/vscode/find_process_gdb.png diff --git a/sources/pyside6/doc/tutorials/debugging/vscode/python_set_interpreter.png b/sources/pyside6/doc/tutorials/debugging/vscode/python_set_interpreter.png Binary files differnew file mode 100644 index 000000000..1a26c9d9c --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/vscode/python_set_interpreter.png diff --git a/sources/pyside6/doc/tutorials/debugging/vscode/vscode.rst b/sources/pyside6/doc/tutorials/debugging/vscode/vscode.rst new file mode 100644 index 000000000..b2a527b0e --- /dev/null +++ b/sources/pyside6/doc/tutorials/debugging/vscode/vscode.rst @@ -0,0 +1,192 @@ +Debugging PySide with VSCode (Linux + Windows) +********************************************** + +VSCode enables you to use more than one debugger in a single debugging session. +This means that we can use Python PDB and GDB (or the MSVC debugger for Windows) +in a single session. With VSCode you would be able to do the following: + +* See the call stacks for both Python and C++ together. +* Put breakpoints in both the Python and the C++ code. +* Step from Python to C++ code and vice versa. + +For Windows, see :ref:`creating_windows_debug_builds`. + +Let's get started with setting up everything and debugging a Python process. + +Setting the Python interpreter +------------------------------ + +In order to debug Python code, it is necessary to set the correct Python +interpreter in VSCode - this will ensure that all Python integrations of VSCode +use the same interpreter. However, this does not affect C++ debugging, and the +Python executable path must be set for the corresponding launch target +separately (see the section below). + +To set the Python interpreter, open a Python file and click the corresponding +option on the right side of the VSCode status bar, which should look similar to +this: + +.. image:: python_set_interpreter.png + :alt: set Python interpreter + :align: center + +Alternatively, open the VSCode command palette (F1 or Ctrl + Shift + P) and +search for "Python: Select Interpreter". + +Creating Configurations in launch.json +-------------------------------------- + +``Run -> Add Configuration -> Python -> Python File`` + +This should create a launch.json file which looks like this: + +.. code-block:: javascript + + { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] + } + +It should already consist of a configuration named "Python: Current File", +which allows us to debug the current open Python file. + +Now, we need to add a configuration to attach the C++ debugger to the Python +process that is already running in debug mode. If you have the C/C++ extension +installed and the appropriate debugger for your system, VSCode should be able +to automatically offer to add a configuration. On Linux, this is suggested with +the name + +* "C/C++: (gdb) Attach" + +and on Windows with the name + +* "C/C++: (Windows) Attach" + +Your launch.json should now look like this on Linux: + +.. code-block:: javascript + + { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "(gdb) Attach", + "type": "cppdbg", + "request": "attach", + "program": "/path/to/python", + "processId": "${command:pickProcess}", + "MIMode": "gdb", "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] + } + +And like this on Windows: + +.. code-block:: javascript + + { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "(Windows) Attach", + "type": "cppvsdbg", + "request": "attach", + "processId": "${command:pickProcess}", + } + ] + } + +For Linux, also make sure that the value of "program" refers to your Python +interpreter inside your virtual environment (for Windows this is not needed). +We need the processId to attach the gdb debugger to the process. With +"${command:pickProcess}", we find the processId on the go, as we will see later. + +Now, we are ready to debug. + +Debug The Process +----------------- + +1. Set a breakpoint in the Python code. + +2. Go to ``Run And Debug`` (Ctrl + Shift + D) and run the "Python: Current File" + by clicking the run symbol (green right-arrow). This will hit the breakpoint + and will halt the Python debugger. + +3. Using the drop-down menu change from "Python: + Current File" to "(gdb) Attach" or "(Windows) Attach". Your setup should now + look like this. + + .. image:: breakpoint_gdb.png + :alt: breakpoint before attach gdb + :align: center + +4. Run "(gdb) Attach" or "(Windows) Attach" and this should ask you for the + processId of the Python process to which you want to attach the C++ debugger. + VSCode also lets you search for the process by its name. + + .. tip:: You can find the processId by running ``ps aux | grep python`` + + .. image:: find_process_gdb.png + :alt: find process vscode + :align: center + +5. VSCode might now ask you for superuser permissions. In that case, type 'y' + and enter your password. + + .. code-block:: bash + + Superuser access is required to attach to a process. Attaching as + superuser can potentially harm your computer. Do you want to continue? + [y/N]_ + +6. That is it. You should now be able to hit the breakpoints that you have set + on the C++ counterparts. + + .. figure:: audioformat_wrapper.png + :alt: Breakpoint set on the shiboken wrapper class + :align: left + + Breakpoint set on the shiboken wrapper class + + .. figure:: audioformat_cpp.png + :alt: Breakpoint set on C++ implementation + :align: left + + Breakpoint set on C++ implementation diff --git a/sources/pyside6/doc/tutorials/expenses/expenses.rst b/sources/pyside6/doc/tutorials/expenses/expenses.rst new file mode 100644 index 000000000..2064488ae --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/expenses.rst @@ -0,0 +1,285 @@ +Expenses Tool Tutorial +====================== + +In this tutorial you will learn the following concepts: + * creating user interfaces programatically, + * layouts and widgets, + * overloading Qt classes, + * connecting signal and slots, + * interacting with QWidgets, + * and building your own application. + +The requirements: + * A simple window for the application + (`QMainWindow <https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMainWindow.html>`_). + * A table to keep track of the expenses + (`QTableWidget <https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html>`_). + * Two input fields to add expense information + (`QLineEdit <https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html>`_). + * Buttons to add information to the table, plot data, clear table, and exit the application + (`QPushButton <https://doc.qt.io/qtforpython/PySide6/QtWidgets/QPushButton.html>`_). + * A verification step to avoid invalid data entry. + * A chart to visualize the expense data + (`QChart <https://doc.qt.io/qtforpython/PySide6/QtCharts/QChart.html>`_) that will + be embedded in a chart view + (`QChartView <https://doc.qt.io/qtforpython/PySide6/QtCharts/QChartView.html>`_). + +Empty window +------------ + +The base structure for a `QApplication` is located inside the `if __name__ == "__main__":` +code block. + +.. code-block:: python + :linenos: + + if __name__ == "__main__": + app = QApplication([]) + # ... + sys.exit(app.exec()) + +Now, to start the development, create an empty window called `MainWindow`. +You could do that by defining a class that inherits from `QMainWindow`. + +.. literalinclude:: steps/01-expenses.py + :linenos: + :lines: 8-22 + :emphasize-lines: 1-4 + +Now that our class is defined, create an instance of it and call `show()`. + +.. literalinclude:: steps/01-expenses.py + :linenos: + :lines: 8-22 + :emphasize-lines: 10-12 + +Menu bar +-------- + +Using a `QMainWindow` gives some features for free, among them a *menu bar*. To use it, you need +to call the method `menuBar()` and populate it inside the `MainWindow` class. + +.. literalinclude:: steps/02-expenses.py + :linenos: + :lines: 9-19 + :emphasize-lines: 10 + +Notice that the code snippet adds a *File* menu with the *Exit* option only. + +The *Exit* option must be connected to a slot that triggers the application to exit. We pass +``QWidget.close()`` here. After the last window has been closed, the application exits. + +Empty widget and data +--------------------- + +The `QMainWindow` enables us to set a central widget that will be displayed when showing the window +(`read more <https://doc.qt.io/qt-5/qmainwindow.html#details>`_). +This central widget could be another class derived from `QWidget`. + +Additionally, you will define example data to visualize later. + +.. literalinclude:: steps/04-expenses.py + :linenos: + :lines: 8-15 + +With the `Widget` class in place, modify `MainWindow`'s initialization code + +.. literalinclude:: steps/04-expenses.py + :linenos: + :lines: 37-40 + +Window layout +------------- + +Now that the main empty window is in place, you need to start adding widgets to achieve the main +goal of creating an expenses application. + +After declaring the example data, you can visualize it on a simple `QTableWidget`. To do so, you +will add this procedure to the `Widget` constructor. + +.. warning:: Only for the example purpose a QTableWidget will be used, + but for more performance-critical applications the combination + of a model and a QTableView is encouraged. + +.. literalinclude:: steps/05-expenses.py + :linenos: + :lines: 11-31 + +As you can see, the code also includes a `QHBoxLayout` that provides the container to place widgets +horizontally. + +Additionally, the `QTableWidget` allows for customizing it, like adding the labels for the two +columns that will be used, and to *stretch* the content to use the whole `Widget` space. + +The last line of code refers to *filling the table**, and the code to perform that task is +displayed below. + +.. literalinclude:: steps/05-expenses.py + :linenos: + :lines: 33-39 + +Having this process on a separate method is a good practice to leave the constructor more readable, +and to split the main functions of the class in independent processes. + + +Right side layout +----------------- + +Because the data that is being used is just an example, you are required to include a mechanism to +input items to the table, and extra buttons to clear the table's content, and also quit the +application. + +For input lines along with descriptive labels, you will use a `QFormLayout`. Then, +you will nest the form layout into a `QVBoxLayout` along with the buttons. + +.. literalinclude:: steps/06-expenses.py + :linenos: + :lines: 27-43 + +Leaving the table on the left side and these newly included widgets to the right side +will be just a matter to add a layout to our main `QHBoxLayout` as you saw in the previous +example: + +.. literalinclude:: steps/06-expenses.py + :linenos: + :lines: 45-48 + +The next step will be connecting those new buttons to slots. + + +Adding elements +--------------- + +Each `QPushButton` have a signal called *clicked*, that is emitted when you click on the button. +This will be more than enough for this example, but you can see other signals in the `official +documentation <https://doc.qt.io/qtforpython/PySide6/QtWidgets/QAbstractButton.html#signals>`_. + +.. literalinclude:: steps/07-expenses.py + :linenos: + :lines: 50-52 + +As you can see on the previous lines, we are connecting each *clicked* signal to different slots. +In this example slots are normal class methods in charge of perform a determined task associated +with our buttons. It is really important to decorate each method declaration with a `@Slot()`, +that way, PySide6 knows internally how to register them into Qt and they +will be invokable from `Signals` of QObjects when connected. + + +.. literalinclude:: steps/07-expenses.py + :linenos: + :lines: 57-82 + :emphasize-lines: 1, 23 + +Since these slots are methods, we can access the class variables, like our `QTableWidget` to +interact with it. + +The mechanism to add elements into the table is described as the following: + + * get the *description* and *price* from the fields, + * insert a new empty row to the table, + * set the values for the empty row in each column, + * clear the input text fields, + * include the global count of table rows. + +To exit the application you can use the `quit()` method of the unique `QApplication` instance, and +to clear the content of the table you can just set the table *row count*, and the internal count to +zero. + +Verification step +----------------- + +Adding information to the table needs to be a critical action that require a verification step +to avoid adding invalid information, for example, empty information. + +You can use a signal from `QLineEdit` called *textChanged* which will be emitted every +time something inside changes, i.e.: each key stroke. + +You can connect two different object's signal to the same slot, and this will be the case +for your current application: + +.. literalinclude:: steps/08-expenses.py + :linenos: + :lines: 57-58 + +The content of the *check_disable* slot will be really simple: + +.. literalinclude:: steps/08-expenses.py + :linenos: + :lines: 77-80 + +You have two options, write a verification based on the current value +of the string you retrieve, or manually get the whole content of both +`QLineEdit`. The second is preferred in this case, so you can verify +if the two inputs are not empty to enable the button *Add*. + +.. note:: Qt also provides a special class called + `QValidator <https://doc.qt.io/qtforpython/PySide6/QtGui/QValidator.html?highlight=qvalidator>`_ + that you can use to validate any input. + +Empty chart view +---------------- + +New items can be added to the table, and the visualization is so far +OK, but you can accomplish more by representing the data graphically. + +First you will include an empty `QChartView` placeholder into the right +side of your application. + +.. literalinclude:: steps/09-expenses.py + :linenos: + :lines: 30-32 + +Additionally the order of how you include widgets to the right +`QVBoxLayout` will also change. + +.. literalinclude:: steps/09-expenses.py + :linenos: + :lines: 46-54 + :emphasize-lines: 8 + +Notice that before we had a line with `self.right.addStretch()` +to fill up the vertical space between the *Add* and the *Clear* buttons, +but now, with the `QChartView` it will not be necessary. + +Also, you need include a *Plot* button if you want to do it on-demand. + +Full application +---------------- + +For the final step, you will need to connect the *Plot* button +to a slot that creates a chart and includes it into your `QChartView`. + +.. literalinclude:: steps/10-expenses.py + :linenos: + :lines: 62-67 + :emphasize-lines: 3 + +That is nothing new, since you already did it for the other buttons, +but now take a look at how to create a chart and include it into +your `QChartView`. + +.. literalinclude:: steps/10-expenses.py + :linenos: + :lines: 95-107 + +The following steps show how to fill a `QPieSeries`: + + * create a `QPieSeries`, + * iterate over the table row IDs, + * get the items at the *i* position, + * add those values to the *series*. + +Once the series has been populated with our data, you create a new `QChart`, +add the series on it, and optionally set an alignment for the legend. + +The final line `self.chart_view.setChart(chart)` is in charge of bringing +your newly created chart to the `QChartView`. + +The application will look like this: + +.. image:: expenses_tool.png + +And now you can see the whole code: + +.. literalinclude:: main.py + :linenos: diff --git a/sources/pyside6/doc/tutorials/expenses/expenses_tool.png b/sources/pyside6/doc/tutorials/expenses/expenses_tool.png Binary files differnew file mode 100644 index 000000000..7a6f6d1f0 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/expenses_tool.png diff --git a/sources/pyside6/doc/tutorials/expenses/main.py b/sources/pyside6/doc/tutorials/expenses/main.py new file mode 100644 index 000000000..a3a998470 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/main.py @@ -0,0 +1,153 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import Qt, Slot +from PySide6.QtGui import QPainter +from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView, + QHBoxLayout, QLineEdit, QMainWindow, + QPushButton, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) +from PySide6.QtCharts import QChartView, QPieSeries, QChart + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Chart + self.chart_view = QChartView() + self.chart_view.setRenderHint(QPainter.Antialiasing) + + # Right + self.description = QLineEdit() + self.description.setClearButtonEnabled(True) + self.price = QLineEdit() + self.price.setClearButtonEnabled(True) + + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + self.plot = QPushButton("Plot") + + # Disabling 'Add' button + self.add.setEnabled(False) + + form_layout = QFormLayout() + form_layout.addRow("Description", self.description) + form_layout.addRow("Price", self.price) + self.right = QVBoxLayout() + self.right.addLayout(form_layout) + self.right.addWidget(self.add) + self.right.addWidget(self.plot) + self.right.addWidget(self.chart_view) + self.right.addWidget(self.clear) + + # QWidget Layout + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.table) + self.layout.addLayout(self.right) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.plot.clicked.connect(self.plot_data) + self.clear.clicked.connect(self.clear_table) + self.description.textChanged.connect(self.check_disable) + self.price.textChanged.connect(self.check_disable) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text() + price = float(self.price.text()) + + self.table.insertRow(self.items) + description_item = QTableWidgetItem(des) + price_item = QTableWidgetItem(f"{price:.2f}") + price_item.setTextAlignment(Qt.AlignRight) + + self.table.setItem(self.items, 0, description_item) + self.table.setItem(self.items, 1, price_item) + + self.description.clear() + self.price.clear() + + self.items += 1 + + @Slot() + def check_disable(self, s): + enabled = bool(self.description.text() and self.price.text()) + self.add.setEnabled(enabled) + + @Slot() + def plot_data(self): + # Get table information + series = QPieSeries() + for i in range(self.table.rowCount()): + text = self.table.item(i, 0).text() + number = float(self.table.item(i, 1).text()) + series.append(text, number) + + chart = QChart() + chart.addSeries(series) + chart.legend().setAlignment(Qt.AlignLeft) + self.chart_view.setChart(chart) + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + description_item = QTableWidgetItem(desc) + price_item = QTableWidgetItem(f"{price:.2f}") + price_item.setTextAlignment(Qt.AlignRight) + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, description_item) + self.table.setItem(self.items, 1, price_item) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.setRowCount(0) + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/main_snake_prop.py b/sources/pyside6/doc/tutorials/expenses/main_snake_prop.py new file mode 100644 index 000000000..055544409 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/main_snake_prop.py @@ -0,0 +1,177 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import QMargins, Qt, Slot, QSize +from PySide6.QtGui import QPainter +from PySide6.QtWidgets import (QAction, QApplication, QHeaderView, QHBoxLayout, QLabel, QLineEdit, + QMainWindow, QPushButton, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) +from PySide6.QtCharts import QtCharts + +from __feature__ import snake_case, true_property + + +class Widget(QWidget): + def __init__(self): + QWidget.__init__(self) + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.column_count = 2 + self.table.horizontal_header_labels = ["Description", "Price"] + self.table.horizontal_header().section_resize_mode = QHeaderView.Stretch + + # Chart + self.chart_view = QtCharts.QChartView() + self.chart_view.render_hint = QPainter.Antialiasing + + # Right + self.description = QLineEdit() + self.price = QLineEdit() + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + self.quit = QPushButton("Quit") + self.plot = QPushButton("Plot") + + # Disabling 'Add' button + self.add.enabled = False + + self.right = QVBoxLayout() + + self.right.contents_margins = QMargins(10, 10, 10, 10) + self.right.add_widget(QLabel("Description")) + self.right.add_widget(self.description) + self.right.add_widget(QLabel("Price")) + self.right.add_widget(self.price) + self.right.add_widget(self.add) + self.right.add_widget(self.plot) + self.right.add_widget(self.chart_view) + self.right.add_widget(self.clear) + self.right.add_widget(self.quit) + + # QWidget Layout + self.layout = QHBoxLayout() + + #self.table_view.setSizePolicy(size) + self.layout.add_widget(self.table) + self.layout.form_layout(self.right) + + # Set the layout to the QWidget + self.set_layout(self.layout) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.quit.clicked.connect(self.quit_application) + self.plot.clicked.connect(self.plot_data) + self.clear.clicked.connect(self.clear_table) + self.description.textChanged[str].connect(self.check_disable) + self.price.textChanged[str].connect(self.check_disable) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text + price = self.price.text + + try: + price_item = QTableWidgetItem(f"{float(price):.2f}") + price_item.text_alignment = Qt.AlignRight + + self.table.insert_row(self.items) + description_item = QTableWidgetItem(des) + + self.table.set_item(self.items, 0, description_item) + self.table.set_item(self.items, 1, price_item) + + self.description.text = "" + self.price.text = "" + + self.items += 1 + except ValueError: + print("Wrong price", price) + + @Slot() + def check_disable(self, s): + if not self.description.text or not self.price.text: + self.add.enabled = False + else: + self.add.enabled = True + + @Slot() + def plot_data(self): + # Get table information + series = QtCharts.QPieSeries() + for i in range(self.table.row_count): + text = self.table.item(i, 0).text() + number = float(self.table.item(i, 1).text()) + series.append(text, number) + + chart = QtCharts.QChart() + chart.add_series(series) + chart.legend().alignment = Qt.AlignLeft + self.chart_view.set_chart(chart) + + @Slot() + def quit_application(self): + QApplication.quit() + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + description_item = QTableWidgetItem(desc) + price_item = QTableWidgetItem(f"{price:.2f}") + price_item.text_alignment = Qt.AlignRight + self.table.insert_row(self.items) + self.table.set_item(self.items, 0, description_item) + self.table.set_item(self.items, 1, price_item) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.row_count = 0 + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + QMainWindow.__init__(self) + self.window_title = "Tutorial" + + # Menu + self.menu = self.menu_bar() + self.file_menu = self.menu.add_menu("File") + + # Exit QAction + exit_action = QAction("Exit", self) + exit_action.shortcut = "Ctrl+Q" + exit_action.triggered.connect(self.exit_app) + + self.file_menu.add_action(exit_action) + self.set_central_widget(widget) + + @Slot() + def exit_app(self, checked): + QApplication.quit() + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.size = QSize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/01-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/01-expenses.py new file mode 100644 index 000000000..27f2aef65 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/01-expenses.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.QtWidgets import QApplication, QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Tutorial") + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + + window = MainWindow() + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/02-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/02-expenses.py new file mode 100644 index 000000000..039b818df --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/02-expenses.py @@ -0,0 +1,31 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import QApplication, QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + + window = MainWindow() + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/03-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/03-expenses.py new file mode 100644 index 000000000..039b818df --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/03-expenses.py @@ -0,0 +1,31 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import QApplication, QMainWindow + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + + window = MainWindow() + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/04-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/04-expenses.py new file mode 100644 index 000000000..6723690a8 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/04-expenses.py @@ -0,0 +1,45 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import QApplication, QMainWindow, QWidget + + +class Widget(QWidget): + def __init__(self): + super().__init__() + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/05-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/05-expenses.py new file mode 100644 index 000000000..df0362fde --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/05-expenses.py @@ -0,0 +1,69 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import (QApplication, QHeaderView, QHBoxLayout, + QMainWindow, QTableWidget, QTableWidgetItem, + QWidget) + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # QWidget Layout + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.table) + + # Fill example data + self.fill_table() + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(desc)) + self.table.setItem(self.items, 1, QTableWidgetItem(str(price))) + self.items += 1 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/06-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/06-expenses.py new file mode 100644 index 000000000..d19a6220f --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/06-expenses.py @@ -0,0 +1,89 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView, + QHBoxLayout, QLineEdit, QMainWindow, QPushButton, + QTableWidget, QTableWidgetItem, QVBoxLayout, + QWidget) + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Right + self.description = QLineEdit() + self.description.setClearButtonEnabled(True) + self.price = QLineEdit() + self.price.setClearButtonEnabled(True) + + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + + form_layout = QFormLayout() + form_layout.addRow("Description", self.description) + form_layout.addRow("Price", self.price) + self.right = QVBoxLayout() + self.right.addLayout(form_layout) + self.right.addWidget(self.add) + self.right.addStretch() + self.right.addWidget(self.clear) + + # QWidget Layout + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.table) + self.layout.addLayout(self.right) + + # Fill example data + self.fill_table() + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(desc)) + self.table.setItem(self.items, 1, QTableWidgetItem(str(price))) + self.items += 1 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/07-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/07-expenses.py new file mode 100644 index 000000000..b1ed3fecc --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/07-expenses.py @@ -0,0 +1,112 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import Slot +from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView, + QHBoxLayout, QLineEdit, QMainWindow, QPushButton, + QTableWidget, QTableWidgetItem, QVBoxLayout, + QWidget) + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Right + self.description = QLineEdit() + self.description.setClearButtonEnabled(True) + self.price = QLineEdit() + self.price.setClearButtonEnabled(True) + + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + + form_layout = QFormLayout() + form_layout.addRow("Description", self.description) + form_layout.addRow("Price", self.price) + self.right = QVBoxLayout() + self.right.addLayout(form_layout) + self.right.addWidget(self.add) + self.right.addStretch() + + # QWidget Layout + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.table) + self.layout.addLayout(self.right) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.clear.clicked.connect(self.clear_table) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text() + price = self.price.text() + + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(des)) + self.table.setItem(self.items, 1, QTableWidgetItem(price)) + + self.description.clear() + self.price.clear() + + self.items += 1 + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(desc)) + self.table.setItem(self.items, 1, QTableWidgetItem(str(price))) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.setRowCount(0) + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/08-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/08-expenses.py new file mode 100644 index 000000000..0e7e21a32 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/08-expenses.py @@ -0,0 +1,123 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import Slot +from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView, + QHBoxLayout, QLineEdit, QMainWindow, QPushButton, + QTableWidget, QTableWidgetItem, QVBoxLayout, + QWidget) + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Right + self.description = QLineEdit() + self.description.setClearButtonEnabled(True) + self.price = QLineEdit() + self.price.setClearButtonEnabled(True) + + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + + # Disabling 'Add' button + self.add.setEnabled(False) + + form_layout = QFormLayout() + form_layout.addRow("Description", self.description) + form_layout.addRow("Price", self.price) + self.right = QVBoxLayout() + self.right.addLayout(form_layout) + self.right.addWidget(self.add) + self.right.addStretch() + self.right.addWidget(self.clear) + + # QWidget Layout + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.table) + self.layout.addLayout(self.right) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.clear.clicked.connect(self.clear_table) + self.description.textChanged.connect(self.check_disable) + self.price.textChanged.connect(self.check_disable) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text() + price = self.price.text() + + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(des)) + self.table.setItem(self.items, 1, QTableWidgetItem(price)) + + self.description.clear() + self.price.clear() + + self.items += 1 + + @Slot() + def check_disable(self, s): + enabled = bool(self.description.text() and self.price.text()) + self.add.setEnabled(enabled) + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(desc)) + self.table.setItem(self.items, 1, QTableWidgetItem(str(price))) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.setRowCount(0) + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/09-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/09-expenses.py new file mode 100644 index 000000000..279a46512 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/09-expenses.py @@ -0,0 +1,131 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import Slot +from PySide6.QtGui import QPainter +from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView, + QHBoxLayout, QLineEdit, QMainWindow, + QPushButton, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) +from PySide6.QtCharts import QChartView + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Chart + self.chart_view = QChartView() + self.chart_view.setRenderHint(QPainter.Antialiasing) + + # Right + self.description = QLineEdit() + self.description.setClearButtonEnabled(True) + self.price = QLineEdit() + self.price.setClearButtonEnabled(True) + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + self.plot = QPushButton("Plot") + + # Disabling 'Add' button + self.add.setEnabled(False) + + form_layout = QFormLayout() + form_layout.addRow("Description", self.description) + form_layout.addRow("Price", self.price) + self.right = QVBoxLayout() + self.right.addLayout(form_layout) + self.right.addWidget(self.add) + self.right.addWidget(self.plot) + self.right.addWidget(self.chart_view) + self.right.addWidget(self.clear) + + # QWidget Layout + self.layout = QHBoxLayout(self) + + self.layout.addWidget(self.table) + self.layout.addLayout(self.right) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.clear.clicked.connect(self.clear_table) + self.description.textChanged.connect(self.check_disable) + self.price.textChanged.connect(self.check_disable) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text() + price = self.price.text() + + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(des)) + self.table.setItem(self.items, 1, QTableWidgetItem(price)) + + self.description.clear() + self.price.clear() + + self.items += 1 + + @Slot() + def check_disable(self, s): + enabled = bool(self.description.text() and self.price.text()) + self.add.setEnabled(enabled) + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, QTableWidgetItem(desc)) + self.table.setItem(self.items, 1, QTableWidgetItem(str(price))) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.setRowCount(0) + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/expenses/steps/10-expenses.py b/sources/pyside6/doc/tutorials/expenses/steps/10-expenses.py new file mode 100644 index 000000000..a3a998470 --- /dev/null +++ b/sources/pyside6/doc/tutorials/expenses/steps/10-expenses.py @@ -0,0 +1,153 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from PySide6.QtCore import Qt, Slot +from PySide6.QtGui import QPainter +from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView, + QHBoxLayout, QLineEdit, QMainWindow, + QPushButton, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) +from PySide6.QtCharts import QChartView, QPieSeries, QChart + + +class Widget(QWidget): + def __init__(self): + super().__init__() + self.items = 0 + + # Example data + self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0, + "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85, + "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120} + + # Left + self.table = QTableWidget() + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels(["Description", "Price"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Chart + self.chart_view = QChartView() + self.chart_view.setRenderHint(QPainter.Antialiasing) + + # Right + self.description = QLineEdit() + self.description.setClearButtonEnabled(True) + self.price = QLineEdit() + self.price.setClearButtonEnabled(True) + + self.add = QPushButton("Add") + self.clear = QPushButton("Clear") + self.plot = QPushButton("Plot") + + # Disabling 'Add' button + self.add.setEnabled(False) + + form_layout = QFormLayout() + form_layout.addRow("Description", self.description) + form_layout.addRow("Price", self.price) + self.right = QVBoxLayout() + self.right.addLayout(form_layout) + self.right.addWidget(self.add) + self.right.addWidget(self.plot) + self.right.addWidget(self.chart_view) + self.right.addWidget(self.clear) + + # QWidget Layout + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.table) + self.layout.addLayout(self.right) + + # Signals and Slots + self.add.clicked.connect(self.add_element) + self.plot.clicked.connect(self.plot_data) + self.clear.clicked.connect(self.clear_table) + self.description.textChanged.connect(self.check_disable) + self.price.textChanged.connect(self.check_disable) + + # Fill example data + self.fill_table() + + @Slot() + def add_element(self): + des = self.description.text() + price = float(self.price.text()) + + self.table.insertRow(self.items) + description_item = QTableWidgetItem(des) + price_item = QTableWidgetItem(f"{price:.2f}") + price_item.setTextAlignment(Qt.AlignRight) + + self.table.setItem(self.items, 0, description_item) + self.table.setItem(self.items, 1, price_item) + + self.description.clear() + self.price.clear() + + self.items += 1 + + @Slot() + def check_disable(self, s): + enabled = bool(self.description.text() and self.price.text()) + self.add.setEnabled(enabled) + + @Slot() + def plot_data(self): + # Get table information + series = QPieSeries() + for i in range(self.table.rowCount()): + text = self.table.item(i, 0).text() + number = float(self.table.item(i, 1).text()) + series.append(text, number) + + chart = QChart() + chart.addSeries(series) + chart.legend().setAlignment(Qt.AlignLeft) + self.chart_view.setChart(chart) + + def fill_table(self, data=None): + data = self._data if not data else data + for desc, price in data.items(): + description_item = QTableWidgetItem(desc) + price_item = QTableWidgetItem(f"{price:.2f}") + price_item.setTextAlignment(Qt.AlignRight) + self.table.insertRow(self.items) + self.table.setItem(self.items, 0, description_item) + self.table.setItem(self.items, 1, price_item) + self.items += 1 + + @Slot() + def clear_table(self): + self.table.setRowCount(0) + self.items = 0 + + +class MainWindow(QMainWindow): + def __init__(self, widget): + super().__init__() + self.setWindowTitle("Tutorial") + + # Menu + self.menu = self.menuBar() + self.file_menu = self.menu.addMenu("File") + + # Exit QAction + exit_action = self.file_menu.addAction("Exit", self.close) + exit_action.setShortcut("Ctrl+Q") + + self.setCentralWidget(widget) + + +if __name__ == "__main__": + # Qt Application + app = QApplication(sys.argv) + # QWidget + widget = Widget() + # QMainWindow using QWidget as central widget + window = MainWindow(widget) + window.resize(800, 600) + window.show() + + # Execute application + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/Main.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/Main.qml new file mode 100644 index 000000000..faa9175d6 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/Main.qml @@ -0,0 +1,196 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import FileSystemModule + +pragma ComponentBehavior: Bound + +ApplicationWindow { + id: root + + property bool expandPath: false + property bool showLineNumbers: true + property string currentFilePath: "" + + width: 1100 + height: 600 + minimumWidth: 200 + minimumHeight: 100 + visible: true + color: Colors.background + flags: Qt.Window | Qt.FramelessWindowHint + title: qsTr("File System Explorer Example") + + function getInfoText() : string { + let out = root.currentFilePath + if (!out) + return qsTr("File System Explorer") + return root.expandPath ? out : out.substring(out.lastIndexOf("/") + 1, out.length) + } + + menuBar: MyMenuBar { + dragWindow: root + infoText: root.getInfoText() + MyMenu { + title: qsTr("File") + + Action { + text: qsTr("Increase Font") + shortcut: StandardKey.ZoomIn + onTriggered: editor.text.font.pixelSize += 1 + } + Action { + text: qsTr("Decrease Font") + shortcut: StandardKey.ZoomOut + onTriggered: editor.text.font.pixelSize -= 1 + } + Action { + text: root.showLineNumbers ? qsTr("Toggle Line Numbers OFF") + : qsTr("Toggle Line Numbers ON") + shortcut: "Ctrl+L" + onTriggered: root.showLineNumbers = !root.showLineNumbers + } + Action { + text: root.expandPath ? qsTr("Toggle Short Path") + : qsTr("Toggle Expand Path") + enabled: root.currentFilePath + onTriggered: root.expandPath = !root.expandPath + } + Action { + text: qsTr("Reset Filesystem") + enabled: sidebar.currentTabIndex === 1 + onTriggered: fileSystemView.rootIndex = undefined + } + Action { + text: qsTr("Exit") + onTriggered: Qt.exit(0) + shortcut: StandardKey.Quit + } + } + + MyMenu { + title: qsTr("Edit") + + Action { + text: qsTr("Cut") + shortcut: StandardKey.Cut + enabled: editor.text.selectedText.length > 0 + onTriggered: editor.text.cut() + } + Action { + text: qsTr("Copy") + shortcut: StandardKey.Copy + enabled: editor.text.selectedText.length > 0 + onTriggered: editor.text.copy() + } + Action { + text: qsTr("Paste") + shortcut: StandardKey.Paste + enabled: editor.text.canPaste + onTriggered: editor.text.paste() + } + Action { + text: qsTr("Select All") + shortcut: StandardKey.SelectAll + enabled: editor.text.length > 0 + onTriggered: editor.text.selectAll() + } + Action { + text: qsTr("Undo") + shortcut: StandardKey.Undo + enabled: editor.text.canUndo + onTriggered: editor.text.undo() + } + } + } + // Set up the layout of the main components in a row: + // [ Sidebar, Navigation, Editor ] + RowLayout { + anchors.fill: parent + spacing: 0 + + // Stores the buttons that navigate the application. + Sidebar { + id: sidebar + dragWindow: root + Layout.preferredWidth: 50 + Layout.fillHeight: true + } + + // Allows resizing parts of the UI. + SplitView { + Layout.fillWidth: true + Layout.fillHeight: true + // Customized handle to drag between the Navigation and the Editor. + handle: Rectangle { + implicitWidth: 10 + color: SplitHandle.pressed ? Colors.color2 : Colors.background + border.color: SplitHandle.hovered ? Colors.color2 : Colors.background + opacity: SplitHandle.hovered || navigationView.width < 15 ? 1.0 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 1400 + } + } + } + + Rectangle { + id: navigationView + color: Colors.surface1 + SplitView.preferredWidth: 250 + SplitView.fillHeight: true + // The stack-layout provides different views, based on the + // selected buttons inside the sidebar. + StackLayout { + anchors.fill: parent + currentIndex: sidebar.currentTabIndex > 1 ? 1 : sidebar.currentTabIndex + + // Shows the help text. + Text { + text: qsTr("This example shows how to use and visualize the file system.\n\n" + + "Customized Qt Quick Components have been used to achieve this look.\n\n" + + "You can edit the files but they won't be changed on the file system.\n\n" + + "Click on the folder icon to the left to get started.") + wrapMode: TextArea.Wrap + color: Colors.text + } + + // Shows the files on the file system. + FileSystemView { + id: fileSystemView + color: Colors.surface1 + onFileClicked: path => root.currentFilePath = path + } + } + } + + // The main view that contains the editor or the scheme-manager. + StackLayout { + currentIndex: sidebar.currentTabIndex > 1 ? 1 : 0 + + SplitView.fillWidth: true + SplitView.fillHeight: true + + Editor { + id: editor + showLineNumbers: root.showLineNumbers + currentFilePath: root.currentFilePath + } + + ColorScheme { + Layout.fillWidth: true + Layout.fillHeight: true + } + + } + } + } + + ResizeButton { + resizeWindow: root + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/app.qrc b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/app.qrc new file mode 100644 index 000000000..ccd9eefec --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/app.qrc @@ -0,0 +1,16 @@ +<RCC> + <qresource prefix="/qt/qml/FileSystemModule"> + <file>qmldir</file> + <file>Main.qml</file> + <file>qml/About.qml</file> + <file>qml/Editor.qml</file> + <file>qml/Colors.qml</file> + <file>qml/FileSystemView.qml</file> + <file>qml/Icon.qml</file> + <file>qml/MyMenu.qml</file> + <file>qml/MyMenuBar.qml</file> + <file>qml/ResizeButton.qml</file> + <file>qml/Sidebar.qml</file> + <file>qml/WindowDragHandler.qml</file> + </qresource> +</RCC> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons.qrc b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons.qrc new file mode 100644 index 000000000..69bddc018 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons.qrc @@ -0,0 +1,15 @@ +<RCC> + <qresource> + <file>icons/app_icon.svg</file> + <file>icons/folder_closed.svg</file> + <file>icons/folder_open.svg</file> + <file>icons/generic_file.svg</file> + <file>icons/globe.svg</file> + <file>icons/info_sign.svg</file> + <file>icons/leaf.svg</file> + <file>icons/light_bulb.svg</file> + <file>icons/qt_logo.svg</file> + <file>icons/read.svg</file> + <file>icons/resize.svg</file> + </qresource> +</RCC> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/app_icon.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/app_icon.svg new file mode 100644 index 000000000..5aae4221f --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/app_icon.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#EBDBB2" d="M13.25 8.5a.75.75 0 1 1-.75-.75.75.75 0 0 1 .75.75zM9.911 21.35l.816.578C10.819 21.798 13 18.666 13 13h-1a15.503 15.503 0 0 1-2.089 8.35zM4 6.703V10a2.002 2.002 0 0 1-2 2v1a2.002 2.002 0 0 1 2 2v3.297A3.707 3.707 0 0 0 7.703 22H9v-1H7.703A2.706 2.706 0 0 1 5 18.297V15a2.999 2.999 0 0 0-1.344-2.5A2.999 2.999 0 0 0 5 10V6.703A2.706 2.706 0 0 1 7.703 4H9V3H7.703A3.707 3.707 0 0 0 4 6.703zM20 10V6.703A3.707 3.707 0 0 0 16.297 3H15v1h1.297A2.706 2.706 0 0 1 19 6.703V10a2.999 2.999 0 0 0 1.344 2.5A2.999 2.999 0 0 0 19 15v3.297A2.706 2.706 0 0 1 16.297 21H15v1h1.297A3.707 3.707 0 0 0 20 18.297V15a2.002 2.002 0 0 1 2-2v-1a2.002 2.002 0 0 1-2-2z"/><path fill="none" d="M0 0h24v24H0z"/></svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/folder_closed.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/folder_closed.svg new file mode 100644 index 000000000..36f119c96 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/folder_closed.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-10 0 1792 1792" + id="svg51" + sodipodi:docname="folder_closed.svg" + width="1792" + height="1792" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs55" /> + <sodipodi:namedview + id="namedview53" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.45033482" + inkscape:cx="842.70632" + inkscape:cy="896" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg51" /> + <path + fill="currentColor" + d="m 1718,672 v 704 q 0,92 -66,158 -66,66 -158,66 H 278 q -92,0 -158,-66 -66,-66 -66,-158 V 416 q 0,-92 66,-158 66,-66 158,-66 h 320 q 92,0 158,66 66,66 66,158 v 32 h 672 q 92,0 158,66 66,66 66,158 z" + id="path49" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/folder_open.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/folder_open.svg new file mode 100644 index 000000000..daa55a7a1 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/folder_open.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-10 0 1792 1792" + id="svg139" + sodipodi:docname="folder_open.svg" + width="1792" + height="1792" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs143" /> + <sodipodi:namedview + id="namedview141" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.24358259" + inkscape:cx="149.84651" + inkscape:cy="1098.1901" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg139" /> + <path + fill="currentColor" + d="M 1590,1376 V 672 q 0,-40 -28,-68 -28,-28 -68,-28 H 790 q -40,0 -68,-28 -28,-28 -28,-68 v -64 q 0,-40 -28,-68 -28,-28 -68,-28 H 278 q -40,0 -68,28 -28,28 -28,68 v 960 q 0,40 28,68 28,28 68,28 h 1216 q 40,0 68,-28 28,-28 28,-68 z m 128,-704 v 704 q 0,92 -66,158 -66,66 -158,66 H 278 q -92,0 -158,-66 -66,-66 -66,-158 V 416 q 0,-92 66,-158 66,-66 158,-66 h 320 q 92,0 158,66 66,66 66,158 v 32 h 672 q 92,0 158,66 66,66 66,158 z" + id="path137" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/generic_file.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/generic_file.svg new file mode 100644 index 000000000..9c855676e --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/generic_file.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-10 0 1792 1792" + id="svg147" + sodipodi:docname="generic_file.svg" + width="1792" + height="1792" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs151" /> + <sodipodi:namedview + id="namedview149" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.12179129" + inkscape:cx="-578.85911" + inkscape:cy="1687.3127" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg147" /> + <path + fill="currentColor" + d="m 1586,476 q 14,14 28,36 H 1142 V 40 q 22,14 36,28 z m -476,164 h 544 v 1056 q 0,40 -28,68 -28,28 -68,28 H 214 q -40,0 -68,-28 -28,-28 -28,-68 V 96 Q 118,56 146,28 174,0 214,0 h 800 v 544 q 0,40 28,68 28,28 68,28 z m 160,736 v -64 q 0,-14 -9,-23 -9,-9 -23,-9 H 534 q -14,0 -23,9 -9,9 -9,23 v 64 q 0,14 9,23 9,9 23,9 h 704 q 14,0 23,-9 9,-9 9,-23 z m 0,-256 v -64 q 0,-14 -9,-23 -9,-9 -23,-9 H 534 q -14,0 -23,9 -9,9 -9,23 v 64 q 0,14 9,23 9,9 23,9 h 704 q 14,0 23,-9 9,-9 9,-23 z m 0,-256 v -64 q 0,-14 -9,-23 -9,-9 -23,-9 H 534 q -14,0 -23,9 -9,9 -9,23 v 64 q 0,14 9,23 9,9 23,9 h 704 q 14,0 23,-9 9,-9 9,-23 z" + id="path145" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/globe.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/globe.svg new file mode 100644 index 000000000..081433813 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/globe.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-10 0 1792 1792" + id="svg155" + sodipodi:docname="globe.svg" + width="1792" + height="1792" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs159" /> + <sodipodi:namedview + id="namedview157" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.12179129" + inkscape:cx="504.9622" + inkscape:cy="1720.1558" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg155" /> + <path + fill="currentColor" + d="m 886,128 q 209,0 385.5,103 176.5,103 279.5,279.5 103,176.5 103,385.5 0,209 -103,385.5 Q 1448,1458 1271.5,1561 1095,1664 886,1664 677,1664 500.5,1561 324,1458 221,1281.5 118,1105 118,896 118,687 221,510.5 324,334 500.5,231 677,128 886,128 Z m 274,521 q -2,1 -9.5,9.5 -7.5,8.5 -13.5,9.5 2,0 4.5,-5 2.5,-5 5,-11 2.5,-6 3.5,-7 6,-7 22,-15 14,-6 52,-12 34,-8 51,11 -2,-2 9.5,-13 11.5,-11 14.5,-12 3,-2 15,-4.5 12,-2.5 15,-7.5 l 2,-22 q -12,1 -17.5,-7 -5.5,-8 -6.5,-21 0,2 -6,8 0,-7 -4.5,-8 -4.5,-1 -11.5,1 -7,2 -9,1 -10,-3 -15,-7.5 -5,-4.5 -8,-16.5 -3,-12 -4,-15 -2,-5 -9.5,-10.5 -7.5,-5.5 -9.5,-10.5 -1,-2 -2.5,-5.5 -1.5,-3.5 -3,-6.5 -1.5,-3 -4,-5.5 -2.5,-2.5 -5.5,-2.5 -3,0 -7,5 -4,5 -7.5,10 -3.5,5 -4.5,5 -3,-2 -6,-1.5 -3,0.5 -4.5,1 -1.5,0.5 -4.5,3 -3,2.5 -5,3.5 -3,2 -8.5,3 -5.5,1 -8.5,2 15,-5 -1,-11 -10,-4 -16,-3 9,-4 7.5,-12 -1.5,-8 -8.5,-14 h 5 q -1,-4 -8.5,-8.5 -7.5,-4.5 -17.5,-8.5 -10,-4 -13,-6 -8,-5 -34,-9.5 -26,-4.5 -33,-0.5 -5,6 -4.5,10.5 0.5,4.5 4,14 3.5,9.5 3.5,12.5 1,6 -5.5,13 -6.5,7 -6.5,12 0,7 14,15.5 14,8.5 10,21.5 -3,8 -16,16 -13,8 -16,12 -5,8 -1.5,18.5 3.5,10.5 10.5,16.5 2,2 1.5,4 -0.5,2 -3.5,4.5 -3,2.5 -5.5,4 -2.5,1.5 -6.5,3.5 l -3,2 q -11,5 -20.5,-6 -9.5,-11 -13.5,-26 -7,-25 -16,-30 -23,-8 -29,1 -5,-13 -41,-26 -25,-9 -58,-4 6,-1 0,-15 -7,-15 -19,-12 3,-6 4,-17.5 1,-11.5 1,-13.5 3,-13 12,-23 1,-1 7,-8.5 6,-7.5 9.5,-13.5 3.5,-6 0.5,-6 35,4 50,-11 5,-5 11.5,-17 6.5,-12 10.5,-17 9,-6 14,-5.5 5,0.5 14.5,5.5 9.5,5 14.5,5 14,1 15.5,-11 1.5,-12 -7.5,-20 12,1 3,-17 -5,-7 -8,-9 -12,-4 -27,5 -8,4 2,8 -1,-1 -9.5,10.5 Q 927,340 919,346 q -8,6 -16,-5 -1,-1 -5.5,-13.5 Q 893,315 888,314 q -8,0 -16,15 3,-8 -11,-15 -14,-7 -24,-8 19,-12 -8,-27 -7,-4 -20.5,-5 -13.5,-1 -19.5,4 -5,7 -5.5,11.5 -0.5,4.5 5,8 5.5,3.5 10.5,5.5 5,2 11.5,4 6.5,2 8.5,3 14,10 8,14 -2,1 -8.5,3.5 -6.5,2.5 -11.5,4.5 -5,2 -6,4 -3,4 0,14 3,10 -2,14 -5,-5 -9,-17.5 -4,-12.5 -7,-16.5 7,9 -25,6 l -10,-1 q -4,0 -16,2 -12,2 -20.5,1 -8.5,-1 -13.5,-8 -4,-8 0,-20 1,-4 4,-2 -4,-3 -11,-9.5 -7,-6.5 -10,-8.5 -46,15 -94,41 6,1 12,-1 5,-2 13,-6.5 8,-4.5 10,-5.5 34,-14 42,-7 l 5,-5 q 14,16 20,25 -7,-4 -30,-1 -20,6 -22,12 7,12 5,18 -4,-3 -11.5,-10 -7.5,-7 -14.5,-11 -7,-4 -15,-5 -16,0 -22,1 -146,80 -235,222 7,7 12,8 4,1 5,9 1,8 2.5,11 1.5,3 11.5,-3 9,8 3,19 1,-1 44,27 19,17 21,21 3,11 -10,18 -1,-2 -9,-9 -8,-7 -9,-4 -3,5 0.5,18.5 3.5,13.5 10.5,12.5 -7,0 -9.5,16 -2.5,16 -2.5,35.5 0,19.5 -1,23.5 l 2,1 q -3,12 5.5,34.5 8.5,22.5 21.5,19.5 -13,3 20,43 6,8 8,9 3,2 12,7.5 9,5.5 15,10 6,4.5 10,10.5 4,5 10,22.5 6,17.5 14,23.5 -2,6 9.5,20 11.5,14 10.5,23 -1,0 -2.5,1 -1.5,1 -2.5,1 3,7 15.5,14 12.5,7 15.5,13 1,3 2,10 1,7 3,11 2,4 8,2 2,-20 -24,-62 -15,-25 -17,-29 -3,-5 -5.5,-15.5 Q 541,919 539,915 q 2,0 6,1.5 4,1.5 8.5,3.5 4.5,2 7.5,4 3,2 2,3 -3,7 2,17.5 5,10.5 12,18.5 7,8 17,19 10,11 12,13 6,6 14,19.5 8,13.5 0,13.5 9,0 20,10 11,10 17,20 5,8 8,26 3,18 5,24 2,7 8.5,13.5 6.5,6.5 12.5,9.5 l 16,8 q 0,0 13,7 5,2 18.5,10.5 13.5,8.5 21.5,11.5 10,4 16,4 6,0 14.5,-2.5 8.5,-2.5 13.5,-3.5 15,-2 29,15 14,17 21,21 36,19 55,11 -2,1 0.5,7.5 2.5,6.5 8,15.5 5.5,9 9,14.5 3.5,5.5 5.5,8.5 5,6 18,15 13,9 18,15 6,-4 7,-9 -3,8 7,20 10,12 18,10 14,-3 14,-32 -31,15 -49,-18 0,-1 -2.5,-5.5 -2.5,-4.5 -4,-8.5 -1.5,-4 -2.5,-8.5 -1,-4.5 0,-7.5 1,-3 5,-3 9,0 10,-3.5 1,-3.5 -2,-12.5 -3,-9 -4,-13 -1,-8 -11,-20 -10,-12 -12,-15 -5,9 -16,8 -11,-1 -16,-9 0,1 -1.5,5.5 -1.5,4.5 -1.5,6.5 -13,0 -15,-1 1,-3 2.5,-17.5 1.5,-14.5 3.5,-22.5 1,-4 5.5,-12 4.5,-8 7.5,-14.5 3,-6.5 4,-12.5 1,-6 -4.5,-9.5 -5.5,-3.5 -17.5,-2.5 -19,1 -26,20 -1,3 -3,10.5 -2,7.5 -5,11.5 -3,4 -9,7 -7,3 -24,2 -17,-1 -24,-5 -13,-8 -22.5,-29 -9.5,-21 -9.5,-37 0,-10 2.5,-26.5 2.5,-16.5 3,-25 0.5,-8.5 -5.5,-24.5 3,-2 9,-9.5 6,-7.5 10,-10.5 2,-1 4.5,-1.5 2.5,-0.5 4.5,0 2,0.5 4,-1.5 2,-2 3,-6 -1,-1 -4,-3 -3,-3 -4,-3 7,3 28.5,-1.5 21.5,-4.5 27.5,1.5 15,11 22,-2 0,-1 -2.5,-9.5 Q 870,931 872,926 q 5,27 29,9 3,3 15.5,5 12.5,2 17.5,5 3,2 7,5.5 4,3.5 5.5,4.5 1.5,1 5,-0.5 3.5,-1.5 8.5,-6.5 10,14 12,24 11,40 19,44 7,3 11,2 4,-1 4.5,-9.5 0.5,-8.5 0,-14 Q 1006,989 1005,982 l -1,-8 v -18 l -1,-8 q -15,-3 -18.5,-12 -3.5,-9 1.5,-18.5 5,-9.5 15,-18.5 1,-1 8,-3.5 7,-2.5 15.5,-6.5 8.5,-4 12.5,-8 21,-19 15,-35 7,0 11,-9 -1,0 -5,-3 -4,-3 -7.5,-5 -3.5,-2 -4.5,-2 9,-5 2,-16 5,-3 7.5,-11 2.5,-8 7.5,-10 9,12 21,2 7,-8 1,-16 5,-7 20.5,-10.5 15.5,-3.5 18.5,-9.5 7,2 8,-2 1,-4 1,-12 0,-8 3,-12 4,-5 15,-9 11,-4 13,-5 l 17,-11 q 3,-4 0,-4 18,2 31,-11 10,-11 -6,-20 3,-6 -3,-9.5 -6,-3.5 -15,-5.5 3,-1 11.5,-0.5 8.5,0.5 10.5,-1.5 15,-10 -7,-16 -17,-5 -43,12 z m -163,877 q 206,-36 351,-189 -3,-3 -12.5,-4.5 -9.5,-1.5 -12.5,-3.5 -18,-7 -24,-8 1,-7 -2.5,-13 -3.5,-6 -8,-9 -4.5,-3 -12.5,-8 -8,-5 -11,-7 -2,-2 -7,-6 -5,-4 -7,-5.5 -2,-1.5 -7.5,-4.5 -5.5,-3 -8.5,-2 -3,1 -10,1 l -3,1 q -3,1 -5.5,2.5 -2.5,1.5 -5.5,3 -3,1.5 -4,3 -1,1.5 0,2.5 -21,-17 -36,-22 -5,-1 -11,-5.5 -6,-4.5 -10.5,-7 -4.5,-2.5 -10,-1.5 -5.5,1 -11.5,7 -5,5 -6,15 -1,10 -2,13 -7,-5 0,-17.5 7,-12.5 2,-18.5 -3,-6 -10.5,-4.5 -7.5,1.5 -12,4.5 -4.5,3 -11.5,8.5 -7,5.5 -9,6.5 -2,1 -8.5,5.5 -6.5,4.5 -8.5,7.5 -3,4 -6,12 -3,8 -5,11 -2,-4 -11.5,-6.5 -9.5,-2.5 -9.5,-5.5 2,10 4,35 2,25 5,38 7,31 -12,48 -27,25 -29,40 -4,22 12,26 0,7 -8,20.5 -8,13.5 -7,21.5 0,6 2,16 z" + id="path153" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/info_sign.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/info_sign.svg new file mode 100644 index 000000000..517f76360 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/info_sign.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-10 0 1792 1792" + id="svg163" + sodipodi:docname="info_sign.svg" + width="1792" + height="1792" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs167" /> + <sodipodi:namedview + id="namedview165" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.48716518" + inkscape:cx="72.870561" + inkscape:cy="896" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg163" /> + <path + fill="currentColor" + d="m 1142,1376 v -160 q 0,-14 -9,-23 -9,-9 -23,-9 h -96 V 672 q 0,-14 -9,-23 -9,-9 -23,-9 H 662 q -14,0 -23,9 -9,9 -9,23 v 160 q 0,14 9,23 9,9 23,9 h 96 v 320 h -96 q -14,0 -23,9 -9,9 -9,23 v 160 q 0,14 9,23 9,9 23,9 h 448 q 14,0 23,-9 9,-9 9,-23 z M 1014,480 V 320 q 0,-14 -9,-23 -9,-9 -23,-9 H 790 q -14,0 -23,9 -9,9 -9,23 v 160 q 0,14 9,23 9,9 23,9 h 192 q 14,0 23,-9 9,-9 9,-23 z m 640,416 q 0,209 -103,385.5 Q 1448,1458 1271.5,1561 1095,1664 886,1664 677,1664 500.5,1561 324,1458 221,1281.5 118,1105 118,896 118,687 221,510.5 324,334 500.5,231 677,128 886,128 1095,128 1271.5,231 1448,334 1551,510.5 1654,687 1654,896 Z" + id="path161" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/leaf.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/leaf.svg new file mode 100644 index 000000000..c1cabb5cf --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/leaf.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 1034 1024"> + <path fill="currentColor" +d="M765 327l-14 -11l-538 502l503 -533l-14 -12q-48 -26 -106 -23.5t-109 19.5q-14 4 -28.5 10t-28.5 13q-30 14 -57.5 32.5t-52.5 41.5q-55 52 -90.5 119t-52.5 140q-5 21 -9 42t-7 42q-3 26 -5.5 52.5t-4.5 53.5q-1 22 1.5 45.5t33.5 21.5h11.5h10.5q78 -2 156 -6t153 -29 +q49 -16 94 -42.5t82 -62.5q27 -27 50 -60t39 -69.5t24 -75.5t5 -78q-3 -35 -12.5 -70t-33.5 -62v0zM844 160l30 30l-563 531z" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/light_bulb.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/light_bulb.svg new file mode 100644 index 000000000..ed2ed55fb --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/light_bulb.svg @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-10 0 1538 1538" + id="svg4" + sodipodi:docname="light_bulb.svg" + width="1538" + height="1538" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs8" /> + <sodipodi:namedview + id="namedview6" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + fit-margin-top="1" + fit-margin-left="1" + fit-margin-right="1" + fit-margin-bottom="1" + lock-margins="true" + inkscape:zoom="0.16" + inkscape:cx="1234.375" + inkscape:cy="409.375" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg4" /> + <path + fill="currentColor" + d="m 983,449 q 0,13 -9.5,22.5 Q 964,481 951,481 938,481 928.5,471.5 919,462 919,449 919,403 865,378 811,353 759,353 746,353 736.5,343.5 727,334 727,321 q 0,-13 9.5,-22.5 9.5,-9.5 22.5,-9.5 50,0 99.5,16 49.5,16 87,54 37.5,38 37.5,90 z m 160,0 q 0,-72 -34.5,-134 -34.5,-62 -90,-101.5 Q 963,174 895.5,151.5 828,129 759,129 690,129 622.5,151.5 555,174 499.5,213.5 444,253 409.5,315 375,377 375,449 q 0,101 68,180 10,11 30.5,33 20.5,22 30.5,33 128,153 141,298 h 228 q 13,-145 141,-298 10,-11 30.5,-33 20.5,-22 30.5,-33 68,-79 68,-180 z m 128,0 q 0,155 -103,268 -45,49 -74.5,87 -29.5,38 -59.5,95.5 -30,57.5 -34,107.5 47,28 47,82 0,37 -25,64 25,27 25,64 0,52 -45,81 13,23 13,47 0,46 -31.5,71 -31.5,25 -77.5,25 -20,44 -60,70 -40,26 -87,26 -47,0 -87,-26 -40,-26 -60,-70 -46,0 -77.5,-25 -31.5,-25 -31.5,-71 0,-24 13,-47 -45,-29 -45,-81 0,-37 25,-64 -25,-27 -25,-64 0,-54 47,-82 Q 514,957 484,899.5 454,842 424.5,804 395,766 350,717 247,604 247,449 247,350 291.5,264.5 336,179 408.5,122.5 481,66 572.5,33.5 664,1 759,1 q 95,0 186.5,32.5 91.5,32.5 164,89 72.5,56.5 117,142 Q 1271,350 1271,449 Z" + id="path2" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/qt_logo.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/qt_logo.svg new file mode 100644 index 000000000..062daff3e --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/qt_logo.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns="http://www.w3.org/2000/svg" + width="462pt" + height="339pt" + viewBox="0 0 462 339" + version="1.1" + id="svg2" +> + <path + fill="#41cd52" + d=" M 63.50 0.00 L 462.00 0.00 L 462.00 274.79 C 440.60 296.26 419.13 317.66 397.61 339.00 L 0.00 339.00 L 0.00 63.39 C 21.08 42.18 42.34 21.13 63.50 0.00 Z" + id="path6"/> + <path + d=" M 122.37 71.33 C 137.50 61.32 156.21 58.79 174.00 58.95 C 190.94 59.16 208.72 62.13 222.76 72.24 C 232.96 79.41 239.59 90.48 244.01 101.93 C 251.16 120.73 253.26 141.03 253.50 161.01 C 253.53 181.13 252.62 201.69 245.96 220.86 C 241.50 233.90 233.01 245.48 221.81 253.52 C 229.87 266.58 238.09 279.54 246.15 292.60 C 236.02 297.27 225.92 301.97 215.78 306.62 C 207.15 292.38 198.56 278.11 189.90 263.89 C 178.19 265.81 166.21 265.66 154.44 264.36 C 140.34 262.67 125.97 258.37 115.09 248.88 C 106.73 241.64 101.48 231.51 97.89 221.21 C 92.01 203.79 90.43 185.25 90.16 166.97 C 90.02 147.21 91.28 127.14 97.24 108.18 C 101.85 93.92 109.48 79.69 122.37 71.33 Z" + id="path8" + fill="#ffffff"/> + <path + d=" M 294.13 70.69 C 304.73 70.68 315.33 70.68 325.93 70.69 C 325.96 84.71 325.92 98.72 325.95 112.74 C 339.50 112.76 353.05 112.74 366.60 112.75 C 366.37 121.85 366.12 130.95 365.86 140.05 C 352.32 140.08 338.79 140.04 325.25 140.07 C 325.28 163.05 325.18 186.03 325.30 209.01 C 325.56 215.30 325.42 221.94 328.19 227.75 C 330.21 232.23 335.65 233.38 340.08 233.53 C 348.43 233.50 356.77 233.01 365.12 232.86 C 365.63 241.22 366.12 249.59 366.60 257.95 C 349.99 260.74 332.56 264.08 316.06 258.86 C 309.11 256.80 302.63 252.19 299.81 245.32 C 294.76 233.63 294.35 220.62 294.13 208.07 C 294.11 185.40 294.13 162.74 294.12 140.07 C 286.73 140.05 279.34 140.08 271.95 140.05 C 271.93 130.96 271.93 121.86 271.95 112.76 C 279.34 112.73 286.72 112.77 294.11 112.74 C 294.14 98.72 294.10 84.71 294.13 70.69 Z" + id="path10" + fill="#ffffff"/> + <path + fill="#41cd52" + d=" M 160.51 87.70 C 170.80 86.36 181.60 86.72 191.34 90.61 C 199.23 93.73 205.93 99.84 209.47 107.58 C 214.90 119.31 216.98 132.26 218.03 145.05 C 219.17 162.07 219.01 179.25 216.66 196.17 C 215.01 206.24 212.66 216.85 205.84 224.79 C 198.92 232.76 188.25 236.18 178.01 236.98 C 167.21 237.77 155.82 236.98 146.07 231.87 C 140.38 228.84 135.55 224.09 132.73 218.27 C 129.31 211.30 127.43 203.69 126.11 196.07 C 122.13 171.91 121.17 146.91 126.61 122.89 C 128.85 113.83 132.11 104.53 138.73 97.70 C 144.49 91.85 152.51 88.83 160.51 87.70 Z" + id="path12"/> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/read.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/read.svg new file mode 100644 index 000000000..c3af473d2 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/read.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="-11 0 1792 1792" + id="svg184" + sodipodi:docname="read.svg" + width="1792" + height="1792" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs188" /> + <sodipodi:namedview + id="namedview186" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="0.24358259" + inkscape:cx="519.33104" + inkscape:cy="1089.9794" + inkscape:window-width="1846" + inkscape:window-height="1016" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg184" /> + <path + fill="currentColor" + d="m 1691.8576,478 q 40,57 18,129 l -275,906 q -19,64 -76.5,107.5 -57.5,43.5 -122.5,43.5 H 312.85764 q -77,0 -148.5,-53.5 Q 92.857644,1557 64.857644,1479 q -24,-67 -2,-127 0,-4 3,-27 3,-23 4,-37 1,-8 -3,-21.5 -4,-13.5 -3,-19.5 2,-11 8,-21 6,-10 16.5,-23.5 10.5,-13.5 16.499996,-23.5 23,-38 45,-91.5 22,-53.5 30,-91.5 3,-10 0.5,-30 -2.5,-20 -0.5,-28 3,-11 17,-28 14,-17 17,-23 21,-36 42,-92 21,-56 25,-90 1,-9 -2.5,-32 -3.5,-23 0.5,-28 4,-13 22,-30.5 18,-17.5 22,-22.5 19,-26 42.5,-84.5 23.5,-58.5 27.5,-96.5 1,-8 -3,-25.5 -4,-17.5 -2,-26.5 2,-8 9,-18 7,-10 18,-23 11,-13 17,-21 8,-12 16.5,-30.5 8.5,-18.5 15,-35 6.5,-16.5 16,-36 9.5,-19.5 19.5,-32 10,-12.5 26.5,-23.5 16.5,-11 36,-11.5 19.5,-0.5 47.5,5.5 l -1,3 q 38,-9 51,-9 h 760.99996 q 74,0 114,56 40,56 18,130 l -274,906 q -36,119 -71.5,153.5 -35.5,34.5 -128.5,34.5 H 208.85764 q -27,0 -38,15 -11,16 -1,43 24,70 144,70 h 922.99996 q 29,0 56,-15.5 27,-15.5 35,-41.5 l 300,-987 q 7,-22 5,-57 38,15 59,43 z m -1063.99996,2 q -4,13 2,22.5 6,9.5 20,9.5 h 607.99996 q 13,0 25.5,-9.5 12.5,-9.5 16.5,-22.5 l 21,-64 q 4,-13 -2,-22.5 -6,-9.5 -20,-9.5 H 690.85764 q -13,0 -25.5,9.5 -12.5,9.5 -16.5,22.5 z m -83,256 q -4,13 2,22.5 6,9.5 20,9.5 h 607.99996 q 13,0 25.5,-9.5 12.5,-9.5 16.5,-22.5 l 21,-64 q 4,-13 -2,-22.5 -6,-9.5 -20,-9.5 H 607.85764 q -13,0 -25.5,9.5 -12.5,9.5 -16.5,22.5 z" + id="path182" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/resize.svg b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/resize.svg new file mode 100644 index 000000000..e86d612f3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/icons/resize.svg @@ -0,0 +1,6 @@ +<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'> + <line x1="00" y1="100" x2="100" y2="00" stroke="black" stroke-width="3" /> + <line x1="20" y1="100" x2="100" y2="20" stroke="black" stroke-width="3" /> + <line x1="40" y1="100" x2="100" y2="40" stroke="black" stroke-width="3" /> + <line x1="60" y1="100" x2="100" y2="60" stroke="black" stroke-width="3" /> +</svg> diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/About.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/About.qml new file mode 100644 index 000000000..178bf03e4 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/About.qml @@ -0,0 +1,93 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.Basic +import FileSystemModule + +ApplicationWindow { + id: root + width: 650 + height: 550 + flags: Qt.Window | Qt.FramelessWindowHint + color: Colors.surface1 + + menuBar: MyMenuBar { + id: menuBar + + dragWindow: root + implicitHeight: 27 + infoText: "About Qt" + } + + Image { + id: logo + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 20 + + source: "../icons/qt_logo.svg" + sourceSize.width: 80 + sourceSize.height: 80 + fillMode: Image.PreserveAspectFit + + smooth: true + antialiasing: true + asynchronous: true + } + + ScrollView { + anchors.top: logo.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 20 + + TextArea { + selectedTextColor: Colors.textFile + selectionColor: Colors.selection + horizontalAlignment: Text.AlignHCenter + textFormat: Text.RichText + + text: qsTr("<h3>About Qt</h3>" + + "<p>This program uses Qt version %1.</p>" + + "<p>Qt is a C++ toolkit for cross-platform application " + + "development.</p>" + + "<p>Qt provides single-source portability across all major desktop " + + "operating systems. It is also available for embedded Linux and other " + + "embedded and mobile operating systems.</p>" + + "<p>Qt is available under multiple licensing options designed " + + "to accommodate the needs of our various users.</p>" + + "<p>Qt licensed under our commercial license agreement is appropriate " + + "for development of proprietary/commercial software where you do not " + + "want to share any source code with third parties or otherwise cannot " + + "comply with the terms of GNU (L)GPL.</p>" + + "<p>Qt licensed under GNU (L)GPL is appropriate for the " + + "development of Qt applications provided you can comply with the terms " + + "and conditions of the respective licenses.</p>" + + "<p>Please see <a href=\"http://%2/\">%2</a> " + + "for an overview of Qt licensing.</p>" + + "<p>Copyright (C) %3 The Qt Company Ltd and other " + + "contributors.</p>" + + "<p>Qt and the Qt logo are trademarks of The Qt Company Ltd.</p>" + + "<p>Qt is The Qt Company Ltd product developed as an open source " + + "project. See <a href=\"http://%4/\">%4</a> for more information.</p>") + .arg(Application.version).arg("qt.io/licensing").arg("2023").arg("qt.io") + color: Colors.textFile + wrapMode: Text.WordWrap + readOnly: true + antialiasing: true + background: null + + onLinkActivated: function(link) { + Qt.openUrlExternally(link) + } + } + } + + ResizeButton { + resizeWindow: root + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/ColorScheme.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/ColorScheme.qml new file mode 100644 index 000000000..19c8cd905 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/ColorScheme.qml @@ -0,0 +1,118 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import FileSystemModule + +ColumnLayout { + id: colorScheme + + spacing: 20 + + // Inline component that customizes TabButton + component MyTabButton: TabButton { + id: root + + implicitWidth: 150 + implicitHeight: 30 + padding: 6 + spacing: 6 + + contentItem: Text { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + text: root.text + font.bold: true + color: Colors.text + } + + background: Rectangle { + anchors.fill: parent + implicitHeight: 40 + + color: root.checked ? Colors.active : Colors.selection + Rectangle { + height: 4 + width: parent.width + color: root.checked ? Colors.color1 : Colors.selection + } + } + } + + Item { + // Spacer item + Layout.fillHeight: true + Layout.fillWidth: true + } + + Text { + Layout.alignment: Qt.AlignHCenter + + text: "Select a Scheme!" + font.pointSize: 30 + font.bold: true + color: Colors.text + } + + // Display all the color-scheme names. The model is a string-list provided + // by our python class. + TabBar { + id: schemeSelector + + Layout.alignment: Qt.AlignHCenter + + background: Rectangle { + color: Colors.surface1 + } + + Repeater { + model: Colors.getKeys() + MyTabButton { + text: modelData + onClicked: { + Colors.setScheme(modelData) + themePreviewContainer.background + = (modelData === "Solarized") ? "#777777" : "#FEFAEC" + } + } + } + } + + // The current colors can be visualized using the same method as above. + Rectangle { + id: themePreviewContainer + + property color background: "#FEFAEC" + + Layout.alignment: Qt.AlignHCenter + + width: 700 + height: 50 + radius: 10 + color: background + + // Display all used colors inside a row + Row { + anchors.centerIn: parent + spacing: 10 + + Repeater { + model: Colors.currentColors + Rectangle { + width: 35 + height: width + radius: width / 2 + color: modelData + } + } + } + } + Item { + // Spacer item + Layout.fillHeight: true + Layout.fillWidth: true + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/Editor.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/Editor.qml new file mode 100644 index 000000000..80f7c04c5 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/Editor.qml @@ -0,0 +1,160 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import FileSystemModule + +pragma ComponentBehavior: Bound + +// This is the text editor that displays the currently open file, including +// their corresponding line numbers. +Rectangle { + id: root + + required property string currentFilePath + required property bool showLineNumbers + property alias text: textArea + property int currentLineNumber: -1 + property int rowHeight: Math.ceil(fontMetrics.lineSpacing) + + color: Colors.background + + onWidthChanged: textArea.update() + onHeightChanged: textArea.update() + + RowLayout { + anchors.fill: parent + // We use a flickable to synchronize the position of the editor and + // the line numbers. This is necessary because the line numbers can + // extend the available height. + Flickable { + id: lineNumbers + + // Calculate the width based on the logarithmic scale. + Layout.preferredWidth: fontMetrics.averageCharacterWidth + * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10 + Layout.fillHeight: true + + interactive: false + contentY: editorFlickable.contentY + visible: textArea.text !== "" && root.showLineNumbers + + Column { + anchors.fill: parent + Repeater { + id: repeatedLineNumbers + + model: LineNumberModel { + lineCount: textArea.text !== "" ? textArea.lineCount : 0 + } + + delegate: Item { + required property int index + + width: parent.width + height: root.rowHeight + Label { + id: numbers + + text: parent.index + 1 + + width: parent.width + height: parent.height + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + color: (root.currentLineNumber === parent.index) + ? Colors.iconIndicator : Qt.darker(Colors.text, 2) + font: textArea.font + } + Rectangle { + id: indicator + + anchors.left: numbers.right + width: 1 + height: parent.height + color: Qt.darker(Colors.text, 3) + } + } + } + } + } + + Flickable { + id: editorFlickable + + property alias textArea: textArea + + // We use an inline component to customize the horizontal and vertical + // scroll-bars. This is convenient when the component is only used in one file. + component MyScrollBar: ScrollBar { + id: scrollBar + background: Rectangle { + implicitWidth: scrollBar.interactive ? 8 : 4 + implicitHeight: scrollBar.interactive ? 8 : 4 + + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0 + color: Colors.background + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + contentItem: Rectangle { + implicitWidth: scrollBar.interactive ? 8 : 4 + implicitHeight: scrollBar.interactive ? 8 : 4 + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0 + color: Colors.color1 + Behavior on opacity { + OpacityAnimator { + duration: 1000 + } + } + } + } + + Layout.fillHeight: true + Layout.fillWidth: true + ScrollBar.horizontal: MyScrollBar {} + ScrollBar.vertical: MyScrollBar {} + + boundsBehavior: Flickable.StopAtBounds + + TextArea.flickable: TextArea { + id: textArea + anchors.fill: parent + + focus: false + topPadding: 0 + leftPadding: 10 + + text: FileSystemModel.readFile(root.currentFilePath) + tabStopDistance: fontMetrics.averageCharacterWidth * 4 + + // Grab the current line number from the C++ interface. + onCursorPositionChanged: { + root.currentLineNumber = FileSystemModel.currentLineNumber( + textArea.textDocument, textArea.cursorPosition) + } + + color: Colors.textFile + selectedTextColor: Colors.textFile + selectionColor: Colors.selection + + textFormat: TextEdit.PlainText + renderType: Text.QtRendering + selectByMouse: true + antialiasing: true + background: null + } + + FontMetrics { + id: fontMetrics + font: textArea.font + } + } + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/FileSystemView.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/FileSystemView.qml new file mode 100644 index 000000000..db955168c --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/FileSystemView.qml @@ -0,0 +1,156 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Effects +import QtQuick.Controls.Basic +import FileSystemModule + +pragma ComponentBehavior: Bound + +// This is the file system view which gets populated by the C++ model. +Rectangle { + id: root + + signal fileClicked(string filePath) + property alias rootIndex: fileSystemTreeView.rootIndex + + TreeView { + id: fileSystemTreeView + + property int lastIndex: -1 + + anchors.fill: parent + model: FileSystemModel + rootIndex: FileSystemModel.rootIndex + boundsBehavior: Flickable.StopAtBounds + boundsMovement: Flickable.StopAtBounds + clip: true + + Component.onCompleted: fileSystemTreeView.toggleExpanded(0) + + // The delegate represents a single entry in the filesystem. + delegate: TreeViewDelegate { + id: treeDelegate + indentation: 8 + implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 + implicitHeight: 25 + + // Since we have the 'ComponentBehavior Bound' pragma, we need to + // require these properties from our model. This is a convenient way + // to bind the properties provided by the model's role names. + required property int index + required property url filePath + required property string fileName + + indicator: Image { + id: directoryIcon + + x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) + anchors.verticalCenter: parent.verticalCenter + source: treeDelegate.hasChildren ? (treeDelegate.expanded + ? "../icons/folder_open.svg" : "../icons/folder_closed.svg") + : "../icons/generic_file.svg" + sourceSize.width: 20 + sourceSize.height: 20 + fillMode: Image.PreserveAspectFit + + smooth: true + antialiasing: true + asynchronous: true + } + + contentItem: Text { + text: treeDelegate.fileName + color: Colors.text + } + + background: Rectangle { + color: (treeDelegate.index === fileSystemTreeView.lastIndex) + ? Colors.selection + : (hoverHandler.hovered ? Colors.active : "transparent") + } + + // We color the directory icons with this MultiEffect, where we overlay + // the colorization color ontop of the SVG icons. + MultiEffect { + id: iconOverlay + + anchors.fill: directoryIcon + source: directoryIcon + colorization: 1.0 + brightness: 1.0 + colorizationColor: { + const isFile = treeDelegate.index === fileSystemTreeView.lastIndex + && !treeDelegate.hasChildren; + if (isFile) + return Qt.lighter(Colors.folder, 3) + + const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; + if (isExpandedFolder) + return Colors.color2 + else + return Colors.folder + } + } + + HoverHandler { + id: hoverHandler + } + + TapHandler { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onSingleTapped: (eventPoint, button) => { + switch (button) { + case Qt.LeftButton: + fileSystemTreeView.toggleExpanded(treeDelegate.row) + fileSystemTreeView.lastIndex = treeDelegate.index + // If this model item doesn't have children, it means it's + // representing a file. + if (!treeDelegate.hasChildren) + root.fileClicked(treeDelegate.filePath) + break; + case Qt.RightButton: + if (treeDelegate.hasChildren) + contextMenu.popup(); + break; + } + } + } + + MyMenu { + id: contextMenu + Action { + text: qsTr("Set as root index") + onTriggered: { + fileSystemTreeView.rootIndex = fileSystemTreeView.index(treeDelegate.row, 0) + } + } + Action { + text: qsTr("Reset root index") + onTriggered: fileSystemTreeView.rootIndex = undefined + } + } + } + + // Provide our own custom ScrollIndicator for the TreeView. + ScrollIndicator.vertical: ScrollIndicator { + active: true + implicitWidth: 15 + + contentItem: Rectangle { + implicitWidth: 6 + implicitHeight: 6 + + color: Colors.color1 + opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + } + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/MyMenu.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/MyMenu.qml new file mode 100644 index 000000000..1f1d30c56 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/MyMenu.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.Basic +import FileSystemModule + +Menu { + id: root + + delegate: MenuItem { + id: menuItem + contentItem: Item { + Text { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 5 + + text: menuItem.text + color: enabled ? Colors.text : Colors.disabledText + } + Rectangle { + id: indicator + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + width: 6 + height: parent.height + + visible: menuItem.highlighted + color: Colors.color2 + } + } + background: Rectangle { + implicitWidth: 210 + implicitHeight: 35 + color: menuItem.highlighted ? Colors.active : "transparent" + } + } + background: Rectangle { + implicitWidth: 210 + implicitHeight: 35 + color: Colors.surface2 + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/MyMenuBar.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/MyMenuBar.qml new file mode 100644 index 000000000..4874a2c03 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/MyMenuBar.qml @@ -0,0 +1,177 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import FileSystemModule + +// The MenuBar also serves as a controller for our window as we don't use any decorations. +MenuBar { + id: root + + required property ApplicationWindow dragWindow + property alias infoText: windowInfo.text + + // Customization of the top level menus inside the MenuBar + delegate: MenuBarItem { + id: menuBarItem + + contentItem: Text { + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + text: menuBarItem.text + font: menuBarItem.font + elide: Text.ElideRight + color: menuBarItem.highlighted ? Colors.textFile : Colors.text + opacity: enabled ? 1.0 : 0.3 + } + + background: Rectangle { + id: background + + color: menuBarItem.highlighted ? Colors.selection : "transparent" + Rectangle { + id: indicator + + width: 0; height: 3 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + color: Colors.color1 + states: State { + name: "active" + when: menuBarItem.highlighted + PropertyChanges { + indicator.width: background.width - 2 + } + } + transitions: Transition { + NumberAnimation { + properties: "width" + duration: 175 + } + } + } + } + } + // We use the contentItem property as a place to attach our window decorations. Beneath + // the usual menu entries within a MenuBar, it includes a centered information text, along + // with the minimize, maximize, and close buttons. + contentItem: RowLayout { + id: windowBar + + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: root.spacing + Repeater { + id: menuBarItems + + Layout.alignment: Qt.AlignLeft + model: root.contentModel + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Text { + id: windowInfo + + width: parent.width; height: parent.height + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: windowActions.width + color: Colors.text + clip: true + } + } + + RowLayout { + id: windowActions + + Layout.alignment: Qt.AlignRight + Layout.fillHeight: true + + spacing: 0 + + component InteractionButton: Rectangle { + id: interactionButton + + signal action() + property alias hovered: hoverHandler.hovered + + Layout.fillHeight: true + Layout.preferredWidth: height + + color: hovered ? Colors.background : "transparent" + HoverHandler { + id: hoverHandler + } + TapHandler { + id: tapHandler + onTapped: interactionButton.action() + } + } + + InteractionButton { + id: minimize + + onAction: root.dragWindow.showMinimized() + Rectangle { + anchors.centerIn: parent + color: parent.hovered ? Colors.iconIndicator : Colors.icon + height: 2 + width: parent.height - 14 + } + } + + InteractionButton { + id: maximize + + onAction: root.dragWindow.showMaximized() + Rectangle { + anchors.fill: parent + anchors.margins: 7 + border.color: parent.hovered ? Colors.iconIndicator : Colors.icon + border.width: 2 + color: "transparent" + } + } + + InteractionButton { + id: close + + color: hovered ? "#ec4143" : "transparent" + onAction: root.dragWindow.close() + Rectangle { + anchors.centerIn: parent + width: parent.height - 8; height: 2 + + rotation: 45 + antialiasing: true + transformOrigin: Item.Center + color: parent.hovered ? Colors.iconIndicator : Colors.icon + + Rectangle { + anchors.centerIn: parent + width: parent.height + height: parent.width + + antialiasing: true + color: parent.color + } + } + } + } + } + + background: Rectangle { + color: Colors.surface2 + // Make the empty space drag the specified root window. + WindowDragHandler { + dragWindow: root.dragWindow + } + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/ResizeButton.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/ResizeButton.qml new file mode 100644 index 000000000..0df65bf82 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/ResizeButton.qml @@ -0,0 +1,23 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick.Controls +import FileSystemModule + +Button { + required property ApplicationWindow resizeWindow + + icon.width: 20; icon.height: 20 + anchors.right: parent.right + anchors.bottom: parent.bottom + rightPadding: 3 + bottomPadding: 3 + + icon.source: "../icons/resize.svg" + icon.color: hovered ? Colors.iconIndicator : Colors.icon + + background: null + checkable: false + display: AbstractButton.IconOnly + onPressed: resizeWindow.startSystemResize(Qt.BottomEdge | Qt.RightEdge) +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/Sidebar.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/Sidebar.qml new file mode 100644 index 000000000..04880a55d --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/Sidebar.qml @@ -0,0 +1,146 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import FileSystemModule + +Rectangle { + id: root + + property alias currentTabIndex: topBar.currentIndex + required property ApplicationWindow dragWindow + readonly property int tabBarSpacing: 10 + + color: Colors.surface2 + + component SidebarEntry: Button { + id: sidebarButton + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + + icon.color: down || checked ? Colors.iconIndicator : Colors.icon + icon.width: 27 + icon.height: 27 + + topPadding: 0 + rightPadding: 0 + bottomPadding: 0 + leftPadding: 0 + background: null + + Rectangle { + id: indicator + + anchors.verticalCenter: parent.verticalCenter + x: 2 + width: 4 + height: sidebarButton.icon.height * 1.2 + + visible: sidebarButton.checked + color: Colors.color1 + } + } + + // TabBar is designed to be horizontal, whereas we need a vertical bar. + // We can easily achieve that by using a Container. + component TabBar: Container { + id: tabBarComponent + + Layout.fillWidth: true + // ButtonGroup ensures that only one button can be checked at a time. + ButtonGroup { + buttons: tabBarComponent.contentChildren + + // We have to manage the currentIndex ourselves, which we do by setting it to the index + // of the currently checked button. We use setCurrentIndex instead of setting the + // currentIndex property to avoid breaking bindings. See "Managing the Current Index" + // in Container's documentation for more information. + onCheckedButtonChanged: tabBarComponent.setCurrentIndex( + Math.max(0, buttons.indexOf(checkedButton))) + } + + contentItem: ColumnLayout { + spacing: tabBarComponent.spacing + Repeater { + model: tabBarComponent.contentModel + } + } + } + + ColumnLayout { + anchors.fill: root + anchors.topMargin: root.tabBarSpacing + anchors.bottomMargin: root.tabBarSpacing + + spacing: root.tabBarSpacing + TabBar { + id: topBar + + spacing: root.tabBarSpacing + // Shows help text when clicked. + SidebarEntry { + id: infoTab + icon.source: "../icons/light_bulb.svg" + checkable: true + checked: true + } + + // Shows the file system when clicked. + SidebarEntry { + id: filesystemTab + + icon.source: "../icons/read.svg" + checkable: true + } + + // Shows the scheme switcher + SidebarEntry { + icon.source: "../icons/leaf.svg" + checkable: true + + Layout.alignment: Qt.AlignHCenter + } + } + + // This item acts as a spacer to expand between the checkable and non-checkable buttons. + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + // Make the empty space drag our main window. + WindowDragHandler { + dragWindow: root.dragWindow + } + } + + TabBar { + id: bottomBar + + spacing: root.tabBarSpacing + // Opens the Qt website in the system's web browser. + SidebarEntry { + id: qtWebsiteButton + icon.source: "../icons/globe.svg" + checkable: false + onClicked: Qt.openUrlExternally("https://www.qt.io/") + } + + // Opens the About Qt Window. + SidebarEntry { + id: aboutQtButton + + icon.source: "../icons/info_sign.svg" + checkable: false + onClicked: aboutQtWindow.visible = !aboutQtWindow.visible + } + } + } + + About { + id: aboutQtWindow + visible: false + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/WindowDragHandler.qml b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/WindowDragHandler.qml new file mode 100644 index 000000000..0e140aca3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qml/WindowDragHandler.qml @@ -0,0 +1,16 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +// Allows dragging the window when placed on an unused section of the UI. +DragHandler { + + required property ApplicationWindow dragWindow + + target: null + onActiveChanged: { + if (active) dragWindow.startSystemMove() + } +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qmldir b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qmldir new file mode 100644 index 000000000..e9c99b07b --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/FileSystemModule/qmldir @@ -0,0 +1,11 @@ +module FileSystemModule +Main 1.0 Main.qml +About 1.0 qml/About.qml +Editor 1.0 qml/Editor.qml +MyMenu 1.0 qml/MyMenu.qml +Sidebar 1.0 qml/Sidebar.qml +MyMenuBar 1.0 qml/MyMenuBar.qml +ColorScheme 1.0 qml/ColorScheme.qml +ResizeButton 1.0 qml/ResizeButton.qml +FileSystemView 1.0 qml/FileSystemView.qml +WindowDragHandler 1.0 qml/WindowDragHandler.qml diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/editormodels.py b/sources/pyside6/doc/tutorials/extendedexplorer/editormodels.py new file mode 100644 index 000000000..688147726 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/editormodels.py @@ -0,0 +1,116 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtWidgets import QFileSystemModel +from PySide6.QtQuick import QQuickTextDocument +from PySide6.QtQml import QmlElement, QmlSingleton +from PySide6.QtCore import (Qt, QDir, QAbstractListModel, Slot, QFile, QTextStream, + QMimeDatabase, QFileInfo, QStandardPaths, QModelIndex, + Signal, Property) + +QML_IMPORT_NAME = "FileSystemModule" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +@QmlSingleton +class FileSystemModel(QFileSystemModel): + + rootIndexChanged = Signal() + + def getDefaultRootDir(): + return QStandardPaths.writableLocation(QStandardPaths.StandardLocation.HomeLocation) + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.mRootIndex = QModelIndex() + self.mDb = QMimeDatabase() + self.setFilter(QDir.Filter.AllEntries | QDir.Filter.Hidden | QDir.Filter.NoDotAndDotDot) + self.setInitialDirectory() + + # check for the correct mime type and then read the file. + # returns the text file's content or an error message on failure + @Slot(str, result=str) + def readFile(self, path): + if path == "": + return "" + + file = QFile(path) + + mime = self.mDb.mimeTypeForFile(QFileInfo(file)) + if ('text' in mime.comment().lower() + or any('text' in s.lower() for s in mime.parentMimeTypes())): + if file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text): + stream = QTextStream(file).readAll() + file.close() + return stream + else: + return self.tr("Error opening the file!") + return self.tr("File type not supported!") + + @Slot(QQuickTextDocument, int, result=int) + def currentLineNumber(self, textDocument, cursorPosition): + td = textDocument.textDocument() + tb = td.findBlock(cursorPosition) + return tb.blockNumber() + + def setInitialDirectory(self, path=getDefaultRootDir()): + dir = QDir(path) + if dir.makeAbsolute(): + self.setRootPath(dir.path()) + else: + self.setRootPath(self.getDefaultRootDir()) + self.setRootIndex(self.index(dir.path())) + + # we only need one column in this example + def columnCount(self, parent): + return 1 + + @Property(QModelIndex, notify=rootIndexChanged) + def rootIndex(self): + return self.mRootIndex + + def setRootIndex(self, index): + if (index == self.mRootIndex): + return + self.mRootIndex = index + self.rootIndexChanged.emit() + + +@QmlElement +class LineNumberModel(QAbstractListModel): + + lineCountChanged = Signal() + + def __init__(self, parent=None): + self.mLineCount = 0 + super().__init__(parent=parent) + + @Property(int, notify=lineCountChanged) + def lineCount(self): + return self.mLineCount + + @lineCount.setter + def lineCount(self, n): + if n < 0: + print("lineCount must be greater then zero") + return + if self.mLineCount == n: + return + + if self.mLineCount < n: + self.beginInsertRows(QModelIndex(), self.mLineCount, n - 1) + self.mLineCount = n + self.endInsertRows() + else: + self.beginRemoveRows(QModelIndex(), n, self.mLineCount - 1) + self.mLineCount = n + self.endRemoveRows() + + def rowCount(self, parent): + return self.mLineCount + + def data(self, index, role): + if not self.checkIndex(index) or role != Qt.ItemDataRole.DisplayRole: + return + return index.row() diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/extendedexplorer.md b/sources/pyside6/doc/tutorials/extendedexplorer/extendedexplorer.md new file mode 100644 index 000000000..0ac7bec18 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/extendedexplorer.md @@ -0,0 +1,210 @@ +# Extending the file system explorer example + +This tutorial shows how to extend the +[Filesystem Explorer Example](filesystemexplorer_example) +by adding a simple scheme manager. This feature will allow you to switch color +schemes during the application's runtime. The color schemes will be declared in +JSON format and made available through a custom Python-QML plugin. + +![Extended Explorer GIF](resources/extendedexplorer.gif) + +## Defining the color schemes + +To define your color scheme, you can use the same color names as the original +example, so you don't have to rename every occurrence. The original colors are +defined in the `Colors.qml` file as follows: + +```{literalinclude} resources/Colors.qml +--- +language: QML +caption: true +linenos: true +lines: 7-22 +--- +``` + +The `schemes.json` file holds the color schemes. To start implementing this, you +can use the [Catppuccin](https://github.com/catppuccin/catppuccin) scheme. + +```{literalinclude} schemes.json +---json +caption: true +linenos: true +start-at: "Catppuccin" +end-at: "}," +--- +``` + +In addition to the "Catppuccin" color scheme, four other color schemes got +implemented: Nordic, One Dark, Gruvbox, and Solarized. However, feel free to get +creative and experiment with **your** schemes. + +To define a new color scheme, copy the structure from above and provide your +color values + +## Implement the scheme manager + +After defining the color schemes, you can implement the actual scheme manager. +The manager will read the `schemes.json` file and provide QML bindings to switch +between schemes during runtime. + +To implement the scheme manager, create a Python-QML plugin that exposes the +`SchemeManager` object to QML. This object will have methods to load the color +schemes from the `schemes.json` file and switch between them. + +Create a new Python file called `schememanager.py` in your project directory. In +this file, define the **SchemeManager** class: + +```{literalinclude} scheme_manager.py +--- +language: python +caption: true +linenos: true +start-at: "QML_IMPORT_NAME" +end-at: "class SchemeManager" +--- +``` + +To integrate smoothly into the already existing code, attach the SchemeManager +to the same QML module that's already present with +`QML_IMPORT_NAME = "FileSystemModule"`. Additionally, use the`@QmlNamedElement` +decorator to smoothly transition to using the custom plugin instead of the +`Colors.qml` file. With these changes, we can avoid editing all previous +assignments like: + +```QML +import FileSystemModule +... +Rectangle { + color: Colors.background +} +``` + +The constructor reads the `schemes.json` file once upon application start and +then calls the `setTheme` member function. + +```{literalinclude} scheme_manager.py +--- +language: python +caption: true +linenos: true +lines: 18-24 +--- +``` + +By adding the `SchemeManager` as a callable QML element named **Colors** to the +FileSystemModule, the class is now accessible in the code without the need to +import it each time or edit previous assignments. This, in turn, will streamline +the workflow. + +After defining the schemes in the JSON format and making the `SchemeManager` +class a callable element from QML under the name **Colors**, there are two +remaining steps to fully integrate the new scheme manager in the example. + +The **first step** is to create a function in the `SchemeManager` class that +loads a color scheme from the JSON file. The **second step** is to make the +individual colors available in QML with the *same name* as used before with the +syntax `Colors.<previousName>` as assignable properties. + + +```{literalinclude} scheme_manager.py +--- +language: python +caption: true +linenos: true +lines: 26-31 +--- +``` + +The `setScheme` method is responsible for switching between color schemes. To +make this method accessible in QML, use the `@Slot(str)` decorator and specify +that it takes a string as its input parameter. In this method, we populate a +dictionary with the color values from the JSON file. + +> Note: For simplicity reasons no other error checking is performed. +> You would probably want to validate the keys contained in the json. + +```{literalinclude} scheme_manager.py +--- +language: python +caption: true +linenos: true +start-at: "@Property(QColor" +end-at: "return" +--- +``` + +To make the color property assignable in QML, use the `@Property` decorator. +We simply return the corresponding color value from the dictionary for each +property. This process is repeated for all other colors that are used in the +application. +At this point the application should start with the colors provided by the +active scheme in the constructor. + +## Add the scheme switching to QML + +To visualize the current scheme and enable interactive scheme switching, start +by adding a new entry to the `Sidebar.qml` file. + +```{literalinclude} FileSystemModule/qml/Sidebar.qml +--- +language: QML +caption: true +linenos: true +lines: 99-105 +--- +``` + +To update the main content area of the application to display the `ColorScheme`, +the logic that checks the active index from the Sidebar buttons needs to be +modified. The necessary changes will be made to the Main.qml file: + +```{literalinclude} FileSystemModule/Main.qml +--- +language: QML +caption: true +linenos: true +lines: 170-187 +--- +``` + +In addition, change the behavior of the application so that there are two +`StackLayouts`: one for the resizable navigation and one for the main content +area where we display our color scheme switching functionality. These changes +will also be made to the Main.qml file. + +```{literalinclude} FileSystemModule/Main.qml +--- +language: QML +caption: true +linenos: true +lines: 147-150 +--- +``` + +To complete our implementation, the `ColorScheme.qml` file needs to be created. +The implementation is straightforward and follows the same principles as in the +original example. If anything is unclear, please refer to the documentation +provided there. To display all colors and scheme names, use a `Repeater`. The +model for the Repeater is provided by our `scheme_manager.py`file as a +`QStringList`. + +```{literalinclude} FileSystemModule/qml/ColorScheme.qml +--- +language: QML +caption: true +linenos: true +lines: 97-111 +--- +``` + +When examining the code in more detail, you will notice that there are different +ways to retrieve the models. The `getKeys()` method is defined as a **Slot** and +therefore requires parentheses when called. On the other hand, the `currentColors` +model is defined as a **property** and is therefore assigned as a property in QML. +The reason for this is to receive notifications when the color scheme is switched +so that the colors displayed in the application can be updated. The keys for the +color schemes are loaded only once at application startup and do not rely on any +notifications. + +![Extended Explorer GIF](resources/colorscheme.png) diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/extendedexplorer.pyproject b/sources/pyside6/doc/tutorials/extendedexplorer/extendedexplorer.pyproject new file mode 100644 index 000000000..77a3969ae --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/extendedexplorer.pyproject @@ -0,0 +1,31 @@ +{ + "files": [ + "main.py", + "editormodels.py", + "scheme_manager.py", + "schemes.json", + "FileSystemModule/qmldir", + "FileSystemModule/app.qrc", + "FileSystemModule/icons.qrc", + "FileSystemModule/Main.qml", + "FileSystemModule/qml/About.qml", + "FileSystemModule/qml/ColorScheme.qml", + "FileSystemModule/qml/Editor.qml", + "FileSystemModule/qml/FileSystemView.qml", + "FileSystemModule/qml/MyMenu.qml", + "FileSystemModule/qml/MyMenuBar.qml", + "FileSystemModule/qml/ResizeButton.qml", + "FileSystemModule/qml/Sidebar.qml", + "FileSystemModule/qml/WindowDragHandler.qml", + "FileSystemModule/icons/app_icon.svg", + "FileSystemModule/icons/folder_closed.svg", + "FileSystemModule/icons/folder_open.svg", + "FileSystemModule/icons/generic_file.svg", + "FileSystemModule/icons/globe.svg", + "FileSystemModule/icons/info_sign.svg", + "FileSystemModule/icons/light_bulb.svg", + "FileSystemModule/icons/qt_logo.svg", + "FileSystemModule/icons/read.svg", + "FileSystemModule/icons/resize.svg" + ] +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/main.py b/sources/pyside6/doc/tutorials/extendedexplorer/main.py new file mode 100644 index 000000000..f1e6e7d93 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/main.py @@ -0,0 +1,50 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +""" +This example shows how to customize Qt Quick Controls by implementing a simple filesystem explorer. +""" + +# Compile both resource files app.qrc and icons.qrc and include them here if you wish +# to load them from the resource system. Currently, all resources are loaded locally +# import FileSystemModule.rc_icons +# import FileSystemModule.rc_app + +from scheme_manager import SchemeManager +from editormodels import FileSystemModel +import PySide6 +from PySide6.QtGui import QGuiApplication, QIcon +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtCore import QCommandLineParser + +import sys + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + app.setOrganizationName("QtProject") + app.setApplicationName("File System Explorer") + app.setApplicationVersion(PySide6.__version__) + app.setWindowIcon(QIcon("FileSystemModule/icons/app_icon.svg")) + + parser = QCommandLineParser() + parser.setApplicationDescription("Qt Filesystemexplorer Example") + parser.addHelpOption() + parser.addVersionOption() + parser.addPositionalArgument("", "Initial directory", "[path]") + parser.process(app) + args = parser.positionalArguments() + + engine = QQmlApplicationEngine() + # Include the path of this file to search for the 'qmldir' module + engine.addImportPath(sys.path[0]) + + engine.loadFromModule("FileSystemModule", "Main") + + if not engine.rootObjects(): + sys.exit(-1) + + if (len(args) == 1): + fsm = engine.singletonInstance("FileSystemModule", "FileSystemModel") + fsm.setInitialDirectory(args[0]) + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/resources/Colors.qml b/sources/pyside6/doc/tutorials/extendedexplorer/resources/Colors.qml new file mode 100644 index 000000000..280f89286 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/resources/Colors.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma Singleton +import QtQuick + +QtObject { + readonly property color background: "#23272E" + readonly property color surface1: "#1E2227" + readonly property color surface2: "#090A0C" + readonly property color text: "#ABB2BF" + readonly property color textFile: "#C5CAD3" + readonly property color disabledText: "#454D5F" + readonly property color selection: "#2C313A" + readonly property color active: "#23272E" + readonly property color inactive: "#3E4452" + readonly property color folder: "#3D4451" + readonly property color icon: "#3D4451" + readonly property color iconIndicator: "#E5C07B" + readonly property color color1: "#E06B74" + readonly property color color2: "#62AEEF" +} diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/resources/colorscheme.png b/sources/pyside6/doc/tutorials/extendedexplorer/resources/colorscheme.png Binary files differnew file mode 100644 index 000000000..410538ca3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/resources/colorscheme.png diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/resources/extendedexplorer.gif b/sources/pyside6/doc/tutorials/extendedexplorer/resources/extendedexplorer.gif Binary files differnew file mode 100644 index 000000000..9e59f64aa --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/resources/extendedexplorer.gif diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/resources/extendedexplorer.webp b/sources/pyside6/doc/tutorials/extendedexplorer/resources/extendedexplorer.webp Binary files differnew file mode 100644 index 000000000..79a14f34c --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/resources/extendedexplorer.webp diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/scheme_manager.py b/sources/pyside6/doc/tutorials/extendedexplorer/scheme_manager.py new file mode 100644 index 000000000..8d732093c --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/scheme_manager.py @@ -0,0 +1,97 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import json +from pathlib import Path +from PySide6.QtCore import Slot, QObject, Property, Signal +from PySide6.QtGui import QColor +from PySide6.QtQml import QmlNamedElement, QmlSingleton + +QML_IMPORT_NAME = "FileSystemModule" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlNamedElement("Colors") +@QmlSingleton +class SchemeManager(QObject): + + schemeChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent=parent) + with open(Path(__file__).parent / "schemes.json", 'r') as f: + self.m_schemes = json.load(f) + self.m_activeScheme = {} + self.m_activeSchemeName = "Catppuccin" + self.setScheme(self.m_activeSchemeName) + + @Slot(str) + def setScheme(self, theme): + for k, v in self.m_schemes[theme].items(): + self.m_activeScheme[k] = QColor.fromString(v) + self.m_activeSchemeName = theme + self.schemeChanged.emit() + + @Slot(result='QStringList') + def getKeys(self): + return self.m_schemes.keys() + + @Property('QStringList', notify=schemeChanged) + def currentColors(self): + return self.m_schemes[self.m_activeSchemeName].values() + + @Property(QColor, notify=schemeChanged) + def background(self): + return self.m_activeScheme["background"] + + @Property(QColor, notify=schemeChanged) + def surface1(self): + return self.m_activeScheme["surface1"] + + @Property(QColor, notify=schemeChanged) + def surface2(self): + return self.m_activeScheme["surface2"] + + @Property(QColor, notify=schemeChanged) + def text(self): + return self.m_activeScheme["text"] + + @Property(QColor, notify=schemeChanged) + def textFile(self): + return self.m_activeScheme["textFile"] + + @Property(QColor, notify=schemeChanged) + def disabledText(self): + return self.m_activeScheme["disabledText"] + + @Property(QColor, notify=schemeChanged) + def selection(self): + return self.m_activeScheme["selection"] + + @Property(QColor, notify=schemeChanged) + def active(self): + return self.m_activeScheme["active"] + + @Property(QColor, notify=schemeChanged) + def inactive(self): + return self.m_activeScheme["inactive"] + + @Property(QColor, notify=schemeChanged) + def folder(self): + return self.m_activeScheme["folder"] + + @Property(QColor, notify=schemeChanged) + def icon(self): + return self.m_activeScheme["icon"] + + @Property(QColor, notify=schemeChanged) + def iconIndicator(self): + return self.m_activeScheme["iconIndicator"] + + @Property(QColor, notify=schemeChanged) + def color1(self): + return self.m_activeScheme["color1"] + + @Property(QColor, notify=schemeChanged) + def color2(self): + return self.m_activeScheme["color2"] diff --git a/sources/pyside6/doc/tutorials/extendedexplorer/schemes.json b/sources/pyside6/doc/tutorials/extendedexplorer/schemes.json new file mode 100644 index 000000000..e4f2d8c81 --- /dev/null +++ b/sources/pyside6/doc/tutorials/extendedexplorer/schemes.json @@ -0,0 +1,82 @@ +{ + "Catppuccin": { + "background": "#1E1E2E", + "surface1": "#181825", + "surface2": "#11111B", + "text": "#CDD6F4", + "textFile": "#CDD6F4", + "disabledText": "#363659", + "selection": "#45475A", + "active": "#1E1E2E", + "inactive": "#6C7086", + "folder": "#6C7086", + "icon": "#6C7086", + "iconIndicator": "#FFCC66", + "color1": "#CBA6F7", + "color2": "#89DCEB" + }, + "Nordic": { + "background": "#2E3440", + "surface1": "#2B2F3A", + "surface2": "#262A35", + "text": "#D8DEE9", + "textFile": "#D8DEE9", + "disabledText": "#4D556A", + "selection": "#495468", + "active": "#2E3440", + "inactive": "#555B6A", + "folder": "#495468", + "icon": "#495468", + "iconIndicator": "#FFC40D", + "color1": "#81B6C6", + "color2": "#5E81AC" + }, + "One Dark": { + "background": "#23272E", + "surface1": "#1E2227", + "surface2": "#090A0C", + "text": "#ABB2BF", + "textFile": "#C5CAD3", + "disabledText": "#2B303B", + "selection": "#2C313A", + "active": "#23272E", + "inactive": "#3E4452", + "folder": "#3D4451", + "icon": "#3D4451", + "iconIndicator": "#E5C07B", + "color1": "#E06B74", + "color2": "#62AEEF" + }, + "Gruvbox": { + "background": "#292828", + "surface1": "#171819", + "surface2": "#090A0C", + "text": "#D4BE98", + "textFile": "#E1D2B7", + "disabledText": "#2C313A", + "selection": "#333130", + "active": "#292828", + "inactive": "#383737", + "folder": "#383737", + "icon": "#383737", + "iconIndicator": "#FFCC66", + "color1": "#A7B464", + "color2": "#D3869B" + }, + "Solarized": { + "background": "#FDF6E3", + "surface1": "#EEE8D5", + "surface2": "#DDD6C1", + "text": "#6D6D6D", + "textFile": "#333333", + "disabledText": "#ADADAD", + "selection": "#D1CBB8", + "active": "#FDF6E3", + "inactive": "#8C8364", + "folder": "#5F5944", + "icon": "#5F5944", + "iconIndicator": "#002B36", + "color1": "#B58900", + "color2": "#6B9E75" + } +} diff --git a/sources/pyside6/doc/tutorials/index.rst b/sources/pyside6/doc/tutorials/index.rst new file mode 100644 index 000000000..8a69a3c6f --- /dev/null +++ b/sources/pyside6/doc/tutorials/index.rst @@ -0,0 +1,206 @@ +Tutorials +========= + +A collection of tutorials with walkthrough guides are +provided with |project| to help new users get started. + +Some of these 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. + +Qt Widgets: Basic tutorials +--------------------------- + +If you want to see the available widgets in action, you can check the +`Qt Widget Gallery <https://doc.qt.io/qt-6/gallery.html>`_ to learn their +names and how they look like. + +.. grid:: 1 3 3 3 + :gutter: 2 + + .. grid-item-card:: Basic Widget + :class-item: cover-img + :link: basictutorial/widgets.html + :img-top: basictutorial/widgets.png + + Your first QtWidgets Application + + .. grid-item-card:: Basic Button + :class-item: cover-img + :link: basictutorial/clickablebutton.html + :img-top: basictutorial/clickablebutton.png + + Using a Simple Button + + .. grid-item-card:: Basic Connections + :class-item: cover-img + :link: basictutorial/signals_and_slots.html + :img-top: basictutorial/signals_slots.png + + Signals and Slots + + .. grid-item-card:: Basic Dialog + :class-item: cover-img + :link: basictutorial/dialog.html + :img-top: basictutorial/dialog.png + + Creating a Dialog Application + + .. grid-item-card:: Basic Table + :class-item: cover-img + :link: basictutorial/tablewidget.html + :img-top: basictutorial/tablewidget.png + + Displaying Data Using a Table Widget + + .. grid-item-card:: Basic Tree + :class-item: cover-img + :link: basictutorial/treewidget.html + :img-top: basictutorial/treewidget.png + + Displaying Data Using a Tree Widget + + .. grid-item-card:: Basic ``ui`` files + :class-item: cover-img + :link: basictutorial/uifiles.html + :img-top: basictutorial/uifiles.png + + Using .ui files from Designer or QtCreator with QUiLoader and pyside6-uic + + .. grid-item-card:: Basic ``qrc`` files + :class-item: cover-img + :link: basictutorial/qrcfiles.html + :img-top: basictutorial/player-new.png + + Using .qrc Files (pyside6-rcc) + + .. grid-item-card:: Basic Translations + :class-item: cover-img + :link: basictutorial/translations.html + :img-top: basictutorial/translations.png + + Translating Applications + + .. grid-item-card:: Basic Widget Style + :class-item: cover-img + :link: basictutorial/widgetstyling.html + :img-top: basictutorial/widgetstyling-yes.png + + Styling the Widgets Application + +.. toctree:: + :hidden: + + basictutorial/widgets.rst + basictutorial/clickablebutton.rst + basictutorial/signals_and_slots.rst + basictutorial/dialog.rst + basictutorial/tablewidget.rst + basictutorial/treewidget.rst + basictutorial/uifiles.rst + basictutorial/qrcfiles.rst + basictutorial/translations.rst + basictutorial/widgetstyling.rst + + +Quick/QML: Basic tutorials +-------------------------- + +.. grid:: 1 3 3 3 + :gutter: 2 + + .. grid-item-card:: Basic Quick + :class-item: cover-img + :link: basictutorial/qml.html + :img-top: basictutorial/greenapplication.png + + Your First QtQuick/QML Application + + .. grid-item-card:: Basic QML Integration + :class-item: cover-img + :link: qmlintegration/qmlintegration.html + :img-top: qmlintegration/textproperties_material.png + + Python-QML integration + + .. grid-item-card:: QML Application + :class-item: cover-img + :link: qmlapp/qmlapplication.html + :img-top: qmlapp/qmlapplication.png + + QML Application Tutorial (QtCreator) + + .. grid-item-card:: Advanced QML Integration + :class-item: cover-img + :link: qmlsqlintegration/qmlsqlintegration.html + :img-top: qmlsqlintegration/example_list_view.png + + QML, SQL and PySide Integration Tutorial + + .. grid-item-card:: Extended Explorer + :class-item: cover-img + :link: extendedexplorer/extendedexplorer.html + :img-top: extendedexplorer/resources/extendedexplorer.webp + + Extending an Qt Quick Controls example + +.. toctree:: + :maxdepth: 1 + :hidden: + + basictutorial/qml.rst + qmlintegration/qmlintegration.rst + qmlapp/qmlapplication.rst + qmlsqlintegration/qmlsqlintegration.rst + extendedexplorer/extendedexplorer.md + +General Applications +-------------------- + +.. grid:: 1 3 3 3 + :gutter: 2 + + .. grid-item-card:: Data Visualization + :class-item: cover-img + :link: datavisualize/index.html + :img-top: datavisualize/images/datavisualization_app.png + + Data Visualization Tool + + .. grid-item-card:: Expenses Application + :class-item: cover-img + :link: expenses/expenses.html + :img-top: expenses/expenses_tool.png + + Expenses administration tool + +.. toctree:: + :hidden: + + datavisualize/index.rst + expenses/expenses.rst + +Qt Overviews +------------ + +.. toctree:: + :maxdepth: 1 + + ../overviews/overviews-main.rst + +C++ and Python +-------------- + +.. toctree:: + :maxdepth: 1 + + portingguide/index.rst + +Debug a PySide6 Application +--------------------------- +.. toctree:: + :maxdepth: 1 + + debugging/mixed_debugging.rst + debugging/qml_debugging.rst + diff --git a/sources/pyside6/doc/tutorials/modelviewprogramming/qlistview-dnd.py b/sources/pyside6/doc/tutorials/modelviewprogramming/qlistview-dnd.py new file mode 100644 index 000000000..3a37cc0f3 --- /dev/null +++ b/sources/pyside6/doc/tutorials/modelviewprogramming/qlistview-dnd.py @@ -0,0 +1,137 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtWidgets import (QAbstractItemView, QApplication, QMainWindow, + QListView) +from PySide6.QtCore import (QByteArray, QDataStream, QIODevice, QMimeData, + QModelIndex, QStringListModel, Qt) + + +class DragDropListModel(QStringListModel): + """A simple model that uses a QStringList as its data source.""" + + def __init__(self, strings, parent=None): + super().__init__(strings, parent) + +#! [0] + + def canDropMimeData(self, data, action, row, column, parent): + if not data.hasFormat("application/vnd.text.list"): + return False + + if column > 0: + return False + + return True +#! [0] +#! [1] + def dropMimeData(self, data, action, row, column, parent): + if not self.canDropMimeData(data, action, row, column, parent): + return False + + if action == Qt.IgnoreAction: + return True +#! [1] + +#! [2] + begin_row = 0 + + if row != -1: + begin_row = row +#! [2] #! [3] + elif parent.isValid(): + begin_row = parent.row() +#! [3] #! [4] + else: + begin_row = self.rowCount(QModelIndex()) +#! [4] + +#! [5] + encoded_data = data.data("application/vnd.text.list") + stream = QDataStream(encoded_data, QIODevice.ReadOnly) + new_items = [] + while not stream.atEnd(): + new_items.append(stream.readQString()) +#! [5] + +#! [6] + self.insertRows(begin_row, len(new_items), QModelIndex()) + for text in new_items: + idx = self.index(begin_row, 0, QModelIndex()) + self.setData(idx, text) + begin_row += 1 + + return True +#! [6] + +#! [7] + def flags(self, index): + default_flags = super().flags(index) + if index.isValid(): + return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | default_flags + return Qt.ItemIsDropEnabled | default_flags +#! [7] + +#! [8] + def mimeData(self, indexes): + mime_data = QMimeData() + encoded_data = QByteArray() + stream = QDataStream(encoded_data, QIODevice.WriteOnly) + for index in indexes: + if index.isValid(): + text = self.data(index, Qt.DisplayRole) + stream.writeQString(text) + + mime_data.setData("application/vnd.text.list", encoded_data) + return mime_data +#! [8] + +#! [9] + def mimeTypes(self): + return ["application/vnd.text.list"] +#! [9] + +#! [10] + def supportedDropActions(self): + return Qt.CopyAction | Qt.MoveAction +#! [10] + + +class MainWindow(QMainWindow): + + def __init__(self, parent=None): + super().__init__(parent) + + file_menu = self.menuBar().addMenu("&File") + quit_action = file_menu.addAction("E&xit") + quit_action.setShortcut("Ctrl+Q") + +#! [mainwindow0] + self._list_view = QListView(self) + self._list_view.setSelectionMode(QAbstractItemView.ExtendedSelection) + self._list_view.setDragEnabled(True) + self._list_view.setAcceptDrops(True) + self._list_view.setDropIndicatorShown(True) +#! [mainwindow0] + + quit_action.triggered.connect(self.close) + + self.setup_list_items() + + self.setCentralWidget(self._list_view) + self.setWindowTitle("List View") + + def setup_list_items(self): + items = ["Oak", "Fir", "Pine", "Birch", "Hazel", "Redwood", "Sycamore", + "Chestnut", "Mahogany"] + model = DragDropListModel(items, self) + self._list_view.setModel(model) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/modelviewprogramming/simplemodel-use.py b/sources/pyside6/doc/tutorials/modelviewprogramming/simplemodel-use.py new file mode 100644 index 000000000..1bacfd829 --- /dev/null +++ b/sources/pyside6/doc/tutorials/modelviewprogramming/simplemodel-use.py @@ -0,0 +1,44 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtWidgets import (QApplication, QFileSystemModel, QLabel, + QVBoxLayout, QWidget) +from PySide6.QtGui import QPalette +from PySide6.QtCore import QDir, Qt + + +if __name__ == '__main__': + app = QApplication(sys.argv) + + window = QWidget() + layout = QVBoxLayout(window) + title = QLabel("Some items from the directory model", window) + title.setBackgroundRole(QPalette.Base) + title.setMargin(8) + layout.addWidget(title) + +#! [0] + model = QFileSystemModel() + model.setRootPath(QDir.currentPath()) + + def on_directory_loaded(directory): + parent_index = model.index(directory) + num_rows = model.rowCount(parent_index) +#! [1] + for row in range(num_rows): + index = model.index(row, 0, parent_index) +#! [1] +#! [2] + text = model.data(index, Qt.DisplayRole) +#! [2] + label = QLabel(text, window) + layout.addWidget(label) + + model.directoryLoaded.connect(on_directory_loaded) +#! [0] + + window.setWindowTitle("A simple model example") + window.show() + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/modelviewprogramming/stringlistmodel.py b/sources/pyside6/doc/tutorials/modelviewprogramming/stringlistmodel.py new file mode 100644 index 000000000..2c8493aa9 --- /dev/null +++ b/sources/pyside6/doc/tutorials/modelviewprogramming/stringlistmodel.py @@ -0,0 +1,124 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtWidgets import (QApplication, QListView) +from PySide6.QtCore import QAbstractListModel, QStringListModel, QModelIndex, Qt + + +#! [0] +class StringListModel(QAbstractListModel): + def __init__(self, strings, parent=None): + super().__init__(parent) + self._strings = strings + +#! [0] + def rowCount(self, parent=QModelIndex()): + """Returns the number of items in the string list as the number of rows + in the model.""" + return len(self._strings) +#! [0] + +#! [1] + def data(self, index, role): + """Returns an appropriate value for the requested data. + If the view requests an invalid index, an invalid variant is returned. + Any valid index that corresponds to a string in the list causes that + string to be returned.""" + row = index.row() + if not index.isValid() or row >= len(self._strings): + return None + if role != Qt.DisplayRole and role != Qt.EditRole: + return None + return self._strings[row] +#! [1] + +#! [2] + def headerData(self, section, orientation, role=Qt.DisplayRole): + """Returns the appropriate header string depending on the orientation of + the header and the section. If anything other than the display role is + requested, we return an invalid variant.""" + if role != Qt.DisplayRole: + return None + if orientation == Qt.Horizontal: + return f"Column {section}" + return f"Row {section}" +#! [2] + +#! [3] + def flags(self, index): + """Returns an appropriate value for the item's flags. Valid items are + enabled, selectable, and editable.""" + + if not index.isValid(): + return Qt.ItemIsEnabled + return super().flags(index) | Qt.ItemIsEditable +#! [3] + + #! [4] + def setData(self, index, value, role=Qt.EditRole): + """Changes an item in the string list, but only if the following conditions + are met: + + # The index supplied is valid. + # The index corresponds to an item to be shown in a view. + # The role associated with editing text is specified. + + The dataChanged() signal is emitted if the item is changed.""" + + if index.isValid() and role == Qt.EditRole: + self._strings[index.row()] = value + self.dataChanged.emit(index, index, {role}) + return True +#! [4] #! [5] + return False +#! [5] + +#! [6] + def insertRows(self, position, rows, parent): + """Inserts a number of rows into the model at the specified position.""" + self.beginInsertRows(QModelIndex(), position, position + rows - 1) + for row in range(rows): + self._strings.insert(position, "") + self.endInsertRows() + return True +#! [6] #! [7] +#! [7] + +#! [8] + def removeRows(self, position, rows, parent): + """Removes a number of rows from the model at the specified position.""" + self.beginRemoveRows(QModelIndex(), position, position + rows - 1) + for row in range(rows): + del self._strings[position] + self.endRemoveRows() + return True +#! [8] #! [9] +#! [9] + + +#! [main0] +if __name__ == '__main__': + app = QApplication(sys.argv) + +#! [main1] + numbers = ["One", "Two", "Three", "Four", "Five"] + model = StringListModel(numbers) +#! [main0] #! [main1] #! [main2] #! [main3] + view = QListView() +#! [main2] + view.setWindowTitle("View onto a string list model") +#! [main4] + view.setModel(model) +#! [main3] #! [main4] + + model.insertRows(5, 7, QModelIndex()) + for row in range(5, 12): + index = model.index(row, 0, QModelIndex()) + model.setData(index, f"{row+1}") + +#! [main5] + view.show() + sys.exit(app.exec()) +#! [main5] 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 diff --git a/sources/pyside6/doc/tutorials/qmlapp/logo.png b/sources/pyside6/doc/tutorials/qmlapp/logo.png Binary files differnew file mode 100644 index 000000000..30c621c9c --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/logo.png diff --git a/sources/pyside6/doc/tutorials/qmlapp/main.py b/sources/pyside6/doc/tutorials/qmlapp/main.py new file mode 100644 index 000000000..8b1b25440 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/main.py @@ -0,0 +1,46 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import urllib.request +import json +from pathlib import Path + +from PySide6.QtQuick import QQuickView +from PySide6.QtCore import QStringListModel, QUrl +from PySide6.QtGui import QGuiApplication + + +if __name__ == '__main__': + + #get our data + url = "http://country.io/names.json" + response = urllib.request.urlopen(url) + data = json.loads(response.read().decode('utf-8')) + + #Format and sort the data + data_list = list(data.values()) + data_list.sort() + + #Set up the application window + app = QGuiApplication(sys.argv) + view = QQuickView() + view.setResizeMode(QQuickView.SizeRootObjectToView) + + #Expose the list to the Qml code + my_model = QStringListModel() + my_model.setStringList(data_list) + view.setInitialProperties({"myModel": my_model}) + + #Load the QML file + qml_file = Path(__file__).parent / "view.qml" + view.setSource(QUrl.fromLocalFile(qml_file.resolve())) + + #Show the window + if view.status() == QQuickView.Error: + sys.exit(-1) + view.show() + + #execute and cleanup + app.exec() + del view diff --git a/sources/pyside6/doc/tutorials/qmlapp/newpyproject.png b/sources/pyside6/doc/tutorials/qmlapp/newpyproject.png Binary files differnew file mode 100644 index 000000000..93968a52d --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/newpyproject.png diff --git a/sources/pyside6/doc/tutorials/qmlapp/projectsmode.png b/sources/pyside6/doc/tutorials/qmlapp/projectsmode.png Binary files differnew file mode 100644 index 000000000..c66d88723 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/projectsmode.png diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyprojname.png b/sources/pyside6/doc/tutorials/qmlapp/pyprojname.png Binary files differnew file mode 100644 index 000000000..98328074d --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/pyprojname.png diff --git a/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.png b/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.png Binary files differnew file mode 100644 index 000000000..e01e2ebeb --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/pyprojxplor.png diff --git a/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.png b/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.png Binary files differnew file mode 100644 index 000000000..ec0ad3dea --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.png diff --git a/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst b/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst new file mode 100644 index 000000000..c6d72e742 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/qmlapplication.rst @@ -0,0 +1,132 @@ +######################### +QML Application Tutorial +######################### + +This tutorial provides a quick walk-through of a python application +that loads a QML file. QML is a declarative language that lets you +design UIs faster than a traditional language, such as C++. The +QtQml and QtQuick modules provides the necessary infrastructure for +QML-based UIs. + +In this tutorial, you'll also learn how to provide data from Python +as a QML initial property, which is then consumed by the ListView +defined in the QML file. + +Before you begin, install the following prerequisites: + +* The `PySide6 <https://pypi.org/project/PySide6/>`_ Python packages. +* *Qt Creator* from + `https://download.qt.io + <https://download.qt.io/snapshots/qtcreator/>`_. + + +The following step-by-step instructions guide you through application +development process using *Qt Creator*: + +#. Open *Qt Creator* and select **File > New File or Project..** menu item + to open following dialog: + + .. image:: newpyproject.png + +#. Select **Qt for Python - Empty** from the list of application templates + and select **Choose**. + + .. image:: pyprojname.png + +#. Give a **Name** to your project, choose its location in the + filesystem, and select **Finish** to create an empty ``main.py`` + and ``main.pyproject``. + + .. image:: pyprojxplor.png + + This should create a ``main.py`` and ```main.pyproject`` files + for the project. + +#. Download :download:`view.qml<view.qml>` and :download:`logo.png <logo.png>` + and move them to your project folder. + +#. Double-click on ``main.pyproject`` to open it in edit mode, and append + ``view.qml`` and ``logo.png`` to the **files** list. This is how your + project file should look after this change: + + .. code:: + + { + "files": ["main.py", "view.qml", "logo.png"] + } + +#. Now that you have the necessary bits for the application, import the + Python modules in your ``main.py``, and download country data and + format it: + + .. literalinclude:: main.py + :linenos: + :lines: 3-23 + :emphasize-lines: 7-9,14-17 + +#. Now, set up the application window using + :ref:`PySide6.QtGui.QGuiApplication<qguiapplication>`, which manages the application-wide + settings. + + .. literalinclude:: main.py + :linenos: + :lines: 3-28 + :emphasize-lines: 23-25 + + .. note:: Setting the resize policy is important if you want the + root item to resize itself to fit the window or vice-a-versa. + Otherwise, the root item will retain its original size on + resizing the window. + +#. You can now expose the ``data_list`` variable as a QML initial + property, which will be consumed by the QML ListView item in ``view.qml``. + + .. literalinclude:: main.py + :linenos: + :lines: 3-33 + :emphasize-lines: 28-31 + +#. Load the ``view.qml`` to the ``QQuickView`` and call ``show()`` to + display the application window. + + .. literalinclude:: main.py + :linenos: + :lines: 3-42 + :emphasize-lines: 33-40 + +#. Finally, execute the application to start the event loop and clean up. + + .. literalinclude:: main.py + :linenos: + :lines: 3- + :emphasize-lines: 42-44 + +#. Your application is ready to be run now. Select **Projects** mode to + choose the Python version to run it. + + .. image:: projectsmode.png + +Run the application by using the ``CTRL+R`` keyboard shortcut to see if it +looks like this: + +.. image:: qmlapplication.png + +You could also watch the following video tutorial for guidance to develop +this application: + +.. raw:: html + + <div style="position: relative; padding-bottom: 56.25%; height: 0; + overflow: hidden; max-width: 100%; height: auto;"> + <iframe src="https://www.youtube.com/embed/JxfiUx60Mbg" frameborder="0" + allowfullscreen style="position: absolute; top: 0; left: 0; + width: 100%; height: 100%;"> + </iframe> + </div> + +******************** +Related information +******************** + +* `QML Reference <https://doc.qt.io/qt-5/qmlreference.html>`_ +* :doc:`../qmlintegration/qmlintegration` diff --git a/sources/pyside6/doc/tutorials/qmlapp/view.qml b/sources/pyside6/doc/tutorials/qmlapp/view.qml new file mode 100644 index 000000000..7f9b1d777 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlapp/view.qml @@ -0,0 +1,56 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Page { + width: 640 + height: 480 + required property var myModel + + header: Label { + color: "#15af15" + text: qsTr("Where do people use Qt?") + font.pointSize: 17 + font.bold: true + font.family: "Arial" + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignHCenter + padding: 10 + } + Rectangle { + id: root + width: parent.width + height: parent.height + + Image { + id: image + fillMode: Image.PreserveAspectFit + anchors.centerIn: root + source: "./logo.png" + opacity: 0.5 + + } + + ListView { + id: view + anchors.fill: root + anchors.margins: 25 + model: myModel + delegate: Text { + anchors.leftMargin: 50 + font.pointSize: 15 + horizontalAlignment: Text.AlignHCenter + text: display + } + } + } + NumberAnimation { + id: anim + running: true + target: view + property: "contentY" + duration: 500 + } +} diff --git a/sources/pyside6/doc/tutorials/qmlintegration/main.py b/sources/pyside6/doc/tutorials/qmlintegration/main.py new file mode 100644 index 000000000..0a751d7d1 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/main.py @@ -0,0 +1,77 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from pathlib import Path + +from PySide6.QtCore import QObject, Slot +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine, QmlElement +from PySide6.QtQuickControls2 import QQuickStyle + +import style_rc + +# To be used on the @QmlElement decorator +# (QML_IMPORT_MINOR_VERSION is optional) +QML_IMPORT_NAME = "io.qt.textproperties" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class Bridge(QObject): + + @Slot(str, result=str) + def getColor(self, s): + if s.lower() == "red": + return "#ef9a9a" + elif s.lower() == "green": + return "#a5d6a7" + elif s.lower() == "blue": + return "#90caf9" + else: + return "white" + + @Slot(float, result=int) + def getSize(self, s): + size = int(s * 34) + if size <= 0: + return 1 + else: + return size + + @Slot(str, result=bool) + def getItalic(self, s): + if s.lower() == "italic": + return True + else: + return False + + @Slot(str, result=bool) + def getBold(self, s): + if s.lower() == "bold": + return True + else: + return False + + @Slot(str, result=bool) + def getUnderline(self, s): + if s.lower() == "underline": + return True + else: + return False + + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + QQuickStyle.setStyle("Material") + engine = QQmlApplicationEngine() + + # Get the path of the current directory, and then add the name + # of the QML file, to load it. + qml_file = Path(__file__).parent / 'view.qml' + engine.load(qml_file) + + if not engine.rootObjects(): + sys.exit(-1) + + sys.exit(app.exec()) diff --git a/sources/pyside6/doc/tutorials/qmlintegration/qmlintegration.rst b/sources/pyside6/doc/tutorials/qmlintegration/qmlintegration.rst new file mode 100644 index 000000000..ff6fe3e31 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/qmlintegration.rst @@ -0,0 +1,128 @@ +Python-QML integration +====================== + +This tutorial provides a quick walk-through of a python application that loads, and interacts with +a QML file. QML is a declarative language that lets you design UIs faster than a traditional +language, such as C++. The QtQml and QtQuick modules provides the necessary infrastructure for +QML-based UIs. + +In this tutorial, you will learn how to integrate Python with a QML application. +This mechanism will help us to understand how to use Python as a backend for certain +signals from the UI elements in the QML interface. Additionally, you will learn how to provide +a modern look to your QML application using one of the features from Qt Quick Controls 2. + +The tutorial is based on an application that allow you to set many text properties, like increasing +the font size, changing the color, changing the style, and so on. Before you begin, install the +`PySide6 <https://pypi.org/project/PySide6/>`_ Python packages. + +The following step-by-step process will guide you through the key elements of the QML based +application and PySide6 integration: + +#. First, let's start with the following QML-based UI: + + .. image:: textproperties_default.png + + The design is based on a `GridLayout`, containing two `ColumnLayout`. + Inside the UI you will find many `RadioButton`, `Button`, and a `Slider`. + +#. With the QML file in place, you can load it from Python: + + .. literalinclude:: main.py + :linenos: + :lines: 63-76 + :emphasize-lines: 4,9 + + Notice that we only need a :code:`QQmlApplicationEngine` to + :code:`load` the QML file. + +#. Define the ``Bridge`` class, containing all the logic for the element + that will be register in QML: + + .. literalinclude:: main.py + :linenos: + :lines: 14-54 + :emphasize-lines: 3,4,7 + + Notice that the registration happens thanks to the :code:`QmlElement` + decorator, that underneath uses the reference to the :code:`Bridge` + class and the variables :code:`QML_IMPORT_NAME` and + :code:`QML_IMPORT_MAJOR_VERSION`. + +#. Now, go back to the QML file and connect the signals to the slots defined in the ``Bridge`` class: + + .. code:: js + + Bridge { + id: bridge + } + + Inside the :code:`ApplicationWindow` we declare a component + with the same name as the Python class, and provide an :code:`id:`. + This :code:`id` will help you to get a reference to the element + that was registered from Python. + + .. literalinclude:: view.qml + :linenos: + :lines: 45-55 + :emphasize-lines: 6-8 + + The properties *Italic*, *Bold*, and *Underline* are mutually + exclusive, this means only one can be active at any time. + To achieve this each time we select one of these options, we + check the three properties via the QML element property as you can + see in the above snippet. + Only one of the three will return *True*, while the other two + will return *False*, that is how we make sure only one is being + applied to the text. + +#. Each slot verifies if the selected option contains the text associated + to the property: + + .. literalinclude:: main.py + :linenos: + :lines: 42-47 + :emphasize-lines: 4,6 + + Returning *True* or *False* allows you to activate and deactivate + the properties of the QML UI elements. + + It is also possible to return other values that are not *Boolean*, + like the slot in charge of returning the font size: + + .. literalinclude:: main.py + :linenos: + :lines: 34-39 + +#. Now, for changing the look of our application, you have two options: + + 1. Use the command line: execute the python file adding the option, ``--style``:: + + python main.py --style material + + 2. Use a ``qtquickcontrols2.conf`` file: + + .. literalinclude:: qtquickcontrols2.conf + :linenos: + + Then add it to your ``.qrc`` file: + + .. literalinclude:: style.qrc + :linenos: + + Generate the *rc* file running, ``pyside6-rcc style.qrc -o style_rc.py`` + And finally import it from your ``main.py`` script. + + .. literalinclude:: main.py + :linenos: + :lines: 4-12 + :emphasize-lines: 9 + + You can read more about this configuration file + `here <https://doc.qt.io/qt-5/qtquickcontrols2-configuration.html>`_. + + The final look of your application will be: + + .. image:: textproperties_material.png + +You can :download:`view.qml <view.qml>` and +:download:`main.py <main.py>` to try this example. diff --git a/sources/pyside6/doc/tutorials/qmlintegration/qtquickcontrols2.conf b/sources/pyside6/doc/tutorials/qmlintegration/qtquickcontrols2.conf new file mode 100644 index 000000000..850646021 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/qtquickcontrols2.conf @@ -0,0 +1,10 @@ +[Controls] +Style=Material + +[Universal] +Theme=System +Accent=Red + +[Material] +Theme=Dark +Accent=Red diff --git a/sources/pyside6/doc/tutorials/qmlintegration/style.qrc b/sources/pyside6/doc/tutorials/qmlintegration/style.qrc new file mode 100644 index 000000000..e313f5ed6 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/style.qrc @@ -0,0 +1,5 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource prefix="/"> + <file>qtquickcontrols2.conf</file> +</qresource> +</RCC> diff --git a/sources/pyside6/doc/tutorials/qmlintegration/textproperties_default.png b/sources/pyside6/doc/tutorials/qmlintegration/textproperties_default.png Binary files differnew file mode 100644 index 000000000..cfeac9368 --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/textproperties_default.png diff --git a/sources/pyside6/doc/tutorials/qmlintegration/textproperties_material.png b/sources/pyside6/doc/tutorials/qmlintegration/textproperties_material.png Binary files differnew file mode 100644 index 000000000..47866c10e --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/textproperties_material.png diff --git a/sources/pyside6/doc/tutorials/qmlintegration/view.qml b/sources/pyside6/doc/tutorials/qmlintegration/view.qml new file mode 100644 index 000000000..635603fac --- /dev/null +++ b/sources/pyside6/doc/tutorials/qmlintegration/view.qml @@ -0,0 +1,160 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + + +import QtQuick 2.0 +import QtQuick.Layouts 1.11 +import QtQuick.Controls 2.1 +import QtQuick.Window 2.1 +import QtQuick.Controls.Material 2.1 + +import io.qt.textproperties 1.0 + +ApplicationWindow { + id: page + width: 800 + height: 400 + visible: true + Material.theme: Material.Dark + Material.accent: Material.Red + + Bridge { + id: bridge + } + + GridLayout { + id: grid + columns: 2 + rows: 3 + + ColumnLayout { + spacing: 2 + Layout.columnSpan: 1 + Layout.preferredWidth: 400 + + Text { + id: leftlabel + Layout.alignment: Qt.AlignHCenter + color: "white" + font.pointSize: 16 + text: "Qt for Python" + Layout.preferredHeight: 100 + Material.accent: Material.Green + } + + RadioButton { + id: italic + Layout.alignment: Qt.AlignLeft + text: "Italic" + onToggled: { + leftlabel.font.italic = bridge.getItalic(italic.text) + leftlabel.font.bold = bridge.getBold(italic.text) + leftlabel.font.underline = bridge.getUnderline(italic.text) + + } + } + RadioButton { + id: bold + Layout.alignment: Qt.AlignLeft + text: "Bold" + onToggled: { + leftlabel.font.italic = bridge.getItalic(bold.text) + leftlabel.font.bold = bridge.getBold(bold.text) + leftlabel.font.underline = bridge.getUnderline(bold.text) + } + } + RadioButton { + id: underline + Layout.alignment: Qt.AlignLeft + text: "Underline" + onToggled: { + leftlabel.font.italic = bridge.getItalic(underline.text) + leftlabel.font.bold = bridge.getBold(underline.text) + leftlabel.font.underline = bridge.getUnderline(underline.text) + } + } + RadioButton { + id: noneradio + Layout.alignment: Qt.AlignLeft + text: "None" + checked: true + onToggled: { + leftlabel.font.italic = bridge.getItalic(noneradio.text) + leftlabel.font.bold = bridge.getBold(noneradio.text) + leftlabel.font.underline = bridge.getUnderline(noneradio.text) + } + } + } + + ColumnLayout { + id: rightcolumn + spacing: 2 + Layout.columnSpan: 1 + Layout.preferredWidth: 400 + Layout.preferredHeight: 400 + Layout.fillWidth: true + + RowLayout { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + + + Button { + id: red + text: "Red" + highlighted: true + Material.accent: Material.Red + onClicked: { + leftlabel.color = bridge.getColor(red.text) + } + } + Button { + id: green + text: "Green" + highlighted: true + Material.accent: Material.Green + onClicked: { + leftlabel.color = bridge.getColor(green.text) + } + } + Button { + id: blue + text: "Blue" + highlighted: true + Material.accent: Material.Blue + onClicked: { + leftlabel.color = bridge.getColor(blue.text) + } + } + Button { + id: nonebutton + text: "None" + highlighted: true + Material.accent: Material.BlueGrey + onClicked: { + leftlabel.color = bridge.getColor(nonebutton.text) + } + } + } + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + Text { + id: rightlabel + color: "white" + Layout.alignment: Qt.AlignLeft + text: "Font size" + Material.accent: Material.White + } + Slider { + width: rightcolumn.width*0.6 + Layout.alignment: Qt.AlignRight + id: slider + value: 0.5 + onValueChanged: { + leftlabel.font.pointSize = bridge.getSize(value) + } + } + } + } + } +} 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() |