在PySide6的QTableView中显示numpy和pandas表(为PySide6更新)

1,792 阅读17分钟

在上一章中,我们介绍了模型视图的架构。然而,我们只触及了其中一个模型视图--QListView 。在Qt5中还有另外两个模型视图--QTableViewQTreeView ,它们使用相同的QStandardItemModel ,提供了表格(类似Excel)和树形(类似文件目录浏览器)视图。

在本教程中,我们将看看如何从PySide中使用QTableView ,包括如何对你的数据进行建模,格式化显示的值和添加条件格式。

你可以对任何数据源使用模型视图,只要你的模型以Qt能够理解的格式返回数据。在Python中与表格数据一起工作,为我们如何加载和处理这些数据提供了许多可能性。在这里,我们将从一个简单的嵌套list list 开始,然后转向将你的Qt应用程序与流行的numpypandas库集成。这将为你建立以数据为中心的应用程序打下良好的基础。

介绍QTableView

QTableView 是一个Qt视图部件,它在一个类似电子表格的视图中展示数据。像模型视图架构中的所有部件一样,它使用一个单独的模型来为视图提供数据和展示信息。模型中的数据可以根据需要进行更新,并通知视图这些变化以重新绘制/显示这些变化。 通过定制模型,可以对数据的呈现方式进行大量的控制。

为了使用这个模型,我们需要一个基本的应用结构和一些假的数据。 下面是一个简单的工作例子,它定义了一个自定义模型,用一个简单的嵌套列表作为数据存储。

我们将在稍后详细介绍替代数据结构。

python

import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # See below for the nested-list data structure.
            # .row() indexes into the outer list,
            # .column() indexes into the sub-list
            return self._data[index.row()][index.column()]

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()

        data = [
          [4, 9, 2],
          [1, 0, 0],
          [3, 5, 0],
          [3, 3, 2],
          [7, 8, 9],
        ]

        self.model = TableModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

就像我们之前的模型视图例子一样,我们创建了QTableView widget,然后创建了我们的自定义模型的实例(我们已经写好接受数据源作为参数),然后我们在视图上设置模型。这就是我们需要做的--视图部件现在使用模型来获取数据,并决定如何绘制它。

Basic QTableView example 基本的QTableView例子

嵌套list 作为一个二维数据存储

对于一个表格,你需要一个二维数据结构,有列和行。如上面的例子所示,你可以用一个嵌套的Pythonlist 来模拟一个简单的二维数据结构。

python

table = [  [4, 1, 3, 3, 7],
  [9, 1, 5, 3, 8],
  [2, 1, 5, 3, 9],
]

嵌套列表是一个 "价值列表的列表"--一个包含若干子列表的外层列表,这些子列表本身就包含价值。在这种结构下,要对单个值(或 "单元格")进行索引,你必须进行两次索引,首先要返回内部list 对象之一,然后再对该list

典型的安排是外层列表保存,每个嵌套列表包含的值。通过这种安排,当你索引时,你首先按行索引,然后按列索引--使我们的例子table ,成为一个3行5列的表格。有用的是,这与源代码中的视觉布局一致。

表的第一个索引将返回一个嵌套的子列表------。

蟒蛇

row = 2
col = 4

>>> table[row]
[2, 1, 5, 3, 9]

然后你再次索引以返回值----。

>>> table[row][col]
9

注意,使用这种类型的结构,你不能轻易地返回整个,而是需要遍历所有的行。然而,你当然可以自由地把事情翻过来,把第一个索引当作列来使用,这取决于按列还是按行访问对你更有用。

table = [  [4, 9, 2],
  [1, 1, 1],
  [3, 5, 5],
  [3, 3, 2],
  [7, 8, 9],
]

row = 4  # reversed
col = 2  # reversed

>>> table[col]
[3, 5, 5]

>>> table[col][row]
9

这种数据结构并没有强制要求行或列的长度相等 - 一行可以是5个元素,另一行可以是200个。不一致的情况会导致表视图上出现意外的错误。如果你正在处理大型或复杂的数据表,请看后面的替代数据存储。

接下来我们将更详细地看看我们的自定义TableModel ,看看它是如何使用这个简单的数据结构来显示数值的。

编写一个自定义的QAbstractTableModel

模型视图架构中,模型负责提供数据和视图显示的元数据。为了在我们的数据对象和视图之间建立接口,我们需要编写我们自己的自定义模型,它能理解我们的数据结构。

为了编写我们的自定义模型,我们可以创建一个QAbstractTableModel 的子类。自定义表模型唯一需要的方法是datarowCountcolumnCount 。 第一个方法为表格中的指定位置返回数据(或展示信息),而后两个方法必须为数据源的尺寸返回一个整数值。

class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # See below for the nested-list data structure.
            # .row() indexes into the outer list,
            # .column() indexes into the sub-list
            return self._data[index.row()][index.column()]

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

QtCore.QAbstractTableModel 是一个抽象的基类,意味着它没有方法的实现。如果你试图直接使用它,它将无法工作。你必须对它进行子类化。

__init__ 构造函数中,我们接受一个单一的参数data ,我们将其存储为实例属性self._data ,这样我们就可以从我们的方法中访问它。传入的数据结构是通过引用来存储的,所以任何外部变化都会反映在这里。

为了通知模型的变化,你需要触发模型的layoutChanged 信号,使用self.model.layoutChanged.emit() 。更多信息请参见前面的ModelView教程

data 方法被调用时有两个值indexroleindex 参数给出了当前被请求的信息在表中的位置,并且有两个方法.row().column() ,分别给出了视图中的行和列号。在我们的例子中,数据被存储为一个嵌套的列表,行和列的索引被用于索引,如下:data[row][column]

视图对源数据的结构一无所知,由模型负责在视图的行和列与你自己的数据存储中的相关位置之间进行转换。

role 参数描述了该方法在这次调用中应该返回什么样信息。role 为了得到要显示的数据,视图调用这个模型方法,Qt.DisplayRole 。然而,role 可以有许多其他的值,包括Qt.BackgroundRoleQt.CheckStateRoleQt.DecorationRoleQt.FontRoleQt.TextAlignmentRoleQt.ForegroundRole ,它们都期望得到特定的响应值(见后面)。

Qt.DisplayRole 实际上期望返回一个字符串,尽管其他基本的Python类型包括float,intbool ,也将使用它们默认的字符串表示法来显示。然而,将这些类型格式化为你的字符串通常是最好的。

Basic QTableView example 基本的QTableView例子

我们将在后面介绍如何使用这些其他的角色类型,现在只需要知道在返回数据显示之前,你必须检查角色类型是Qt.DisplayRole

两个自定义方法columnCountrowCount 返回我们数据结构中的列数和行数。list 在我们这里使用的list 嵌套的情况下,行数只是外层列表中的元素数,列数是其中一个内部列表中的元素数--假设它们都是相等的。

如果这些方法返回的值太高,你会看到越界错误,如果它们返回的值太低,你会看到表格被切断。

数字和日期的格式化

模型返回的用于显示的数据被期望为一个字符串。 虽然intfloat 值也将被显示,使用其默认的字符串表示,但复杂的 Python 类型将不会。要显示这些,或者要覆盖float,intbool 值的默认格式,你必须自己把它们格式化为字符串。

你可能想通过事先将你的数据转换为一个字符串表来做到这一点。然而,这样做会使你很难继续处理你表中的数据,无论是计算还是更新。

相反,你应该使用模型的data 方法来按需执行字符串转换。通过这样做,你可以继续使用原始数据,但可以完全控制它是如何呈现给用户的--包括在通过配置时临时改变它。

下面是一个简单的自定义格式,它在我们的数据表中查找数值,并根据数据的Pythontype ,以多种不同的方式显示它们。

python

def data(self, index, role):
    if role == Qt.DisplayRole:
        # Get the raw value
        value = self._data[index.row()][index.column()]

        # Perform per-type checks and render accordingly.
        if isinstance(value, datetime):
            # Render time to YYY-MM-DD.
            return value.strftime("%Y-%m-%d")

        if isinstance(value, float):
            # Render float to 2 dp
            return "%.2f" % value

        if isinstance(value, str):
            # Render strings with quotes
            return '"%s"' % value

        # Default (anything not captured above: e.g. int)
        return value

把它和下面修改过的样本数据一起使用,可以看到它的实际效果。

data = [    [4, 9, 2],
    [1, -1, 'hello'],
    [3.023, 5, -5],
    [3, 3, datetime(2017,10,1)],
    [7.555, 8, 9],
]

QTableView data formatting QTableView的数据格式化

到目前为止,我们只看了如何定制数据本身的格式化方式。然而,模型界面为你提供了对表格单元格显示的更多控制,包括颜色和图标。在下一部分,我们将看看如何使用模型来定制QTableView 的外观。

使用角色的样式和颜色

使用颜色和图标来突出数据表中的单元格,可以使数据更容易找到和理解,或者帮助用户选择或标记感兴趣的数据。Qt允许从模型中完全控制所有这些,通过对data 方法上的相关角色进行响应。

响应各种role ,预计将返回的类型如下所示。

角色类型
Qt.BackgroundRoleQBrush (也是 )QColor
Qt.CheckStateRoleQt.CheckState
Qt.DecorationRoleQIcon,QPixmapQColor
Qt.DisplayRoleQString (也是 , , )int float bool
Qt.FontRoleQFont
Qt.SizeHintRoleQSize
Qt.TextAlignmentRoleQt.Alignment
Qt.ForegroundRoleQBrush (也是 )=QColor

通过响应roleindex 的特定组合,我们可以修改表格中特定单元格、列或行的外观--例如,为第三列的所有单元格设置蓝色背景。

def data(self, index, role):
    # existing `if role == Qt.DisplayRole:` block hidden
    # hidden for clarity.

    if role == Qt.BackgroundRole and index.column() == 2:
        # See below for the data structure.
        return QtGui.QColor('blue')

通过使用index ,从我们自己的数据中查找数值,我们也可以根据我们的数据中的数值来定制外观。下面我们将介绍一些比较常见的使用情况。

文本对齐

在我们之前的格式化例子中,我们使用文本格式化来显示float ,小数点后2位。然而,在显示数字的时候,将其右对齐也是很常见的,这样更容易在数字列表中进行比较。Qt.TextAlignmentRole 对于任何数字值,可以通过返回Qt.AlignRight 来实现。

修改后的data 方法如下所示。我们检查role == Qt.TextAlignmentRole ,像以前一样通过索引查找值,然后确定该值是否是数字。如果是的话,我们可以返回Qt.AlignVCenter + Qt.AlignRight ,使其在垂直方向上居中,在水平方向上居右。

def data(self, index, role):
    # existing `if role == Qt.DisplayRole:` block hidden
    # hidden for clarity.

    if role == Qt.TextAlignmentRole:
        value = self._data[index.row()][index.column()]

        if isinstance(value, int) or isinstance(value, float):
            # Align right, vertical middle.
            return Qt.AlignVCenter + Qt.AlignRight

其他的排列方式也是可以的,包括Qt.AlignHCenter ,以水平方向对齐中心。你可以将它们组合在一起,例如:Qt.AlignBottom + Qt.AlignRight

QTableView cell alignment QTableView单元格对齐

文本颜色

如果你使用过Excel等电子表格,你可能对条件格式化的概念很熟悉。这些规则你可以应用于单元格(或行或列),根据它们的值来改变单元格的文本和背景颜色。

这对于帮助数据的可视化非常有用,例如,用红色表示负数,或者用蓝色到红色的渐变来突出数字的范围(例如,低...高)。

首先,下面的例子实现了一个Qt.ForegroundRole 处理程序,它检查被索引的单元格中的值是否是数字,并且低于零。如果是,那么处理程序就会返回文本(前景)颜色为红色。

def data(self, index, role):
    # existing `if role == Qt.DisplayRole:` block hidden
    # hidden for clarity.

    if role == Qt.ForegroundRole:
        value = self._data[index.row()][index.column()]

        if (
            (isinstance(value, int) or isinstance(value, float))
            and value < 0
        ):
            return QtGui.QColor('red')

如果你把它添加到你的模型的数据处理程序中,所有的负数现在将显示为红色。

QTableView text formatting, with red negative numbers QTableView文本格式化,红色的负数

数字范围梯度

同样的原理可以用来对表格中的数字值应用梯度,例如,突出低值和高值。首先我们定义我们的色标,它取自colorbrewer2.org

COLORS = ['#053061', '#2166ac', '#4393c3', '#92c5de', '#d1e5f0', '#f7f7f7', '#fddbc7', '#f4a582', '#d6604d', '#b2182b', '#67001f']

接下来我们定义我们的自定义处理程序,这次是为Qt.BackgroundRole 。这个处理程序接收给定索引的值,检查它是否为数字,然后执行一系列操作,将其限制在0...10的范围内,以便索引到我们的列表中。

def data(self, index, role):
    # existing `if role == Qt.DisplayRole:` block hidden
    # hidden for clarity.

    if role == Qt.BackgroundRole:
        value = self._data[index.row()][index.column()]
        if (isinstance(value, int) or isinstance(value, float)):
            value = int(value)  # Convert to integer for indexing.

            # Limit to range -5 ... +5, then convert to 0..10
            value = max(-5, value) # values < -5 become -5
            value = min(5, value)  # valaues > +5 become +5
            value = value + 5     # -5 becomes 0, +5 becomes + 10

            return QtGui.QColor(colors[value])

这里用于将数值转换为梯度的逻辑是非常基本的,切断高/低值,并且不根据数据的范围进行调整。然而,你可以根据需要调整,只要你的处理程序的最终结果是返回一个QColorQBrush

QTableView with number-range color gradients 具有数字范围颜色梯度的QTableView

图标和图像装饰

每个表格单元包含一个小的装饰区域,可以用来显示图标、图像或一个纯色块,在数据旁边的左侧。这可以用来指示数据类型,例如日期的日历,bool ,或者为数字范围提供一个更微妙的条件格式化。

下面是这些想法的一些简单实现。

用图标表示bool/date数据类型

对于日期,我们将使用 Python 内置的datetime 类型。首先,在你的文件顶部添加下面的导入,以导入这种类型。

from datetime import datetime

然后,更新数据(设置在MainWindow.__init__),增加datetimebool (TrueFalse 值),例如。

蟒蛇

data = [
    [True, 9, 2],
    [1, 0, -1],
    [3, 5, False],
    [3, 3, 2],
    [datetime(2019, 5, 4), 8, 9],
]

有了这些,你可以更新你的模型data 方法,以显示日期类型的图标和格式化的日期,方法如下。

蟒蛇

#  icons indicating data type

def data(self, index, role):
    if role == Qt.DisplayRole:
        value = self._data[index.row()][index.column()]
        if isinstance(value, datetime):
            return value.strftime('%Y-%m-%d')

        return value

    if role == Qt.DecorationRole:
        value = self._data[index.row()][index.column()]
        if isinstance(value, datetime):
            return QtGui.QIcon('calendar.png')

QTableView formatted dates with indicator icon QTableView的格式化日期与指标图标

下面显示了如何对布尔值TrueFalse ,分别使用刻度线和十字。

蟒蛇

# ticks and crosses for `bool`values
def data(self, index, role):
    # existing `if role == Qt.DisplayRole:` block hidden
    # hidden for clarity.

    if role == Qt.DecorationRole:
        value = self._data[index.row()][index.column()]
        if isinstance(value, bool):
            if value:
                return QtGui.QIcon('tick.png')

            return QtGui.QIcon('cross.png')

当然,你可以将上述内容结合在一起,或任何其他混合的Qt.DecorationRoleQt.DisplayRole 处理程序。如果是分支的话,通常更简单的做法是将每种类型归入同一个角色,或者当你的模型变得更加复杂时,创建子方法来处理每个角色。

QTableView boolean indicators QTableView的布尔指标

色块

Qt.DecorationRole 如果你为QColor ,一个小方块的颜色将显示在单元格的左侧,在图标的位置。这与先前的Qt.BackgroundRole 条件格式化的例子相同,只是现在处理和响应Qt.DecorationRole

蟒蛇

# color blocks
if role == Qt.DecorationRole:
    value = self._data[index.row()][index.column()]
    if (isinstance(value, int) or isinstance(value, float)):
        value = int(value)

        # Limit to range -5 ... +5, then convert to 0..10
        value = max(-5, value)  # values < -5 become -5
        value = min(5, value)   # valaues > +5 become +5
        value = value + 5       # -5 becomes 0, +5 becomes + 10

        return QtGui.QColor(COLORS[value])

QTableView color block decorations QTableView的色块装饰

替代的Python数据结构

到目前为止,在我们的例子中,我们使用了简单的嵌套的Python列表来保存我们的数据以便显示。这对于简单的数据表格来说是很好的,但是如果你要处理大的数据表格,在Python中还有一些其他更好的选择,它们有额外的好处。 在接下来的部分中,我们将看看两个Python数据表库--numpypandas--以及如何将它们与Qt集成。

Numpy

Numpy是一个库,它为Python中的大型多维数组或矩阵数据结构提供支持。对大型数组的高效和高性能处理使numpy成为科学和数学应用的理想选择。这也使得numpy数组成为PySide中大型单类型数据表的良好数据存储。

使用numpy作为数据源

为了支持numpy数组,我们需要对模型做一些改变,首先修改data 方法中的索引,然后改变rowCountcolumnCount 的行和列计数计算。

标准的numpyAPI提供了对二维数组的元素级访问,通过在同一个切片操作中传递行和列,例如:_data[index.row(), index.column()]list 这比分两步进行索引更有效率,如list 的例子。

numpy中,数组的尺寸可以通过.shape ,它依次返回每个轴上的尺寸的一个元组。我们通过从这个元组中选择正确的项目来获得每个轴的长度,例如,_data.shape[0] 获得第一个轴的尺寸。

下面这个完整的例子展示了如何通过一个自定义的模型用Qt的QTableView 来显示一个numpy数组。

蟒蛇

import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt
import numpy as np


class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # Note: self._data[index.row()][index.column()] will also work
            value = self._data[index.row(), index.column()]
            return str(value)

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()

        data = np.array([
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ])

        self.model = TableModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

虽然简单的Python类型如intfloat 不需要转换为字符串就可以显示,但numpy使用它自己的类型(如numpy.int32)来显示数组值。为了使这些显示出来,我们必须首先将它们转换为字符串。

QTableView with numpy array 带有numpy数组的QTableView

使用QTableView ,只能显示二维数组,但是如果你有一个更高维的数据结构,你可以将QTableView 与一个标签或滚动条UI结合起来,以允许访问和显示这些更高的维度。

潘达

Pandas是一个常用于数据处理和分析的Python库。它提供了一个很好的API,用于从各种数据源加载2D表格数据并对其进行数据分析。通过使用pandas DataTable 作为你的QTableView 模型,你可以使用这些API从你的应用程序中直接加载和分析你的数据。

使用Pandas作为数据源

为了与pandas一起工作,对模型的修改是相当小的,需要对data 方法中的索引进行修改,并对rowCountcolumnCount 进行修改。对rowCountcolumnCount 的修改与numpy相同,pandas使用_data.shape 元组来表示数据的维度。

对于索引,我们使用pandas.iloc 方法,用于索引位置--即通过列和/或行索引进行查找。这是通过将行,然后列传递给slice_data.iloc[index.row(), index.column()]

下面这个完整的例子显示了如何通过一个自定义的模型用QtQTableView 来显示一个pandas数据框。

蟒蛇

import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt
import pandas as pd


class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            value = self._data.iloc[index.row(), index.column()]
            return str(value)

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, section, orientation, role):
        # section is the index of the column/row.
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])

            if orientation == Qt.Vertical:
                return str(self._data.index[section])


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()

        data = pd.DataFrame([
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ], columns = ['A', 'B', 'C'], index=['Row 1', 'Row 2', 'Row 3', 'Row 4', 'Row 5'])

        self.model = TableModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

这里一个有趣的扩展是使用QTableView 的表头来显示行和pandas列头的值,这些值可以分别从DataFrame.indexDataFrame.columns

QTableView pandas DataTable, with column and row headers QTableView pandas DataTable, with column and row headers

为此,我们需要在一个自定义的headerData 方法中实现一个Qt.DisplayRole 处理程序。这个处理程序接收section ,即行/列的索引(0...n),orientation ,可以是列头的Qt.Horizontal ,也可以是行头的Qt.Vertical ,还有role ,与data 方法的工作原理相同。

headerData 方法还接收其他角色,这些角色可以用来进一步定制标题的外观。

总结

在本教程中,我们已经介绍了使用QTableView 和自定义模型在你的应用程序中显示表格数据的基本知识。在此基础上,我们还演示了如何格式化数据和用图标和颜色来装饰单元格。最后,我们演示了用来自numpypandas数据结构的表格数据使用QTableView ,包括显示自定义的列和行标题。