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