使用 QAbstractItemModel 的時機

當資料結構較為複雜、是一個自己實作的物件時,考慮使用 QAbstractItemModel

在這個範例中,資料是一個「出差核銷系統」,每一筆資料包含

  • 出差人
  • Group (小組)
  • Organization (大組)
  • 起始日
  • 結束日
  • 已呈報

事後,我們需要根據每一筆資料計算些資訊,例如: 差旅長度。並且預期這個系統有延伸的可能,也續會增加「報帳金額」、「日均花費金額」….

QStandardItemModel

使用 QStandardItemModel,資料有如 2D excel 表格,優點是 Model 不需要實作。缺點是計算相關資料 (如差旅長度) 時,資料的關聯性差,需要透過 row (同一筆資料)、column (起始日、結束日) 找到需要的資料。

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import delegates  
data = [
    ["Amy",  "GroupA", "orgX", "2023-01-01", "2023-01-05", False],
    ["Alex", "GroupA", "orgY", "2023-01-03", "2023-01-05", False],
    ["Sam",  "GroupB", "orgP", "2023-02-01", "2023-02-06", False],
    ["Xem",  "", "", "", "", False],
]
headers = [ "Name", "Group", "Org", "Start Date", "End Date", "Reported"]

def set_model(app, data):
    model = QStandardItemModel(app)
    for row in data:
        lt = []
        for i, val in enumerate(row):
            it = QStandardItem(val)
            if i == 5: # Reported
                it.setCheckable(True)
            lt.append(it)
        model.appendRow(lt)
    return model

def get_duration(st, et):
    st = (QDate.fromString(st, "yyyy-MM-dd")).toPyDate()
    et = (QDate.fromString(et, "yyyy-MM-dd")).toPyDate()
    res = et - st
    print(res)

不使用 QStandardItemModel

如果已經確定顯示的方法只有 Table 或 List,可以使用 QAbstractTableModel,相較於 base class QAbstractItemModel,它多實作了 index, parent,只需要實作 rowCount, columnCount 之類的方法。使用 QAbstractItemModel 則具有面對 QTreeView 的彈性,但連 index 都需要自己實作。

本例使用 QAbstractTableModel 。缺點是需要自己處理放資料的地方、並實作 rowCount, columnCount 之類的方法。本例用一個 list 儲存 objects。

優點是每一筆資料直接對應一個物件,事後在算差旅天數的時候,可以直接對物件呼叫 get_duration 直接取得 – 把相關的資料跟工具封裝在一起。如果有一些不需要顯示在 GUI 上,卻與物件有關連的資料,也可以直接記載物件上。

import sys
from datetime import datetime

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

def convert_to_datetime(s: str):
    if s =="":
        return datetime(2024, 1, 1)
    return datetime.strptime(s, '%Y-%m-%d').date()

class TravelData:
    attrs = ["name", "group", "org", "startDate", "endDate", "reported"]
    def __init__(self, n, g, o, sd, ed, r) -> None:
        self.name = n
        self.group = g
        self.org = o
        self.startDate = sd
        self.endDate = ed
        self.reported = r
    
    def get_duration(self, ):
        return convert_to_datetime(self.endDate) - convert_to_datetime(self.startDate)

class MyModel(QAbstractTableModel):
    def __init__(self, data, p):
        super(MyModel, self).__init__(p)
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(TravelData.attrs)

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            obj = self._data[index.row()]
            attr_name = TravelData.attrs[index.column()]
            val = getattr(obj, attr_name)
            return val
        return None

    def setData(self, index, value, role=Qt.EditRole):
        if role == Qt.EditRole:
            obj = self._data[index.row()]
            attr_name = TravelData.attrs[index.column()]
            setattr(obj, attr_name, value)
            self.dataChanged.emit(index, index, [Qt.EditRole])
            return True
        return False
    
    def flags(self, index): # Let cell content is editable
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable