본문 바로가기
PyQt5_

Tabular data in ModelViews, with numpy & pandas

by 자동매매 2023. 3. 13.

모델이 Qt 이해할 수있는 형식으로 해당 데이터를 반환하는 모든 데이터 소스에서 모델 뷰를 사용할 있습니다. Python에서 형식 데이터로 작업하면 해당 데이터를 로드하고 작업하는 방법에 대한 여러 가지 가능성이 열립니다. 여기에서는 간단한 중첩 목록 목록으로 시작한 다음 Qt 응용 프로그램을 인기있는 numpy pandas 라이브러리와 통합하는 단계로 이동합니다. 이렇게 하면 데이터 중심 응용 프로그램을 빌드하기 위한 훌륭한 기반이 제공됩니다.

 

Introduction to QTableView

QTableView 스프레드 시트와 같은 테이블보기로 데이터를 표시하는 Qt 위젯입니다. 모델 아키텍처의 모든 위젯과 마찬가지로 위젯은 별도의 모델을 사용하여 뷰에 데이터 프리젠테이션 정보를 제공합니다. 모델의 데이터는 필요에 따라 업데이트할 있으며 뷰는 이러한 변경 사항을 통지하여 변경 사항을 다시 그리거나 표시합니다. 모델을 사용자 정의하면 데이터가 표시되는 방식을 크게 제어 있습니다.

 

tableview_demo.py

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


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

    def data(self, index, role):
        if role == Qt.ItemDataRole.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, 1, 3, 3, 7],
            [9, 1, 5, 3, 8],
            [2, 1, 5, 3, 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 위젯을 만든 다음 사용자 정의 모델 (데이터 소스를 매개 변수로 허용하기 위해 작성) 인스턴스를 만든 다음보기에 모델을 설정합니다. 이것이 우리가 해야 전부입니다보기 위젯은 이제 모델을 사용하여 데이터를 가져오고 데이터를 그리는 방법을 결정합니다.

 

 

Nested list as a 2-dimensional data store

테이블의 경우 있는 2D 데이터 구조가 필요합니다. 

table = [

[ 4 , 1 , 3 , 3 , 7 ],

[ 9 , 1 , 5 , 3 , 8 ],

[ 2 , 1 , 5 , 3 , 9 ],

]

 

row = 2

col = 4

>>> table[row]

[ 2 , 1 , 5 , 3 , 9 ]

 

>>> table[row][col]

9

 
flip하여  열 또는 행으로 액세스하는 것이 더 유용, 첫 번째 인덱스를 열로 사용할 수 있습니다.

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

 

Formatting numbers and dates

 

tableview_format_1.py

import sys
from datetime import datetime  # <1>

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    def data(self, index, role):
        if role == Qt.ItemDataRole.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

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])




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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

Use this together with the modified sample data below to see it in action.

data = [

[ 4 , 9 , 2 ],

[ 1 , - 1 , 'hello'],

[ 3.023 , 5 , - 5 ],

[ 3 , 3 , datetime( 2017 , 10 , 1 )],

[ 7.555 , 8 , 9 ],

]

 

지금까지는 데이터 자체의 형식을 지정하는 방법을 사용자 지정하는 방법만 살펴보았습니다. 그러나 모델 인터페이스를 사용하면 색상과 아이콘을 포함한 테이블 셀의 표시를 훨씬  세부적으로 제어할  있습니다. 다음 부분에서는 모델을 사용하여 QTableView 모양을 사용자 정의하는 방법을 살펴 보겠습니다.

 

Styles & Colors with Roles

 

색과 아이콘을 사용하여 데이터 테이블의 셀을 강조 표시하면 데이터를 쉽게 찾고 이해할 있으며 사용자가 관심 있는 데이터를 선택하거나 표시하는 도움이 됩니다. Qt 데이터 방법에 대한 관련 역할에 응답하여 모델에서 이러한 모든 것을 완벽하게 제어 있습니다.

 

다양한 역할 유형에 대한 응답으로 반환될 것으로 예상되는 유형은 다음과 같습니다.

Role Type
Qt.ItemDataRole.BackgroundRole QBrush (also QColor)
Qt.ItemDataRole.CheckStateRole Qt.CheckState
Qt.ItemDataRole.DecorationRole QIcon, QPixmap, QColor
Qt.ItemDataRole.DisplayRole QString (also int, float, bool)
Qt.ItemDataRole.FontRole QFont
Qt.ItemDataRole.SizeHintRole QSize
Qt.ItemDataRole.TextAlignmentRole Qt.Alignment
Qt.ItemDataRole.ForegroundRole QBrush (also QColor)

 

tableview_format_2.py

import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    # tag::data[]
    def data(self, index, role):
        if (
            role == Qt.ItemDataRole.BackgroundRole
            and index.column() == 2
        ):
            # See below for the data structure.
            return QtGui.QColor(Qt.GlobalColor.blue)

        # existing `if role == Qt.ItemDataRole.DisplayRole:` block hidden
        # hidden for clarity.

        # end::data[]

        if role == Qt.ItemDataRole.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

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

Text alignment

 

Listing 107. tableview_format_3.py

 

import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    # tag::data[]
    def data(self, index, role):
        if role == Qt.ItemDataRole.TextAlignmentRole:
            value = self._data[index.row()][index.column()]

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

        # existing `if role == Qt.ItemDataRole.DisplayRole:` block hidden
        # hidden for clarity.
        # end::data[]

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

        if role == Qt.ItemDataRole.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

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

 

Text colors

 

tableview_format_4.py

import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    # tag::data[]
    def data(self, index, role):
        if role == Qt.ItemDataRole.ForegroundRole:
            value = self._data[index.row()][index.column()]

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

        # existing `if role == Qt.ItemDataRole.DisplayRole:` block hidden
        # hidden for clarity.
        # end::data[]

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

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

        if role == Qt.ItemDataRole.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

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

 

Number range gradients

The same principle can be used to apply gradients to numeric values in a table to, for example, highlight low and high values. First we define our color scale, which is taken from colorbrewer2.org.

COLORS = ['#053061', '#2166ac', '#4393c3', '#92c5de', '#d1e5f0',

'#f7f7f7', '#fddbc7', '#f4a582', '#d6604d', '#b2182b', '#67001f']

Next we define our custom handler, this time for Qt.ItemDataRole.BackgroundRole.

This takes the value at the given index, checks that this is numeric then performs a series of operations to constrain it to the range 0...10 required to index into our list.

 

tableview_format_5.py

import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt

# Color range -5 to +5; 0 = light gray
COLORS = [
    "#053061",
    "#2166ac",
    "#4393c3",
    "#92c5de",
    "#d1e5f0",
    "#f7f7f7",
    "#fddbc7",
    "#f4a582",
    "#d6604d",
    "#b2182b",
    "#67001f",
]


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

    # tag::data[]
    def data(self, index, role):
        if role == Qt.ItemDataRole.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])

        # existing `if role == Qt.ItemDataRole.DisplayRole:` block hidden
        # hidden for clarity.
        # end::data[]

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

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

 

값을 그라디언트로 변환하기 위해 여기에 사용되는 논리는 매우 기본적이며 높거나 낮은 값을 차단하고 데이터 범위에 맞게 조정하지 않습니다. 그러나 핸들러의 최종 결과가 QColor 또는 QBrush 반환하는 필요에 따라이를 조정할 있습니다.

 

Icon & Image decoration

Each table cell contains a small decoration area which can be used to display icons, images or a solid block of color, on the left hand side next to the data. This can be used to indicate data type, e.g. calendars for dates, ticks and crosses for bool values, or for a more subtle conditional-formatting for number ranges.

Below are some simple implementations of these ideas.

 

Indicating bool/date data types with icons

For dates we’ll use Python’s built-in datetime type. First, add the following import to the top of your file to import this type.

from datetime import datetime

Then, update the data (set in the MainWindow.init) to add datetime and bool (True or False values), for example.

data = [

[True, 9 , 2 ],

[ 1 , 0 , - 1 ],

[ 3 , 5 , False],

[ 3 , 3 , 2 ],

[datetime( 2019 , 5 , 4 ), 8 , 9 ],

]

With these in place, you can update your model data method to show icons and formatted dates for date types, with the following code.

 

tableview_format_6.py

import os

import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


basedir = os.path.dirname(__file__)


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

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

            return value

        if role == Qt.ItemDataRole.DecorationRole:
            value = self._data[index.row()][index.column()]
            if isinstance(value, datetime):
                return QtGui.QIcon(
                    os.path.join(basedir, "calendar.png")
                )

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])




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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

 

The following shows how to use ticks and cross for boolean True and False values respectively.

 

tableview_format_7.py

import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    # tag::data[]
    def data(self, index, role):
        if role == Qt.ItemDataRole.DecorationRole:
            value = self._data[index.row()][index.column()]
            if isinstance(value, bool):
                if value:
                    return QtGui.QIcon("tick.png")

                return QtGui.QIcon("cross.png")
        # end::data[]

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

            return value

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

You can of course combine the above together, or any other mix of Qt.ItemDataRole.DecorationRole and Qt.ItemDataRole.DisplayRole handlers. It’s usually simpler to keep each type grouped under the same if branch, or as your model becomes more complex, to create sub-methods to handle each role.

 

Color blocks

If you return a QColor for Qt.ItemDataRole.DecorationRole a small square of color will be displayed on the left hand side of the cell, in the icon location. This is identical to the earlier Qt.ItemDataRole.BackgroundRole conditional formatting example, except now handling and responding to Qt.ItemDataRole.DecorationRole.

 

tableview_format_8.py

import os
import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt

basedir = os.path.dirname(__file__)

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


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

    # tag::data[]
    def data(self, index, role):
        if role == Qt.ItemDataRole.DecorationRole:
            value = self._data[index.row()][index.column()]

            if isinstance(value, datetime):
                return QtGui.QIcon(
                    os.path.join(basedir, "calendar.png")
                )

            if isinstance(value, bool):
                if value:
                    return QtGui.QIcon(
                        os.path.join(basedir, "tick.png")
                    )

                return QtGui.QIcon(os.path.join(basedir, "cross.png"))

            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])
        # end::data[]

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

            return value

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


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

        self.table = QtWidgets.QTableView()

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

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

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

 

tableview_format_9.py

import os
import sys
from datetime import datetime

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt

basedir = os.path.dirname(__file__)


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

        # Row colors empty by default.
        self._row_colors = {}

    def data(self, index, role):
        if role == Qt.ItemDataRole.DecorationRole:
            value = self._data[index.row()][index.column()]
            if isinstance(value, bool):
                if value:
                    return QtGui.QIcon(
                        os.path.join(basedir, "tick.png")
                    )

                return QtGui.QIcon(os.path.join(basedir, "cross.png"))

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

            return value

        if role == Qt.ItemDataRole.BackgroundRole:
            color = self._row_colors.get(
                index.row()
            )  # returns None if not in.
            if color:
                return QtGui.QColor(color)

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])

    def set_row_color(self, row, color):
        self._row_colors[row] = color


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

        self.table = QtWidgets.QTableView()

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

        self.model = TableModel(data)
        self.model.set_row_color(1, "#b2182b")
        self.model.set_row_color(3, "#92c5de")
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)
        self.setGeometry(600, 100, 400, 200)


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

 

Alternative Python data structures

So far in our examples we’ve used simple nested Python lists to hold our data for display. This is fine for simple tables of data, however if you’re working with large data tables there are some other better options in Python, which come with additional benefits. In the next parts we’ll look at two Python data table libraries — numpy and pandas — and how to integrate these with Qt.

 

Numpy

Numpy is a library which provides support for large multi-dimensional arrays or matrix data structures in Python. The efficient and high-performance handling of large arrays makes numpy ideal for scientific and mathematical applications. This also makes numpy arrays a good data store for large, single-typed, data tables in PyQt6.

 

Using numpy as a data source

To support numpy arrays we need to make a number of changes to the model, first modifying the indexing in the data method and then changing the row and column count calculations for rowCount and columnCount.

The standard numpy API provides element-level access to 2D arrays, by passing the row and column in the same slicing operation, e.g. _data[index.row(), index.column()]. This is more efficient than indexing in two steps, as for the list of list examples.

In numpy the dimensions of an array are available through .shape which returns a tuple of dimensions along each axis in turn. We get the length of each axis by selecting the correct item from this tuple, e.g. _data.shape[0] gets the size of the first axis.

The following complete example shows how to display a numpy array using Qt’s QTableView via a custom model.

 

model-views/tableview_numpy.py

import sys

import numpy as np
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    def data(self, index, role):
        if role == Qt.ItemDataRole.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)
        self.setGeometry(600, 100, 400, 200)


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

 

QTableView 사용하면 2D 배열 표시 있지만 높은 차원의 데이터 구조가있는 경우 QTableView 또는 스크롤바 UI 결합하여 이러한 높은 차원에 액세스하고 표시 있습니다.

 

Pandas

Pandas is a Python library commonly used for data manipulation and analysis. It provides a nice API for loading 2D tabular data from various data sources and performing data analysis on it. By using the numpy DataTable as your QTableView model you can use these APIs to load and analyse your data from right within your application.

 

Using Pandas as a data source

The modifications of the model to work with numpy are fairly minor, requiring changes to the indexing in the data method and modifications to rowCount and columnCount. The changes for rowCount and columnCount are identical to numpy with pandas using a _data.shape tuple to represent the dimensions of the data.

For indexing we use the pandas .iloc method, for indexed locations — i.e. lookup by column and/or row index. This is done by passing the row and then column to the slice _data.iloc[index.row(), index.column()].

The following complete example shows how to display a pandas data frame using Qt QTableView via a custom model.

 

model-views/tableview_pandas.py

import sys

import pandas as pd
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt


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

    def data(self, index, role):
        if role == Qt.ItemDataRole.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):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return str(self._data.columns[section])

            if orientation == Qt.Orientation.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)
        self.setGeometry(600, 100, 400, 200)


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

 

An interesting extension here is to use the table header of the QTableView to display row and pandas column header values, which can be taken from DataFrame.index and DataFrame.columns respectively.

For this we need to implement a Qt.ItemDataRole.DisplayRole handler in a custom headerData method. This receives section, the index of the row/column (0...n), orientation which can be either Qt.Orientations.Horizontal for the column headers, or Qt.Orientations.Vertical for the row headers, and role which works the same as for the data method.

 

Conclusion

In this chapter we’ve covered the basics of using QTableView and a custom model

to display tabular data in your applications. This was extended to demonstrate how to format data and decorate cells with icons and colors. Finally, we demonstrated using QTableView with tabular data from numpy and pandas structures including displaying custom column and row headers.

'PyQt5_' 카테고리의 다른 글

Bitmap Graphics in Qt  (0) 2023.03.13
Querying SQL databases with Qt models  (0) 2023.03.13
The Model View Architecture — Model View Controller  (0) 2023.03.13
Qt Style Sheets (QSS)  (0) 2023.03.13
Icons  (0) 2023.03.13

댓글