{
"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
}