Вывод данных

Изображения

Для вывода изображений используется QPixmap, которую можно выставить, например, у виджета QLabel.

Код под спойлером ниже создаёт QPixmap непосредственно передавая ему в качестве аргумента путь к изображению и выводит его в QLabel.

../_images/pixmap.png
Код.
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.

../_images/qimage.png
Код.
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, которым он должен управлять.

../_images/mpl.png

Код под спойлером ниже применяет эти виджеты для вывода графика синуса. Результат выглядит очень похоже на дефолтные окна 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).

../_images/tables.png

Под спойлером ниже приводится код, который воплощает все перечисленные шаги.

Код.
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 может сделать запрос на изменения данных, обратившись к модели данных.

../_images/modelview.png

Для того, чтобы создать свою модель табличных данных, необходимо наследовать от 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()