{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Вывод данных\n", "\n", "## Изображения\n", "\n", "Для вывода изображений используется [QPixmap](https://doc.qt.io/qtforpython/PySide6/QtGui/QPixmap.html), которую можно выставить, например, у виджета `QLabel`. \n", "\n", "Код под спойлером ниже создаёт `QPixmap` непосредственно передавая ему в качестве аргумента путь к изображению и выводит его в `QLabel`.\n", "\n", "```{figure} ../_static/lecture_specific/qt/pixmap.png\n", "```\n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "import os\n", "\n", "from PySide6.QtWidgets import *\n", "from PySide6.QtGui import *\n", "\n", "\n", "path_to_the_image = os.path.join(\"..\", \"images\", \"msu.jpg\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " label = QLabel()\n", " pixmap = QPixmap(path_to_the_image)\n", " label.setPixmap(pixmap)\n", " self.setCentralWidget(label)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "Чтобы производить манипуляции с пикселями необходимо использовать [QImage](https://doc.qt.io/qtforpython/PySide6/QtGui/QImage.html). Следующий пример создаёт изображения с шаблоном шахматной доски, манипулируя значениями яркостей пикселей. Заметим что, чтобы вывести, необходимо опять создать `QPixmap`. \n", "\n", "```{figure} ../_static/lecture_specific/qt/qimage.png\n", "```\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "\n", "from PySide6.QtWidgets import *\n", "from PySide6.QtGui import *\n", "\n", "dark = (119, 149, 86)\n", "light = (235, 236, 208)\n", "\n", "\n", "class ChessBoard(QImage):\n", " def __init__(self):\n", " super().__init__(8, 8, QImage.Format_RGB16)\n", " for i in range(8):\n", " for j in range(8):\n", " color = dark if (i + j) % 2 else light\n", " self.setPixelColor(i, j, QColor(*color))\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " label = QLabel()\n", " image = ChessBoard()\n", " pixmap = QPixmap(image)\n", " pixmap.setDevicePixelRatio(0.025)\n", " label.setPixmap(pixmap)\n", " label.setAlignment(Qt.AlignCenter)\n", " self.setCentralWidget(label)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "## Встраивание графиков `matplotlib`\n", "\n", "### Статическое изображение\n", "\n", "Библиотека `matplotlib` поддерживает рендеринг на разных так называемых бэкендах (`backend`). Так, например, графики при работе простых скриптов выводятся в специально создаваемых отдельных окнах, а при работе в `jupyter notebooks` выводятся внутри вывода ячейки. Для того чтобы выводить графики в приложениях `Qt` достаточно использовать правильный `backend`. На момент написания `matplotlib` не успел подготовить `backend` для `Qt6`, но `backend` для `Qt5` с небольшими издержками, но работает. Когда вы читаете это, возможно выйдет соответствующее обновление полностью поддерживающее `Qt6`, а если нет, то всегда можно откатиться к `PySide5` без особой потери функционала в `Qt`. \n", "\n", "Виджет `FigureCanvasQTAgg` из подмодуля `matplotlib.backends.backend_qt5agg` позволяет отображать `matplotlib` фигуры (`figure`). Конструктор в качестве параметра принимает `figure` для отображения. `NavigationToolbar2QT` позволяет выводить так же и панель инструментов к графику. Этот виджет принимает в конструкторе экземпляр `FigureCanvasQTAgg`, которым он должен управлять.\n", "\n", "```{figure} ../_static/lecture_specific/qt/mpl.png\n", "```\n", "\n", "Код под спойлером ниже применяет эти виджеты для вывода графика синуса. Результат выглядит очень похоже на дефолтные окна `matplotlib`. \n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT\n", "\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "def plot():\n", " fig, ax = plt.subplots(figsize=(2, 2))\n", " x = np.linspace(0, 2 * np.pi, 100)\n", " y = np.sin(x)\n", " ax.plot(x, y)\n", " return fig\n", "\n", "\n", "class MPLGraph(QWidget):\n", " def __init__(self):\n", " super().__init__()\n", " self.fig = plot()\n", "\n", " # widgets\n", " self.canvas = FigureCanvasQTAgg(self.fig)\n", " self.navigation_bar = NavigationToolbar2QT(self.canvas, parent=self)\n", "\n", " # layout\n", " layout = QVBoxLayout()\n", " layout.addWidget(self.navigation_bar)\n", " layout.addWidget(self.canvas)\n", " self.setLayout(layout)\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " layout = QVBoxLayout()\n", " graph = MPLGraph()\n", " graph.setLayout(layout)\n", " self.setCentralWidget(graph)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "### Анимация\n", "\n", "Чтобы изменять нарисованный график, например, в результате взаимодействия пользователя с интерфейсом, можно изменять содержимое фигуры и осей в нем, а затем вызывать у объекта `FigureCanvasQTAgg` метод `draw`, чтобы изменения перенеслись и на экран. При этом тут доступны две опции:\n", "- очищать содержимое осей методом [Axes.clear](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.clear.html) и рисовать всё заново;\n", "- модифицировать прежде нарисованные элементы графика (`artist` в терминах `matplotlib`).\n", "В простеньких приложениях первый подход может работать с приемлемой производительностью и даже порождать более простой код, так как отсутствует необходимость запоминать каждого `artist`, чтобы подом его модифицировать. Но в нагруженных приложения просадка производительности, вызванная перерисовкой графика с нуля, может оказаться существенной и тогда второй подход просто необходим.\n", "\n", "В качестве примера расширим предыдущий пример таким образом, чтобы синусоида изменяла фазу с течением времени. Для этого добавим метод `update_plot`, который модифицирует `y` координаты линии уже нарисованной синусоиды. Чтобы достичь эффекта анимации, будем периодически вызывать этот метод. Для этого удобно воспользоваться [QtCore.QTimer](https://doc.qt.io/qtforpython/PySide6/QtCore/QTimer.html). `QTimer` может испускать сигнал `timeout` через каждый промежуток времени `interval`, который можно настроить методом [setInterval](https://doc.qt.io/qtforpython/PySide6/QtCore/QTimer.html#PySide6.QtCore.PySide6.QtCore.QTimer.setInterval), передав ему величину интервала в миллисекундах в качестве аргумента.\n", "\n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT\n", "\n", "from PySide6.QtCore import QTimer\n", "from PySide6.QtWidgets import QApplication, QWidget, QMainWindow, QVBoxLayout\n", "\n", "\n", "class MPLLiveGraph(QWidget):\n", " def __init__(self):\n", " super().__init__()\n", " self.setup_plot()\n", "\n", " # widgets\n", " self.canvas = FigureCanvasQTAgg(self.fig)\n", " self.navigation_bar = NavigationToolbar2QT(self.canvas, parent=self)\n", "\n", " # layout\n", " layout = QVBoxLayout()\n", " layout.addWidget(self.navigation_bar)\n", " layout.addWidget(self.canvas)\n", " self.setLayout(layout)\n", " self.t = 0\n", "\n", " def setup_plot(self):\n", " self.fig, self.ax = plt.subplots(figsize=(2, 2))\n", " self.x = np.linspace(0, 2 * np.pi, 100)\n", " y = np.sin(self.x)\n", " self.line, = self.ax.plot(self.x, y)\n", "\n", " def update_plot(self):\n", " self.t += 0.1\n", " y = np.sin(self.x - self.t)\n", " self.line.set_ydata(y)\n", " self.canvas.draw()\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self, fps=24):\n", " super().__init__()\n", " graph = MPLLiveGraph()\n", " self.setCentralWidget(graph)\n", "\n", " # timer\n", " self.timer = QTimer()\n", " self.timer.setInterval(1000 / fps)\n", " self.timer.timeout.connect(graph.update_plot)\n", " self.timer.start()\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "\n", "## Табличные данные\n", "\n", "В `Qt` есть два виджета, позволяющих отображать табличные данные: [QTableView](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableView.html) и [QTableWidget](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html). \n", "- `QTableWidget` является несколько более высокоуровневым: он и хранит значения ячеек в виджетах [QTableWidgetItem](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidgetItem.html) и выводит их на экран. \n", "- `QTableView` предназначен только для предоставляет пользователю средства для взаимодействия (вывод на экран и/или изменения данных) с таблицами, которые обычно хранятся снаружи самого `QTableView`.\n", "\n", "Так как работа с таблицами в `python` чаще всего осуществляется средствами `pandas`, то обычно вопрос стоит в синхронизации отображаемой в интерфейсе таблицы и таблицы в `pd.DataFrame`. Для таких целей `QTableView` идеологически подходит гораздо лучше, но в образовательных целях разберем сначала решение этой проблемы средствами `QTableWidget`.\n", "\n", "\n", "### `QTableWidget`\n", "\n", "[QTableWidget](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html) хранит значения в ячейках в виджетах [QTableWidgetItem](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidgetItem.html) и выводит их. Добиться того, чтобы содержимое `QTableWidget` заполнилось значениями из таблицы `pandas`, можно следующими шагами:\n", "\n", "- выставить правильное количество строк и столбцов. Сделать это можно указав два целых числа при инициализации виджета или методами [setRowCount](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setRowCount) и [setColumnCount](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setColumnCount).\n", "- заполнить ячейки соответствующими данными. Для этого необходимо создать экземпляр `QTableWidgetItem` с правильным значением и поставить его в правильную ячейку таблицы методом [setItem](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setItem), передав ему номер строки, номер столбца и созданный `QTableWidgetItem`.\n", "- выставить названия столбцов методом [setHorizontalHeaderLabels](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setHorizontalHeaderLabels), передав ему список строк. Также можно заполнять названия столбцов по одному методом [setHorizontalHeaderItem](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setHorizontalHeaderItem), но в большинстве ситуаций этой перебор.\n", "- выставить названия строк, используя аналогичные методы [setVerticalHeaderLabels](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setVerticalHeaderLabels) и [setVerticalHeaderItem](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.setVerticalHeaderItem).\n", "\n", "Теперь, чтобы добиться синхронизации, необходимо, чтобы при изменении содержимого одной из таблиц, содержимое другой таблицы претерпевало те же изменения. Изменять содержимое `QTableWidget` при изменении значений в таблице `pandas` можно используя уже перечисленные методы. Чтобы значения в таблице `pandas` изменялись, при изменении значений ячеек `QTableWidget`, можно использовать сигнал [cellChanged](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.cellChanged).\n", "\n", "```{note}\n", "Сигнал `cellChanged` передаёт в качестве параметров номера строки и столбца изменившейся ячейки.\n", "```\n", "\n", "```{note}\n", "`QTableWidgetItem` может хранить не только строковые значения, но и другие виджеты. Например, флажки (`checkbox`).\n", "```\n", "\n", "```{figure} ../_static/lecture_specific/qt/tables.png\n", "```\n", "\n", "Под спойлером ниже приводится код, который воплощает все перечисленные шаги.\n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import os.path\n", "import sys\n", "\n", "import pandas as pd\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "path_to_table = os.path.join(\"..\", \"data\", \"planets.csv\")\n", "\n", "\n", "class PandasTable(QTableWidget):\n", " def __init__(self, df):\n", " self.df = df\n", " nrows, ncols = df.shape\n", " super().__init__(nrows, ncols)\n", "\n", " self.setHorizontalHeaderLabels(self.df.columns)\n", " self.setVerticalHeaderLabels(self.df.index)\n", "\n", " for i, (label, row) in enumerate(self.df.iterrows()):\n", " for j, value in enumerate(row):\n", " self.setItem(i, j, QTableWidgetItem(str(value)))\n", "\n", " self.cellChanged.connect(self.on_cell_changed)\n", "\n", " def on_cell_changed(self, i, j):\n", " self.df.iloc[i, j] = float(self.item(i, j).text())\n", " print(self.df.head())\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " df = pd.read_csv(path_to_table, index_col=\"planet\")\n", " table = PandasTable(df)\n", " self.setCentralWidget(table)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "\n", "### `QTableView`\n", "\n", "[QTableView](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableView.html) данные сам по себе не хранит. В самом простом своём варианте `QTable` просто отображает данные, которые хранятся где-то в другом месте. Чтобы это работало, необходимо выставить для этого виджета правильную модель данных.\n", "\n", "```{note}\n", "Подробнее об этом принципе можно почитать [здесь](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#model-view-programming).\n", "```\n", "\n", "Грубо говоря, модель данных выступает прослойкой между `QTableView` и самими данными. Модель данных говорит `QTableView` какие значения в каких ячейках отображать, а `QTableView` может сделать запрос на изменения данных, обратившись к модели данных.\n", "\n", "```{figure} ../_static/lecture_specific/qt/modelview.png\n", "```\n", "\n", "Для того, чтобы создать свою модель табличных данных, необходимо наследовать от [QAbstractTableModel](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractTableModel.html), а затем перегрузить минимум следующие методы:\n", "\n", "- [rowCount](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.rowCount), чтобы он возвращал корректное количество строк;\n", "- [columnCount](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.columnCount), чтобы он возвращал корректное количество столбцов;\n", "- [data](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.data), который должен принимать на вход индекс в виде объекта [QModelIndex](https://doc.qt.io/qtforpython/PySide6/QtCore/QModelIndex.html#PySide6.QtCore.PySide6.QtCore.QModelIndex) (у этого объекта методами `row` и `column`).\n", "\n", "Переопределив эти методы, вы получаете работающую таблицу в режиме `read-only`. Чтобы добавить обратную связь, необходимо в добавок переопределить:\n", "- [setData](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.setData), чтобы он изменял значение ячейки с указанным индексом в таблице на переданное значение. Этот метод должен возвращать `True`, если изменение данных проходит успешно, и `False` иначе.\n", "- [flags](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.flags), чтобы он для ячеек, которые можно редактировать, возвращал правильные флаги из `enum` [QtCore.Qt.ItemFlag](https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.ItemFlag): `ItemIsEditable` --- можно редактировать, `ItemIsEnabled` --- можно кликнуть на него мышкой и поставить курсов.\n", "\n", "\n", "Код под спойлером ниже создаёт модель данных для таблицы `pandas` и `QTableView`, чтобы вывести её на экран.\n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import os.path\n", "import sys\n", "\n", "import pandas as pd\n", "from PySide6.QtCore import *\n", "from PySide6.QtWidgets import *\n", "\n", "path_to_table = os.path.join(\"..\", \"data\", \"planets.csv\")\n", "\n", "\n", "class PandasModel(QAbstractTableModel):\n", " def __init__(self, df, parent=None):\n", " QAbstractTableModel.__init__(self, parent)\n", " self.df = df\n", "\n", " def rowCount(self, parent=None):\n", " return len(self.df)\n", "\n", " def columnCount(self, parent=None):\n", " return self.df.columns.size\n", "\n", " def data(self, index, role=Qt.DisplayRole):\n", " if index.isValid():\n", " if role == Qt.DisplayRole:\n", " r, c = index.row(), index.column()\n", " return str(self.df.iat[r, c])\n", " return None\n", "\n", " def setData(self, index, value, role=Qt.EditRole):\n", " if index.isValid():\n", " if role == Qt.EditRole:\n", " r, c = index.row(), index.column()\n", " self.df.iat[r, c] = float(value)\n", " self.dataChanged.emit(index, index, value)\n", " return True\n", " return False\n", "\n", " def flags(self, index):\n", " return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable\n", "\n", " def headerData(self, i, orientation, role):\n", " if orientation == Qt.Horizontal and role == Qt.DisplayRole:\n", " return self.df.columns[i]\n", " if orientation == Qt.Vertical and role == Qt.DisplayRole:\n", " return self.df.index[i]\n", " return None\n", "\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " df = pd.read_csv(path_to_table, index_col=\"planet\")\n", " self.table_model = PandasModel(df)\n", " self.table_model.dataChanged.connect(self.print_data)\n", " table_view = QTableView()\n", " table_view.setModel(self.table_model)\n", "\n", " self.setCentralWidget(table_view)\n", "\n", " def print_data(self):\n", " print(self.table_model.df.head())\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
" ] } ], "metadata": { "language_info": { "name": "python" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }